Initial commit: tischlerctrl home automation project

This commit is contained in:
sebseb7
2025-12-22 23:32:55 +01:00
commit f3cca149f9
31 changed files with 3243 additions and 0 deletions

View 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;