zigbee2mqtt icon indicating copy to clipboard operation
zigbee2mqtt copied to clipboard

[New device support]: Additional fingerprint for Merrytek MSA201Z

Open jortan opened this issue 2 weeks ago • 1 comments

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)

jortan avatar Jan 02 '26 02:01 jortan