diff --git a/usermods/LM75_Temperature/LM75.cpp b/usermods/LM75_Temperature/LM75.cpp new file mode 100644 index 0000000000..087d2a8dc3 --- /dev/null +++ b/usermods/LM75_Temperature/LM75.cpp @@ -0,0 +1,301 @@ +#include "LM75.h" + +static Generic_LM75 LM75temperature(USERMOD_LM75TEMPERATURE_I2C_ADDRESS); + +void UsermodLM75Temperature::overtempFailure() { + overtempTriggered = true; + if(bri >0) { + toggleOnOff(); + stateUpdated(CALL_MODE_BUTTON); + } + } + + void UsermodLM75Temperature::overtempReset() { + overtempTriggered = false; + if(bri == 0) { + toggleOnOff(); + stateUpdated(CALL_MODE_BUTTON); + } + } + + /** + * Get auto off Temperature at which WLED Output is swiched off + */ + int8_t UsermodLM75Temperature::getAutoOffHighThreshold() { + return autoOffHighThreshold; + } + + /** + * Get auto off Temperature at which WLED Output is swiched on again + */ + int8_t UsermodLM75Temperature::getAutoOffLowThreshold() { + return autoOffLowThreshold; + } + + /** + * Set auto off Temperature at which WLED Output is swiched off + */ + void UsermodLM75Temperature::setAutoOffHighThreshold(uint8_t threshold) { + autoOffHighThreshold = min((uint8_t)212, max((uint8_t)1, threshold)); + } + + /** + * Set auto off Temperature at which WLED Output is swiched on again + */ + void UsermodLM75Temperature::setAutoOffLowThreshold(uint8_t threshold) { + autoOffLowThreshold = min((uint8_t)211, max((uint8_t)0, threshold)); + // when low power indicator is enabled the auto-off threshold cannot be above indicator threshold + autoOffLowThreshold = autoOffEnabled ? min(autoOffHighThreshold-1, (int)autoOffLowThreshold) : autoOffLowThreshold; + } + + bool UsermodLM75Temperature::findSensor() { + uint8_t devicepresent; + //Let's try to communicate with the LM75 sensor + Wire.beginTransmission(USERMOD_LM75TEMPERATURE_I2C_ADDRESS); + // End Transmission will return 0 is device has acknowledged communication + devicepresent = Wire.endTransmission(); + if(devicepresent == 0) { + DEBUG_PRINTLN(F("Sensor found.")); + sensorFound = 1; + return true; + } else { + DEBUG_PRINTLN(F("Sensor NOT found.")); + return false; + } + } + +void UsermodLM75Temperature::readTemperature() { + if(degC) { + temperature = LM75temperature.readTemperatureC(); + } else { + temperature = LM75temperature.readTemperatureF(); + } + lastMeasurement = millis(); + DEBUG_PRINT(F("Read temperature ")); + DEBUG_PRINTLN(temperature); + } + + + #ifndef WLED_DISABLE_MQTT + void UsermodLM75Temperature::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")] = getTemperatureUnit(); + 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 UsermodLM75Temperature::setup() { + int retries = 5; + sensorFound = 0; + if (enabled) { + // config says we are enabled + DEBUG_PRINTLN(F("Searching LM75 IC...")); + while (!findSensor() && retries--) { + delay(25); // try to find sensor + } + if (sensorFound && !initDone) DEBUG_PRINTLN(F("Init not completed")); + } + lastMeasurement = millis() - readingInterval + 10000; + initDone = true; + } + + void UsermodLM75Temperature::loop() { + if (!enabled || !sensorFound || strip.isUpdating()) return; + + 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; + + readTemperature(); + + if(autoOffEnabled) { + if (!overtempTriggered && temperature >= autoOffHighThreshold) { + overtempFailure(); + } else if(overtempTriggered && temperature <= autoOffLowThreshold) { + overtempReset(); + } + } + + #ifndef WLED_DISABLE_MQTT + if (WLED_MQTT_CONNECTED) { + char subuf[64]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/temperature")); + mqtt->publish(subuf, 0, false, String(temperature).c_str()); + } + #endif + } + + + #ifndef WLED_DISABLE_MQTT + /** + * subscribe to MQTT topic if needed + */ + void UsermodLM75Temperature::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 UsermodLM75Temperature::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 (!sensorFound) { + 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 UsermodLM75Temperature::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 UsermodLM75Temperature::readFromJsonState(JsonObject &root) { + // if (!initDone) return; // prevent crash on boot applyPreset() + //} + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void UsermodLM75Temperature::addToConfig(JsonObject &root) { + // we add JSON object: {"Temperature":} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top["degC"] = degC; // usermodparam + top[FPSTR(_readInterval)] = readingInterval / 1000; + + JsonObject ao = top.createNestedObject(FPSTR(_ao)); // auto off section + ao[FPSTR(_enabled)] = autoOffEnabled; + ao[FPSTR(_thresholdhigh)] = autoOffHighThreshold; + ao[FPSTR(_thresholdlow)] = autoOffLowThreshold; + 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 UsermodLM75Temperature::readFromConfig(JsonObject &root) { + // we look for JSON object: {"Temperature":} + 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; + degC = top["degC"] | degC; + readingInterval = top[FPSTR(_readInterval)] | readingInterval/1000; + readingInterval = min(120,max(10,(int)readingInterval)) * 1000; // convert to ms + + JsonObject ao = top[FPSTR(_ao)]; + autoOffEnabled = ao[FPSTR(_enabled)] | autoOffEnabled; + setAutoOffHighThreshold (ao[FPSTR(_thresholdhigh)] | autoOffHighThreshold); + setAutoOffLowThreshold (ao[FPSTR(_thresholdlow)] | autoOffLowThreshold); + + if (!initDone) { + // first run: reading from cfg.json + DEBUG_PRINTLN(F(" config loaded.")); + } + else { + // changing parameters from settings page + setup(); + DEBUG_PRINTLN(F(" config (re)loaded.")); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_name)].isNull(); + } + + void UsermodLM75Temperature::appendConfigData() { + if(degC) { + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_ao)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_thresholdhigh)).c_str()); + oappend(SET_F("',1,'°C');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_ao)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_thresholdlow)).c_str()); + oappend(SET_F("',1,'°C');")); // 0 is field type, 1 is actual field + } else { + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_ao)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_thresholdhigh)).c_str()); + oappend(SET_F("',1,'°F');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_ao)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_thresholdlow)).c_str()); + oappend(SET_F("',1,'°F');")); // 0 is field type, 1 is actual field + } + + } + + + /** ++ * Gets the current temperature in the configured unit (C or F) ++ * @return Temperature in the unit specified by degC setting ++ */ + float UsermodLM75Temperature::getTemperature() { + return temperature; + } + + const char *UsermodLM75Temperature::getTemperatureUnit() { + return degC ? "°C" : "°F"; + } + + // strings to reduce flash memory usage (used more than twice) + const char UsermodLM75Temperature::_name[] PROGMEM = "Temperature"; + const char UsermodLM75Temperature::_enabled[] PROGMEM = "enabled"; + const char UsermodLM75Temperature::_readInterval[] PROGMEM = "read-interval-s"; + const char UsermodLM75Temperature::_ao[] PROGMEM = "Overtemperature-Protection"; + const char UsermodLM75Temperature::_thresholdhigh[] PROGMEM = "shutdown-temperature"; + const char UsermodLM75Temperature::_thresholdlow[] PROGMEM = "reactivate-temperature"; + +static UsermodLM75Temperature temperature; +REGISTER_USERMOD(temperature); diff --git a/usermods/LM75_Temperature/LM75.h b/usermods/LM75_Temperature/LM75.h new file mode 100644 index 0000000000..9ac25cff40 --- /dev/null +++ b/usermods/LM75_Temperature/LM75.h @@ -0,0 +1,106 @@ +#pragma once + +#include "wled.h" +#include "Temperature_LM75_Derived.h" + +// the frequency to check temperature, 1 minute +#ifndef USERMOD_LM75TEMPERATURE_MEASUREMENT_INTERVAL +#define USERMOD_LM75TEMPERATURE_MEASUREMENT_INTERVAL 60000 +#endif + +// auto-off feature +#ifndef USERMOD_LM75TEMPERATURE_ENABLED + #define USERMOD_LM75TEMPERATURE_ENABLED true +#endif + +#ifndef USERMOD_LM75TEMPERATURE_AUTO_OFF_HIGH_THRESHOLD + #define USERMOD_LM75TEMPERATURE_AUTO_OFF_HIGH_THRESHOLD 60 +#endif + +#ifndef USERMOD_LM75TEMPERATURE_AUTO_OFF_LOW_THRESHOLD + #define USERMOD_LM75TEMPERATURE_AUTO_OFF_LOW_THRESHOLD 40 +#endif + +#ifndef USERMOD_LM75TEMPERATURE_AUTO_TEMPERATURE_OFF_ENABLED + #define USERMOD_LM75TEMPERATURE_AUTO_TEMPERATURE_OFF_ENABLED true +#endif + +#ifndef USERMOD_LM75TEMPERATURE_I2C_ADDRESS + #define USERMOD_LM75TEMPERATURE_I2C_ADDRESS 0x48 +#endif + +class UsermodLM75Temperature : public Usermod { + + private: + bool initDone = false; + + // measurement unit (true==°C, false==°F) + bool degC = true; + + // how often do we read from sensor? + unsigned long readingInterval = USERMOD_LM75TEMPERATURE_MEASUREMENT_INTERVAL; + // set last reading as "40 sec before boot", so first reading is taken after 20 sec + unsigned long lastMeasurement = UINT32_MAX - USERMOD_LM75TEMPERATURE_MEASUREMENT_INTERVAL; + // Tracks the last time temperature was requested from the sensor + unsigned long lastTemperaturesRequest; + float temperature; + // flag set at startup if LM75 sensor not found, avoids trying to keep getting + // temperature if flashed to a board without a sensor attached + byte sensorFound; + // Temperature limits for master off feature + uint8_t autoOffHighThreshold = USERMOD_LM75TEMPERATURE_AUTO_OFF_HIGH_THRESHOLD; + uint8_t autoOffLowThreshold = USERMOD_LM75TEMPERATURE_AUTO_OFF_LOW_THRESHOLD; + // Has an overtemperature event occured ? + bool overtempTriggered = false; + + bool enabled = USERMOD_LM75TEMPERATURE_ENABLED; + + // Tracks whether Home Assistant autodiscovery has been published + bool HApublished = false; + + // auto shutdown/shutoff/master off feature + bool autoOffEnabled = USERMOD_LM75TEMPERATURE_AUTO_TEMPERATURE_OFF_ENABLED; + + // 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 _ao[]; + static const char _thresholdhigh[]; + static const char _thresholdlow[]; + + // Functions for LM75 sensor + void readTemperature(); + bool findSensor(); +#ifndef WLED_DISABLE_MQTT + void publishHomeAssistantAutodiscovery(); +#endif + + /* + * API calls to enable data exchange between WLED modules + */ + float getTemperature(); + const char *getTemperatureUnit(); + uint16_t getId() { return USERMOD_ID_LM75TEMPERATURE; } + + void setup(); + void loop(); + //void connected(); +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent); +#endif + + void addToJsonInfo(JsonObject& root); + void addToConfig(JsonObject &root); + bool readFromConfig(JsonObject &root); + + void appendConfigData(); + + void overtempFailure(); + void overtempReset(); + int8_t getAutoOffHighThreshold(); + int8_t getAutoOffLowThreshold(); + void setAutoOffHighThreshold(uint8_t threshold); + void setAutoOffLowThreshold(uint8_t threshold); +}; + diff --git a/usermods/LM75_Temperature/library.json b/usermods/LM75_Temperature/library.json new file mode 100644 index 0000000000..b1027ad8fc --- /dev/null +++ b/usermods/LM75_Temperature/library.json @@ -0,0 +1,7 @@ +{ + "name": "LM75Temperature", + "build": { "libArchive": false}, + "dependencies": { + "jeremycole/I2C Temperature Sensors derived from the LM75":"~1.0.3" + } + } \ No newline at end of file diff --git a/usermods/LM75_Temperature/readme.md b/usermods/LM75_Temperature/readme.md new file mode 100644 index 0000000000..a1a730b9c9 --- /dev/null +++ b/usermods/LM75_Temperature/readme.md @@ -0,0 +1,58 @@ +# LM75 temperature sensor with overtemperature protection + +This usermod utilizes LM75 style I2C temperature sensors. +It is based on the regular temperature usermod for One Wire sensors. + +## Parameters: +| Parameter | Effect | Default | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| Temperature Enable | Activates the LM75 mod | Enabled | +| DegC | Switch between °C and °F | °C | +| Read Interval | Time between temperature measurements | 60 s | +| Overtemperature Enable | Enables or disables temperature protection. According to the defined limits | Enabled | +| Shutdown Temperature | Temperature at which the WLED output is switched off. Value can be between 1 and 100°C | 60°C | +| Reactivate Temperature | Switches the WLED output back on once the temperature drops below set value. Has a hysteresis of minimum 1 towards the shutdown temperature. Value can be between 0 and 99°C | 40°C | + +## Compilation: +- modify `platformio.ini` and add wanted `build_flags` to your configuration +
+ + +- `-D USERMOD_LM75TEMPERATURE_MEASUREMENT_INTERVAL=60000` + Parameter time in ms +
+ +- `-D USERMOD_LM75TEMPERATURE_ENABLED=true` + Parameter true or false +
+ +- `-D USERMOD_LM75TEMPERATURE_AUTO_OFF_HIGH_THRESHOLD=60` + Parameter 1 to 212 °C +
+ +- `-D USERMOD_LM75TEMPERATURE_AUTO_OFF_LOW_THRESHOLD=40` + Parameter 0 to 211 °C +
+ +- `-D USERMOD_LM75TEMPERATURE_AUTO_TEMPERATURE_OFF_ENABLED=true` + Paremeter true or false +
+ +- `-D USERMOD_LM75TEMPERATURE_I2C_ADDRESS=0x48` + Parameter I2C Address of the LM75 IC in Hex 0x** + +## Remarks +The °F setting is not optimal to be used in combination with the overtemperature protection feature. WLED only allows uint8 values as inputs. Hence, the maximum temperature is capped at 212 °F or 100 °C. Also be carefull not to set the lower limit below 32 °F to avoid negative values in the inner workings of the mod. + +
+ +To avoid flickering of the WLED output, the overtemperature feature has a hysteresis of minimum 1. It is advised to use a bigger hysteresis when setting the limits. Still the mod checks the inputs and adjust the lower value not to be equal or higher than the upper limit. + +
+ +There is no cross check if the user enables or disables the master light output during an overtemperature event. For example this can lead to the following behavior. User switches light off. Still an overtemperature event occurs. Since the light is already switched off, the mod does not change the current status. Now the temperature drops. And the mod will reenable the light, switching it on. + +
+ +During power up, the mod tries to scan for the I2C devices under the configured adress. The info page shows an error message if the sensor cannot be reached. + diff --git a/wled00/const.h b/wled00/const.h index 2b460f3f18..88aee5b3dd 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -210,6 +210,7 @@ #define USERMOD_ID_DEEP_SLEEP 55 //Usermod "usermod_deep_sleep.h" #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" +#define USERMOD_ID_LM75TEMPERATURE 58 //Usermod "LM75.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot