Skip to content

matter_remote_bridge.tc

matter_remote_bridge.tc — bridge ANOTHER Tasmota ESP's sensors + actuators

Source on GitHub

// matter_remote_bridge.tc — bridge ANOTHER Tasmota ESP's sensors + actuators
// into THIS device's matter_c node, the way Berry Matter's HTTP bridge does it,
// but in TinyC. Requires a USE_MATTER_C build on THIS device; pair it on /mt.
//
// THIS device becomes the Matter node. The REMOTE ESP keeps running plain
// Tasmota — we just talk to its HTTP API:
//   • read  sensors   : GET /cm?cmnd=Status 10   (StatusSNS  — Temperature, Humidity, …)
//   • read  actuators : GET /cm?cmnd=Status 11   (StatusSTS  — POWER, POWER1.., Dimmer, …)
//   • write actuators : GET /cm?cmnd=Power ON | Dimmer 50 | …
// Sensor values are pushed into Matter sensor attributes; Matter on/off/level
// commands from the controller are forwarded back to the remote as `cmnd=`.
//
// This is exactly Berry Matter's "http_*" bridge model: a sensor/actuator type
// + a "Sensor#Attribute" filter. The Tasmota Workbench "Sensors / Outputs" scan
// tells you which keys a given device exposes — copy them into the CONFIG below.
//
// SAFETY (project rule): ALL httpGet runs on ONE task (TaskLoop). The
// MatterInvoke callback runs on the Matter/main task and must NOT call httpGet
// (two blocking httpGets from different tasks corrupt the HTTP-client heap →
// delayed crash). So MatterInvoke only QUEUES a command (flag set last) and
// TaskLoop performs the round-trip.

// ───────────────────────── CONFIG — edit for your remote ─────────────────────
char REMOTE[] = "192.168.188.51";    // <-- the remote Tasmota ESP's IP
// This example matches a common device: a temp/humidity sensor + ONE relay.
//   SHT3X/BME280 -> Temperature, Humidity   (two read-only sensors)
//   POWER        -> a relay                  (plug: read + control)
// For a multi-relay board use "POWER1"/"POWER2"; a dimmable light adds a
// "Dimmer" key — see the commented "dimmable light" block at the bottom.
// ─────────────────────────────────────────────────────────────────────────────

int e_temp; int e_hum; int e_plug;   // Matter endpoint ids

char buf[512];                 // shared httpGet response (>64 B -> heap)

// Control queue: MatterInvoke fills `pending` then sets `cmd_ready` LAST
// (publish-after-fill). TaskLoop is the only place that sends it over HTTP.
char pending[48];
int  cmd_ready = 0;

// ── JSON-ish field parser (same pattern as daikin_control.tc / dfield) ──
// key already includes the colon, e.g. "\"Temperature\":" — atof stops at ','.
float jget(char src[], char key[]) {
    char v[16]; int p;
    p = strFind(src, key);
    if (p < 0) { return -99999.0; }
    strSub(v, src, p + strlen(key), 14);
    return atof(v);
}
// 1 if the relay key is followed by "ON", 0 if "OFF", -1 if absent.
int jbool(char src[], char key[]) {
    char v[10]; int p;
    p = strFind(src, key);
    if (p < 0) { return -1; }
    strSub(v, src, p + strlen(key), 8);     // v = :"ON" or :"OFF"
    if (strFind(v, "ON") >= 0) { return 1; }
    return 0;
}

// ── A Matter controller changed the relay → queue the cmnd ──
void MatterInvoke(int ep, int cluster, int cmd) {
    char c[48];
    if (ep == e_plug) {                          // relay (OnOff)
        if (matterGet(e_plug, CLUSTER_ONOFF, 0)) { strcpy(c, "Power%20ON"); }
        else                                     { strcpy(c, "Power%20OFF"); }
        strcpy(pending, c); cmd_ready = 1;       // flag LAST
        return;
    }
}

// ── Forward one queued control command to the remote (TaskLoop only) ──
void send_pending() {
    char url[96]; char c[48];
    strcpy(c, pending); cmd_ready = 0;           // snapshot + clear BEFORE httpGet
    sprintf(url, "http://%s/cm?cmnd=%s", REMOTE, c);
    httpGet(url, buf);
    addLog(url);
}

