Análisis Zemismart ZPS-Z1: sensor de presencia Zigbee a pilas para Home Assistant

El Zemismart ZPS-Z1 es un sensor de presencia Zigbee a pilas que usa radar de 24 GHz y sensor de luz ambiental. Lo he estado probando en casa desde que salió a la venta, integrado en Home Assistant mediante Zigbee2MQTT, para comprobar si realmente puede competir con otros sensores de presencia a pilas como el Aqara FP300, el Meross MS605 o el SwitchBot Presence Sensor.

Sensor de presencia Zigbee Zemismart ZPS-Z1 probado en casa para automatizaciones con Home Assistant

La ventaja de este tipo de sensores es evidente: al no depender de cables, podemos colocarlos en muchas más zonas de la casa sin tener que buscar un enchufe cerca. Eso, para automatizar luces en habitaciones, baños, o zonas de paso, es muy cómodo. Pero también hay que comprobar si esa libertad compensa frente a sensores más caros, más completos o menos delicados con la ubicación.

En este análisis voy a repasar la instalación, qué expone en Home Assistant, cómo se comporta en automatizaciones reales, qué tal funciona el sensor de luz, cómo responde según la ubicación y si merece la pena por los aproximadamente 25 euros por los que se puede encontrar en AliExpress.

Ficha técnica y características

Características del Zemismart ZPS-Z1
ModeloZemismart ZPS-Z1
Tipo de dispositivoSensor de presencia Zigbee a pilas
Tecnología de detecciónRadar de 24 GHz
Sensor adicionalSensor de luz ambiental
ProtocoloZigbee
Integración probadaHome Assistant con Zigbee2MQTT
Alcance ajustableDe 0 a 5 metros
Zonas de detección10 zonas, divididas en tramos de 50 cm
Tiempo de ausenciaAjustable de 2 a 60 segundos
SensibilidadBaja, media, alta y personalizada
AlimentaciónPila CR2450 incluida
Resistencia al aguaNo tiene resistencia IP
SoporteBase con cinta 3M y fijación magnética orientable

Unboxing y diseño

Al abrir el paquete del Zemismart ZPS-Z1 nos encontramos básicamente con el sensor y el manual. No hay mucho más, aunque en este tipo de dispositivos tampoco hace falta una presentación especialmente elaborada.

Unboxing del sensor de presencia Zigbee Zemismart ZPS-Z1 con su caja y manual de usuario

El cuerpo es de plástico y al tacto se nota sencillo. No transmite una sensación premium, pero tampoco parece mal rematado. En la parte frontal tiene una zona de plástico transparente que se ilumina cuando detecta presencia. Durante la instalación puede venir bien para comprobar si está reaccionando, aunque después seguramente interese desactivar ese LED si no queremos verlo encenderse cada vez que pasamos por delante.

Al abrir el Zemismart ZPS-Z1, tenemos acceso a la pila CR2450, que viene incluida, y al botón de reset. De momento es pronto para saber cuanto durará la batería, pero actualizaré el artículo cuando tenga más información sobre este sensor mmWave Zigbee a pilas.

Interior del sensor de presencia Zigbee Zemismart ZPS-Z1 con la pila CR2450 instalada

Eso sí, no tiene resistencia IP. Para un baño puede funcionar si lo colocamos con cuidado, pero no me parece el candidato ideal si va a estar expuesto a humedad de forma habitual. En mi caso lo he probado también en el baño, pero más como prueba real de comportamiento que como ubicación perfecta para dejarlo instalado de forma definitiva.

Lo que más me ha gustado del diseño es la base. Viene preparada para colocarlo de pie, pero también incluye cinta 3M por si queremos pegarlo en la pared. La unión con el sensor es magnética, y eso permite girarlo con bastante libertad hasta encontrar el ángulo adecuado. En un sensor como este, donde la orientación afecta muchísimo al resultado, este soporte me parece uno de sus mejores puntos.

Instalar el Zemismart ZPS-Z1 en Home Assistant con Zigbee2MQTT

En el momento de mis pruebas, el Zemismart ZPS-Z1 no me apareció integrado de forma directa en Zigbee2MQTT, así que tuve que usar un convertidor externo. En la web oficial se enlaza a una página de Drive para descargar el código de configuración, pero en mi caso tuve que modificarlo porque no me funcionaba correctamente.

Mientras no aparezca una integración más directa en Zigbee2MQTT, se puede añadir usando un convertidor externo. En mi instalación, el proceso fue entrar en Zigbee2MQTT, ir a configuración, abrir la consola de desarrollo y entrar en el apartado de convertidores externos.

Una vez ahí, hay que crear un nuevo convertidor con el nombre zps-z1.js y pegar el código correspondiente.

Configuración del convertidor externo del Zemismart ZPS-Z1 en Zigbee2MQTT desde Home Assistant

