Initial commit: tischlerctrl home automation project
This commit is contained in:
208
agents/ac-infinity/src/ac-client.js
Normal file
208
agents/ac-infinity/src/ac-client.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 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
|
||||
const normalizedPassword = this.password.substring(0, 25);
|
||||
|
||||
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; charset=utf-8',
|
||||
},
|
||||
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) {
|
||||
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}`;
|
||||
|
||||
// Normalize device name for use as identifier
|
||||
const deviceId = devName
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
// Extract sensor data from device settings or sensor fields
|
||||
// Temperature is stored as Celsius * 100
|
||||
if (device.devSettings?.temperature !== undefined) {
|
||||
readings.push({
|
||||
device: deviceId,
|
||||
channel: 'temperature',
|
||||
value: device.devSettings.temperature / 100,
|
||||
});
|
||||
} else if (device.temperature !== undefined) {
|
||||
readings.push({
|
||||
device: deviceId,
|
||||
channel: 'temperature',
|
||||
value: device.temperature / 100,
|
||||
});
|
||||
}
|
||||
|
||||
// Humidity is stored as % * 100
|
||||
if (device.devSettings?.humidity !== undefined) {
|
||||
readings.push({
|
||||
device: deviceId,
|
||||
channel: 'humidity',
|
||||
value: device.devSettings.humidity / 100,
|
||||
});
|
||||
} else if (device.humidity !== undefined) {
|
||||
readings.push({
|
||||
device: deviceId,
|
||||
channel: 'humidity',
|
||||
value: device.humidity / 100,
|
||||
});
|
||||
}
|
||||
|
||||
// VPD if available
|
||||
if (device.devSettings?.vpdnums !== undefined) {
|
||||
readings.push({
|
||||
device: deviceId,
|
||||
channel: 'vpd',
|
||||
value: device.devSettings.vpdnums / 100,
|
||||
});
|
||||
}
|
||||
|
||||
// Check for port-level sensors (some controllers have multiple ports)
|
||||
if (device.devPortList && Array.isArray(device.devPortList)) {
|
||||
for (const port of device.devPortList) {
|
||||
const portId = port.portId || port.port;
|
||||
const portDeviceId = `${deviceId}-port${portId}`;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return readings;
|
||||
}
|
||||
}
|
||||
|
||||
export default ACInfinityClient;
|
||||
Reference in New Issue
Block a user