Koenkk / Koenkk/zigbee2mqtt

[New device support]: Aqara FP300 (PS-S04D) frozen presence state on firmware 6542 - Stuck state fix via external converter

Open
#33,011 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

new device support
Dominant language
TypeScript
Stars
15.7k
Forks
2k
Avg merge
18h 55m
Merged PRs (30d)
35

Description

Link

https://www.aqara.com/en/product/presence-multi-sensor-fp300/

Database entry

{"id":141,"type":"EndDevice","ieeeAddr":"0x54ef441001744fcf","nwkAddr":48485,"manufId":4447,"manufName":"Aqara","powerSource":"Battery","modelId":"lumi.sensor_occupy.agl8","epList":[1],"endpoints":{"1":{"profId":260,"epId":1,"devId":263,"inClusterList":[18,1024,1029,1026,1,3,0,64704],"outClusterList":[10,25],"clusters":{"genBasic":{"attributes":{"61441":2,"modelId":"lumi.sensor_occupy.agl8"}},"manuSpecificLumi":{"attributes":{"22":"EF384394DEE3DE710CAFB7D73BAC185E5C901F487041B820BF35EB9225E6177B","223":{"type":"Buffer","data":[3,35,98,0,0,0,4,33,61,0,5,33,8,0,41,35,19,0,0,0,42,35,5,0,0,0,43,33,217,0,44,41,171,255,45,35,0,0,0,0,46,35,1,6,0,0,47,33,1,0]},"238":16682,"247":{"type":"Buffer","data":[5,33,27,0,10,33,217,0,12,32,10,13,35,42,65,0,0,19,32,0,23,33,123,11,24,32,100,28,16,0,100,32,1,101,32,2,103,32,1]},"268":2,"322":1,"333":1,"334":255,"335":30,"349":1,"350":0,"351":0,"354":600000,"355":3600000,"356":100,"357":3,"362":3600000,"363":1500,"364":3,"368":1,"402":1,"403":10000,"404":3600000,"405":1500,"406":3,"407":10,"409":0,"410":{"type":"Buffer","data":[0,3,255,15,0]},"515":0,"574":589845}},"msIlluminanceMeasurement":{"attributes":{"measuredValue":15798}},"msTemperatureMeasurement":{"attributes":{"measuredValue":2513}},"msRelativeHumidity":{"attributes":{"measuredValue":5406}},"genPowerCfg":{"attributes":{"batteryVoltage":0,"batteryPercentageRemaining":0}}},"binds":[{"cluster":64704,"type":"endpoint","deviceIeeeAddress":"0x00124b0024c2b665","endpointID":1},{"cluster":1024,"type":"endpoint","deviceIeeeAddress":"0x00124b0024c2b665","endpointID":1},{"cluster":1029,"type":"endpoint","deviceIeeeAddress":"0x00124b0024c2b665","endpointID":1},{"cluster":1026,"type":"endpoint","deviceIeeeAddress":"0x00124b0024c2b665","endpointID":1}],"configuredReportings":[{"cluster":64704,"attrId":322,"minRepIntval":0,"maxRepIntval":3600,"repChange":1,"manufacturerCode":4447},{"cluster":64704,"attrId":333,"minRepIntval":0,"maxRepIntval":3600,"repChange":1,"manufacturerCode":4447},{"cluster":1024,"attrId":0,"minRepIntval":10,"maxRepIntval":3600,"repChange":5},{"cluster":1029,"attrId":0,"minRepIntval":10,"maxRepIntval":3600,"repChange":100},{"cluster":1026,"attrId":0,"minRepIntval":10,"maxRepIntval":3600,"repChange":100}],"meta":{}}},"appVersion":42,"stackVersion":27,"hwVersion":1,"dateCode":"20260515","swBuildId":"0.0.0_6542","zclVersion":3,"interviewCompleted":true,"interviewState":"SUCCESSFUL","meta":{"lumiFileVersion":16682,"configured":"0.0.2"},"lastSeen":1788389750750,"checkinInterval":3600}

Zigbee2MQTT version

2.14.0

External definition
const lumiDefinitions = require('zigbee-herdsman-converters/devices/lumi').definitions;
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const {Zcl} = require('zigbee-herdsman');

const manufacturerCode = 0x115f;
const endpointID = 1;
const attrPresence = 0x0142; 
const attrPirDetection = 0x014d; 