Como el código es bastante largo, lo dejo dentro de un desplegable para no cortar la lectura del análisis. Solo tienes que abrirlo si vas a usar el Zemismart ZPS-Z1 con Zigbee2MQTT y necesitas añadir el convertidor externo.

Ver código del convertidor externo para Zigbee2MQTT
'use strict';

// ─── Constants ────────────────────────────────────────────────────────────────

const TUYA_CLUSTER = 'manuSpecificTuya';

const DP = {
PRESENCE_STATE: 1,
DETECTION_RANGE: 2,
ILLUMINANCE: 101,
ENERGY_VALUE: 102,
AI_SELF_LEARNING: 103,
HEARTBEAT_ENABLE: 104,
HEART: 105,
SENSITIVITY_PRESET: 112,
ZONE_MAP: 117,
NO_PERSON_TIME: 119,
INDICATOR: 123,
ENERGY_THRESHOLD: 124,
};

const DT = {RAW: 0x00, BOOL: 0x01, VALUE: 0x02, ENUM: 0x04};

const ZONE_COUNT = 10;

// ─── Exposes ─────────────────────────────────────────────────────────────────

const exposes = require('zigbee-herdsman-converters/lib/exposes');
const ea = exposes.access;

// ─── Scaling helpers 0-100 / 0-255 ───────────────────────────────────────────

function toApp(raw) {
return Math.round((raw / 255) * 100);
}

function toRaw(app) {
return Math.round((app / 100) * 255);
}

// ─── Keep-alive DP104 ────────────────────────────────────────────────────────

const keepAliveTimers = {};

function startKeepAlive(device, endpoint) {
stopKeepAlive(device.ieeeAddr);

keepAliveTimers[device.ieeeAddr] = setInterval(async () => {
try {
await endpoint.command(
TUYA_CLUSTER,
'dataRequest',
{
seq: Math.round(Math.random() * 0xFFFF),
dpValues: [
{
dp: DP.HEARTBEAT_ENABLE,
datatype: DT.BOOL,
data: [1],
},
],
},
{disableDefaultResponse: true},
);
} catch (_) {
// Error temporal. Se reintentará en 5 segundos.
}
}, 5000);
}

function stopKeepAlive(ieeeAddr) {
if (keepAliveTimers[ieeeAddr]) {
clearInterval(keepAliveTimers[ieeeAddr]);
delete keepAliveTimers[ieeeAddr];
}
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

async function sendDP(endpoint, dp, datatype, data) {
await endpoint.command(
TUYA_CLUSTER,
'dataRequest',
{
seq: Math.round(Math.random() * 0xFFFF),
dpValues: [
{
dp,
datatype,
data,
},
],
},
{disableDefaultResponse: true},
);
}

function currentZoneArray(meta) {
const state = meta.state ?? {};

return Array.from({length: ZONE_COUNT}, (_, i) => {
const v = state[`zone_${i + 1}_active`];
return v === false ? false : true;
});
}

function encodeZoneMap(zones) {
const buf = Buffer.alloc(ZONE_COUNT, 1);

zones.forEach((active, i) => {
buf[i] = active ? 1 : 0;
});

return buf;
}

function decodeEnergyBuffer(data) {
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
const a = [];
const b = [];

for (let i = 0; i < ZONE_COUNT; i++) {
a.push(buf[i] ?? 0);
b.push(buf[ZONE_COUNT + i] ?? 0);
}

return {a, b};
}

function encodeEnergyBuffer(arrA, arrB) {
const buf = Buffer.alloc(20, 0);

for (let i = 0; i < ZONE_COUNT; i++) {
buf[i] = Math.max(0, Math.min(255, Math.round(arrA[i] ?? 0)));
buf[ZONE_COUNT + i] = Math.max(0, Math.min(255, Math.round(arrB[i] ?? 0)));
}

return buf;
}

// ─── fromZigbee ───────────────────────────────────────────────────────────────

const fzConverter = {
cluster: TUYA_CLUSTER,
type: ['commandDataResponse', 'commandDataReport'],

convert(model, msg, publish, options, meta) {
const result = {};

for (const dpv of msg.data?.dpValues ?? []) {
const {dp, data} = dpv;
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);

switch (dp) {
case DP.PRESENCE_STATE: {
const map = {
0: 'absence',
1: 'presence',
2: 'sensor_close',
};

const state = map[buf[0]] ?? 'unknown';

result.presence_state = state;
result.occupancy = state === 'presence';

break;
}

case DP.DETECTION_RANGE:
result.detection_range = buf.readUInt32BE(0);
break;

case DP.ILLUMINANCE:
result.illuminance = buf.readUInt32BE(0);
break;

case DP.ENERGY_VALUE: {
if (buf.length < 20) break;

const {a: motion, b: presence} = decodeEnergyBuffer(buf);

for (let i = 0; i < ZONE_COUNT; i++) {
result[`zone_${i + 1}_motion_energy`] = toApp(motion[i]);
result[`zone_${i + 1}_presence_energy`] = toApp(presence[i]);
}

break;
}

case DP.AI_SELF_LEARNING: {
const map = {
0: 'standby',
1: 'start',
2: 'learning',
3: 'success',
4: 'fail',
5: 'cancel',
};

result.auto_calibration_status = map[buf[0]] ?? 'unknown';

break;
}

case DP.HEARTBEAT_ENABLE:
result.energy_streaming = buf[0] === 1;
break;

case DP.HEART:
break;

case DP.SENSITIVITY_PRESET: {
const map = {
0: 'high',
1: 'medium',
2: 'low',
3: 'custom',
};

result.sensitivity_preset = map[buf[0]] ?? 'unknown';

break;
}

case DP.ZONE_MAP: {
if (buf.length < ZONE_COUNT) break;

for (let i = 0; i < ZONE_COUNT; i++) {
result[`zone_${i + 1}_active`] = buf[i] !== 0;
}

break;
}

case DP.NO_PERSON_TIME:
result.presence_clear_cooldown = buf.readUInt32BE(0);
break;

case DP.INDICATOR:
result.led_indicator = buf[0] === 1;
break;

case DP.ENERGY_THRESHOLD: {
if (buf.length < 20) break;

const {a: motionThr, b: presenceThr} = decodeEnergyBuffer(buf);

meta.device._lastThresholds = {
motionThr: [...motionThr],
presenceThr: [...presenceThr],
};

for (let i = 0; i < ZONE_COUNT; i++) {
result[`zone_${i + 1}_motion_threshold`] = toApp(motionThr[i]);
result[`zone_${i + 1}_presence_threshold`] = toApp(presenceThr[i]);
}

break;
}

default:
meta.logger.debug(`[ZPS-Z1] Unknown DP ${dp}: ${buf.toString('hex')}`);
break;
}
}

return result;
},
};

