The Unit of Measurement ("tempScale") of the MQTT message is set for each published measurement - but not for the homeassistant discovery.
Thus, openhab (and mabye other systems?) don't recognize the value as "Number" - so it uses String instead. This is somehow annoying when trying to configure the sensor channel to be linked to an existing item in OpenHAB.
(Items that are created automatically or with "Add point to model" can be configured in a way that the String is transformed to Number or Number:Temperature, but existing Items cannot be linked).
When a "Unit of Measurement" is set in HomeAssistant discovery, the HA binding of OpenHAB notices that the MQTT String is a number and automatically converts it.
483 lines
18 KiB
C++
483 lines
18 KiB
C++
// force the compiler to show a warning to confirm that this file is included
|
|
#warning **** Included USERMOD_BME280 version 2.0 ****
|
|
|
|
#ifndef WLED_ENABLE_MQTT
|
|
#error "This user mod requires MQTT to be enabled."
|
|
#endif
|
|
|
|
#pragma once
|
|
|
|
#include "wled.h"
|
|
#include <Arduino.h>
|
|
#include <BME280I2C.h> // BME280 sensor
|
|
#include <EnvironmentCalculations.h> // BME280 extended measurements
|
|
|
|
class UsermodBME280 : public Usermod
|
|
{
|
|
private:
|
|
|
|
// NOTE: Do not implement any compile-time variables, anything the user needs to configure
|
|
// should be configurable from the Usermod menu using the methods below
|
|
// key settings set via usermod menu
|
|
uint8_t TemperatureDecimals = 0; // Number of decimal places in published temperaure values
|
|
uint8_t HumidityDecimals = 0; // Number of decimal places in published humidity values
|
|
uint8_t PressureDecimals = 0; // Number of decimal places in published pressure values
|
|
uint16_t TemperatureInterval = 5; // Interval to measure temperature (and humidity, dew point if available) in seconds
|
|
uint16_t PressureInterval = 300; // Interval to measure pressure in seconds
|
|
BME280I2C::I2CAddr I2CAddress = BME280I2C::I2CAddr_0x76; // i2c address, defaults to 0x76
|
|
bool PublishAlways = false; // Publish values even when they have not changed
|
|
bool UseCelsius = true; // Use Celsius for Reporting
|
|
bool HomeAssistantDiscovery = false; // Publish Home Assistant Device Information
|
|
bool enabled = true;
|
|
|
|
// set the default pins based on the architecture, these get overridden by Usermod menu settings
|
|
#ifdef ESP8266
|
|
//uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8
|
|
#endif
|
|
bool initDone = false;
|
|
|
|
BME280I2C bme;
|
|
|
|
uint8_t sensorType;
|
|
|
|
// Measurement timers
|
|
long timer;
|
|
long lastTemperatureMeasure = 0;
|
|
long lastPressureMeasure = 0;
|
|
|
|
// Current sensor values
|
|
float sensorTemperature;
|
|
float sensorHumidity;
|
|
float sensorHeatIndex;
|
|
float sensorDewPoint;
|
|
float sensorPressure;
|
|
String tempScale;
|
|
// Track previous sensor values
|
|
float lastTemperature;
|
|
float lastHumidity;
|
|
float lastHeatIndex;
|
|
float lastDewPoint;
|
|
float lastPressure;
|
|
|
|
// MQTT topic strings for publishing Home Assistant discovery topics
|
|
bool mqttInitialized = false;
|
|
|
|
// strings to reduce flash memory usage (used more than twice)
|
|
static const char _name[];
|
|
static const char _enabled[];
|
|
|
|
// Read the BME280/BMP280 Sensor (which one runs depends on whether Celsius or Fahrenheit being set in Usermod Menu)
|
|
void UpdateBME280Data(int SensorType)
|
|
{
|
|
float _temperature, _humidity, _pressure;
|
|
|
|
if (UseCelsius) {
|
|
BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
|
|
EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius);
|
|
BME280::PresUnit presUnit(BME280::PresUnit_hPa);
|
|
|
|
bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit);
|
|
|
|
sensorTemperature = _temperature;
|
|
sensorHumidity = _humidity;
|
|
sensorPressure = _pressure;
|
|
tempScale = F("°C");
|
|
if (sensorType == 1)
|
|
{
|
|
sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit);
|
|
sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit);
|
|
}
|
|
} else {
|
|
BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit);
|
|
EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Fahrenheit);
|
|
BME280::PresUnit presUnit(BME280::PresUnit_hPa);
|
|
|
|
bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit);
|
|
|
|
sensorTemperature = _temperature;
|
|
sensorHumidity = _humidity;
|
|
sensorPressure = _pressure;
|
|
tempScale = F("°F");
|
|
if (sensorType == 1)
|
|
{
|
|
sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit);
|
|
sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Procedure to define all MQTT discovery Topics
|
|
void _mqttInitialize()
|
|
{
|
|
char mqttTemperatureTopic[128];
|
|
char mqttHumidityTopic[128];
|
|
char mqttPressureTopic[128];
|
|
char mqttHeatIndexTopic[128];
|
|
char mqttDewPointTopic[128];
|
|
snprintf_P(mqttTemperatureTopic, 127, PSTR("%s/temperature"), mqttDeviceTopic);
|
|
snprintf_P(mqttPressureTopic, 127, PSTR("%s/pressure"), mqttDeviceTopic);
|
|
snprintf_P(mqttHumidityTopic, 127, PSTR("%s/humidity"), mqttDeviceTopic);
|
|
snprintf_P(mqttHeatIndexTopic, 127, PSTR("%s/heat_index"), mqttDeviceTopic);
|
|
snprintf_P(mqttDewPointTopic, 127, PSTR("%s/dew_point"), mqttDeviceTopic);
|
|
|
|
if (HomeAssistantDiscovery) {
|
|
_createMqttSensor(F("Temperature"), mqttTemperatureTopic, "temperature", tempScale);
|
|
_createMqttSensor(F("Pressure"), mqttPressureTopic, "pressure", F("hPa"));
|
|
_createMqttSensor(F("Humidity"), mqttHumidityTopic, "humidity", F("%"));
|
|
_createMqttSensor(F("HeatIndex"), mqttHeatIndexTopic, "temperature", tempScale);
|
|
_createMqttSensor(F("DewPoint"), mqttDewPointTopic, "temperature", tempScale);
|
|
}
|
|
}
|
|
|
|
// Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.
|
|
void _createMqttSensor(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")] = String(serverDescription) + " " + 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());
|
|
}
|
|
|
|
void publishMqtt(const char *topic, const char* state) {
|
|
//Check if MQTT Connected, otherwise it will crash the 8266
|
|
if (WLED_MQTT_CONNECTED){
|
|
char subuf[128];
|
|
snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic);
|
|
mqtt->publish(subuf, 0, false, state);
|
|
}
|
|
}
|
|
|
|
void initializeBmeComms()
|
|
{
|
|
BME280I2C::Settings settings{
|
|
BME280::OSR_X16, // Temperature oversampling x16
|
|
BME280::OSR_X16, // Humidity oversampling x16
|
|
BME280::OSR_X16, // Pressure oversampling x16
|
|
BME280::Mode_Forced,
|
|
BME280::StandbyTime_1000ms,
|
|
BME280::Filter_Off,
|
|
BME280::SpiEnable_False,
|
|
I2CAddress
|
|
};
|
|
|
|
bme.setSettings(settings);
|
|
|
|
if (!bme.begin())
|
|
{
|
|
sensorType = 0;
|
|
DEBUG_PRINTLN(F("Could not find BME280 I2C sensor!"));
|
|
}
|
|
else
|
|
{
|
|
switch (bme.chipModel())
|
|
{
|
|
case BME280::ChipModel_BME280:
|
|
sensorType = 1;
|
|
DEBUG_PRINTLN(F("Found BME280 sensor! Success."));
|
|
break;
|
|
case BME280::ChipModel_BMP280:
|
|
sensorType = 2;
|
|
DEBUG_PRINTLN(F("Found BMP280 sensor! No Humidity available."));
|
|
break;
|
|
default:
|
|
sensorType = 0;
|
|
DEBUG_PRINTLN(F("Found UNKNOWN sensor! Error!"));
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
void setup()
|
|
{
|
|
if (i2c_scl<0 || i2c_sda<0) { enabled = false; sensorType = 0; return; }
|
|
|
|
initializeBmeComms();
|
|
initDone = true;
|
|
}
|
|
|
|
void loop()
|
|
{
|
|
if (!enabled || strip.isUpdating()) return;
|
|
|
|
// BME280 sensor MQTT publishing
|
|
// Check if sensor present and Connected, otherwise it will crash the MCU
|
|
if (sensorType != 0)
|
|
{
|
|
// Timer to fetch new temperature, humidity and pressure data at intervals
|
|
timer = millis();
|
|
|
|
if (timer - lastTemperatureMeasure >= TemperatureInterval * 1000)
|
|
{
|
|
lastTemperatureMeasure = timer;
|
|
|
|
UpdateBME280Data(sensorType);
|
|
|
|
float temperature = roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
float humidity, heatIndex, dewPoint;
|
|
|
|
// If temperature has changed since last measure, create string populated with device topic
|
|
// from the UI and values read from sensor, then publish to broker
|
|
if (temperature != lastTemperature || PublishAlways)
|
|
{
|
|
publishMqtt("temperature", String(temperature, TemperatureDecimals).c_str());
|
|
}
|
|
|
|
lastTemperature = temperature; // Update last sensor temperature for next loop
|
|
|
|
if (sensorType == 1) // Only if sensor is a BME280
|
|
{
|
|
humidity = roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals);
|
|
heatIndex = roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
dewPoint = roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
|
|
if (humidity != lastHumidity || PublishAlways)
|
|
{
|
|
publishMqtt("humidity", String(humidity, HumidityDecimals).c_str());
|
|
}
|
|
|
|
if (heatIndex != lastHeatIndex || PublishAlways)
|
|
{
|
|
publishMqtt("heat_index", String(heatIndex, TemperatureDecimals).c_str());
|
|
}
|
|
|
|
if (dewPoint != lastDewPoint || PublishAlways)
|
|
{
|
|
publishMqtt("dew_point", String(dewPoint, TemperatureDecimals).c_str());
|
|
}
|
|
|
|
lastHumidity = humidity;
|
|
lastHeatIndex = heatIndex;
|
|
lastDewPoint = dewPoint;
|
|
}
|
|
}
|
|
|
|
if (timer - lastPressureMeasure >= PressureInterval * 1000)
|
|
{
|
|
lastPressureMeasure = timer;
|
|
|
|
float pressure = roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals);
|
|
|
|
if (pressure != lastPressure || PublishAlways)
|
|
{
|
|
publishMqtt("pressure", String(pressure, PressureDecimals).c_str());
|
|
}
|
|
|
|
lastPressure = pressure;
|
|
}
|
|
}
|
|
}
|
|
|
|
void onMqttConnect(bool sessionPresent)
|
|
{
|
|
if (WLED_MQTT_CONNECTED && !mqttInitialized)
|
|
{
|
|
_mqttInitialize();
|
|
mqttInitialized = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* API calls te enable data exchange between WLED modules
|
|
*/
|
|
inline float getTemperatureC() {
|
|
if (UseCelsius) {
|
|
return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
} else {
|
|
return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;
|
|
}
|
|
}
|
|
|
|
inline float getTemperatureF() {
|
|
if (UseCelsius) {
|
|
return ((float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;
|
|
} else {
|
|
return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
}
|
|
}
|
|
|
|
inline float getHumidity() {
|
|
return (float)roundf(sensorHumidity * powf(10, HumidityDecimals));
|
|
}
|
|
|
|
inline float getPressure() {
|
|
return (float)roundf(sensorPressure * powf(10, PressureDecimals));
|
|
}
|
|
|
|
inline float getDewPointC() {
|
|
if (UseCelsius) {
|
|
return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
} else {
|
|
return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;
|
|
}
|
|
}
|
|
|
|
inline float getDewPointF() {
|
|
if (UseCelsius) {
|
|
return ((float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;
|
|
} else {
|
|
return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
}
|
|
}
|
|
|
|
inline float getHeatIndexC() {
|
|
if (UseCelsius) {
|
|
return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
} else {
|
|
return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;
|
|
}
|
|
}
|
|
|
|
inline float getHeatIndexF() {
|
|
if (UseCelsius) {
|
|
return ((float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;
|
|
} else {
|
|
return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
|
|
}
|
|
}
|
|
|
|
// Publish Sensor Information to Info Page
|
|
void addToJsonInfo(JsonObject &root)
|
|
{
|
|
JsonObject user = root[F("u")];
|
|
if (user.isNull()) user = root.createNestedObject(F("u"));
|
|
|
|
if (sensorType==0) //No Sensor
|
|
{
|
|
// if we sensor not detected, let the user know
|
|
JsonArray temperature_json = user.createNestedArray(F("BME/BMP280 Sensor"));
|
|
temperature_json.add(F("Not Found"));
|
|
}
|
|
else if (sensorType==2) //BMP280
|
|
{
|
|
JsonArray temperature_json = user.createNestedArray(F("Temperature"));
|
|
JsonArray pressure_json = user.createNestedArray(F("Pressure"));
|
|
temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));
|
|
temperature_json.add(tempScale);
|
|
pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals));
|
|
pressure_json.add(F("hPa"));
|
|
}
|
|
else if (sensorType==1) //BME280
|
|
{
|
|
JsonArray temperature_json = user.createNestedArray(F("Temperature"));
|
|
JsonArray humidity_json = user.createNestedArray(F("Humidity"));
|
|
JsonArray pressure_json = user.createNestedArray(F("Pressure"));
|
|
JsonArray heatindex_json = user.createNestedArray(F("Heat Index"));
|
|
JsonArray dewpoint_json = user.createNestedArray(F("Dew Point"));
|
|
temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));
|
|
temperature_json.add(tempScale);
|
|
humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals));
|
|
humidity_json.add(F("%"));
|
|
pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals));
|
|
pressure_json.add(F("hPa"));
|
|
heatindex_json.add(roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));
|
|
heatindex_json.add(tempScale);
|
|
dewpoint_json.add(roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));
|
|
dewpoint_json.add(tempScale);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Save Usermod Config Settings
|
|
void addToConfig(JsonObject& root)
|
|
{
|
|
JsonObject top = root.createNestedObject(FPSTR(_name));
|
|
top[FPSTR(_enabled)] = enabled;
|
|
top[F("I2CAddress")] = static_cast<uint8_t>(I2CAddress);
|
|
top[F("TemperatureDecimals")] = TemperatureDecimals;
|
|
top[F("HumidityDecimals")] = HumidityDecimals;
|
|
top[F("PressureDecimals")] = PressureDecimals;
|
|
top[F("TemperatureInterval")] = TemperatureInterval;
|
|
top[F("PressureInterval")] = PressureInterval;
|
|
top[F("PublishAlways")] = PublishAlways;
|
|
top[F("UseCelsius")] = UseCelsius;
|
|
top[F("HomeAssistantDiscovery")] = HomeAssistantDiscovery;
|
|
DEBUG_PRINTLN(F("BME280 config saved."));
|
|
}
|
|
|
|
// Read Usermod Config Settings
|
|
bool readFromConfig(JsonObject& root)
|
|
{
|
|
// 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)];
|
|
if (top.isNull()) {
|
|
DEBUG_PRINT(F(_name));
|
|
DEBUG_PRINTLN(F(": No config found. (Using defaults.)"));
|
|
return false;
|
|
}
|
|
bool configComplete = !top.isNull();
|
|
|
|
configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);
|
|
// A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing
|
|
uint8_t tmpI2cAddress;
|
|
configComplete &= getJsonValue(top[F("I2CAddress")], tmpI2cAddress, 0x76);
|
|
I2CAddress = static_cast<BME280I2C::I2CAddr>(tmpI2cAddress);
|
|
|
|
configComplete &= getJsonValue(top[F("TemperatureDecimals")], TemperatureDecimals, 1);
|
|
configComplete &= getJsonValue(top[F("HumidityDecimals")], HumidityDecimals, 0);
|
|
configComplete &= getJsonValue(top[F("PressureDecimals")], PressureDecimals, 0);
|
|
configComplete &= getJsonValue(top[F("TemperatureInterval")], TemperatureInterval, 30);
|
|
configComplete &= getJsonValue(top[F("PressureInterval")], PressureInterval, 30);
|
|
configComplete &= getJsonValue(top[F("PublishAlways")], PublishAlways, false);
|
|
configComplete &= getJsonValue(top[F("UseCelsius")], UseCelsius, true);
|
|
configComplete &= getJsonValue(top[F("HomeAssistantDiscovery")], HomeAssistantDiscovery, false);
|
|
tempScale = UseCelsius ? "°C" : "°F";
|
|
|
|
DEBUG_PRINT(FPSTR(_name));
|
|
if (!initDone) {
|
|
// first run: reading from cfg.json
|
|
DEBUG_PRINTLN(F(" config loaded."));
|
|
} else {
|
|
// changing parameters from settings page
|
|
DEBUG_PRINTLN(F(" config (re)loaded."));
|
|
|
|
// Reset all known values
|
|
sensorType = 0;
|
|
sensorTemperature = 0;
|
|
sensorHumidity = 0;
|
|
sensorHeatIndex = 0;
|
|
sensorDewPoint = 0;
|
|
sensorPressure = 0;
|
|
lastTemperature = 0;
|
|
lastHumidity = 0;
|
|
lastHeatIndex = 0;
|
|
lastDewPoint = 0;
|
|
lastPressure = 0;
|
|
|
|
initializeBmeComms();
|
|
}
|
|
|
|
return configComplete;
|
|
}
|
|
|
|
uint16_t getId() {
|
|
return USERMOD_ID_BME280;
|
|
}
|
|
};
|
|
|
|
const char UsermodBME280::_name[] PROGMEM = "BME280/BMP280";
|
|
const char UsermodBME280::_enabled[] PROGMEM = "enabled";
|