[External Converter]: EKAZA 24 GHz presence sensor — TS0225 / _TZ3210_eep3fewj
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.7k
- Forks
- 2k
- Avg merge
- 18h 55m
- Merged PRs (30d)
- 35
Description
Link
Database entry
{"id":30,"type":"Router","ieeeAddr":"0xf84477fffee5a089","nwkAddr":63188,"manufId":4098,"manufName":"_TZ3210_eep3fewj","powerSource":"Mains (single phase)","modelId":"TS0225","epList":[1],"endpoints":{"1":{"profId":260,"epId":1,"devId":1026,"inClusterList":[0,1280,57346,61184],"outClusterList":[25,10],"clusters":{"57346":{"attributes":{"57355":6}},"genBasic":{"attributes":{"65503":"\u0002�-2\u0012P�-2\u0012","65506":31,"65508":0,"65534":0,"modelId":"TS0225","manufacturerName":"_TZ3210_eep3fewj","powerSource":1,"zclVersion":3,"appVersion":5,"stackVersion":0,"hwVersion":1,"dateCode":""}},"ssIasZone":{"attributes":{"iasCieAddr":"0x8c8b48fffe4f1220","zoneState":1,"zoneType":32771,"zoneStatus":1,"zoneId":23}},"ekazaRadar":{"attributes":{"attrE00B":4}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":5,"stackVersion":0,"hwVersion":1,"dateCode":"","zclVersion":3,"interviewCompleted":true,"interviewState":"SUCCESSFUL","meta":{"configured":"0.0.0"},"lastSeen":1788893979899}
Zigbee2MQTT version
2.14.1 (unknown)
External converter
import * as fz from "zigbee-herdsman-converters/converters/fromZigbee";
import * as exposes from "zigbee-herdsman-converters/lib/exposes";
import {logger} from "zigbee-herdsman-converters/lib/logger";
import {
deviceAddCustomCluster,
} from "zigbee-herdsman-converters/lib/modernExtend";
import * as tuya from "zigbee-herdsman-converters/lib/tuya";
import {Zcl} from "zigbee-herdsman";
const e = exposes.presets;
const ea = exposes.access;
const NS = "zhc:ekaza";
const E002_CLUSTER = "ekazaRadar";
const DETECTION_DISTANCE_ATTRIBUTE = 0xe00b;
const addEkazaRadarCluster = deviceAddCustomCluster(E002_CLUSTER, {
name: E002_CLUSTER,
ID: 0xe002,
attributes: {
attrE00B: {
name: "attrE00B",
ID: DETECTION_DISTANCE_ATTRIBUTE,
type: Zcl.DataType.UINT16,
write: true,
min: 1,
max: 6,
},
},
commands: {},
commandsResponse: {},
});
const fzEkazaDetectionDistance = {
cluster: E002_CLUSTER,
type: [
"attributeReport",
"readResponse",
],
convert: (model, msg) => {
const value =
msg.data?.attrE00B ??
msg.data?.[DETECTION_DISTANCE_ATTRIBUTE] ??
msg.data?.[String(DETECTION_DISTANCE_ATTRIBUTE)];
if (typeof value !== "number") {
return undefined;
}
logger.debug(
`EKAZA detection distance report=${value}`,
NS,
);
return {
detection_distance: value,
};
},
};
const tzEkazaDetectionDistance = {
key: ["detection_distance"],
convertSet: async (entity, key, value) => {
const requested = Number(value);
if (
!Number.isInteger(requested) ||
requested < 1 ||
requested > 6
) {
throw new Error(
"detection_distance deve ser um número inteiro entre 1 e 6",
);
}
logger.info(
`EKAZA detection distance write start ` +
`requested=${requested} type=UINT16`,
NS,
);
await entity.write(
E002_CLUSTER,
{
[DETECTION_DISTANCE_ATTRIBUTE]: {
value: requested,
type: Zcl.DataType.UINT16,
},
},
{
timeout: 15000,
disableDefaultResponse: false,
},
);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
const response = await entity.read(
E002_CLUSTER,
[DETECTION_DISTANCE_ATTRIBUTE],
{
timeout: 15000,
disableDefaultResponse: true,
},
);
const confirmed =
response?.attrE00B ??
response?.[DETECTION_DISTANCE_ATTRIBUTE] ??
response?.[String(DETECTION_DISTANCE_ATTRIBUTE)];
logger.info(
`EKAZA detection distance write result ` +
`requested=${requested} confirmed=${confirmed}`,
NS,
);
if (Number(confirmed) !== requested) {
throw new Error(
`O sensor não confirmou o alcance solicitado. ` +
`Solicitado=${requested}, retornado=${confirmed}`,
);
}
return {
state: {
detection_distance: confirmed,
},
};
},
convertGet: async (entity) => {
await entity.read(
E002_CLUSTER,
[DETECTION_DISTANCE_ATTRIBUTE],
{
timeout: 15000,
disableDefaultResponse: true,
},
);
},
};
async function ensureIasEnrollment(
endpoint,
coordinatorEndpoint,
) {
let iasState;
try {
iasState = await endpoint.read(
"ssIasZone",
[
"zoneState",
"zoneType",
"zoneStatus",
"iasCieAddr",
"zoneId",
],
{
timeout: 15000,
disableDefaultResponse: true,
},
);
logger.info(
`EKAZA IAS BEFORE ${JSON.stringify(iasState)}`,
NS,
);
} catch (error) {
logger.warning(
`EKAZA IAS initial read failed: ${error}`,
NS,
);
}
const coordinatorIeeeAddress =
coordinatorEndpoint.deviceIeeeAddress;
const correctCieAddress =
iasState?.iasCieAddr === coordinatorIeeeAddress;
const enrolled = Number(iasState?.zoneState) === 1;
if (!correctCieAddress || !enrolled) {
logger.info(
`EKAZA IAS enrollment required ` +
`zoneState=${iasState?.zoneState} ` +
`currentCie=${iasState?.iasCieAddr} ` +
`expectedCie=${coordinatorIeeeAddress}`,
NS,
);
await endpoint.write(
"ssIasZone",
{
iasCieAddr: coordinatorIeeeAddress,
},
{
timeout: 15000,
disableDefaultResponse: false,
},
);
const zoneId =
typeof iasState?.zoneId === "number" ?
iasState.zoneId :
23;
try {
await endpoint.commandResponse(
"ssIasZone",
"enrollRsp",
{
enrollrspcode: 0,
zoneid: zoneId,
},
{
timeout: 15000,
disableDefaultResponse: true,
},
);
logger.info(
`EKAZA IAS enroll response sent zoneId=${zoneId}`,
NS,
);
} catch (error) {
logger.warning(
`EKAZA IAS enroll response failed: ${error}`,
NS,
);
}
}
try {
const finalState = await endpoint.read(
"ssIasZone",
[
"zoneState",
"zoneType",
"zoneStatus",
"iasCieAddr",
"zoneId",
],
{
timeout: 15000,
disableDefaultResponse: true,
},
);
logger.info(
`EKAZA IAS AFTER ${JSON.stringify(finalState)}`,
NS,
);
} catch (error) {
logger.warning(
`EKAZA IAS final read failed: ${error}`,
NS,
);
}
}
const definition = {
fingerprint: [
{
modelID: "TS0225",
manufacturerName: "_TZ3210_eep3fewj",
},
],
model: "TS0225_EKAZA",
vendor: "EKAZA",
description: "Radar de presença MWave 24 GHz",
extend: [
addEkazaRadarCluster,
],
fromZigbee: [
tuya.fz.datapoints,
fz.ias_occupancy_alarm_1,
fz.ias_occupancy_alarm_1_report,
fzEkazaDetectionDistance,
],
/*
* A ordem é importante.
*
* O conversor específico de detection_distance precisa executar
* antes do conversor genérico de datapoints Tuya.
*/
toZigbee: [
tzEkazaDetectionDistance,
tuya.tz.datapoints,
],
exposes: [
e.occupancy()
.withDescription(
"Presença detectada pelo IAS Zone Status bit 0",
),
e.numeric("illuminance", ea.STATE)
.withDescription(
"Valor atual de luminosidade informado pelo DP104",
),
e.numeric("presence_delay", ea.STATE_SET)
.withUnit("s")
.withValueMin(1)
.withValueMax(300)
.withValueStep(1)
.withDescription(
"Tempo para declarar ausência depois que a presença desaparece",
),
e.numeric("detection_distance", ea.STATE_SET)
.withUnit("m")
.withValueMin(1)
.withValueMax(6)
.withValueStep(1)
.withDescription(
"Distância máxima de detecção do radar",
),
],
meta: {
tuyaSendCommand: "sendData",
tuyaDatapoints: [
[
101,
"presence_delay",
tuya.valueConverter.raw,
],
[
104,
"illuminance",
tuya.valueConverter.raw,
],
],
},
configure: async (
device,
coordinatorEndpoint,
) => {
const endpoint = device.getEndpoint(1);
if (!endpoint) {
throw new Error(
"Endpoint 1 do sensor EKAZA não encontrado",
);
}
logger.info(
`EKAZA CONFIG START ieee=${device.ieeeAddr}`,
NS,
);
try {
await tuya.configureMagicPacket(
device,
coordinatorEndpoint,
);
logger.info(
"EKAZA TUYA magic packet completed",
NS,
);
} catch (error) {
logger.warning(
`EKAZA TUYA magic packet failed: ${error}`,
NS,
);
}
await ensureIasEnrollment(
endpoint,
coordinatorEndpoint,
);
try {
const distance = await endpoint.read(
E002_CLUSTER,
[DETECTION_DISTANCE_ATTRIBUTE],
{
timeout: 15000,
disableDefaultResponse: true,
},
);
logger.info(
`EKAZA detection distance ` +
`${JSON.stringify(distance)}`,
NS,
);
} catch (error) {
logger.warning(
`EKAZA detection distance read failed: ${error}`,
NS,
);
}
logger.info(
"EKAZA CONFIG FINISHED",
NS,
);
},
};
export default definition;
What does/doesn't work with the external definition?
| Feature | Cluster / attribute or datapoint | Mapping |
|---|---|---|
| Presence | IAS Zone 0x0500, zoneStatus bit 0 | 0 = unoccupied; 1 = occupied |
| Illuminance | Tuya 0xEF00, DP104, datatype 2 | Raw numeric value |
| Presence delay | Tuya 0xEF00, DP101, datatype 2 | Seconds; adjustment tested |
| Maximum detection distance | Cluster 0xE002, attribute 0xE00B | Integer distance in metres; UINT16 writes tested |
The vendor app displays maximum detection distance, presence delay and current illuminance.
The converter currently exposes detection distance from 1 to 6 metres and presence delay from 1 to 300 seconds. These are the configured converter limits; the full accepted range, particularly for presence delay, has not been exhaustively verified.
The illuminance value matches the type of measurement shown in the vendor app, but its physical unit and calibration have not been independently verified.
Validation
After pairing, IAS enrollment completed with the Zigbee2MQTT coordinator.
The sensor sends spontaneous
commandStatusChangeNotificationmessages when occupancy changes. Continuous polling is not required.A recorded unoccupied event at 17:12:07 on 2026-09-04 contained
zonestatus: 0.A subsequent occupied event at 17:12:37 contained
zonestatus: 1.Presence delay can be changed through DP101.
Maximum detection distance can be written through attribute
0xE00Busing UINT16 and verified by reading it back.Following further use and positioning adjustments, the device appears to be operating correctly.
Implementation notes
The dedicated detection_distance toZigbee converter must be selected before the generic Tuya datapoint converter. With the generic converter first, writes failed locally with:
Error: No datapoint defined for 'detection_distance'
After correcting converter ordering, distance adjustment worked.
The working converter uses UINT16 for 0xE00B. Earlier failed attempts do not establish whether UINT8 is rejected by the device, because the converter selection issue prevented the write from reaching the sensor.
A new pairing/reset was followed by fresh reports of DP101 = 3 and 0xE00B = 6, rather than the previously selected vendor-app settings.
Request
Please consider adding native support for this exact fingerprint. I can provide the working external converter, additional device information and targeted logs, and test a proposed native definition.
Notes
software_build_id: undefined
date_code: ``
endpoints:
{"1":{"clusters":{"input":["genBasic","ssIasZone","ekazaRadar","manuSpecificTuya"],"output":["genOta","genTime"]}}}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by comparing the supplied external converter with existing Zigbee2MQTT device definitions and the TS0225 database entry. Verify the IAS occupancy, Tuya DP101/DP104 values, and E002 attribute 0xE00B mappings against the reported behavior. Done means the device is supported with the listed controls and its behavior is covered by the relevant converter tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- embedded-iot
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100