Jakarta, Indonesia
Posts

Optimistic-ON: Slashing Smart Home Motion Latency to Under 4 Seconds

You step into a dark bathroom, and... nothing happens. By the time the smart light finally turns on, five to eight seconds have passed, and you've already stubbed your toe in the dark. This is the frustrating reality of battery-powered Tuya WiFi PIR motion sensors. To conserve battery, they live in a deep sleep state. When they detect motion, they must wake up, boot their WiFi radio, associate with the Access Point (AP), obtain an IP address, and then connect to either the local network or the Tuya Cloud to report the event. By the time an automation engine like Node-RED polls the status or receives a cloud push notification, the delay is too long for a practical "walk-in" light trigger. I wanted to see if I could slash this latency. Using a pattern I call Optimistic-ON with Async Verify, I managed to bring the response time down to ~4 seconds—the absolute physical limit of the sensor's boot-up and WiFi association time. To understand the solution, here's what we are working with:
  • Tuya PIR Motion Sensor (WiFi-based, battery-powered)
  • UniFi U6-Pro Access Point (running syslog forwarding to local server)
  • Mini PC running Node-RED and our monitoring services
  • Tuya Smart Bulb & Exhaust Fan (local TCP control)
  • tinytuya Python library for local status checks
Originally, my automation was sequential:
  1. Wait for the PIR sensor to wake up and connect to WiFi.
  2. Poll the device's status locally or wait for Tuya Cloud to trigger a webhook.
  3. Verify that the sensor is reporting "pir" (motion).
  4. Turn on the bathroom light and exhaust fan.
This flow was incredibly slow. The WiFi connection and TCP handshake alone took ~3.5 seconds. Querying the device or waiting for the cloud added another 2–5 seconds. I realized I didn't need to wait for a full connection handshake to know someone was in the bathroom. The moment the PIR sensor powers up, it attempts to associate with the UniFi AP. The AP immediately logs this association request and forwards it to the Mini PC's syslog service on UDP port 1514. If I could intercept this syslog packet, I could turn the light on before the sensor even completed its connection!
Optimistic-ON with Async Verify Architecture
The catch is that battery-powered PIR sensors also wake up periodically (every few hours) to send a heartbeat/battery status check, even if there is no motion. If we turned the light on for every WiFi association, the bathroom light would randomly turn on in the middle of the night. To prevent this, I split the automation logic into two parallel execution paths inside Node-RED:
  1. Path 1 (Optimistic-ON): The moment a syslog packet matches the PIR sensor's MAC address (10:5a:17:7e:e1:d3), turn the light and exhaust fan ON immediately and cancel any active off-timers.
  2. Path 2 (Async Verify): In parallel, Node-RED launches a background verification script to query the actual state of the sensor.
If the verification script reports a real motion event (motion: true), the light stays on. If it reports a heartbeat wake-up (motion: false), Node-RED starts a 10-minute delay-off timer to turn the light back off automatically, minimizing nuisance runs. Because the verification script runs concurrently with the sensor's boot cycle, it starts executing before the sensor has finished connecting to the local network. To handle this, I wrote a Python script /opt/tuyamon/check_pir.py that employs a two-tier verification approach: The script attempts to connect to the PIR sensor over local TCP (port 6668) using tinytuya. Since the device is still finishing its connection, we use a loop of 12 attempts with 200ms sleeps in between (~5.4s total window). We keep timeouts very short to fail fast and retry:
Python
def query_local(ip, dev_id, key):
    # Short-timeout local TCP query with retries
    for attempt in range(1, 13):
        try:
            d = tinytuya.OutletDevice(dev_id, ip, key)
            d.set_version(3.3)
            d.set_socketTimeout(0.25) # Short timeout
            d.set_socketRetryLimit(1) # Fail fast
            status = d.status()
            if status and "dps" in status:
                val = status["dps"].get("1") or status["dps"].get(1)
                return {"success": True, "motion_state": val, "attempt": attempt}
        except Exception:
            pass
        time.sleep(0.2)
    return {"success": False}
If the device finishes its transmission and goes to sleep before we establish a local TCP connection (or if local WiFi is temporarily congested), the script falls back to querying the Tuya Cloud API. Before querying, it sleeps for at least 1.0s from start of execution to allow the sensor to upload its event.
Python
def query_cloud(dev_id):
    config = load_cloud_config()
    try:
        cloud = tinytuya.Cloud(
            apiRegion=config.get("apiRegion", "us"),
            apiKey=config.get("apiKey"),
            apiSecret=config.get("apiSecret")
        )
        status = cloud.getstatus(dev_id)
        if status and status.get("success") and "result" in status:
            for item in status["result"]:
                if item.get("code") == "pir":
                    return {"success": True, "motion_state": item.get("value")}
    except Exception:
        pass
    return {"success": False}
This architecture shifts the heavy lifting of connection verification to run in the background. The latency timeline under ideal conditions now looks like this:
  • T + 0.0s: User steps into the bathroom. The PIR sensor detects heat, powers up, and starts WiFi association.
  • T + 3.5s: The sensor completes WiFi association.
  • T + 3.9s: The UniFi AP generates a syslog event and sends it to the Mini PC.
  • T + 4.0s: Node-RED parses the syslog and fires the ON command. The light turns ON. (Total latency: ~4.0s).
  • T + 4.5s: The parallel check_pir.py script makes local TCP contact on attempt 3, confirms motion: true, and returns JSON.
By turning the light on optimistically based on the AP's syslog, we completely bypass the 2–5 seconds of polling/negotiation delay. The light is already on by the time the script finishes confirming why it's on. Here is how the new Optimistic-ON syslog method performs compared to our old sequential cloud-polling setup:
Metric
Old Sequential / Cloud Method
Optimistic-ON / Syslog Method
Total Turn-ON Latency5.0 – 10.0+ seconds~4.0 seconds
Trigger Signal SourceTuya Cloud Push / TCP PollUniFi AP WiFi Association Syslog
WiFi Boot & Assoc Time3.5s (blocking flow)3.5s (parallelized)
Verification MethodWait for TCP handshake + replyImmediate ON, async verification check
Heartbeat HandlingIgnored (no light trigger)Turns on, triggers off-timer after 5s verification
  1. Leverage the network layer for low-latency triggers. Battery-powered IoT devices are bound by physics; they take time to boot and connect. Sniffing WiFi association events (via syslog, ARP, or DHCP packets) gives you an early warning signal seconds before the device is ready to communicate.
  2. Decouple trigger from verification. In UX design, perceived speed is everything. Act "optimistically" on the earliest signal, then verify and correct in the background.
  3. Local API fallback is critical. WiFi devices on battery have erratic networks. Having a dual local TCP + Cloud API fallback ensures that even if local polling fails, the automation recovers and handles the state correctly.

This optimization is part of my ongoing Home Operations project, where I use custom Node-RED automation, local syslog parsing, and InfluxDB telemetry to build a responsive smart home.
On this page