// ─── toZigbee ─────────────────────────────────────────────────────────────────

const ZONE_ACTIVE_KEYS = Array.from({length: ZONE_COUNT}, (_, i) => `zone_${i + 1}_active`);
const ZONE_MOTION_THR_KEYS = Array.from({length: ZONE_COUNT}, (_, i) => `zone_${i + 1}_motion_threshold`);
const ZONE_PRESENCE_THR_KEYS = Array.from({length: ZONE_COUNT}, (_, i) => `zone_${i + 1}_presence_threshold`);

const tzConverter = {
key: [
'detection_range',
'sensitivity_preset',
'presence_clear_cooldown',
'led_indicator',
'energy_streaming',
'auto_calibration',
...ZONE_ACTIVE_KEYS,
...ZONE_MOTION_THR_KEYS,
...ZONE_PRESENCE_THR_KEYS,
],

async convertSet(entity, key, value, meta) {
const endpoint = meta.device.getEndpoint(1);

if (key === 'detection_range') {
const clamped = Math.max(0, Math.min(500, Math.round(value / 50) * 50));
const buf = Buffer.alloc(4);

buf.writeUInt32BE(clamped, 0);

await sendDP(endpoint, DP.DETECTION_RANGE, DT.VALUE, [...buf]);

return {state: {detection_range: clamped}};
}

if (key === 'sensitivity_preset') {
const map = {
high: 0,
medium: 1,
low: 2,
custom: 3,
};

const val = map[value];

if (val === undefined) {
throw new Error(`[ZPS-Z1] Invalid sensitivity_preset: ${value}`);
}

await sendDP(endpoint, DP.SENSITIVITY_PRESET, DT.ENUM, [val]);

return {state: {sensitivity_preset: value}};
}

if (key === 'presence_clear_cooldown') {
const clamped = Math.max(2, Math.min(60, Math.round(value)));
const buf = Buffer.alloc(4);

buf.writeUInt32BE(clamped, 0);

await sendDP(endpoint, DP.NO_PERSON_TIME, DT.VALUE, [...buf]);

return {state: {presence_clear_cooldown: clamped}};
}

if (key === 'led_indicator') {
const on = value === true || value === 'ON';

await sendDP(endpoint, DP.INDICATOR, DT.BOOL, [on ? 1 : 0]);

return {state: {led_indicator: on}};
}

if (key === 'energy_streaming') {
const on = value === true || value === 'ON';

await sendDP(endpoint, DP.HEARTBEAT_ENABLE, DT.BOOL, [on ? 1 : 0]);

if (on) {
startKeepAlive(meta.device, endpoint);
} else {
stopKeepAlive(meta.device.ieeeAddr);
}

return {state: {energy_streaming: on}};
}

if (key === 'auto_calibration') {
const map = {
start: 1,
cancel: 5,
};

const cmd = map[value];

if (cmd === undefined) {
throw new Error(`[ZPS-Z1] Invalid auto_calibration value: ${value}`);
}

await sendDP(endpoint, DP.AI_SELF_LEARNING, DT.ENUM, [cmd]);

return {state: {}};
}

if (ZONE_ACTIVE_KEYS.includes(key)) {
const zoneIdx = parseInt(key.split('_')[1], 10) - 1;
const active = value === true || value === 'ON';
const zones = currentZoneArray(meta);

zones[zoneIdx] = active;

const buf = encodeZoneMap(zones);

await sendDP(endpoint, DP.ZONE_MAP, DT.RAW, [...buf]);

const stateUpdate = {};

zones.forEach((a, i) => {
stateUpdate[`zone_${i + 1}_active`] = a;
});

return {state: stateUpdate};
}

if (ZONE_MOTION_THR_KEYS.includes(key)) {
const zoneIdx = parseInt(key.split('_')[1], 10) - 1;
const cached = meta.device._lastThresholds;
const state = meta.state ?? {};

const motionThr = cached
? [...cached.motionThr]
: Array.from(
{length: ZONE_COUNT},
(_, i) => toRaw(state[`zone_${i + 1}_motion_threshold`] ?? 50),
);

const presenceThr = cached
? [...cached.presenceThr]
: Array.from(
{length: ZONE_COUNT},
(_, i) => toRaw(state[`zone_${i + 1}_presence_threshold`] ?? 50),
);

motionThr[zoneIdx] = toRaw(Math.max(0, Math.min(100, Math.round(value))));

const buf = encodeEnergyBuffer(motionThr, presenceThr);

await sendDP(endpoint, DP.ENERGY_THRESHOLD, DT.RAW, [...buf]);
await new Promise((resolve) => setTimeout(resolve, 150));
await sendDP(endpoint, DP.SENSITIVITY_PRESET, DT.ENUM, [3]);

const stateUpdate = {
sensitivity_preset: 'custom',
};

motionThr.forEach((v, i) => {
stateUpdate[`zone_${i + 1}_motion_threshold`] = toApp(v);
});

presenceThr.forEach((v, i) => {
stateUpdate[`zone_${i + 1}_presence_threshold`] = toApp(v);
});

return {state: stateUpdate};
}

if (ZONE_PRESENCE_THR_KEYS.includes(key)) {
const zoneIdx = parseInt(key.split('_')[1], 10) - 1;
const cached = meta.device._lastThresholds;
const state = meta.state ?? {};

const motionThr = cached
? [...cached.motionThr]
: Array.from(
{length: ZONE_COUNT},
(_, i) => toRaw(state[`zone_${i + 1}_motion_threshold`] ?? 50),
);

const presenceThr = cached
? [...cached.presenceThr]
: Array.from(
{length: ZONE_COUNT},
(_, i) => toRaw(state[`zone_${i + 1}_presence_threshold`] ?? 50),
);

presenceThr[zoneIdx] = toRaw(Math.max(0, Math.min(100, Math.round(value))));

const buf = encodeEnergyBuffer(motionThr, presenceThr);

await sendDP(endpoint, DP.ENERGY_THRESHOLD, DT.RAW, [...buf]);
await new Promise((resolve) => setTimeout(resolve, 150));
await sendDP(endpoint, DP.SENSITIVITY_PRESET, DT.ENUM, [3]);

const stateUpdate = {
sensitivity_preset: 'custom',
};

motionThr.forEach((v, i) => {
stateUpdate[`zone_${i + 1}_motion_threshold`] = toApp(v);
});

presenceThr.forEach((v, i) => {
stateUpdate[`zone_${i + 1}_presence_threshold`] = toApp(v);
});

return {state: stateUpdate};
}

meta.logger.warn(`[ZPS-Z1] convertSet: unhandled key "${key}"`);
},

async convertGet(entity, key, meta) {
// Los TS0601 Tuya no usan lecturas ZCL normales para estos datapoints.
// El estado se actualiza mediante dataQuery y reportes del propio dispositivo.
},
};

