UNPKG

@viguza/homebridge-ezviz

Version:

A short description about what your plugin does.

170 lines 7.09 kB
import axios from 'axios'; import mqtt from 'mqtt'; import { MQTT_APP_KEY, MQTT_APP_SECRET, MQTT_PORT } from '../api/ezviz-constants.js'; // Field order of the comma-separated "ext" payload from EZVIZ MQTT messages const EXT_FIELD_NAMES = [ 'channel_type', 'time', 'device_serial', 'channel_no', 'alert_type_code', 'default_pic_url', 'media_url_alt1', 'media_url_alt2', 'resource_type', 'status_flag', 'file_id', 'is_encrypted', 'picChecksum', 'is_dev_video', 'metadata', 'msgId', 'image', 'device_name', 'reserved', 'sequence_number', ]; const EXT_INT_FIELDS = new Set([ 'channel_type', 'channel_no', 'alert_type_code', 'resource_type', 'status_flag', 'is_encrypted', 'is_dev_video', 'sequence_number', ]); export class EzvizMqttClient { pushAddr; sessionId; username; log; mqttClient = null; clientId = null; ticket = null; topic = `${MQTT_APP_KEY}/#`; onMessage; constructor(pushAddr, sessionId, username, onMessage, log) { this.pushAddr = pushAddr; this.sessionId = sessionId; this.username = username; this.log = log; this.onMessage = onMessage; } async connect() { this.clientId = await this.register(); await this.startPush(); this.connectMqtt(); } stop() { this.stopPush().catch(() => undefined); if (this.mqttClient) { this.mqttClient.end(true); this.mqttClient = null; } } async register() { const auth = Buffer.from(`${MQTT_APP_KEY}:${MQTT_APP_SECRET}`).toString('base64'); const response = await axios.post(`https://${this.pushAddr}/v1/getClientId`, new URLSearchParams({ appKey: MQTT_APP_KEY, clientType: '5', mac: 'homebridge', token: '123456', version: 'v1.3.0', }).toString(), { headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded' } }); this.log.debug('MQTT register response:', JSON.stringify(response.data)); if (response.data?.status !== 200) { throw new Error(`MQTT registration failed: ${JSON.stringify(response.data)}`); } return response.data.data.clientId; } async startPush() { this.log.debug(`MQTT startPush: clientId=${this.clientId} username=${this.username} sessionId=${this.sessionId?.slice(0, 8)}...`); const response = await axios.post(`https://${this.pushAddr}/api/push/start`, new URLSearchParams({ appKey: MQTT_APP_KEY, clientId: this.clientId, clientType: '5', sessionId: this.sessionId, username: this.username, token: '123456', }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); this.log.debug('MQTT startPush response:', JSON.stringify(response.data)); if (response.data?.status !== 200) { throw new Error(`MQTT push start failed: ${JSON.stringify(response.data)}`); } this.ticket = response.data.ticket ?? null; this.log.debug(`MQTT push started (clientId: ${this.clientId} ticket: ${this.ticket})`); } async stopPush() { if (!this.clientId) { return; } await axios.post(`https://${this.pushAddr}/api/push/stop`, new URLSearchParams({ appKey: MQTT_APP_KEY, clientId: this.clientId, clientType: '5', sessionId: this.sessionId, username: this.username, }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).catch(() => undefined); } connectMqtt() { const client = mqtt.connect(`mqtt://${this.pushAddr}:${MQTT_PORT}`, { clientId: this.clientId, username: MQTT_APP_KEY, password: MQTT_APP_SECRET, protocolVersion: 4, clean: false, keepalive: 30, reconnectPeriod: 5000, }); client.on('connect', (connack) => { this.log.debug(`MQTT connected (sessionPresent=${connack.sessionPresent})`); // With clean:false the broker may restore a stored session that already has our // subscription; re-subscribing is only needed for fresh sessions. if (!connack.sessionPresent) { client.subscribe(this.topic, { qos: 1 }, (err, granted) => { if (err) { this.log.error('MQTT subscribe error:', err.message); } else { this.log.debug('MQTT subscribed:', JSON.stringify(granted)); } }); } }); client.on('message', (topic, payload) => { this.log.debug(`MQTT raw message: topic=${topic} bytes=${payload.length}`); try { const decoded = this.decodePayload(payload); const ext = decoded.ext; const serial = ext?.device_serial; if (!serial) { this.log.debug('MQTT message: no device_serial, raw ext:', JSON.stringify(decoded.ext)); return; } // ext.time (field index 1) is an EZVIZ internal code, not a Unix timestamp. // MQTT is real-time push so Date.now() is the correct alarm time. const rawMsgId = ext?.msgId; const msgId = (typeof rawMsgId === 'string' && rawMsgId.trim() !== '') ? rawMsgId : serial; this.log.debug(`MQTT message: serial=${serial} alert_type_code=${ext?.alert_type_code} msgId=${msgId}`); this.onMessage(serial); } catch (err) { this.log.debug('MQTT message decode error:', err, 'raw:', payload.toString('utf-8').slice(0, 200)); } }); client.on('error', (err) => { this.log.error('MQTT error:', err.message); }); client.on('close', () => { this.log.debug('MQTT connection closed'); }); client.on('offline', () => { this.log.debug('MQTT client offline, will reconnect'); }); this.mqttClient = client; } decodePayload(payload) { const data = JSON.parse(payload.toString('utf-8')); if (typeof data.ext === 'string') { const parts = data.ext.split(','); const ext = {}; for (let i = 0; i < EXT_FIELD_NAMES.length; i++) { const name = EXT_FIELD_NAMES[i]; let value = parts[i]; if (value !== undefined && EXT_INT_FIELDS.has(name)) { const n = parseInt(value, 10); if (!isNaN(n)) { value = n; } } ext[name] = value; } data.ext = ext; } return data; } } //# sourceMappingURL=mqtt-client.js.map