const baseDefinition = lumiDefinitions.find((definition) => definition.model === "PS-S04D");

const fp300PresencePir = {
    cluster: "manuSpecificLumi",
    type: ["attributeReport", "readResponse"],
    convert: (model, msg, publish, options, meta) => {
        const payload = {};

        if (msg.data && msg.data[attrPresence] !== undefined) {
            const rawPresence = msg.data[attrPresence];
            payload.presence = rawPresence === 255 ? null : rawPresence === 1;
        }

        if (msg.data && msg.data[attrPirDetection] !== undefined) {
            payload.pir_detection = msg.data[attrPirDetection] === 1;
        }

        return Object.keys(payload).length ? payload : undefined;
    },
};

const reportingPayload = (attributeID) => [
    {
        attribute: attributeID, 
        dataType: Zcl.DataType.uint8, 
        minimumReportInterval: 0, 
        maximumReportInterval: 300, // 5 minutes prevents the firmware state freeze
        reportableChange: 1,
    },
];

async function tryConfigure(step) {
    try {
        await step();
    } catch (e) {
        // Silence proprietary cluster engineering errors
    }
}

const definition = {
    ...baseDefinition,
    description: `${baseDefinition.description || 'Presence sensor FP300'} (local FP300 reporting patch v4 - Fixed Stuck State)`,
    fromZigbee: [fp300PresencePir, ...(baseDefinition.fromZigbee ?? [])],
    configure: async (device, coordinatorEndpoint, definition) => {
        
        await tryConfigure(async () => {
            if (baseDefinition.configure) {
                await baseDefinition.configure(device, coordinatorEndpoint, definition);
            }
        });

        const endpoint = device.getEndpoint(endpointID);

        await tryConfigure(async () => reporting.bind(endpoint, coordinatorEndpoint, ["manuSpecificLumi", 0xfcc0]));
        
        await tryConfigure(async () =>
            endpoint.configureReporting("manuSpecificLumi", reportingPayload(attrPresence), {manufacturerCode}),
        );
        await tryConfigure(async () =>
            endpoint.configureReporting("manuSpecificLumi", reportingPayload(attrPirDetection), {manufacturerCode}),
        );

        await tryConfigure(async () =>
            endpoint.read("manuSpecificLumi", [attrPresence, attrPirDetection], {manufacturerCode}),
        );
        
        await tryConfigure(async () =>
            endpoint.read("genPowerCfg", ["batteryVoltage", "batteryPercentageRemaining"]),
        );
    },
};

module.exports = definition;
What does/doesn't work with the external definition?

Context & Device Details

  • Device: Aqara Presence Sensor FP300 (Model PS-S04D)
  • Firmware Version: 0.0.0_6542 (Build date: 2026-05-15)
  • Platform: Zigbee2MQTT (running inside a Docker container)

The Problem

After updating the Aqara FP300 to firmware build 6542, the device would randomly freeze its state to presence: true or pir_detection: true indefinitely. Even when leaving the room for several minutes with absence_delay_timer set to 10 seconds and motion_sensitivity set to Low, the sensor completely stopped transmitting state updates to the Zigbee coordinator.

A standard external converter would either fail to bind or result in a silent failure, leading Zigbee2MQTT to rename the .js/.mjs converter file to .invalid upon restart due to syntax/import mismatches or unhandled exceptions within the native Node.js CommonJS architecture.


Root Cause Analysis

  1. Strict Cluster Enforcement: The 6542 firmware structure enforces strict type definitions. Custom configurations mapping to the proprietary manuSpecificLumi cluster (0xfcc0) get rejected by the hardware if the primitive data type (dataType) is omitted during configuration.
  2. Missing Reporting Tables: This firmware version acts in a more strict compliance mode, ceasing autonomous state reporting after a certain period of deep-sleep unless a hard-coded attribute reporting map is explicitly injected into the hardware bindings during pairing or reconfiguration.
  3. Buffer Deadlocks: Due to aggressive battery-saving routines, the device enters a deep sleep that disconnects it from active pollings, caching old state registers inside the Zigbee-Herdsman loop.

Solution: Custom External Converter Patch (v4)

The stuck states, false positives, and file rejection issues were solved (hopefully) by deploying a fully audited CommonJS-compliant external converter (module.exports).

