Skip to content

matter_fake_sensors.tc

Matter "fake" sensors — surface UNSUPPORTED quantities through SUPPORTED tiles.

Source on GitHub

// Matter "fake" sensors — surface UNSUPPORTED quantities through SUPPORTED tiles.
//
// Apple Home (and partly Google/Alexa) only render a fixed set of numeric Matter
// sensor tiles: Temperature, Humidity, Light, and the Air-Quality concentration
// clusters (CO2/PM2.5/VOC/...). Pressure (0x0403), Electrical Power/Voltage/
// Current (0x0090) and Energy (0x0091) are valid Matter but show NOTHING in Apple.
//
// Trick: carry the real value in a cluster the controller DOES display:
//
//   carrier            wire encoding              range / notes
//   -----------------  -------------------------  ----------------------------------
//   Temperature 0x0402 int16 x0.01  (MTR_S16)     -327.68 .. +327.67, SIGNED, 0.01 res
//   Humidity    0x0405 uint16 x0.01 (MTR_U16)     clamped 0..100  -> perfect for %  / SoC
//   CO2 (conc.) 0x040D single float (MTR_FLOAT)   any magnitude, full float precision
//
// Choose by value range:
//   * fits +-327 (volts, amps, pressure-in-kPa, small power) -> Temperature
//   * 0..100 percentage (battery SoC, valve %, load %)        -> Humidity
//   * large / arbitrary float (watts, pressure-in-hPa, lux)   -> CO2 float
//
// CAVEATS
//   - The tile's UNIT is fixed by the cluster (deg / % / ppm). Name the endpoint
//     (matterName) to carry the REAL meaning + unit; the controller still appends
//     its own unit, so you get e.g. "Netz V" showing "230.00 deg".
//   - A CO2 carrier feeds Apple's Air-Quality verdict (a "watts=3680" reads as
//     3680 ppm -> "poor air"). We pin the AirQuality enum to Good(1); if that
//     still bothers you, prefer the Temperature route (scale the unit to fit +-327).
//   - Temperature is SIGNED (good for current that can go negative on export);
//     concentration floats are treated as >= 0.
//   - Temperature overflows if value*100 > 32767  (i.e. value > 327.67) -> rescale
//     the unit (hPa->kPa, W->kW) or move it to the CO2 float.
//   - Bonus: Apple sees these as real temps/humidity, so you can build HomeKit
//     automations on them ("when [voltage-as-temp] > 250 -> notify").
//
// BATTERY SoC: matter_c has no PowerSource (0x002F) cluster, and Apple shows a
// Matter battery only as an attribute of a functional accessory (a low-battery
// badge + % in settings) — NOT as a standalone numeric tile. So to get a visible
// "0..100 %" SoC gauge, fake it as a Humidity sensor (see ep_soc below).
//
// Open pairing from the web /mt page (Bind), then add in Apple/Google/Alexa.
// Values are SIMULATED so it runs on any board; replace the EverySecond() math
// with sensorGet("...") for real readings.

int ep_press;   // air pressure   -> Temperature (kPa)
int ep_volt;    // mains voltage  -> Temperature (V)
int ep_curr;    // mains current  -> Temperature (A, signed)
int ep_power;   // active power   -> CO2 float (W, any magnitude)
int ep_soc;     // battery SoC    -> Humidity (0..100 %)
int tick;

