Skip to content

[New device support]: Additional fingerprint for Merrytek MSA201Z #30438

@jortan

Description

@jortan

Link

https://www.merrytek.com/products/msa201-z/

Database entry

{"id":90,"type":"Router","ieeeAddr":"0x44e2f8fffeb08a84","nwkAddr":13958,"manufId":4098,"manufName":"_TZE200_hyhl5y36","powerSource":"Mains (single phase)","modelId":"TS0601","epList":[1],"endpoints":{"1":{"profId":260,"epId":1,"devId":81,"inClusterList":[0,4,5,61184],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"65503":"�e�0f�e�0\u0012�e�0\u0012","65506":31,"65508":0,"65534":0,"modelId":"TS0601","manufacturerName":"_TZE200_hyhl5y36","stackVersion":0,"dateCode":"","zclVersion":3,"appVersion":65,"powerSource":1}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":65,"stackVersion":0,"hwVersion":1,"dateCode":"","zclVersion":3,"interviewCompleted":true,"interviewState":"SUCCESSFUL","meta":{"configured":332242049},"lastSeen":1767320402463}

Zigbee2MQTT version

2.7.2

External definition

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const modernExtend = require('zigbee-herdsman-converters/lib/modernExtend');
const e = exposes.presets;
const ea = exposes.access;
const tuya = require('zigbee-herdsman-converters/lib/tuya');

const definition = {
    fingerprint: [
        {
            modelID: 'TS0601',
            manufacturerName: '_TZE200_hyhl5y36',
        },
    ],
    model: 'MSA201Z',
    vendor: 'Merrytek',
    description: '24 GHz human presence sensor (TS0601, _TZE200_hyhl5y36)',

    extend: [
        tuya.modernExtend.tuyaBase({
            dp: true,
            // timeStart: "1970",
        }),
    ],

    exposes: [
        e.enum('state', ea.STATE, ['Absence', 'Presence', 'Disabled']).withLabel('Status'),
        e.presence(),
        e.enum('current_status', ea.STATE, ['Approaching', 'Departing', 'Clear']),
        e.illuminance().withLabel('Luminance'),

        e.numeric('hold_delay_time', ea.STATE_SET)
            .withUnit('s')
            .withValueMin(0)
            .withValueMax(300)
            .withValueStep(1)
            .withCategory('config')
            .withDescription('Delay (seconds) before switching from Presence to Absence after no motion is detected. Recommended ≥ 15s to avoid premature Absence switching.'),

        e.enum('sensitivity', ea.STATE_SET, ['Low', 'Medium', 'High'])
            .withCategory('config')
            .withDescription('Sensitivity of human presence detection. High: minimal motion interference; Medium: most scenarios; Low: some motion interference.'),

        e.numeric('trigger_distance', ea.STATE_SET)
            .withUnit('m')
            .withValueMin(0.5)
            .withValueMax(4)
            .withValueStep(0.5)
            .withCategory('config')
            .withDescription('Distance within which the sensor detects motion, adjustable 0.5–4m in 0.5m steps.'),

        e.numeric('forbidden_area', ea.STATE_SET)
            .withUnit('m')
            .withValueMin(0)
            .withValueMax(1.8)
            .withValueStep(0.1)
            .withCategory('config')
            .withDescription('Distance from the sensor within which motion is ignored (0–1.8m).'),

        e.enum('ai_self_learning', ea.SET, ['AI self-learning'])
            .withLabel('AI environment self-learning')
            .withCategory('config')
            .withDescription('AI self-learning to ignore non-human motion; takes ~1 minute with the area empty.'),

        e.enum('fast_setting', ea.STATE_SET, ['Small', 'Medium', 'Large'])
            .withCategory('config')
            .withDescription('Fast setting by space size: Small <16m²; Medium 16–25m²; Large >25m².'),

        e.binary('indicator', ea.STATE_SET, 'ON', 'OFF')
            .withLabel('LED indicator')
            .withCategory('config')
            .withDescription('LED indicator when motion is detected or state changes.'),

        e.enum('sensor_mode', ea.STATE_SET, ['Presence', 'Motion'])
            .withCategory('config')
            .withDescription('Presence: micro-movements; Motion: larger movements, ignores small activity.'),

        e.binary('single_mode', ea.STATE_SET, 'ON', 'OFF')
            .withCategory('config')
            .withDescription('Single-person mode; keeps Presence while a person is in range, then Absence after 15s.'),

        e.binary('absence_circling_report', ea.STATE_SET, 'ON', 'OFF')
            .withCategory('config')
            .withDescription('Periodic reporting of Absence state after switching to Absence.'),

        e.numeric('absence_circling_interval', ea.STATE_SET)
            .withUnit('min')
            .withValueMin(2)
            .withValueMax(30)
            .withValueStep(1)
            .withCategory('config')
            .withDescription('Interval (minutes) between periodic Absence reports.'),

        e.binary('find_device', ea.STATE_SET, 'ON', 'OFF')
            .withCategory('config')
            .withDescription('Indicator LED flashes to help locate the sensor.'),

        e.binary('enable_sensor', ea.STATE_SET, 'ON', 'OFF')
            .withCategory('config')
            .withDescription('Enable or disable the sensor.'),

        e.enum('factory_reset', ea.SET, ['Factory reset'])
            .withCategory('config')
            .withDescription('Restores factory defaults and removes custom settings.'),

        e.enum('lux_mode', ea.STATE_SET, ['Threshold', 'Report'])
            .withCategory('config')
            .withDescription('Lux mode: Threshold for fixed daylight level, Report for periodic reports.'),

        e.numeric('daylight_threshold', ea.STATE_SET)
            .withUnit('lux')
            .withValueMin(1)
            .withValueMax(3000)
            .withValueStep(1)
            .withCategory('config')
            .withDescription('Lux level defining sufficient daylight when Lux mode = Threshold.'),

        e.enum('lux_report_mode', ea.STATE_SET, ['Timed', 'Difference'])
            .withCategory('config')
            .withDescription('Lux report style: Timed for fixed intervals; Difference (not implemented here).'),

        e.numeric('lux_timed_interval', ea.STATE_SET)
            .withUnit('s')
            .withValueMin(5)
            .withValueMax(3600)
            .withValueStep(5)
            .withCategory('config')
            .withDescription('Interval (seconds) for timed lux reports.'),

        e.numeric('lux_difference_threshold', ea.STATE_SET)
            .withUnit('lux')
            .withValueMin(1)
            .withValueMax(2000)
            .withValueStep(1)
            .withCategory('config')
            .withDescription('Lux change needed to trigger a Difference-mode report (not actually implemented).'),

        e.numeric('lux_difference_value', ea.STATE)
            .withCategory('diagnostic')
            .withDescription('Reported lux value for Difference mode (not actually implemented).'),

        e.text('interference_positions', ea.STATE)
            .withCategory('diagnostic')
            .withDescription('Distances (m) where non-human interference was detected.'),

        e.enum('home_environment', ea.STATE, ['Normal', 'Slight', 'Strong', 'Severe'])
            .withCategory('diagnostic')
            .withDescription('Environmental interference level detected by the sensor.'),
    ],

    meta: {
        tuyaDatapoints: [
            [
                1,
                null,
                {
                    from: (v) => {
                        switch (v) {
                            case 0:
                                return {state: 'Absence', presence: false};
                            case 1:
                                return {state: 'Presence', presence: true};
                            case 2:
                                return {state: 'Disabled', presence: false};
                            default:
                                console.warn('Unknown DP1 value:', v);
                                return {state: 'Absence', presence: false};
                        }
                    },
                    to: (value) => {
                        switch (value.state) {
                            case 'Presence':
                                return 1;
                            case 'Absence':
                                return 0;
                            case 'Disabled':
                                return 2;
                            default:
                                return 0;
                        }
                    },
                },
            ],
            [2, 'trigger_distance', tuya.valueConverter.divideBy10],
            [101, 'illuminance', tuya.valueConverter.raw],
            [102, 'lux_difference_value', tuya.valueConverter.raw],
            [
                103,
                'ai_self_learning',
                {
                    from: (v) => ({0: 'end', 4: 'learning'})[v],
                    to: () => 1,
                },
            ],
            [
                104,
                'factory_reset',
                {
                    from: () => 'idle',
                    to: (v) => ({'Factory reset': 1})[v] || 0,
                },
            ],
            [
                105,
                'fast_setting',
                {
                    from: (v) => ({1: 'Large', 2: 'Medium', 3: 'Small'})[v] ?? 'Medium',
                    to: (v) => ({Small: 3, Medium: 2, Large: 1})[v] ?? 2,
                },
            ],
            [107, 'indicator', tuya.valueConverter.onOff],
            [106, 'hold_delay_time', tuya.valueConverter.raw],
            [
                108,
                'current_status',
                tuya.valueConverterBasic.lookup({
                    Approaching: tuya.enum(0),
                    Departing: tuya.enum(1),
                    Clear: tuya.enum(2),
                }),
            ],
            [109, 'enable_sensor', tuya.valueConverter.onOff],
            [
                110,
                'sensitivity',
                tuya.valueConverterBasic.lookup({
                    Low: tuya.enum(3),
                    Medium: tuya.enum(2),
                    High: tuya.enum(1),
                }),
            ],
            [112, 'status_flip', tuya.valueConverter.onOff],
            [113, 'interference_positions', tuya.valueConverter.raw],
            [114, 'forbidden_area', tuya.valueConverter.divideBy10],
            [115, 'daylight_threshold', tuya.valueConverter.raw],
            [
                116,
                'sensor_mode',
                {
                    from: (v) => ({1: 'Presence', 2: 'Motion'})[v] ?? 'Unknown',
                    to: (v) => ({Presence: 1, Motion: 2})[v] ?? 1,
                },
            ],
            [117, 'single_mode', tuya.valueConverter.onOff],
            [118, 'find_device', tuya.valueConverter.onOff],
            [
                119,
                'lux_mode',
                tuya.valueConverterBasic.lookup({
                    Threshold: tuya.enum(0),
                    Report: tuya.enum(1),
                }),
            ],
            [
                120,
                'lux_report_mode',
                tuya.valueConverterBasic.lookup({
                    Timed: tuya.enum(0),
                    Difference: tuya.enum(1),
                }),
            ],
            [121, 'lux_difference_threshold', tuya.valueConverter.raw],
            [122, 'lux_timed_interval', tuya.valueConverter.raw],
            [123, 'absence_circling_report', tuya.valueConverter.onOff],
            [124, 'absence_circling_interval', tuya.valueConverter.raw],
            [
                125,
                'home_environment',
                {
                    from: (v) => ({0: 'Normal', 1: 'Slight', 2: 'Strong', 3: 'Severe'})[v] ?? 'Unknown',
                    to: (v) => ({Normal: 0, Slight: 1, Strong: 2, Severe: 3})[v] ?? 0,
                },
            ],
        ],
    },
};

module.exports = definition;

What does/doesn't work with the external definition?

Fully functional.

Notes

This is a duplicate fingerprint for existing supported device:

New fingerprint: TS0601, _TZE200_hyhl5y36
Existing fingerprint: TS0601, _TZE284_ajuasrmx

This device was purchased from here:
https://forgeelectrical.com.au/forge-lifebing-breath-detection-motion-sensor.html

It might be useful to note in the documentation alternative name/branding for this device:
FORGE Zigbee Presence & Occupancy Sensor with Milliwave Radar (name from retailer)
Millimeter Microwave Lifebeing Sensor, Model MSA201 Z (product name on device label - the device doesn't include any manufacturer name)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions