@viguza/homebridge-ezviz
Version:
A short description about what your plugin does.
249 lines • 8.78 kB
JavaScript
import axios from 'axios';
import querystring from 'querystring';
import crypto, { randomBytes } from 'crypto';
import { EZVIZ_CLIENT_TYPE, EZVIZ_USER_AGENT, EZVIZ_BASE_API_URL, EZVIZ_DOMAINS_ENDPOINT, EZVIZ_AUTH_ENDPOINT, EZVIZ_DEVICES_ENDPOINT, EZVIZ_SWITCH_STATUS_ENDPOINT, RUSSIA_DOMAIN, RUSSIA_AREA_ID, } from './ezviz-constants.js';
import { sendRequest } from './ezviz-requests.js';
/**
* EZVIZ API client for interacting with EZVIZ services
*/
export class EZVIZAPI {
config;
sessionId;
log;
constructor(config, log) {
this.config = config;
this.sessionId = null;
this.log = log;
}
/**
* Generates a random string of specified length
* @param length - The length of the string to generate
* @returns Random string
*/
randomStr(length) {
return randomBytes(length)
.toString('base64')
.slice(0, length)
.replace(/\+/g, '0')
.replace(/\//g, '0');
}
/**
* Authenticates with the EZVIZ API
* @returns Promise resolving to credentials or undefined if authentication fails
*/
async authenticate() {
if (!this.config.email || !this.config.password) {
this.log?.error('Email and password are required for authentication');
return;
}
const emailHash = crypto.createHash('md5').update(this.config.email).digest('hex');
const passHash = crypto.createHash('md5').update(this.config.password).digest('hex');
const data = querystring.stringify({
account: this.config.email,
featureCode: emailHash,
password: passHash,
});
const config = {
method: 'post',
url: `${this.config.domain}${EZVIZ_AUTH_ENDPOINT}`,
headers: {
'content-type': 'application/x-www-form-urlencoded',
'clienttype': EZVIZ_CLIENT_TYPE,
'user-agent': EZVIZ_USER_AGENT,
},
data,
};
try {
const response = await axios(config);
const auth = response.data;
if (auth.retcode) {
this.log?.error(`Login error: ${auth.retcode}`);
return;
}
if (auth.meta?.code === 6002) {
this.log?.error('2 Factor Authentication accounts are not supported at this time.');
return;
}
if (auth.meta?.code !== 200) {
this.log?.error('Login error code:', auth.meta?.code);
return;
}
if (auth.loginSession?.sessionId) {
const login = auth;
const credentials = {
sessionId: login.loginSession.sessionId,
rfSessionId: login.loginSession.rfSessionId,
featureCode: emailHash,
cuName: this.randomStr(24),
};
this.sessionId = login.loginSession.sessionId;
this.config.credentials = credentials;
return credentials;
}
else {
this.log?.error('No sessionId found in login response');
return;
}
}
catch (error) {
this.log?.error('Unable to login:', error);
throw error;
}
}
/**
* Gets the domain URL for the specified region
* @param id - The region ID
* @returns Promise resolving to the domain URL
*/
async getDomain(id) {
if (id === RUSSIA_AREA_ID) {
return `https://${RUSSIA_DOMAIN}`;
}
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'clientType': EZVIZ_CLIENT_TYPE,
'User-Agent': EZVIZ_USER_AGENT,
};
const domainReq = {
headers: headers,
method: 'POST',
url: `${EZVIZ_BASE_API_URL}${EZVIZ_DOMAINS_ENDPOINT}`,
data: querystring.stringify({
areaId: id,
}),
};
try {
const response = await axios(domainReq);
const domain = response.data;
if (!domain.domain) {
throw new Error('Invalid domain response from API');
}
return `https://${domain.domain}`;
}
catch (error) {
this.log?.error('Error fetching domain:', error);
throw error;
}
}
/**
* Lists all devices for the authenticated user
* @returns Promise resolving to device list or undefined if failed
*/
async listDevices() {
if (!this.sessionId) {
try {
await this.authenticate();
}
catch (error) {
this.log?.error('Failed to authenticate before listing devices:', error);
return;
}
}
try {
const query = querystring.stringify({
filter: 'CONNECTION,SWITCH,STATUS,NODISTURB,P2P,FEATURE,DETECTOR',
groupId: -1,
limit: 30,
offset: 0,
});
const info = await sendRequest(this.config, this.config.domain, `${EZVIZ_DEVICES_ENDPOINT}?${query}`, 'GET');
return info;
}
catch (error) {
this.log?.error('Error fetching devices:', error);
throw error;
}
}
/**
* Sets the state of a switch/plug
* @param serialNumber - The device serial number
* @param type - The switch type
* @param value - The value to set (true/false)
*/
async setSwitchState(serialNumber, type, value) {
if (!serialNumber) {
throw new Error('Serial number is required');
}
if (!this.sessionId) {
try {
await this.authenticate();
}
catch (error) {
this.log?.error('Failed to authenticate before setting switch state:', error);
throw error;
}
}
const config = {
method: 'post',
url: `${this.config.domain}${EZVIZ_SWITCH_STATUS_ENDPOINT}`,
headers: {
'sessionid': this.sessionId,
'clienttype': EZVIZ_CLIENT_TYPE,
'user-agent': EZVIZ_USER_AGENT,
},
data: querystring.stringify({
channel: 0,
clientType: 1,
enable: value ? 1 : 0,
serial: serialNumber,
type: type,
}),
};
try {
const response = await axios(config);
if (response.data?.retcode) {
throw new Error(`Switch state update failed: ${response.data.retcode}`);
}
return response.data;
}
catch (error) {
this.log?.error('Error setting switch state:', error);
throw error;
}
}
/**
* Gets the current state of a switch/plug
* @param serialNumber - The device serial number
* @param type - The switch type
* @returns Promise resolving to the switch state
*/
async getSwitchState(serialNumber, type) {
if (!serialNumber) {
throw new Error('Serial number is required');
}
if (!this.sessionId) {
try {
await this.authenticate();
}
catch (error) {
this.log?.error('Failed to authenticate before getting switch state:', error);
throw error;
}
}
const deviceList = await this.listDevices();
if (!deviceList) {
const message = 'No devices found';
this.log?.debug(message);
throw new Error(message);
}
const deviceInfo = deviceList.deviceInfos?.find((device) => device.deviceSerial === serialNumber);
if (!deviceInfo) {
const message = `Device with serial ${serialNumber} was not found`;
this.log?.debug(message);
throw new Error(message);
}
if (deviceInfo?.status !== 1) {
const message = `Device with serial ${serialNumber} is offline`;
this.log?.debug(message);
throw new Error(message);
}
const deviceSwitch = deviceList.SWITCH?.[serialNumber]?.find((device) => device.type === type);
if (!deviceSwitch) {
const message = `Switch for device serial ${serialNumber} was not found`;
this.log?.debug(message);
throw new Error(message);
}
return deviceSwitch?.enable;
}
}
//# sourceMappingURL=ezviz-api.js.map