MqttModule
Source:
MqttModule.h
MqttModule¶
src/core/MqttModule.h:74Inherits:
MoonModule
MQTT client service: bridges the device's light controls (on / brightness / palette) to an MQTT broker so home-automation hubs can drive it.
The headline consumer is Homebridge (via the homebridge-mqttthing "lightbulb" accessory), which publishes to set topics and reads get topics — a bare on/off + brightness + colour surface. It is a network sub-service, a code-wired child of NetworkModule alongside Improv/Devices, and it drives the shared apply-core exactly as IR and the WLED bridge do: every command routes through Scheduler::setControl("Drivers", …), so MQTT adds a transport, not new control plumbing.
The client is our own. MQTT 3.1.1 is a small, standard protocol (the same framing mosquitto and mqttthing speak), so the wire format lives in a dependency-free, golden-vector-tested header ([MqttPacket.h]) and this module owns only the socket lifecycle over platform::TcpConnection (connect + the non-blocking read/writeSome). No library — the framing is pinned by tests the way the Improv frames are.
Topics (prefix projectMM/<last6-of-MAC> — a STABLE id, so a rename never repoints topics; the friendly deviceName rides the separate retained <prefix>/name topic): <prefix>/on/set ← "true"/"false" → Drivers.on<prefix>/on/get → publish current on <prefix>/brightness/set ← 0..100 (mqttthing) → 255/100 → Drivers.brightness<prefix>/brightness/get → publish brightness100/255 <prefix>/hsv/set ← "h,s,v" → hue+sat → nearest palette → Drivers.palette<prefix>/hsv/get → publish the chosen palette's representative "h,s,v"
Home Assistant MQTT Discovery (the haDiscovery control, default off / opt-in). When on, the device publishes a RETAINED JSON-schema light config to homeassistant/light/projectMM_<mac6>/config, so HA (and any Discovery-aware hub — the Tasmota/ESPHome/Zigbee2MQTT convention) auto-creates a wired light entity — no hand-matching topics. It defaults off because the WLED-compat surface (the HttpServerModule/json shim) already gives HA a richer light — colour, palette, sensors — over mDNS with no broker, so a device on defaults appears in HA once, not twice; MQTT discovery is the opt-in for broker-only / cross-subnet setups where mDNS doesn't reach. When on it speaks HA's own schema alongside the mqttthing topics above: <prefix>/ha/set ← {"state":"ON"|"OFF"[,"brightness":0-255]} → Drivers.on/brightness <prefix>/ha/state → retained {"state":…,"brightness":0-255} on change (HA-scale, no rescale) <prefix>/status → retained "online"; the CONNECT Last-Will publishes "offline" here on an ungraceful drop, so HA greys the entity out (avty_t) Toggling haDiscovery re-announces / retracts live (an empty retained config removes the entity), no reconnect. uniq_id is the MAC-stable projectMM_<mac6>, never the editable name. The schema is JSON (not the default schema): a control maps to a config key rather than a topic, so HA's native effect/effect_list renders a picker on the one command topic — the reason JSON is chosen here.
The same discovery-gate also publishes a second HA component — an update entity — on homeassistant/update/projectMM_<mac6>/config. HA renders it as a "Firmware: <prefix>/update/state, install commands from <prefix>/update/set. installed_version = build-time MM_VERSION; latest_version equals it today (no on-device release check yet, so HA shows "up-to-date"), and the install path routes to platform::http_fetch_to_ota. Prevents HA's WLED integration from mis-flagging a WLED firmware update against a projectMM device — the /json``info.ver reports our semver so WLED's comparison is always-newer, and this update entity is where a real projectMM update surfaces once the release-check component lands.
Lifecycle (all on tick1s(), off the render hot path — MQTT is slow control): connect lazily once networkReady() && enabled, CONNECT → CONNACK → SUBSCRIBE to the set topics, PINGREQ every keepalive/2, drain inbound bytes through MqttInboundParser and route PUBLISHes to Drivers, and publish the get topics whenever the local value changes (and on connect, so mqttthing never reads "No Response"). A dropped socket reconnects with a backoff.
Prior art: the OASIS MQTT 3.1.1 standard, homebridge-mqttthing's topic conventions, and Home Assistant's MQTT-discovery format (the same retained-homeassistant/…/config announce Tasmota / ESPHome / Zigbee2MQTT use). projectMM writes its own lean client over the platform socket primitive rather than a framework MQTT library. See docs/moonmodules/core/system.md#mqtt for the Homebridge accessory config; docs/usecases/home-automation.md for the HA setup.

Public Methods¶
inline void setSystemModule(SystemModule * s)
virtual void setup() override
: Default lifecycle propagates to children.
virtual void release() override
virtual void defineControls() override
: defineControls MUST be idempotent and pure: only controls_.clear() + controls_.addX().
virtual void onControlChanged(constchar *) override
: Cheap per-control reaction, tier 1 of the three-tier control-change split (mirrors MoonLight's onUpdate / requestMappings / onSizeChanged; see architecture.md § Rebuild propagation).
virtual void onEnabled(bool) override
: Called once when the enabled flag flips (the Scheduler runs a full prepareTree() right after, which routes through applyState() to re-derive state on the same toggle).
virtual void tick1s() override
void feedForTest(constuint8_t * bytes, size_t len)
: Feed inbound bytes as if they arrived from the broker socket — the entry the host unit tests drive (there's no live broker in ctest).
void enableSendCaptureForTest(uint8_t * buf, size_t cap)
: Test seam: capture every outbound packet sendPacket() writes, so a unit test can assert what the module emits (e.g.
inline size_t sentCaptureLenForTest() const
Public Static Attributes¶
constexprsize_t kDiscoveryDynamicBytes = 320 + 448
: The heap footprint dynamicBytes() reports while HA discovery is announcing — the sum of the two discovery scratch regions.