// ─── Expose builders ──────────────────────────────────────────────────────────

function buildZoneActiveExposes() {
return Array.from({length: ZONE_COUNT}, (_, i) =>
exposes.binary(`zone_${i + 1}_active`, ea.ALL, true, false)
.withDescription(`Zona ${i + 1}: ${i * 50}-${(i + 1) * 50} cm`),
);
}

function buildEnergyExposes() {
const items = [];

for (let i = 1; i <= ZONE_COUNT; i++) {
items.push(
exposes.numeric(`zone_${i}_motion_energy`, ea.STATE)
.withDescription(`Zone ${i} live motion energy 0-100.`)
.withValueMin(0)
.withValueMax(100)
.withCategory('diagnostic'),

exposes.numeric(`zone_${i}_presence_energy`, ea.STATE)
.withDescription(`Zone ${i} live presence energy 0-100.`)
.withValueMin(0)
.withValueMax(100)
.withCategory('diagnostic'),
);
}

return items;
}

function buildThresholdExposes() {
const items = [];

for (let i = 1; i <= ZONE_COUNT; i++) {
items.push(
exposes.numeric(`zone_${i}_motion_threshold`, ea.ALL)
.withDescription(`Zone ${i} motion trigger threshold 0-100. Switches sensitivity to custom.`)
.withValueMin(0)
.withValueMax(100)
.withValueStep(1),

exposes.numeric(`zone_${i}_presence_threshold`, ea.ALL)
.withDescription(`Zone ${i} presence trigger threshold 0-100. Switches sensitivity to custom.`)
.withValueMin(0)
.withValueMax(100)
.withValueStep(1),
);
}

return items;
}

// ─── Device definition ────────────────────────────────────────────────────────

const definition = {
fingerprint: [
{
modelID: 'TS0601',
manufacturerName: '_TZE284_ft7qqpx3',
},
],

model: 'ZPS-Z1',
vendor: 'Zemismart',
description: '24 GHz mmWave presence sensor',

fromZigbee: [fzConverter],
toZigbee: [tzConverter],

onEvent: async (type, data, device) => {
if (type === 'deviceLeave' || type === 'deviceRemoved') {
stopKeepAlive(device.ieeeAddr);
}
},

configure: async (device, coordinatorEndpoint, logger) => {
const endpoint = device.getEndpoint(1);

await endpoint.command(
TUYA_CLUSTER,
'dataQuery',
{},
{disableDefaultResponse: true},
);

const log = logger?.debug ?? logger?.info ?? logger?.log ?? (() => {});
log('[ZPS-Z1] Configured - initial state query sent.');
},

exposes: [
exposes.binary('occupancy', ea.STATE, true, false)
.withDescription('Person detected true or false.'),

exposes.enum('presence_state', ea.STATE, ['absence', 'presence', 'sensor_close'])
.withDescription('Presence state reported by the sensor.'),

exposes.numeric('illuminance', ea.STATE)
.withUnit('lx')
.withDescription('Ambient light level 0-1300 lx.')
.withValueMin(0)
.withValueMax(1300),

exposes.numeric('detection_range', ea.ALL)
.withUnit('cm')
.withDescription('Maximum radar detection distance 0-500 cm, 50 cm steps.')
.withValueMin(0)
.withValueMax(500)
.withValueStep(50),

exposes.numeric('presence_clear_cooldown', ea.ALL)
.withUnit('s')
.withDescription('Time before switching to absence, from 2 to 60 seconds.')
.withValueMin(2)
.withValueMax(60)
.withValueStep(1),

exposes.enum('sensitivity_preset', ea.ALL, ['high', 'medium', 'low', 'custom'])
.withDescription('Sensitivity preset.'),

exposes.enum('auto_calibration', ea.SET, ['start', 'cancel'])
.withDescription('Start or cancel AI self-learning calibration.'),

exposes.enum('auto_calibration_status', ea.STATE, ['standby', 'start', 'learning', 'success', 'fail', 'cancel'])
.withDescription('Auto-calibration status.'),

exposes.binary('led_indicator', ea.ALL, true, false)
.withDescription('Physical LED indicator.'),

exposes.binary('energy_streaming', ea.ALL, true, false)
.withDescription('Enable diagnostic per-zone radar energy reporting. Turn off when not tuning.'),

...buildEnergyExposes(),

...buildZoneActiveExposes(),

...buildThresholdExposes(),
],

meta: {
tuyaDatapoints: null,
},
};

module.exports = definition;

Después de guardar el convertidor, hay que reiniciar Zigbee2MQTT desde Ajustes, aplicaciones, Zigbee2MQTT y Reiniciar. Si ya habías añadido el sensor antes, lo mejor es eliminarlo y volver a emparejarlo para que lo reconozca correctamente con el nuevo convertidor.

No es una instalación difícil si ya estás acostumbrado a Home Assistant y Zigbee2MQTT, pero tampoco es tan limpia como emparejar un sensor que ya viene soportado de serie. Esto conviene tenerlo en cuenta, porque quien busque una experiencia completamente plug and play quizá prefiera esperar o irse a un modelo más asentado.

Qué expone en Home Assistant y para qué sirve

Una vez integrado, el sensor expone bastantes entidades en Home Assistant. No se limita a decir si hay presencia o no, sino que permite ajustar varios parámetros importantes para afinar su comportamiento según la habitación.