void EverySecond() {
    float p_hpa; float volt; float amp; float watt; float soc;
    tick = tick + 1;

    // --- simulated real-world readings (swap for sensorGet(...)) ---
    p_hpa = 1013.0 + 12.0 * sin((float)tick / 90.0);   // ~1001 .. 1025 hPa
    volt  = 230.0  + 8.0  * sin((float)tick / 40.0);    // ~222 .. 238 V
    amp   = 6.0    + 6.0  * sin((float)tick / 25.0);    // ~0 .. 12 A
    watt  = 1400.0 + 1200.0 * sin((float)tick / 55.0);  // ~200 .. 2600 W
    soc   = 60.0   + 35.0 * sin((float)tick / 120.0);   // ~25 .. 95 %

    // --- publish through the carrier clusters ---
    // pressure: hPa -> kPa so it fits Temperature's +-327 window (1013 hPa = 101.3 kPa)
    matterSetFloat(ep_press, CLUSTER_TEMP, 0, p_hpa / 10.0, 100); // -> "101.30 deg"
    matterSetFloat(ep_volt,  CLUSTER_TEMP, 0, volt,         100); // -> "230.xx deg"
    matterSetFloat(ep_curr,  CLUSTER_TEMP, 0, amp,          100); // -> "6.xx deg"
    matterSetFloat(ep_power, CLUSTER_CO2,  0, watt,         1);   // -> "1400 ppm"
    matterSet     (ep_power, CLUSTER_AIRQUALITY, 0, 1);          // pin AirQuality = Good
    matterSetFloat(ep_soc,   CLUSTER_HUM,  0, soc,          100); // -> "60 %"
}

int main() {
    tick = 0;
    matterReset();                       // clean data model (root node only)

    // ---- Pressure as a Temperature sensor (shown in kPa) ----
    ep_press = matterAdd(MATTER_TEMP_SENSOR);
    matterName(ep_press, "Luftdruck kPa");
    matterCluster(ep_press, CLUSTER_TEMP);
    matterAttr(ep_press, CLUSTER_TEMP, 0, MTR_S16);
    matterSet(ep_press, CLUSTER_TEMP, 0, 10130);        // seed 101.30 kPa

    // ---- Voltage as a Temperature sensor ----
    ep_volt = matterAdd(MATTER_TEMP_SENSOR);
    matterName(ep_volt, "Netz V");
    matterCluster(ep_volt, CLUSTER_TEMP);
    matterAttr(ep_volt, CLUSTER_TEMP, 0, MTR_S16);
    matterSet(ep_volt, CLUSTER_TEMP, 0, 23000);         // seed 230.00 V

    // ---- Current as a Temperature sensor (signed: export = negative) ----
    ep_curr = matterAdd(MATTER_TEMP_SENSOR);
    matterName(ep_curr, "Strom A");
    matterCluster(ep_curr, CLUSTER_TEMP);
    matterAttr(ep_curr, CLUSTER_TEMP, 0, MTR_S16);
    matterSet(ep_curr, CLUSTER_TEMP, 0, 600);           // seed 6.00 A

    // ---- Active power as a CO2 float (any magnitude; shows "W" as ppm) ----
    ep_power = matterAdd(MATTER_AIRQUALITY_SENSOR);
    matterName(ep_power, "Leistung W");
    matterCluster(ep_power, CLUSTER_AIRQUALITY);
    matterAttr(ep_power, CLUSTER_AIRQUALITY, 0, MTR_ENUM8);  // mandatory for the device type
    matterCluster(ep_power, CLUSTER_CO2);
    matterAttr(ep_power, CLUSTER_CO2, 0, MTR_FLOAT);        // carries watts
    matterSet(ep_power, CLUSTER_AIRQUALITY, 0, 1);          // Good (don't alarm the room)
    matterSetFloat(ep_power, CLUSTER_CO2, 0, 1400.0, 1);

    // ---- Battery SoC as a Humidity sensor (0..100 % tile) ----
    ep_soc = matterAdd(MATTER_HUM_SENSOR);
    matterName(ep_soc, "Akku SoC %");
    matterCluster(ep_soc, CLUSTER_HUM);
    matterAttr(ep_soc, CLUSTER_HUM, 0, MTR_U16);
    matterSet(ep_soc, CLUSTER_HUM, 0, 6000);            // seed 60.00 %

    matterStart();                       // Matter on; press Bind on /mt to pair
    return 0;
}