device_uuid, device_ieee o device_base58.Guía de integración · Ingeniería frontend
Alta y sustitución de dispositivos desde modo instalador: endpoint a endpoint, con feedback MQTT, polling autoritativo y cierre seguro del dispositivo anterior.
El endpoint trabaja sobre un slot lógico que ya existe en BMS. No descubre una habitación ni inventa una categoría: reemplaza o completa el nodo físico asociado a ese slot.
device_uuid, device_ieee o device_base58.new_device
El slot ya existe, pero no tiene IEEE activo. Requiere room_uuid. El IEEE nuevo se detecta durante el pairing.
replacement
El slot tiene IEEE activo. La room se deriva del slot y se exige new_device_ieee para aceptar solo el equipo esperado.
join_backend
Es opcional. auto es el camino normal; coordinator y device son herramientas de recuperación.
No implementar MQTT de escritura. El frontend no publica permit_join, no renombra dispositivos y no acepta/rechaza candidatos. Todo eso pertenece al backend.
La operación HTTP, el job y el feed MQTT avanzan juntos. El frontend debe abrir el canal de eventos antes de iniciar el pairing para no perder los primeros mensajes.
sequenceDiagram
autonumber
participant UI as Frontend instalador
participant API as BMS API
participant JOB as Job de join
participant Z2M as Zigbee2MQTT
participant EMQX as EMQX
participant ACT as Activity
UI->>API: GET /installer/rooms/{roomUuid}/devices
UI->>API: GET /buildings/{buildingUuid}/jwt
UI->>EMQX: SUB {hubUuid}/installer/join-events
UI->>API: POST /installer/hubs/{hubUuid}/join-requests
API-->>UI: 202 + operation_uuid + Retry-After: 5
API->>JOB: Encola operación
JOB->>Z2M: GET bridge/devices (snapshot)
JOB->>Z2M: request/permit_join (120 s)
Z2M-->>EMQX: device_joined
EMQX-->>UI: evento efímero
EMQX-->>API: POST interno /hubs/join-events
API->>ACT: installer.join_request.event_received
Z2M-->>EMQX: device_interview started
Z2M-->>EMQX: device_interview successful / failed
EMQX-->>UI: progreso de entrevista
UI->>API: GET join-request cada 5 s
alt entrevista correcta
JOB->>Z2M: rename from IEEE nuevo to topic canónico
JOB->>API: crea o reutiliza Device por IEEE
JOB->>Z2M: remove IEEE anterior
JOB->>API: soft-delete del Device anterior
API-->>UI: status=succeeded + joined_device
else fallo o timeout
JOB->>Z2M: limpia nodo parcial cuando aplica
API-->>UI: status=failed + error
end
Antes del pairing hacen falta los UUID públicos, el slot, el hub y una suscripción MQTT de solo lectura.
Bases recomendadas: producción https://api.voltic.es/api/v1/bms, desarrollo https://api-dev.voltic.es/api/v1/bms y local http://localhost:8090/api/v1/bms. Las llamadas requieren bearer o sesión autenticada.
Usar data[] para mostrar los slots y context.hub.uuid como hubUuid. Para recuperación con router explícito, ofrecer únicamente otros dispositivos con categoría switch, thermostat o repeater.
const room = await api.get(`/installer/rooms/${roomUuid}/devices`);
const slots = room.data;
const hubUuid = room.context.hub.uuid;
El host se devuelve en cloud_mqtt_server. En browser se usa la URL WebSocket configurada por la webapp; para herramientas MQTT TLS se utiliza el puerto 8883.
La implementación devuelve {"jwt":"..."}. Ese JWT incluye permiso de suscripción a {hubUuid}/installer/join-events para roles installer, maintenance_technician, admin y super_admin, sujeto a las features MQTT del plan.
{
"jwt": "eyJhbGciOiJIUzUxMiIs..."
}
const topic = `${hubUuid}/installer/join-events`;
const mqttClient = connectToConfiguredBroker({
username: authenticatedUser.username,
password: buildingJwt.jwt,
});
mqttClient.subscribe(topic);
mqttClient.on("message", (receivedTopic, payload) => {
if (receivedTopic !== topic) return;
renderJoinEvent(JSON.parse(payload.toString()));
});
El feed es no retained. Si la pantalla se suscribe tarde o se reconecta, recupera el estado mediante el GET de la operación; MQTT no es un historial.
El body contiene el modo y exactamente un identificador del slot. Una operación activa en el hub puede ser reutilizada por el backend; guardar siempre el operation_uuid realmente devuelto.
new_device{
"mode": "new_device",
"room_uuid": "ROOM_UUID",
"device_uuid": "EMPTY_SLOT_UUID"
}
También puede identificarse el slot con device_base58 al llegar desde QR, o con device_ieee cuando exista un identificador compatible. No enviar más de uno.
replacement{
"mode": "replacement",
"device_ieee": "0x94b216fffe90b13d",
"new_device_ieee": "0x583bc2fffe9021f1"
}
new_device_ieee debe ser un IEEE completo con forma 0x + 16 hexadecimales y distinto del actual. La room se deriva del slot.
HTTP/1.1 202 Accepted
Retry-After: 5
{
"data": {
"operation_uuid": "019f8e9d-928f-70e3-8481-d2c3af57310c",
"mode": "replacement",
"new_device_ieee": "0x583bc2fffe9021f1",
"requested_backend": {
"type": "auto",
"device_uuid": null
},
"status": "queued",
"message": "Join request queued.",
"selected_backend": null,
"attempts": 0,
"retry_after_seconds": 5
}
}
| Valor | Comportamiento | Uso de frontend |
|---|---|---|
auto | Escoge el mejor router disponible de la room por orden switch → thermostat → repeater; coordinador si no hay router. | Predeterminado. No hace falta enviarlo. |
coordinator | Fuerza una única ventana a través del coordinador. | Mostrar como opción avanzada tras un fallo de reconexión. |
device | Fuerza el router indicado en join_backend_device_uuid. Debe ser otro dispositivo de la misma room y hub y aparecer como Router en Zigbee2MQTT. | Mostrar un selector filtrado. No hay fallback si no está disponible. |
{
"mode": "replacement",
"device_ieee": "OLD_IEEE",
"new_device_ieee": "NEW_IEEE",
"join_backend": "device",
"join_backend_device_uuid": "ROUTER_DEVICE_UUID"
}
No existe una actualización HTTP distinta para cada fase. El frontend compone la experiencia con los eventos MQTT y confirma el resultado mediante polling.
{
"operation_uuid": "019f8e9d-928f-70e3-8481-d2c3af57310c",
"type": "device_interview",
"status": "successful",
"ieee_address": "0x583bc2fffe9021f1",
"friendly_name": "0x583bc2fffe9021f1",
"supported": true,
"model": "Frostline 2",
"vendor": "Voltic",
"reported_at": "2026-07-23T10:56:12Z"
}
| Evento | Copy recomendado | Acción |
|---|---|---|
device_joined | “Dispositivo Zigbee detectado.” | Mantener la operación abierta. |
device_interview / started | “Identificando modelo y capacidades…” | Mostrar progreso indeterminado. |
device_interview / successful | “Dispositivo identificado. Finalizando configuración…” | No declarar éxito todavía; esperar GET succeeded. |
device_interview / failed | “No se ha podido identificar el dispositivo.” | Esperar estado terminal y mostrar error.message. |
Usar retry_after_seconds y/o el header Retry-After. Detener el polling en succeeded o failed.
async function pollJoin(hubUuid, operationUuid) {
while (true) {
const { data } = await api.get(
`/installer/hubs/${hubUuid}/join-requests/${operationUuid}`
);
updateScreen(data);
if (data.status === "succeeded" || data.status === "failed") return data;
await wait((data.retry_after_seconds ?? 5) * 1000);
}
}
Operación persistida, todavía pendiente de worker.
El job está procesando snapshot, permit join, entrevista o configuración final.
Rename, persistencia y cleanup obligatorio han terminado correctamente.
Mostrar error.message y permitir un nuevo intento, opcionalmente con backend explícito.
join_event en el GET contiene solo el último evento reducido. La secuencia completa queda auditada en Activity como installer.join_request.event_received, correlacionada por operation_uuid.
La entrevista correcta no basta. El backend solo debe cerrar con éxito después de configurar el topic nuevo y limpiar el dispositivo sustituido.
Se acepta únicamente el IEEE esperado en replacement.
Zigbee2MQTT recibe rename con from = IEEE nuevo y to = ruta del slot + IEEE nuevo. No se renombra temporalmente el anterior.
Se crea un registro BMS o se reutiliza el existente si ese IEEE ya estaba en la base de datos.
Se fuerza la eliminación del IEEE anterior en Zigbee2MQTT. Solo después se hace soft-delete de su fila BMS.
joined_device devuelve IEEE, friendly name descubierto, fabricante, modelo, tipo y topic final.
{
"status": "succeeded",
"message": "Join request completed successfully.",
"joined_device": {
"ieee_address": "0x583bc2fffe9021f1",
"friendly_name": "hotel/room/1132/bed/thermostat/0x583bc2fffe9021f1",
"discovered_friendly_name": "0x583bc2fffe9021f1",
"manufacturer": "Voltic",
"model": "Frostline 2",
"type": "Router",
"topic": "hotel/room/1132/bed/thermostat/0x583bc2fffe9021f1"
},
"error": null
}
V1 no añade el IEEE anterior a una blocklist. Si la eliminación en Zigbee2MQTT falla, la operación debe quedar failed; el registro anterior no se elimina de BMS antes de tiempo.
Hay errores síncronos del request y errores asíncronos del pairing. Se presentan de forma distinta.
| HTTP / estado | Causa habitual | Respuesta frontend |
|---|---|---|
202 | Operación creada o activa reutilizada. | Guardar el UUID devuelto y abrir polling. |
400 | UUID de ruta mal formado. | Bloquear acción y registrar error de navegación/contexto. |
401 | Sesión o bearer inválido. | Renovar sesión; no reintentar pairing automáticamente. |
404 | Hub, room, slot u operación inexistente o sin acceso. | Recargar inventario y mostrar pérdida de acceso. |
409 | Modo incompatible: slot ocupado en new_device, vacío en replacement, o sin topic/category. | Proponer el modo correcto o completar configuración del slot. |
422 | Body inválido, identificadores múltiples, IEEE nuevo ausente o backend explícito incoherente. | Mostrar el mensaje de details[field] junto al control. |
500 | Error inesperado al crear/leer el recurso. | Mostrar reintento seguro solo si no se recibió operation_uuid. |
| failed | Timeout, interview fallida, router explícito no disponible, rename o cleanup fallido. | Mostrar error.message. Permitir nueva operación; ofrecer selección avanzada de backend. |
Si se pierde la respuesta del POST, consultar primero el estado visible del hub o repetir el POST sabiendo que el backend reutiliza la operación activa del hub. Nunca lanzar operaciones en paralelo desde dos pantallas para el mismo hub.
Una función para crear, una para consultar y un listener MQTT. No hace falta modelar el protocolo Zigbee2MQTT en la webapp.
type JoinBackend =
| { type?: "auto" }
| { type: "coordinator" }
| { type: "device"; deviceUuid: string };
type ReplacementInput = {
hubUuid: string;
currentDeviceIeee: string;
newDeviceIeee: string;
backend?: JoinBackend;
};
async function replaceDevice(input: ReplacementInput) {
const backend = input.backend ?? { type: "auto" as const };
const body = {
mode: "replacement",
device_ieee: input.currentDeviceIeee,
new_device_ieee: input.newDeviceIeee,
...(backend.type !== "auto" && { join_backend: backend.type }),
...(backend.type === "device" && {
join_backend_device_uuid: backend.deviceUuid,
}),
};
const response = await api.post(
`/installer/hubs/${input.hubUuid}/join-requests`,
body
);
return response.data;
}
Slot, IEEE nuevo y broker resueltos; feed MQTT conectado.
POST aceptado, queued/running, todavía sin device_joined.
Eventos device_joined e interview started.
Interview successful, backend renombrando, persistiendo y limpiando.
Solo depende de succeeded/failed en el GET autoritativo.
La prueba completa necesita ver los tres planos a la vez: UI/feed, API y Zigbee2MQTT.
{hubUuid}/installer/join-events antes del POST.operation_uuid; comprobar 202 y Retry-After: 5.device_joined → interview started → interview successful en el feed.join_event durante el polling.succeeded y el topic nuevo terminado en el IEEE nuevo.# Feed reducido para frontend
mosquitto_sub -h "$MQTT_HOST" -p 8883 \
-u "$MQTT_USER" -P "$MQTT_JWT" \
-t "$HUB_UUID/installer/join-events" -v
# Operación autoritativa
curl -sS -H "Authorization: Bearer $API_TOKEN" \
"$API_BASE/installer/hubs/$HUB_UUID/join-requests/$OPERATION_UUID"
No considerar correcta una prueba que solo vea la entrevista en Zigbee2MQTT. Deben verificarse también el rename, la fila BMS nueva/reutilizada, la eliminación Zigbee del anterior y el estado HTTP terminal.