Lo básico es la ocupación, el estado de presencia y la iluminación en lux. Esto permite crear automatizaciones bastante normales, como encender una luz solo si detecta presencia y la habitación está por debajo de un nivel concreto de luminosidad.

  • Entidades del Zemismart ZPS-Z1 en Zigbee2MQTT con ocupación, iluminación, distancia de detección y calibración

También podemos ajustar la distancia de detección de 0 a 5 metros, el tiempo que tarda en pasar a ausencia, podemos ajustarlo de 2 a 60 segundos, y la sensibilidad entre baja, media, alta y personalizada. Además, permite activar la autocalibración y desactivar el LED frontal que se enciende cuando detecta presencia.

Una de las partes más interesantes es el energy streaming. Esta opción permite ver el nivel de movimiento y presencia por zonas, algo útil para calibrar el sensor manualmente. El dispositivo divide el alcance en 10 zonas de 50 cm, y desde Home Assistant podemos ver cómo se comporta cada una.

También podemos desactivar zonas concretas. Esto puede ser útil, por ejemplo, si tenemos un ventilador a una distancia determinada y no queremos que el sensor lo interprete como presencia. En ese caso, podemos desactivar esa zona y evitar detecciones no deseadas.

Eso sí, el energy streaming no es algo que dejaría siempre activado. En mis pruebas, cuando está activo el sensor ofrece mucha más información y resulta más fácil ajustarlo, además responde mucho más rápido, pero también puede agotar antes la batería y generar bastante tráfico en la red Zigbee. Para calibrar está bien; para uso diario, mejor dejarlo apagado.

Otra cosa que he visto durante las pruebas es que, para iniciar la calibración automática, el energy streaming debe estar activado. Si no lo activaba, la calibración no se iniciaba.

Prueba real en una habitación de trabajo

Llevo probando el sensor Zemismart ZPS-Z1 desde que salió a la venta, y lo he usado en tres ubicaciones distintas: la habitación donde trabajo, el baño y el salón. Los resultados han sido bastante diferentes según dónde lo colocara, y eso ya dice mucho de este dispositivo.

La primera prueba fue en la habitación. Al principio lo puse en la puerta de entrada, con el ordenador al fondo. En esa posición pude comprobar uno de sus puntos débiles: le costaba encender la luz al entrar. En más de una ocasión me tocó hacer unos cuantos movimientos delante del sensor para que reaccionara.

Zemismart ZPS-Z1 instalado en la pared con su soporte magnético orientable

Lo curioso es que, una vez encendidas las luces, la presencia se mantenía bien. Es decir, el radar de 24 GHz funciona bastante mejor manteniendo la presencia que detectando el primer movimiento si la ubicación no es la adecuada.

Probé a ponerlo en un lateral de la habitación, el sensor no reaccionaba la mayoría de las veces, por lo que en esta situacion tampoco era útil.

Después decidí cambiarlo de sitio y ponerlo en la pared de enfrente. A partir de ese momento el comportamiento mejoró bastante. El sensor empezó a funcionar mucho mejor, siempre que me dirigía hacia él, este se encendía sin ningún tipo de fallos y conseguía mantener la presencia mientras estaba sentado frente al ordenador. En ese escenario el resultado fue mucho mejor.

Aquí la comparación con el Aqara FP300 es inevitable, porque es el sensor que uso en esa habitación. El Aqara me parece más fiable para encender la luz nada más entrar, sobre todo porque perdona mejor la ubicación. El Zemismart puede responder rápido si lo colocas bien, pero necesita más cuidado con la posición.

Prueba en el baño frente al Meross MS605

La siguiente prueba fue poner el Zemismart ZPS-Z1 en el baño, donde lo comparé directamente con el Meross MS605. En mi caso, el Meross lo tengo colocado encima de la puerta de entrada al baño, pegado a la pared, así que puse el Zemismart en esa misma ubicación para ver si podía sustituirlo.

El resultado no fue especialmente bueno. Al entrar, el sensor detectaba rara vez. Si conseguía detectarme, después el radar mantenía la presencia bastante bien, pero el problema estaba justo en el inicio de la automatización. Probé a calibrarlo, a ajustar parámetros manualmente y a tocar la sensibilidad, pero seguía fallando en la detección inicial.

Después lo coloqué encima del espejo del baño y volví a probar. En esa posición funcionó algo mejor, pero no se encendía siempre, imagino que al ser poca distancia desde el sensor hasta la puerta, no funcionaba bien. Después de todas las pruebas, la conclusión fue bastante clara: este sensor funciona mucho mejor cuando caminamos directamente hacia él, pero debe haber algo de distancia. Si entramos de lado o cruzamos la zona sin dirigirnos hacia el frontalmente, puede fallar.

