235 lines
8.0 KiB
JavaScript
235 lines
8.0 KiB
JavaScript
/**
|
|
* AC Infinity API Client
|
|
* Ported from TypeScript homebridge-acinfinity plugin
|
|
*/
|
|
|
|
const API_URL_LOGIN = '/api/user/appUserLogin';
|
|
const API_URL_GET_DEVICE_INFO_LIST_ALL = '/api/user/devInfoListAll';
|
|
|
|
export class ACInfinityClientError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = 'ACInfinityClientError';
|
|
}
|
|
}
|
|
|
|
export class ACInfinityClientCannotConnect extends ACInfinityClientError {
|
|
constructor() {
|
|
super('Cannot connect to AC Infinity API');
|
|
}
|
|
}
|
|
|
|
export class ACInfinityClientInvalidAuth extends ACInfinityClientError {
|
|
constructor() {
|
|
super('Invalid authentication credentials');
|
|
}
|
|
}
|
|
|
|
export class ACInfinityClient {
|
|
constructor(host, email, password) {
|
|
this.host = host;
|
|
this.email = email;
|
|
this.password = password;
|
|
this.userId = null;
|
|
}
|
|
|
|
async login() {
|
|
try {
|
|
// AC Infinity API does not accept passwords greater than 25 characters - UPDATE: Reference impl uses full password?
|
|
// const normalizedPassword = this.password.substring(0, 25);
|
|
const normalizedPassword = this.password;
|
|
|
|
const response = await fetch(`${this.host}${API_URL_LOGIN}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'User-Agent': 'ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2',
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams({
|
|
appEmail: this.email,
|
|
appPasswordl: normalizedPassword, // Note: intentional typo in API
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.code !== 200) {
|
|
if (data.code === 10001) {
|
|
throw new ACInfinityClientInvalidAuth();
|
|
}
|
|
throw new ACInfinityClientError(`Login failed: ${JSON.stringify(data)}`);
|
|
}
|
|
|
|
this.userId = data.data.appId;
|
|
console.log('[AC] Successfully logged in to AC Infinity API');
|
|
return this.userId;
|
|
} catch (error) {
|
|
console.error('[AC] Login error details:', error); // Added detailed logging
|
|
if (error instanceof ACInfinityClientError) {
|
|
throw error;
|
|
}
|
|
throw new ACInfinityClientCannotConnect();
|
|
}
|
|
}
|
|
|
|
isLoggedIn() {
|
|
return this.userId !== null;
|
|
}
|
|
|
|
getAuthHeaders() {
|
|
if (!this.userId) {
|
|
throw new ACInfinityClientError('Client is not logged in');
|
|
}
|
|
return {
|
|
token: this.userId,
|
|
phoneType: '1',
|
|
appVersion: '1.9.7',
|
|
};
|
|
}
|
|
|
|
async getDevicesListAll() {
|
|
if (!this.isLoggedIn()) {
|
|
throw new ACInfinityClientError('AC Infinity client is not logged in');
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${this.host}${API_URL_GET_DEVICE_INFO_LIST_ALL}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'User-Agent': 'ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2',
|
|
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
|
...this.getAuthHeaders(),
|
|
},
|
|
body: new URLSearchParams({
|
|
userId: this.userId,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.code !== 200) {
|
|
throw new ACInfinityClientError(`Request failed: ${JSON.stringify(data)}`);
|
|
}
|
|
|
|
return data.data || [];
|
|
} catch (error) {
|
|
if (error instanceof ACInfinityClientError) {
|
|
throw error;
|
|
}
|
|
throw new ACInfinityClientCannotConnect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract sensor readings from device list
|
|
* @returns {Array} Array of {device, channel, value} objects
|
|
*/
|
|
async getSensorReadings() {
|
|
const devices = await this.getDevicesListAll();
|
|
const readings = [];
|
|
|
|
for (const device of devices) {
|
|
const devId = device.devId;
|
|
const devName = device.devName || `device-${devId}`;
|
|
// Use deviceInfo if available (newer API structure), otherwise fallback to root/devSettings
|
|
const info = device.deviceInfo || device;
|
|
const settings = device.devSettings || info;
|
|
|
|
// Normalize device name for use as identifier
|
|
const deviceId = devName
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
|
|
// --- Device Level Sensors ---
|
|
|
|
// Temperature (Celsius * 100)
|
|
if (info.temperature !== undefined) {
|
|
readings.push({
|
|
device: deviceId,
|
|
channel: 'temperature',
|
|
value: info.temperature / 100,
|
|
});
|
|
} else if (settings.temperature !== undefined) {
|
|
readings.push({
|
|
device: deviceId,
|
|
channel: 'temperature',
|
|
value: settings.temperature / 100,
|
|
});
|
|
}
|
|
|
|
// Humidity (% * 100)
|
|
if (info.humidity !== undefined) {
|
|
readings.push({
|
|
device: deviceId,
|
|
channel: 'humidity',
|
|
value: info.humidity / 100,
|
|
});
|
|
} else if (settings.humidity !== undefined) {
|
|
readings.push({
|
|
device: deviceId,
|
|
channel: 'humidity',
|
|
value: settings.humidity / 100,
|
|
});
|
|
}
|
|
|
|
// VPD
|
|
if (info.vpdnums !== undefined) {
|
|
readings.push({
|
|
device: deviceId,
|
|
channel: 'vpd',
|
|
value: info.vpdnums / 100,
|
|
});
|
|
} else if (settings.vpdnums !== undefined) {
|
|
readings.push({
|
|
device: deviceId,
|
|
channel: 'vpd',
|
|
value: settings.vpdnums / 100,
|
|
});
|
|
}
|
|
|
|
// --- Port Level Sensors/State ---
|
|
const ports = info.ports || device.devPortList;
|
|
if (ports && Array.isArray(ports)) {
|
|
for (const port of ports) {
|
|
const portId = port.port || port.portId;
|
|
const portName = port.portName || `port${portId}`;
|
|
// Create a descriptive suffix for the port device, e.g. "wall-fan" or "wall-port1"
|
|
// If portName is generic "Port X", use number. If it's specific "Fan", use that.
|
|
const suffix = portName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
const portDeviceId = `${deviceId}-${suffix}`;
|
|
|
|
// Port specific sensors (if any - sometimes temp usually on device)
|
|
if (port.temperature !== undefined) {
|
|
readings.push({
|
|
device: portDeviceId,
|
|
channel: 'temperature',
|
|
value: port.temperature / 100,
|
|
});
|
|
}
|
|
if (port.humidity !== undefined) {
|
|
readings.push({
|
|
device: portDeviceId,
|
|
channel: 'humidity',
|
|
value: port.humidity / 100,
|
|
});
|
|
}
|
|
|
|
// Level / Speed (speak)
|
|
if (port.speak !== undefined) {
|
|
readings.push({
|
|
device: portDeviceId,
|
|
channel: 'level',
|
|
value: port.speak,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return readings;
|
|
}
|
|
}
|
|
|
|
export default ACInfinityClient;
|