Compare commits

..

7 Commits

Author SHA1 Message Date
Dustin c933b4cdcd mqtt discovery: Set icon for soil moisture sensor 2022-05-18 20:30:32 -05:00
Dustin 1ea3de712e values: Report battery level in percentage
The LiPo battery apparently ranges from 3.0 V to 3.95 V.  Since we know
the range, we can report the current level as a percentage, which Home
Assistant will display.

We'll leave the original voltage reading in the MQTT state payload for
diagnostic purposes.
2022-05-16 21:41:38 -05:00
Dustin d0e546571e values: Include firmware build date
Home Assistant will display a "Firmware Version" field in the device
information panel if the `sw_version` field is populated in the
discovery configuration payload.  We'll fill this with the build date
and time of the firmware as a proxy.
2022-05-16 21:32:00 -05:00
Dustin c9cacc540f main: Deep sleep after network failures
Instead of entering a "hot" reboot loop when there is a problem
connecting to WiFi or the MQTT broker, the chip will now go into deep
sleep for a minute before trying again.  This should conserve battery in
situations where simply trying again right away won't fix the problem
(e.g. WiFi is actually down, or the band conditions are just really bad
for now).
2022-05-16 21:17:20 -05:00
Dustin 38df5d00c4 values: Include boot count in state messages
This should be useful for diagnostic purposes.  It's not brought out
into a proper sensor in HA, but it can be seen using the MQTT message
viewer in the HA device configuration tool.
2022-05-16 21:17:20 -05:00
Dustin 6c1757b43d main: Deep sleep for up to an hour
It really isn't necessary to have minute-level granularity of soil
moisture (especially since it's just been sitting at "max" for 2
weeks!).  Updating frequently is helpful for diagnostics, though.  To
compromise, the sensor will now publish data every minute for the first
few minutes after it starts up, then reduce its update frequency to once
every hour.
2022-05-16 21:17:20 -05:00
Dustin e628508ff5 mqtt_discovery: Add state_class, entity_category
Populating these options helps Home Assistant display the sensors better
in the UI.

The `publish_client` function has too many parameters as it is, making
it difficult to keep track of which value is passed as which argument.
Using a structure and designated initializers makes this a lot cleaner.
2022-05-16 21:17:20 -05:00
5 changed files with 108 additions and 52 deletions

View File

@ -9,4 +9,7 @@
#define TOPIC_ERRORS "garden/errors"
#define TOPIC_STATE "garden/state"
#define SLEEP_MILLIS (1000 * 60)
#define SLEEP_MILLIS_EARLY (1000 * 60)
#define SLEEP_MILLIS (1000 * 60 * 60)
#define VERSION __DATE__ " " __TIME__

View File