Por eso no me parece la mejor opción para un baño pequeño si queremos que la luz se encienda siempre al entrar. Puede funcionar, pero hay que colocarlo con mucho cuidado. En mi caso, el Meross MS605, una vez calibrado, me da mejor resultado en esa estancia.

Prueba en el salón: la prueba del divorcio

No tenía muy claro si hacer la prueba del salón. Es lo que suelo llamar la prueba del divorcio, porque cuando una automatización de luces falla en una zona compartida de la casa, molesta mucho más que en un despacho o en una habitación de pruebas donde solo me afecta a mi.

El salón es bastante grande, y aquí el comportamiento fue justo el que esperaba después de las pruebas anteriores. El sensor se encendía cuando me dirigía hacia él, pero no inmediatamente al entrar en la estancia. En mi caso, llegaba a encender cuando ya estaba a unos 4 metros del sensor.

Zemismart ZPS-Z1 probado en el salón junto al Shelly Presence Gen4 para comparar la detección de presencia

Una vez detectaba presencia, el resultado era bastante bueno. Como lo coloqué cerca de la mesa del salón y del sofá, se mantuvo encendido sin problema mientras estábamos en la zona. En ese sentido, el sensor cumplió mejor de lo que esperaba.

Aun así, aquí tengo instalado el Shelly Presence Gen4, y la diferencia se nota. El Shelly enciende nada más entrar en el salón, mientras que el Zemismart necesita que avances más hacia él. La comparación tampoco es del todo justa, porque el Shelly es más caro y cableado, pero sirve para poner en contexto lo que ofrece este sensor a pilas.

Sensor de luz y calibración

Una de las cosas que más me ha gustado del Zemismart ZPS-Z1 es el sensor de iluminación. En mis pruebas ha funcionado bastante bien y responde mejor de lo que esperaba.

Además, no me parece un problema que mida la luz cuando detecta presencia. Para este tipo de automatizaciones, lo importante es saber cuánta luz hay en el momento en el que entras en la estancia, no tener una lectura constante cada pocos segundos. Si lo vamos a usar para encender luces según presencia e iluminación, cumple bastante bien.

La calibración es otra parte importante. El sensor permite ajustar zonas, sensibilidad, distancia y umbrales, pero hay que dedicarle tiempo. No es uno de esos sensores que colocas en cualquier sitio y funciona perfecto a la primera. Aquí la orientación importa mucho, y el energy streaming ayuda bastante a entender qué está detectando en cada zona.

Comparativa con Aqara FP300, Meross MS605 y SwitchBot Presence Sensor

SensorConectividadAlimentaciónLo mejorLo peorDónde lo usaría
Zemismart ZPS-Z1ZigbeePila CR2450Precio bajo, soporte magnético cómodo, sensor de luz rápido y muchas opciones en Home AssistantLa detección inicial depende mucho de la orientaciónHabitaciones, despachos o zonas donde pueda colocarse de frente
Aqara FP300Thread y ZigbeePilasDetecta antes, responde más rápido y añade temperatura y humedadEs más caro y el soporte me parece peor que el del ZemismartEstancias donde quiero una detección más rápida y estable
Meross MS605Matter over ThreadPilaUna vez calibrado responde mejor, tiene IP67 y encaja mejor en bañoHay que calibrarlo bien y el sensor de luz tarda más en actualizarBaños o zonas donde interese resistencia IP67
SwitchBot Presence SensorBluetoothPilasResponde bastante bien, es rápido y tiene IP55Depende más de cómo lo integres en Home AssistantZonas donde quieras un sensor rápido y no te importe usar Bluetooth

Comparado con el Aqara FP300, el Zemismart ZPS-Z1 sale ganando en precio y en soporte físico. La base magnética del Zemismart me parece mucho más cómoda que la del Aqara, sobre todo porque permite orientar mejor el sensor. Pero en detección, el Aqara funciona bastante más rápido y enciende antes. Además, también mide temperatura y humedad, y tiene la ventaja de ser compatible con Thread y Zigbee. Eso sí, su precio ronda los 49 euros, así que estamos hablando de otra gama.

Comparativa del Zemismart ZPS-Z1 junto al Aqara FP300 durante las pruebas de presencia en Home Assistant

Frente al Meross MS605, la situación es parecida. El Meross tiene el problema de que hay que calibrarlo manualmente para sacarle todo el partido, pero una vez ajustado responde mucho mejor que el Zemismart en mis pruebas. Además, cuenta con resistencia IP67, lo que lo hace más adecuado para el baño. Donde sí me ha gustado más el Zemismart es en el sensor de iluminación, porque en el Meross tarda bastante en actualizar. El soporte del Meross también está bien, aunque personalmente sigo prefiriendo el del Zemismart. El MS605 funciona con Matter over Thread, mientras que el Zemismart usa Zigbee.

