matter_home_bridge.tc¶
Matter Home Bridge — TinyC port of the HomeKit "script-3" scripter.
// Matter Home Bridge — TinyC port of the HomeKit "script-3" scripter.
//
// One Matter node that bridges the same accessories the old HomeKit script
// exposed, over Tasmota UDP global variables (the Scripter `g:` mechanism).
//
// OUT (controller -> assign a `global` -> auto-broadcast to the real devices)
// Licht color light -> mh_pwr / mh_hue / mh_sat / mh_bri
// Ecklicht switch -> elamp
// IN (`global` auto-updated by other devices -> Matter sensor attribute)
// Buero temp/hum/press -> btemp / bhumi / bpress
// Aussen temperature -> atmp
// WZ temp/hum/AQ -> wtemp / whumi / wco2 / wtvoc
// AZ temp/hum/AQ -> aztemp / azhumi / azeco2 / aztvoc
// POLL (HTTP)
// Wohnzimmer / Schlafzimmer Daikin aircon -> two temperature sensors
//
// Shared values use the `global` keyword: assigning one auto-broadcasts it,
// reading one returns the latest value received from your other Scripter
// devices (multicast 239.255.255.250:1999) — no explicit udpSend/udpRecv.
// HomeKit ranges are restored when writing the light (Matter hue/sat/level are
// 0..254; the remote light expects HomeKit 0..360 / 0..100).
//
// Differences vs the scripter: Powerwall removed; WZ/AZ TVOC+CO2 folded into one
// Matter Air-Quality endpoint per room. Requires a USE_MATTER_C build; Bind /mt.
// ── Shared (UDP) globals — names MUST match the Scripter g:<name> ──
global float mh_pwr; global float mh_hue; global float mh_sat; global float mh_bri; // OUT: Licht
global float elamp; // OUT: Ecklicht
global float btemp; global float bhumi; global float bpress; // IN: Buero
global float atmp; // IN: outside
global float wtemp; global float whumi; global float wco2; global float wtvoc; // IN: WZ
global float aztemp; global float azhumi; global float azeco2; global float aztvoc; // IN: AZ
global float pwl; global float sip; global float sop; global float bip; global float hip;
// ── Matter endpoint ids (assigned 1..N in matterAdd order) ──
int licht; int ecklicht;
int e_btemp; int e_bhumi; int e_bpress; int e_atmp;
int e_wtemp; int e_whumi; int e_waq;
int e_aztemp; int e_azhumi; int e_azaq;
int e_acwz; int e_acsz;
int tick;
char resp[256];
char scratch[256]; // sprintf workspace for the clock header BLOCK below
// ═══════════════ BEGIN CLOCK HEADER BLOCK (from examples/clock_header.tc) ═════
// Reusable WebUI clock header — big green HH:MM:SS + German weekday/date
// + sunrise/sunset + live-tick "spinner", wrapped in a dark-grey
// rounded card spanning both columns. See examples/clock_header.tc
// for the standalone reference. Requires `char scratch[256]` in scope.
int web_clock_tick = 0;
void web_clock_header() {
web_clock_tick = web_clock_tick + 1;
char wd_names[] = "So|Mo|Di|Mi|Do|Fr|Sa";
char mo_names[] = "Jan|Feb|Mar|Apr|Mai|Jun|Jul|Aug|Sep|Okt|Nov|Dez";
char wd_label[4];
char mo_label[4];
strToken(wd_label, wd_names, '|', tasm_wday);
strToken(mo_label, mo_names, '|', tasm_month);
// Single dark-grey rounded card spanning both columns. Two
// sprintfs because the full card exceeds 256-byte scratch.
sprintf(scratch, "<tr><td colspan=2 style='text-align:center;background:#333;padding:8px;border-radius:8px'><span style='color:green;font-size:40px;font-weight:bold'>%02d:%02d:%02d</span><br>%s %d. %s %d <span style='font-size:0.7em;color:#888;'>● %d</span><br>",
tasm_hour, tasm_minute, tasm_second,
wd_label, tasm_day, mo_label, tasm_year, web_clock_tick);
webSend(scratch);
int sr = tasm_sunrise;
int ss = tasm_sunset;
int dl = ss - sr;
sprintf(scratch, "🌞 %02d:%02d <--- %02d:%02d ---> %02d:%02d 🌙</td></tr>",
sr / 60, sr % 60,
dl / 60, dl % 60,
ss / 60, ss % 60);
webSend(scratch);
}
// ═══════════════ END CLOCK HEADER BLOCK ════════════════════════════════════
// ── Air-quality enum (0x005B) from a CO2 ppm value ──
int aq_from_co2(float co2) {
if (co2 > 1000.0) { return 5; } // Very poor
if (co2 > 800.0) { return 3; } // Moderate
return 1; // Good
}
// ── A Matter controller changed one of the writable accessories ──
// Assigning a `global` auto-broadcasts it to the real light/lamp. Matter stores
// hue/sat/level as 0..254; convert to HomeKit hue 0..360 / sat,bri 0..100.
void MatterInvoke(int e, int cluster, int cmd) {
if (e == licht) {
if (cluster == CLUSTER_ONOFF) {
mh_pwr = matterGet(licht, CLUSTER_ONOFF, 0);
}
if (cluster == CLUSTER_LEVEL) { // MoveToLevelWithOnOff
mh_pwr = matterGet(licht, CLUSTER_ONOFF, 0);
mh_bri = (matterGet(licht, CLUSTER_LEVEL, 0) * 100) / 254;
}
if (cluster == 0x0300) { // MoveToHueAndSaturation
mh_hue = (matterGet(licht, 0x0300, 0) * 360) / 254;
mh_sat = (matterGet(licht, 0x0300, 1) * 100) / 254;
}
return;
}
if (e == ecklicht) {
if (cluster == CLUSTER_ONOFF) { elamp = matterGet(ecklicht, CLUSTER_ONOFF, 0); }
return;
}
}
// ── Push the received globals into the Matter sensor attributes ──
void pull_sensors() {
matterSetFloat(e_btemp, CLUSTER_TEMP, 0, btemp, 100); // 0.01 C
matterSetFloat(e_bhumi, CLUSTER_HUM, 0, bhumi, 100); // 0.01 %
matterSetFloat(e_bpress, CLUSTER_PRESS, 0, bpress, 1); // hPa
matterSetFloat(e_atmp, CLUSTER_TEMP, 0, atmp, 100);
matterSetFloat(e_wtemp, CLUSTER_TEMP, 0, wtemp, 100);
matterSetFloat(e_whumi, CLUSTER_HUM, 0, whumi, 100);
matterSetFloat(e_waq, CLUSTER_CO2, 0, wco2, 1);
matterSetFloat(e_waq, CLUSTER_VOC, 0, wtvoc, 1);
matterSet(e_waq, CLUSTER_AIRQUALITY, 0, aq_from_co2(wco2));
matterSetFloat(e_aztemp, CLUSTER_TEMP, 0, aztemp, 100);
matterSetFloat(e_azhumi, CLUSTER_HUM, 0, azhumi, 100);
matterSetFloat(e_azaq, CLUSTER_CO2, 0, azeco2, 1);
matterSetFloat(e_azaq, CLUSTER_VOC, 0, aztvoc, 1);
matterSet(e_azaq, CLUSTER_AIRQUALITY, 0, aq_from_co2(azeco2));
}
// ── Poll one Daikin aircon and push its room temperature to a Matter sensor ──
// MUST run from TaskLoop, not EverySecond: httpGet blocks until the HTTP round
// trip finishes, which stalls (and can crash) the main loop. The aircon returns
// plain text "ret=OK,htemp=26.0,hhum=-,otemp=25.0,...". Parse htemp with the
// strFind/strSub builtins — a string literal passed *through* a user-function
// char[] param doesn't index reliably, so keep the literal at the builtin site.
void poll_aircon(char ip[], int ep) {
float t; char url[64]; char buf[128]; char hv[12]; int hp;
sprintf(url, "http://%s/aircon/get_sensor_info", ip);
buf[0] = 0;
if (httpGet(url, buf) > 0) {
hp = strFind(buf, "htemp=");
if (hp >= 0) {
strSub(hv, buf, hp + 6, 10); // "26.0,hhum=" — atof stops at ','
t = atof(hv);
if (t > -50.0 && t < 80.0) { matterSetFloat(ep, CLUSTER_TEMP, 0, t, 100); }
}
}
}
// Daikin polling loop — its own FreeRTOS task so the blocking httpGet never
// stalls EverySecond / the Matter network handling.
void TaskLoop() {
delay(15000); // let WiFi + Matter come up first
while (1) {
poll_aircon("192.168.188.24", e_acwz); // Wohnzimmer
delay(3000);
poll_aircon("192.168.188.43", e_acsz); // Schlafzimmer
delay(57000); // ~once per minute
lcd_values();
}
}
// ── LCD status screen (no-op if no display attached) ──
int lcd_ready = 0;
void lcd_labels() {
dspText("[zD0]");
dspText("[x0y50h296]");
dspText("[f1x15y60]Batterie:");
dspText("[f1x15y75]Solar:");
dspText("[f1x15y90]Verbrauch:");
dspText("[f1x15y105]Netz:");
}
void lcd_values() {
char b[40];
strcpy(b, "[f4x5y10T]"); dspText(b); // // clock HH:MM
strcpy(b, "[f4x155y10tS]"); dspText(b);
sprintf(b, "[f2x160y60p-8]%.2f %%", pwl); dspText(b);
sprintf(b, "[f2x170y95p-6]%.1f C", atmp); dspText(b);
sprintf(b, "[f1x85y60p-10]%.2f W", bip); dspText(b);
sprintf(b, "[f1x85y75p-10]%.2f W", sop); dspText(b);
sprintf(b, "[f1x85y90p-10]%.2f W", hip); dspText(b);
sprintf(b, "[f1x85y105p-10]%.2f W", sip); dspText(b);
strcpy(b, "[d]"); dspText(b);
}
void EverySecond() {
tick = tick + 1;
// longtime stable now, no longer needed
//if (tasm_hour == 1 && tasm_uptime > 4000) { char r[16]; tasmCmd("Restart 1", r); }
if (tick % 5 == 0) { pull_sensors(); } // received globals -> Matter attrs (every 5s)
// Daikin polling moved to TaskLoop() — httpGet must not block EverySecond.
if (lcd_ready == 0 && tasm_uptime > 3) { lcd_labels(); lcd_ready = 1; }
if (lcd_ready) {
//dspText("[Ci3x10y36T]"); // clock HH:MM
// if (tick % 5 == 0) { lcd_values(); }
}
}
// ── Web status page ──
void WebCall() {
web_clock_header();
char b[80];
sprintf(b, "{s}<b>Licht</b>{m}%d%%{e}", (matterGet(licht, CLUSTER_LEVEL, 0) * 100) / 254); webSend(b);
sprintf(b, "{s}Aussentemperatur{m}%.1f C{e}", atmp); webSend(b);
sprintf(b, "{s}Buero Temp{m}%.1f C{e}", btemp); webSend(b);
sprintf(b, "{s}Buero Feuchte{m}%.0f %%{e}", bhumi); webSend(b);
sprintf(b, "{s}Buero Druck{m}%.0f hPa{e}", bpress); webSend(b);
sprintf(b, "{s}WZ Temp{m}%.1f C{e}", wtemp); webSend(b);
sprintf(b, "{s}WZ Feuchte{m}%.0f %%{e}", whumi); webSend(b);
sprintf(b, "{s}WZ CO2{m}%.0f ppm{e}", wco2); webSend(b);
sprintf(b, "{s}WZ TVOC{m}%.0f{e}", wtvoc); webSend(b);
sprintf(b, "{s}AZ Temp{m}%.1f C{e}", aztemp); webSend(b);
sprintf(b, "{s}AZ Feuchte{m}%.0f %%{e}", azhumi); webSend(b);
sprintf(b, "{s}AZ CO2{m}%.0f ppm{e}", azeco2); webSend(b);
sprintf(b, "{s}Wohnzimmer (Klima){m}%.1f C{e}", matterGet(e_acwz, CLUSTER_TEMP, 0) / 100.0); webSend(b);
sprintf(b, "{s}Schlafzimmer (Klima){m}%.1f C{e}", matterGet(e_acsz, CLUSTER_TEMP, 0) / 100.0); webSend(b);
sprintf(b, "{s}heap{m}%d kb, %d %%{e}", tasm_heap / 1000, tasm_frag); webSend(b);
}
// ── endpoint-declaration helpers ──
int add_temp() {
int e; e = matterAdd(MATTER_TEMP_SENSOR);
matterCluster(e, CLUSTER_TEMP); matterAttr(e, CLUSTER_TEMP, 0, MTR_S16); // signed: outside < 0
return e;
}
int add_hum() {
int e; e = matterAdd(MATTER_HUM_SENSOR);
matterCluster(e, CLUSTER_HUM); matterAttr(e, CLUSTER_HUM, 0, MTR_U16);
return e;
}
int add_aq() {
int e; e = matterAdd(MATTER_AIRQUALITY_SENSOR);
matterCluster(e, CLUSTER_AIRQUALITY); matterAttr(e, CLUSTER_AIRQUALITY, 0, MTR_ENUM8);
matterCluster(e, CLUSTER_CO2); matterAttr(e, CLUSTER_CO2, 0, MTR_FLOAT);
matterCluster(e, CLUSTER_VOC); matterAttr(e, CLUSTER_VOC, 0, MTR_FLOAT);
return e;
}
int main() {
tick = 0;
matterReset();
// OUT: colour light (Licht) — controller drives mh_pwr/hue/sat/bri globals
licht = matterAdd(MATTER_COLOR_LIGHT);
matterCluster(licht, CLUSTER_ONOFF); matterAttr(licht, CLUSTER_ONOFF, 0, MTR_BOOL);
matterCluster(licht, CLUSTER_LEVEL); matterAttr(licht, CLUSTER_LEVEL, 0, MTR_U8);
matterCluster(licht, 0x0300); matterAttr(licht, 0x0300, 0, MTR_U8); matterAttr(licht, 0x0300, 1, MTR_U8);
matterSet(licht, CLUSTER_LEVEL, 0, 254);
matterName(licht, "Licht");
// OUT: corner lamp (Ecklicht) switch -> elamp global
ecklicht = matterAdd(MATTER_ONOFF_LIGHT);
matterName(ecklicht, "Ecklicht");
// IN: Buero BMP280 — temp / humidity / pressure
e_btemp = add_temp(); matterName(e_btemp, "Buero Temperatur");
e_bhumi = add_hum(); matterName(e_bhumi, "Buero Feuchte");
e_bpress = matterAdd(MATTER_PRESS_SENSOR);
matterCluster(e_bpress, CLUSTER_PRESS); matterAttr(e_bpress, CLUSTER_PRESS, 0, MTR_S16);
matterName(e_bpress, "Buero Luftdruck");
// IN: outside temperature
e_atmp = add_temp(); matterName(e_atmp, "Aussentemperatur");
// IN: Wohnzimmer — temp / humidity / air quality (CO2 + TVOC)
e_wtemp = add_temp(); matterName(e_wtemp, "Wohnzimmer Temp");
e_whumi = add_hum(); matterName(e_whumi, "Wohnzimmer Feuchte");
e_waq = add_aq(); matterName(e_waq, "Wohnzimmer Luft");
// IN: AZ room — temp / humidity / air quality (CO2 + TVOC)
e_aztemp = add_temp(); matterName(e_aztemp, "Arbeitszimmer Temp");
e_azhumi = add_hum(); matterName(e_azhumi, "Arbeitszimmer Feuchte");
e_azaq = add_aq(); matterName(e_azaq, "Arbeitszimmer Luft");
// POLL: two Daikin aircons as temperature sensors
e_acwz = add_temp(); matterName(e_acwz, "Wohnzimmer Klima");
e_acsz = add_temp(); matterName(e_acsz, "Schlafzimmer Klima");
matterStart(); // Matter on; Bind on /mt to pair
return 0;
}