@ -11,6 +11,8 @@
INCTXT(RootCA, "isrgrootx1.pem");
RTC_DATA_ATTR uint32_t boot_count = 0;
WiFiClientSecure sock;
PubSubClient mqtt(sock);
@ -18,19 +20,24 @@ Values values;
Adafruit_seesaw ss;
void setup() {
delay(1000); // VSCode is slow to open the serial console after upload
Serial.begin(115200);
pinMode(13, OUTPUT);
digitalWrite(13, 1);
// VSCode is slow to open the serial console after upload
if (boot_count == 0) {
delay(1000);
}
boot_count++;
Serial.printf("This is boot number %d\n", boot_count);
if (!wifi_connect()) {
Serial.printf("Failed to connect to WiFi, status %s\n", WiFi.status());
reboot();
retry_after_minutes(1);
}
if (!mqtt_connect()) {
Serial.println("Could not connect to MQTT");
reboot();
retry_after_minutes(1);
}
if (!publish_all_config(&mqtt, WiFi.macAddress().c_str())) {
@ -42,20 +49,22 @@ void setup() {
auto msg = "Seesaw not found";
Serial.println(msg);
mqtt_send_error(msg);
reboot();
retry_after_minutes(1);
}
values.boot_count = boot_count;
if (!values.read(&ss)) {
auto msg = "Failed to get sensor values";
Serial.println(msg);
mqtt_send_error(msg);
reboot();
retry_after_minutes(1);
} else {
Serial.println("Got Values:");
Serial.printf(" Moisture: %d\n", values.moisture);
Serial.printf(" Temperature: %.02f\n", values.temperature);
Serial.printf(" Battery: %.02f\n", values.battery);
Serial.printf(" RSSI: %d\n", values.rssi);
Serial.printf(" Boot Count: %d\n", values.boot_count);
if (!values.send(&mqtt, TOPIC_STATE)) {
Serial.println("Failed to send sensor values");
}
@ -65,9 +74,11 @@ void setup() {
mqtt.disconnect();
delay(1000);
Serial.println("Entering deep sleep ...");
unsigned long timer =
((boot_count <= 5 ? SLEEP_MILLIS_EARLY : SLEEP_MILLIS) - millis());
Serial.printf("Entering deep sleep for %d milliseconds...\n", timer);
Serial.flush();
esp_sleep_enable_timer_wakeup((SLEEP_MILLIS - millis()) * 1000);
esp_sleep_enable_timer_wakeup(timer * 1000);
esp_deep_sleep_start();
}
@ -76,15 +87,6 @@ void loop() {
while (1) delay(1471228928);
}
void error_led_blink() {
while (1) {
digitalWrite(13, 1);
delay(500);
digitalWrite(13, 0);
delay(500);
}
}
boolean wifi_connect() {
Serial.printf("Connecting to WiFi (%s) ", CFG_WIFI_SSID);
WiFi.begin(CFG_WIFI_SSID, CFG_WIFI_PSK);
@ -137,10 +139,6 @@ boolean mqtt_connect() {
return true;
}
void mqtt_send_error(const __FlashStringHelper* msg) {
mqtt_send_error((const char*)msg);
}
void mqtt_send_error(const char* msg) {
if (!mqtt.connected()) {
Serial.println("MQTT client not connected, cannot send error message");
@ -149,11 +147,10 @@ void mqtt_send_error(const char* msg) {
mqtt.publish(TOPIC_ERRORS, msg);
}
void reboot() {
void retry_after_minutes(int minutes) {
if (mqtt.connected()) {
mqtt.disconnect();
}
Serial.println("Rebooting in 2 seconds");
delay(2000);
ESP.restart();
esp_sleep_enable_timer_wakeup(minutes * 60 * 1000 * 1000);
esp_deep_sleep_start();
}

View File

@ -4,32 +4,52 @@
#include "constants.h"
struct sensor_config {
const char* name;
const char* unique_id;
const char* value_template;
const char* identifier;
const char* device_class;
const char* unit;
const char* state_class;
const char* entity_category;
const char* icon;
};
static bool publish_config(PubSubClient* mqtt, const char* topic,
const char* name, const char* unique_id,
const char* value_template, const char* identifier,
const char* device_class, const char* unit) {
StaticJsonDocument<256> doc;
doc["unique_id"] = unique_id;
doc["name"] = name;
const struct sensor_config* config) {
StaticJsonDocument<512> doc;
doc["unique_id"] = config->unique_id;
doc["name"] = config->name;
doc["state_topic"] = TOPIC_STATE;
doc["value_template"] = value_template;
if (device_class != NULL) {
doc["device_class"] = device_class;
doc["value_template"] = config->value_template;
if (config->device_class != NULL) {
doc["device_class"] = config->device_class;
}
if (unit != NULL) {
doc["unit_of_measurement"] = unit;
if (config->unit != NULL) {
doc["unit_of_measurement"] = config->unit;
}
if (config->state_class != NULL) {
doc["state_class"] = config->state_class;
}
if (config->entity_category != NULL) {
doc["entity_category"] = config->entity_category;
}
if (config->icon != NULL) {
doc["icon"] = config->icon;
}
auto device = doc.createNestedObject("device");
device["manufacturer"] = DEVICE_MANUFACTURER;
device["name"] = DEVICE_NAME;
device["model"] = DEVICE_MODEL;
device["sw_version"] = VERSION;
auto ident = device.createNestedArray("identifiers");
ident.add(identifier);
ident.add(config->identifier);
String msg;
serializeJson(doc, msg);
auto payload = msg.c_str();
Serial.printf("Publishing configuration for %s (%zd bytes) to %s ... ",
unique_id, strlen(payload), topic);
config->unique_id, strlen(payload), topic);
auto r = mqtt->publish(topic, payload, true);
Serial.println(r ? "OK" : "FAILED");
return r;
@ -37,18 +57,51 @@ static bool publish_config(PubSubClient* mqtt, const char* topic,
bool publish_all_config(PubSubClient* mqtt, const char* ident) {
bool ret = true;
publish_config(mqtt, TOPIC_CFG_MOISTURE, "Garden Sensor Moisture",
"sensor.garden_sensor_moisture",
"{{ value_json.moisture }}", ident, NULL, NULL);
publish_config(mqtt, TOPIC_CFG_TEMPERATURE, "Garden Sensor Temperature",
"sensor.garden_sensor_temperature",
"{{ value_json.temperature }}", ident, "temperature", "°C");
publish_config(mqtt, TOPIC_CFG_BATTERY, "Garden Sensor Battery Level",
"sensor.garden_sensor_battery_level",
"{{ value_json.battery_level }}", ident, "voltage", "V");
publish_config(mqtt, TOPIC_CFG_RSSI, "Garden Sensor Signal Strength",
"sensor.garden_sensor_rssi", "{{ value_json.rssi }}", ident,
"signal_strength", "dBm");
struct sensor_config moisture = {
.name = "Garden Sensor Moisture",
.unique_id = "sensor.garden_sensor_moisture",
.value_template = "{{ value_json.moisture }}",
.identifier = ident,
.device_class = NULL,
.unit = NULL,
.state_class = "measurement",
.entity_category = NULL,
.icon = "mdi:water",
};
publish_config(mqtt, TOPIC_CFG_MOISTURE, &moisture);
struct sensor_config temperature = {
.name = "Garden Sensor Temperature",
.unique_id = "sensor.garden_sensor_temperature",
.value_template = "{{ value_json.temperature }}",
.identifier = ident,
.device_class = "temperature",
.unit = "°C",
.state_class = NULL,
.entity_category = NULL,
};
publish_config(mqtt, TOPIC_CFG_TEMPERATURE, &temperature);
struct sensor_config battery = {
.name = "Garden Sensor Battery",
.unique_id = "sensor.garden_sensor_battery",
.value_template = "{{ value_json.battery }}",
.identifier = ident,
.device_class = "battery",
.unit = "%",
.state_class = NULL,
.entity_category = "diagnostic",
};
publish_config(mqtt, TOPIC_CFG_BATTERY, &battery);
struct sensor_config signal = {
.name = "Garden Sensor Signal Strength",
.unique_id = "sensor.garden_sensor_rssi",
.value_template = "{{ value_json.rssi }}",
.identifier = ident,
.device_class = "signal_strength",
.unit = "dBm",
.state_class = NULL,
.entity_category = "diagnostic",
};
publish_config(mqtt, TOPIC_CFG_RSSI, &signal);
return ret;
}

View File

@ -16,11 +16,13 @@ bool Values::read(Adafruit_seesaw* ss) {
}
bool Values::send(PubSubClient* mqtt, const char* topic) {
StaticJsonDocument<64> doc;
StaticJsonDocument<96> doc;
doc["moisture"] = moisture;
doc["temperature"] = round2(temperature);
doc["battery_level"] = round2(battery);
doc["battery"] = (int)((battery - 3) / 0.95 * 100);
doc["rssi"] = rssi;
doc["boot_count"] = boot_count;
String msg;
serializeJson(doc, msg);

View File

@ -7,6 +7,7 @@ class Values {
float temperature;
float battery;
int8_t rssi;
uint32_t boot_count;
Values() {};
~Values() {};