Con el SwitchBot Presence Sensor hay que tener en cuenta que hablamos de un sensor Bluetooth. En mi caso, para usarlo con Home Assistant no he necesitado el hub de SwitchBot, aunque esto dependerá de cómo lo integres. El SwitchBot responde bastante bien, es rápido y tiene resistencia IP55. Aun así, el sensor de iluminación del Zemismart me ha parecido más rápido y mejor para automatizaciones. En cuanto a la base, ambos son cómodos.

Dónde lo usaría y dónde no

Después de varias pruebas, tengo bastante claro dónde colocaría este sensor. Lo usaría en una habitación, un despacho o una zona donde pueda orientarlo siempre de frente, de forma que al entrar en la estancia nos dirijamos a el. En esas condiciones puede funcionar bastante bien, sobre todo porque mantiene la presencia correctamente una vez te ha detectado.

Donde no lo usaría como primera opción es en baños, pasillos o zonas donde entras de lado respecto al sensor. También tendría cuidado en automatizaciones donde necesitas que la luz se encienda al instante. Si el sensor está mal colocado, puede obligarte a moverte delante de él para que reaccione, y eso en el día a día termina molestando.

Precio y dónde comprar

El Zemismart ZPS-Z1 se puede encontrar por unos 25 euros en AliExpress, lo que lo convierte en una de las opciones más baratas dentro de los sensores de presencia a pilas que he probado.

Sensor de presencia Zigbee Zemismart ZPS-Z1 con radar de 24 GHz
Zemismart ZPS-Z1

Sensor de presencia Zigbee a pilas con radar de 24 GHz, sensor de luz ambiental, ajuste por zonas y soporte magnético orientable.

Ese precio es precisamente lo que lo hace interesante. No compite de tú a tú con sensores más caros en rapidez o fiabilidad de detección inicial, pero sí ofrece muchas opciones de ajuste, integración con Zigbee2MQTT y un soporte muy cómodo para orientarlo bien.

Conclusión tras varias semanas de uso

El Zemismart ZPS-Z1 me parece una buena compra si buscas un sensor de presencia Zigbee barato y no te importa dedicar tiempo a colocarlo bien. No es el sensor que mejor perdona una mala ubicación, pero cuando está bien orientado responde rápido y mantiene bien la presencia. Además, tiene varias cosas a su favor: funciona a pilas, es barato, el soporte magnético es muy cómodo, el sensor de iluminación responde bien y en Home Assistant expone bastantes opciones para ajustar su comportamiento.

Su punto débil está en la detección inicial cuando la colocación no es la adecuada. Si lo instalas en una zona donde entras de lado o no caminas directamente hacia él, puede fallar. En cambio, cuando lo orientas bien y detecta presencia, el radar de 24 GHz mantiene bastante bien la ocupación.

No lo elegiría como primera opción para un baño pequeño, un pasillo o una zona donde entras de lado respecto al sensor. En cambio, en una habitación, despacho o estancia donde puedas colocarlo mirando hacia la zona de paso, puede funcionar bastante bien.

El Zemismart ZPS-Z1 tiene sentido si buscas un sensor de presencia Zigbee a pilas, económico y muy configurable, siempre que puedas colocarlo de frente a la zona de paso. Cuando la ubicación es buena, responde rápido y mantiene bien la presencia. Su problema no es tanto la velocidad, sino que no perdona igual de bien una mala colocación como otros sensores más caros.

Puntuación final
Zemismart ZPS-Z1
3,5 / 5

✅ Pros

  • Precio muy competitivo frente a otros sensores de presencia a pilas.
  • Funciona con Zigbee y se puede integrar en Home Assistant mediante Zigbee2MQTT.
  • Soporte magnético muy cómodo, permite orientar el sensor con bastante libertad.
  • Sensor de iluminación bastante rápido y útil para automatizaciones de luces.
  • Si está bien colocado, responde rápido y mantiene bien la presencia.
  • Permite ajustar distancia de detección, sensibilidad, tiempo de ausencia y zonas.
  • Funciona a pilas, así que se puede colocar sin depender de cables ni enchufes.

❌ Contras

  • La detección inicial depende mucho de la orientación del sensor.
  • No perdona tan bien una mala ubicación como otros sensores más caros.
  • Puede fallar si entras de lado o si lo colocas en una zona poco favorable.
  • No tiene resistencia IP, así que no es ideal para zonas con humedad.
  • De momento, en mis pruebas no apareció integrado de forma directa en Zigbee2MQTT y tuve que usar un convertidor externo.

También podría interesarte:

Los mejores sensores de presencia para tu hogar en 2026: guía basada en pruebas reales


Descubre más desde ANALISISGADGETS

Suscríbete y recibe las últimas entradas en tu correo electrónico.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Un comentario