The patch addresses the firmware bugs through three critical optimizations:

  • Explicit Datatype Injection: Added Zcl.DataType.uint8 explicitly into the configureReporting structure for both the Presence (0x0142) and PIR (0x014d) proprietary attributes, forcing the Aqara hardware to accept the write parameters.
  • Keep-Alive Heartbeat: Reduced the maximumReportInterval from the generic 30 minutes down to 5 minutes (300 seconds). This acts as a software pacemaker, preventing the 6542 firmware network stack from clogging and freezing the telemetry.
  • Fallback Hardware Reset Sequence: Overcame the initial Offline/Disabled lock on the interface by disabling active network availability checks on devices.yaml (availability: false), waking up the physical sensor through continuous button pressing while triggering the Reconfigure process.

Optimized aqara_fp300_patch.js Code:

const lumiDefinitions = require('zigbee-herdsman-converters/devices/lumi').definitions;const reporting = require('zigbee-herdsman-converters/lib/reporting');const {Zcl} = require('zigbee-herdsman');
const manufacturerCode = 0x115f;const endpointID = 1;const attrPresence = 0x0142; const attrPirDetection = 0x014d;
const baseDefinition = lumiDefinitions.find((definition) => definition.model === "PS-S04D");
const fp300PresencePir = {
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const payload = {};

    if (msg.data && msg.data[attrPresence] !== undefined) {
        const rawPresence = msg.data[attrPresence];
        payload.presence = rawPresence === 255 ? null : rawPresence === 1;
    }

    if (msg.data && msg.data[attrPirDetection] !== undefined) {
        payload.pir_detection = msg.data[attrPirDetection] === 1;
    }

    return Object.keys(payload).length ? payload : undefined;
},

};
const reportingPayload = (attributeID) => [
{
attribute: attributeID,
dataType: Zcl.DataType.uint8,
minimumReportInterval: 0,
maximumReportInterval: 300, // 5 minutes prevents the firmware state freeze
reportableChange: 1,
},
];
async function tryConfigure(step) {
try {
await step();
} catch (e) {
// Silence proprietary cluster engineering errors
}
}
const definition = {
...baseDefinition,
description: ${baseDefinition.description || 'Presence sensor FP300'} (local FP300 reporting patch v4 - Fixed Stuck State),
fromZigbee: [fp300PresencePir, ...(baseDefinition.fromZigbee ?? [])],
configure: async (device, coordinatorEndpoint, definition) => {

    await tryConfigure(async () => {
        if (baseDefinition.configure) {
            await baseDefinition.configure(device, coordinatorEndpoint, definition);
        }
    });

    const endpoint = device.getEndpoint(endpointID);

    await tryConfigure(async () => reporting.bind(endpoint, coordinatorEndpoint, ["manuSpecificLumi", 0xfcc0]));
    
    await tryConfigure(async () =>
        endpoint.configureReporting("manuSpecificLumi", reportingPayload(attrPresence), {manufacturerCode}),
    );
    await tryConfigure(async () =>
        endpoint.configureReporting("manuSpecificLumi", reportingPayload(attrPirDetection), {manufacturerCode}),
    );

    await tryConfigure(async () =>
        endpoint.read("manuSpecificLumi", [attrPresence, attrPirDetection], {manufacturerCode}),
    );
    
    await tryConfigure(async () =>
        endpoint.read("genPowerCfg", ["batteryVoltage", "batteryPercentageRemaining"]),
    );
},

};

module.exports = definition;

Steps to Apply

  1. Drop the script into the data/external_converters/ directory.
  2. Declare it via path relative string inside configuration.yaml.
  3. Clear internal device caches by doing a battery pull (3 minutes cut-off) and performing a physical 10-click factory reset on the sensor button.
  4. Set hardware parameters to: absence_delay_timer: 10, motion_sensitivity: low. Run spatial_learning with an empty room right after binding.

The sensor now should respond instantly to presence changes, reporting clear empty states smoothly without any further transmission crashes on the 6542 firmware.

Notes

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by comparing the existing PS-S04D definition in zigbee-herdsman-converters/devices/lumi with the proposed data/external_converters script and its use of lib/reporting. Check how configuration.yaml and devices.yaml handle external converters and availability. Done means the FP300 on firmware 6542 reports presence and pir_detection changes reliably without the external converter being rejected.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
backend, embedded-iot
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.