temperature-mqtt
The smallest sensor module in the tree. One DS18B20 probe on a 1-Wire pin, read every interval, published two ways. lib/thesada-mod-example-temperature-mqtt/ on the firmware dev branch; ships with the next release.
What it demonstrates
| Thing | Where in the code |
|---|---|
Config in begin(), nothing in the constructor |
begin() reads pin, interval_s, name and allocates the bus driver there |
The periodic loop() shape |
compare millis() against the interval, return early, work when due |
| Two publish paths | a bare number on <prefix>/sensor/temperature/<name> for dashboards, a JSON event named temperature on the bus for Lua rules and the SD logger |
module.status |
one line: name=<n> last_c=<c> |
| Registration | MODULE_REGISTER(ExampleTemperatureMqtt, PRIORITY_SENSOR), guarded by ENABLE_EXAMPLE_TEMPERATURE_MQTT so an unbuilt example costs nothing |
The event payload is a subset of what the full temperature module publishes: sensors[] with name, temp_c and temp, without the address the full module adds. A rule that reads those three fields works against either.
Config
"example_temperature_mqtt": {
"enabled": true,
"pin": 12,
"interval_s": 60,
"name": "example"
}
name becomes the last topic segment. enabled is required: optional modules default off.
Turn it on
In src/thesada_config.h, uncomment the define under Example modules, then build and flash as for any module (tutorial, step 7). Boot log:
[INF][Registry] registry.module_init priority=50 name=ExampleTemperatureMqtt
[INF][ExTemp] example_temp.ready pin=12 interval_s=60 name=example
If the probe is missing, example_temp.sensor_disconnected is logged each interval and nothing publishes.
Expected output
mosquitto_sub -t 'thesada/+/sensor/temperature/example' -v
thesada/sht31/sensor/temperature/example 21.37
The code
src/ExampleTemperatureMqtt.h:
// thesada-fw - ExampleTemperatureMqtt.h
// Starter module: read one DS18B20 and publish it. The sensor module shape in
// its smallest form - config in begin(), a timer in loop(), one publish path.
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <Arduino.h>
#include <Module.h>
class OneWire;
class DallasTemperature;
class ExampleTemperatureMqtt : public Module {
public:
void begin() override;
void loop() override;
const char* name() override { return "ExampleTemperatureMqtt"; }
const char* configKey() override { return "example_temperature_mqtt"; }
void status(ShellOutput out) override;
private:
void readAndPublish();
OneWire* _wire = nullptr;
DallasTemperature* _sensors = nullptr;
uint32_t _intervalMs = 60000;
uint32_t _lastRead = 0;
float _lastC = 0.0f; // set to the driver sentinel in begin()
char _name[32] = "example";
};
src/ExampleTemperatureMqtt.cpp:
// thesada-fw - ExampleTemperatureMqtt.cpp
// SPDX-License-Identifier: GPL-3.0-only
#include <thesada_config.h>
#include "ExampleTemperatureMqtt.h"
#include <Config.h>
#include <EventBus.h>
#include <MQTTClient.h>
#include <Log.h>
#include <ModuleRegistry.h>
#include <ArduinoJson.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#ifdef ENABLE_EXAMPLE_TEMPERATURE_MQTT
static const char* TAG = "ExTemp";
// Everything with a side effect lives here, never in the constructor: the
// registry constructs every module at static-init time, before Config exists.
// in: config block example_temperature_mqtt (pin, interval_s, name).
// out: bus + driver allocated, first read on the next loop() after interval.
void ExampleTemperatureMqtt::begin() {
JsonObject cfg = Config::get();
int pin = cfg["example_temperature_mqtt"]["pin"] | 12;
_intervalMs = (uint32_t)(cfg["example_temperature_mqtt"]["interval_s"] | 60) * 1000;
strlcpy(_name, cfg["example_temperature_mqtt"]["name"] | "example", sizeof(_name));
_lastC = DEVICE_DISCONNECTED_C;
_wire = new OneWire(pin);
_sensors = new DallasTemperature(_wire);
_sensors->begin();
Log::kvf(TAG, "example_temp.ready pin=%d interval_s=%lu name=%s",
pin, (unsigned long)(_intervalMs / 1000), _name);
}
// loop() runs every main-loop pass, so it must not block. The timer is the
// whole scheduler: do the work when the interval has elapsed, otherwise return.
// in: none. out: one readAndPublish() per interval.
void ExampleTemperatureMqtt::loop() {
uint32_t now = millis();
if (now - _lastRead < _intervalMs) return;
_lastRead = now;
readAndPublish();
}
// Read the first probe on the bus and publish it on MQTT and the event bus.
// in: none. out: <prefix>/sensor/temperature/<name> + EventBus "temperature".
void ExampleTemperatureMqtt::readAndPublish() {
_sensors->requestTemperatures();
float c = _sensors->getTempCByIndex(0);
if (c == DEVICE_DISCONNECTED_C) {
Log::kvfw(TAG, "example_temp.sensor_disconnected");
return;
}
_lastC = roundf(c * 100.0f) / 100.0f;
// Two outputs, same as the full temperature module: a plain value on a
// per-sensor topic for Home Assistant, and a JSON event on the bus so Lua
// rules and the SD logger see it without knowing about MQTT.
JsonObject cfg = Config::get();
const char* prefix = cfg["mqtt"]["topic_prefix"] | "thesada/node";
char topic[96];
snprintf(topic, sizeof(topic), "%s/sensor/temperature/%s", prefix, _name);
char val[16];
snprintf(val, sizeof(val), "%.2f", _lastC);
MQTTClient::publish(topic, val);
JsonDocument doc;
JsonObject s = doc["sensors"].add<JsonObject>();
s["name"] = _name;
s["temp_c"] = _lastC;
s["temp"] = _lastC;
EventBus::publish("temperature", doc.as<JsonObject>());
}
// One line for `module.status`.
// in: out sink. out: "name=<n> last_c=<c>".
void ExampleTemperatureMqtt::status(ShellOutput out) {
char line[64];
snprintf(line, sizeof(line), "name=%s last_c=%.2f", _name, _lastC);
out(line);
}
MODULE_REGISTER(ExampleTemperatureMqtt, PRIORITY_SENSOR)
#endif // ENABLE_EXAMPLE_TEMPERATURE_MQTT