// ── Poll the remote and mirror its state into Matter (TaskLoop only) ──
void poll_remote() {
    char url[72]; char k[20]; float v; int on;

    // sensors — Status 10 (StatusSNS)
    sprintf(url, "http://%s/cm?cmnd=Status%%2010", REMOTE);
    buf[0] = 0;
    if (httpGet(url, buf) > 0) {
        strcpy(k, "\"Temperature\":"); v = jget(buf, k);
        if (v > -90.0) { matterSetFloat(e_temp, CLUSTER_TEMP, 0, v, 100); }   // 0.01 C
        strcpy(k, "\"Humidity\":");    v = jget(buf, k);
        if (v >= 0.0)  { matterSetFloat(e_hum,  CLUSTER_HUM,  0, v, 100); }   // 0.01 %
    }

    // actuator state — Status 11 (StatusSTS) — keeps Matter in sync if the
    // remote is toggled physically / by another controller
    sprintf(url, "http://%s/cm?cmnd=Status%%2011", REMOTE);
    buf[0] = 0;
    if (httpGet(url, buf) > 0) {
        strcpy(k, "\"POWER\""); on = jbool(buf, k);
        if (on >= 0) { matterSet(e_plug, CLUSTER_ONOFF, 0, on); }
    }
}

// ── The only task that touches HTTP: control first (snappy), then sensors ──
void TaskLoop() {
    int t = 0;
    delay(15000);                                // let WiFi + Matter come up
    while (1) {
        if (cmd_ready)     { send_pending(); }   // forward a control cmnd ASAP (~0.3 s)
        if (t % 17 == 0)   { poll_remote(); }    // refresh sensors/state ~every 5 s
        t = t + 1;
        delay(300);
    }
}

// ── Web status page on THIS device ──
void WebCall() {
    char b[96];
    sprintf(b, "{s}<b>Remote</b>{m}%s{e}", REMOTE);                                       webSend(b);
    sprintf(b, "{s}Temperatur{m}%.1f C{e}",  matterGet(e_temp, CLUSTER_TEMP, 0) / 100.0); webSend(b);
    sprintf(b, "{s}Feuchte{m}%.0f %%{e}",     matterGet(e_hum,  CLUSTER_HUM,  0) / 100.0); webSend(b);
    sprintf(b, "{s}Steckdose{m}%d{e}",        matterGet(e_plug, CLUSTER_ONOFF, 0));        webSend(b);
    sprintf(b, "{s}heap{m}%d kb{e}", tasm_heap / 1000);                                   webSend(b);
}

int main() {
    matterReset();

    // read-only sensors
    e_temp = matterAdd(MATTER_TEMP_SENSOR);
    matterCluster(e_temp, CLUSTER_TEMP); matterAttr(e_temp, CLUSTER_TEMP, 0, MTR_S16);   // signed
    matterName(e_temp, "Remote Temperatur");

    e_hum = matterAdd(MATTER_HUM_SENSOR);
    matterCluster(e_hum, CLUSTER_HUM); matterAttr(e_hum, CLUSTER_HUM, 0, MTR_U16);
    matterName(e_hum, "Remote Feuchte");

    // actuator — read state AND accept controller commands (forwarded via HTTP)
    e_plug = matterAdd(MATTER_PLUG);                       // OnOff auto-attached
    matterName(e_plug, "Remote Steckdose");

    cmd_ready = 0;
    matterStart();                                         // Matter on; pair on /mt
    return 0;
}

// ─────────────────── OPTIONAL: a dimmable light on the remote ────────────────
// If the remote also has a dimmable light (a "Dimmer" key + e.g. POWER2),
// add a Matter endpoint for it and wire read + control like the relay above:
//
//   int e_lamp;                                   // declare near e_plug
//
//   // in main(), after the plug:
//   e_lamp = matterAdd(MATTER_DIMM_LIGHT);        // OnOff + Level
//   matterCluster(e_lamp, CLUSTER_ONOFF); matterAttr(e_lamp, CLUSTER_ONOFF, 0, MTR_BOOL);
//   matterCluster(e_lamp, CLUSTER_LEVEL); matterAttr(e_lamp, CLUSTER_LEVEL, 0, MTR_U8);
//   matterSet(e_lamp, CLUSTER_LEVEL, 0, 254);
//   matterName(e_lamp, "Remote Lampe");
//
//   // in MatterInvoke():
//   if (ep == e_lamp) {
//       char c[48];
//       if (cluster == CLUSTER_LEVEL) {
//           int pct = (matterGet(e_lamp, CLUSTER_LEVEL, 0) * 100) / 254;
//           sprintf(c, "Dimmer%%20%d", pct);
//       } else {
//           if (matterGet(e_lamp, CLUSTER_ONOFF, 0)) { strcpy(c, "Power2%20ON"); }
//           else                                     { strcpy(c, "Power2%20OFF"); }
//       }
//       strcpy(pending, c); cmd_ready = 1;
//       return;
//   }
//
//   // in poll_remote(), in the Status 11 block:
//   strcpy(k, "\"POWER2\"");  on = jbool(buf, k);
//   if (on >= 0) { matterSet(e_lamp, CLUSTER_ONOFF, 0, on); }
//   strcpy(k, "\"Dimmer\":"); v = jget(buf, k);
//   if (v >= 0.0) { matterSet(e_lamp, CLUSTER_LEVEL, 0, (int)(v * 254.0 / 100.0)); }