@viguza/homebridge-ezviz
Version:
A short description about what your plugin does.
327 lines • 14.5 kB
JavaScript
import { SmartPlug } from './accessories/smart-plug.js';
import { IPCamera } from './accessories/ip-camera.js';
import { AlarmModeSwitch } from './accessories/alarm-mode-switch.js';
import { MotionSensor } from './accessories/motion-sensor.js';
import { EzvizMqttClient } from './utils/mqtt-client.js';
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
import { EZVIZAPI } from './api/ezviz-api.js';
import { DeviceTypes, CAMERA_DEVICE_TYPES } from './utils/enums.js';
/**
* EZVIZ Platform for Homebridge
* Handles device discovery, authentication, and accessory management
*/
export class EZVIZPlatform {
log;
config;
api;
Service;
Characteristic;
accessories = new Map();
discoveredCacheUUIDs = [];
motionSensors = new Map();
mqttClient = null;
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.Service = api.hap.Service;
this.Characteristic = api.hap.Characteristic;
this.api.on('didFinishLaunching', this.didFinishLaunching.bind(this));
this.log.debug('Finished initializing platform:', this.config.name);
}
/**
* Called when Homebridge finishes launching
* Handles authentication and device discovery
*/
async didFinishLaunching() {
try {
const ezvizAPI = new EZVIZAPI(this.config, this.log);
const credentials = await this.authenticate(ezvizAPI);
if (credentials) {
// Refresh session every 12 hours (uses refresh token, falls back to full re-auth)
setInterval(async () => {
this.log.debug('Refreshing EZVIZ session');
try {
await ezvizAPI.refreshSession();
}
catch (error) {
this.log.error('Session refresh failed:', error);
}
}, 3600000 * 12);
await this.discoverDevices(ezvizAPI);
await this.startMqtt(ezvizAPI);
}
else {
this.log.error('Could not authenticate with EZVIZ API. Please check your credentials.');
}
}
catch (error) {
this.log.error('Error during platform initialization:', error);
}
this.log.debug('Executed didFinishLaunching callback');
}
/**
* Authenticates with the EZVIZ API
* @param ezvizAPI - The EZVIZ API instance
* @returns Promise resolving to credentials or undefined if authentication fails
*/
async authenticate(ezvizAPI) {
const region = this.config.region;
const email = this.config.email;
const password = this.config.password;
if (!email || !password) {
this.log.error('You must provide your email and password in config.json.');
return;
}
if (!region) {
this.log.error('You must provide your region in config.json.');
return;
}
try {
this.config.domain = await ezvizAPI.getDomain(region);
const credentials = await ezvizAPI.authenticate();
if (credentials) {
this.log.info('Successfully authenticated with EZVIZ API');
}
return credentials;
}
catch (error) {
this.log.error('Authentication failed:', error);
return;
}
}
/**
* Configures an accessory from cache
* @param accessory - The accessory to configure
*/
configureAccessory(accessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
this.accessories.set(accessory.UUID, accessory);
}
/**
* Discovers and manages EZVIZ devices
* @param ezvizAPI - The EZVIZ API instance
*/
async discoverDevices(ezvizAPI) {
try {
const devicesResponse = await ezvizAPI.listDevices();
if (!devicesResponse) {
this.log.error('No devices found or failed to retrieve device list');
return;
}
const devices = this.extractDevicesData(devicesResponse);
this.log.info(`Found ${devices.length} devices`);
for (const device of devices) {
const existingAccessory = this.accessories.get(device.UUID);
if (existingAccessory) {
this.log.debug(`Restoring existing ${device.Type} from cache: ${existingAccessory.displayName}`);
existingAccessory.context.device = device;
this.createAccessory(ezvizAPI, existingAccessory, device.Type);
}
else {
this.log.info(`Adding new ${device.Type}: ${device.Name}`);
const accessory = new this.api.platformAccessory(device.Name, device.UUID);
accessory.context.device = device;
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.createAccessory(ezvizAPI, accessory, device.Type);
}
this.discoveredCacheUUIDs.push(device.UUID);
if (device.Type === DeviceTypes.IPC || device.Type === DeviceTypes.CatEye) {
if (device.HBConfig?.motionSensor) {
this.createMotionSensor(ezvizAPI, device);
}
}
}
// Create a single alarm mode switch accessory
const alarmUuid = this.api.hap.uuid.generate('EZVIZ-AlarmMode');
const existingAlarmAccessory = this.accessories.get(alarmUuid);
if (existingAlarmAccessory) {
this.log.debug('Restoring existing alarm mode switch from cache');
new AlarmModeSwitch(ezvizAPI, this, existingAlarmAccessory);
}
else {
this.log.info('Adding new alarm mode switch');
const alarmAccessory = new this.api.platformAccessory('Alarm Mode', alarmUuid);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [alarmAccessory]);
new AlarmModeSwitch(ezvizAPI, this, alarmAccessory);
}
this.discoveredCacheUUIDs.push(alarmUuid);
// Remove accessories that are no longer available
for (const [uuid, accessory] of this.accessories) {
if (!this.discoveredCacheUUIDs.includes(uuid)) {
this.log.info('Removing existing accessory from cache:', accessory.displayName);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
}
catch (error) {
this.log.error('Error discovering devices:', error);
}
}
/**
* Creates the appropriate accessory based on device type
* @param ezvizAPI - The EZVIZ API instance
* @param accessory - The platform accessory
* @param deviceType - The type of device
*/
createAccessory(ezvizAPI, accessory, deviceType) {
try {
if (deviceType === DeviceTypes.Socket) {
new SmartPlug(ezvizAPI, this, accessory);
}
else if (CAMERA_DEVICE_TYPES.has(deviceType)) {
new IPCamera(ezvizAPI, this, accessory);
}
else {
this.log.warn(`Unsupported device type: ${deviceType}`);
}
}
catch (error) {
this.log.error(`Error creating accessory for ${accessory.displayName}:`, error);
}
}
createMotionSensor(ezvizAPI, device) {
const uuid = this.api.hap.uuid.generate(`${device.Serial}-motion`);
const name = `${device.Name} Motion`;
const existing = this.accessories.get(uuid);
let sensor;
if (existing) {
this.log.debug(`Restoring existing motion sensor from cache: ${existing.displayName}`);
existing.context.serial = device.Serial;
sensor = new MotionSensor(ezvizAPI, this, existing);
}
else {
this.log.info(`Adding new motion sensor: ${name}`);
const accessory = new this.api.platformAccessory(name, uuid);
accessory.context.serial = device.Serial;
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
sensor = new MotionSensor(ezvizAPI, this, accessory);
}
this.motionSensors.set(device.Serial, sensor);
this.discoveredCacheUUIDs.push(uuid);
}
async startMqtt(ezvizAPI) {
if (this.motionSensors.size === 0) {
return;
}
try {
const pushAddr = await ezvizAPI.getServiceUrls();
const creds = this.config.credentials;
if (!pushAddr || !creds?.username || !creds?.sessionId) {
this.log.debug('MQTT: missing pushAddr or credentials, skipping');
return;
}
this.mqttClient = new EzvizMqttClient(pushAddr, creds.sessionId, creds.username, (serial) => {
const sensor = this.motionSensors.get(serial);
if (sensor) {
sensor.onMqttAlarm();
}
else {
this.log.debug(`MQTT: no motion sensor for serial=${serial}, ignoring`);
}
}, this.log);
await this.mqttClient.connect();
this.log.info('MQTT push connected — real-time alerts active, polling continues as fallback');
}
catch (error) {
this.log.warn('MQTT push failed to connect, motion sensors will fall back to polling:', error.message);
}
}
/**
* Extracts device data from the API response
* @param devicesResponse - The API response containing device information
* @returns Array of processed device data
*/
extractDevicesData(devicesResponse) {
const devices = [];
for (const device of devicesResponse.deviceInfos) {
const uuid = this.api.hap.uuid.generate(device.deviceSerial);
const deviceType = DeviceTypes[device.deviceCategory];
if (!deviceType) {
this.log.error(`Device ${device.name} has an unsupported type ${device.deviceCategory} and will be skipped`);
continue;
}
let deviceConfig;
if (deviceType === DeviceTypes.Socket) {
deviceConfig = this.config.plugs?.find((plug) => plug.serial === device.deviceSerial);
}
else if (CAMERA_DEVICE_TYPES.has(deviceType)) {
deviceConfig = this.config.cameras?.find((camera) => camera.serial === device.deviceSerial);
if (!deviceConfig) {
this.log.info(`Camera ${device.name} (${device.deviceSerial}) is not configured and will be skipped`);
continue;
}
const error = this.cameraConfigErrors(deviceConfig);
if (error) {
this.log.info(`Device ${device.name} (${device.deviceSerial}) is not configured correctly and will be skipped: ${error}`);
continue;
}
}
// Check if this is a dual camera
const isDualCamera = deviceType === DeviceTypes.IPC && !!deviceConfig?.dualCamera;
if (isDualCamera) {
// Create two separate camera accessories for dual camera devices
const cameraChannels = [
{ suffix: '1', channelNumber: 101 },
{ suffix: '2', channelNumber: 201 },
];
for (const { suffix, channelNumber } of cameraChannels) {
const deviceSerial = `${device.deviceSerial}_${suffix}`;
const deviceUuid = this.api.hap.uuid.generate(deviceSerial);
const deviceName = `${device.name} - Camera ${suffix}`;
const deviceData = { ...device };
deviceData.channelNumber = channelNumber;
const data = {
UUID: deviceUuid,
Serial: deviceSerial,
Name: deviceName,
Type: deviceType,
Connection: devicesResponse.CONNECTION[device.deviceSerial],
Wifi: devicesResponse.WIFI?.[device.deviceSerial],
Status: devicesResponse.STATUS[device.deviceSerial],
Switches: devicesResponse.SWITCH[device.deviceSerial],
P2P: devicesResponse.P2P[device.deviceSerial],
ResourceInfo: devicesResponse.resourceInfos.find((resource) => resource.deviceSerial === device.deviceSerial),
DeviceInfo: deviceData,
HBConfig: deviceConfig,
};
devices.push(data);
}
}
else {
const data = {
UUID: uuid,
Serial: device.deviceSerial,
Name: device.name,
Type: deviceType,
Connection: devicesResponse.CONNECTION[device.deviceSerial],
Wifi: devicesResponse.WIFI?.[device.deviceSerial],
Status: devicesResponse.STATUS[device.deviceSerial],
Switches: devicesResponse.SWITCH[device.deviceSerial],
P2P: devicesResponse.P2P[device.deviceSerial],
ResourceInfo: devicesResponse.resourceInfos.find((resource) => resource.deviceSerial === device.deviceSerial),
DeviceInfo: device,
HBConfig: deviceConfig,
};
devices.push(data);
}
}
;
return devices;
}
/**
* Validates camera configuration
* @param camera - The camera configuration to validate
* @returns Error message if validation fails, empty string if valid
*/
cameraConfigErrors(camera) {
if (!camera.username) {
return 'No Username';
}
if (!camera.code) {
return 'No Verification Code';
}
return '';
}
}
//# sourceMappingURL=platform.js.map