This commit is contained in:
sebseb7
2025-12-20 17:52:57 +01:00
commit eeaaac1153
6 changed files with 1199 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
.env
.DS_Store

348
API_REFERENCE.md Normal file
View File

@@ -0,0 +1,348 @@
# AC Infinity API Reference
This document provides technical details about the AC Infinity API integration used by this Homebridge plugin. It covers the API endpoints, payload formats, and controller-specific approaches discovered through reverse engineering.
## API Base URL
```
http://www.acinfinityserver.com
```
## Authentication
The API uses simple token-based authentication:
1. Login with email/password to get a user token
2. Include token in subsequent requests via the `token` header
### Login Request
```http
POST /api/user/appUserLogin
Content-Type: application/x-www-form-urlencoded
appEmail=user@example.com&appPasswordl=password123
```
**Note**: The API parameter is `appPasswordl` (with 'l') - this is intentional and matches the official API.
**Password Limitation**: API only accepts first 25 characters of password.
### Login Response
```json
{
"msg": "success.",
"code": 200,
"data": {
"appId": "1234567890123456789",
"nickName": "user@example.com",
"appEmail": "user@example.com"
}
}
```
The `appId` is used as the authentication token for subsequent requests.
## Core Endpoints
### Get Device List
```http
POST /api/user/devInfoListAll
Content-Type: application/x-www-form-urlencoded
userId=1234567890123456789
```
Headers:
```
token: 1234567890123456789
phoneType: 1
appVersion: 1.9.7
```
### Get Device Mode Settings
```http
POST /api/dev/getdevModeSettingList
Content-Type: application/x-www-form-urlencoded
devId=1234567890123456789&port=2
```
Headers:
```
token: 1234567890123456789
phoneType: 1
appVersion: 1.9.7
minversion: 3.5
```
### Set Device Mode (Fan Control)
```http
POST /api/dev/addDevMode
Content-Type: application/x-www-form-urlencoded
[See Controller-Specific Payloads below]
```
## Device Types
AC Infinity manufactures different types of devices:
### Supported: Controller-Based Devices
These devices have a central controller with USB-C ports for connecting fans and sensors:
#### UIS 89 AI+ (Type 20)
- **Device Type**: 20
- **newFrameworkDevice**: true
- **API Approach**: Hardcoded static payload
- **User-Agent**: `ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2`
#### UIS 69 PRO (Type 11)
- **Device Type**: 11
- **newFrameworkDevice**: false
- **API Approach**: Static payload with real device settings
- **User-Agent**: `ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2`
#### UIS 69 PRO+ (Type 18)
- **Device Type**: 18
- **newFrameworkDevice**: false
- **API Approach**: Static payload with real device settings
- **User-Agent**: `ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2`
### Unsupported: Standalone Devices
**Airtap T4/T6 Register Booster Fans** - These standalone Wi-Fi devices don't have a controller or ports structure. The API likely returns device data without the `deviceInfo.ports` array.
**Status**: Not yet supported. Need device data from actual Airtap users to implement support.
**Workaround**: Use [ESP32 module replacement](https://silocitylabs.com/post/2025/esp32-airtap-esphome/) for ESPHome/Home Assistant integration.
## Controller-Specific Payloads
### UIS 89 AI+ (Hardcoded Static Payload)
For newer controllers, use a static payload with hardcoded values:
```http
POST /api/dev/addDevMode
Content-Type: application/x-www-form-urlencoded
acitveTimerOff=0&acitveTimerOn=0&activeCycleOff=0&activeCycleOn=0&activeHh=0&activeHt=0&activeHtVpd=0&activeHtVpdNums=0&activeLh=0&activeLt=0&activeLtVpd=0&activeLtVpdNums=0&atType=2&co2FanHighSwitch=0&co2FanHighValue=0&co2LowSwitch=0&co2LowValue=0&devHh=0&devHt=0&devHtf=32&devId=1234567890123456789&devLh=0&devLt=0&devLtf=32&devMacAddr=&ecOrTds=0&ecTdsLowSwitchEc=0&ecTdsLowSwitchTds=0&ecTdsLowValueEcMs=1&ecTdsLowValueEcUs=0&ecTdsLowValueTdsPpm=0&ecTdsLowValueTdsPpt=1&ecUnit=0&externalPort=1&hTrend=0&humidity=0&isOpenAutomation=0&masterPort=0&modeType=2&moistureLowSwitch=0&moistureLowValue=0&offSpead=0&onSelfSpead=7&onSpead=7&onlyUpdateSpeed=0&phHighSwitch=0&phHighValue=0&phLowSwitch=0&phLowValue=0&schedEndtTime=65535&schedStartTime=65535&settingMode=0&speak=0&surplus=0&tTrend=0&targetHumi=0&targetHumiSwitch=0&targetTSwitch=0&targetTemp=0&targetTempF=32&targetVpd=0&targetVpdSwitch=0&tdsUnit=0&temperature=0&temperatureF=0&trend=0&unit=0&vpdSettingMode=0&waterLevelLowSwitch=0&waterTempHighSwitch=0&waterTempHighValue=0&waterTempHighValueF=32&waterTempLowSwitch=0&waterTempLowValue=0&waterTempLowValueF=32
```
**Key Parameters**:
- `onSpead=7`: Target fan speed (0-10)
- `modeType=2`: Set to ON mode (use `0` to turn off)
- `externalPort=1`: Port number
Headers:
```
token: 1234567890123456789
phoneType: 1
appVersion: 1.9.7
minversion: 3.5
```
### UIS 69 PRO (Static Payload with Real Settings)
For older controllers, first fetch current settings, then send them in static payload format:
1. **Fetch Current Settings** (as shown above)
2. **Send Static Payload** with real values:
```http
POST /api/dev/addDevMode
Content-Type: application/x-www-form-urlencoded
acitveTimerOff=[REAL_VALUE]&acitveTimerOn=[REAL_VALUE]&activeCycleOff=[REAL_VALUE]&...&onSpead=7&...
```
Headers:
```
token: 1234567890123456789
phoneType: 1
appVersion: 1.9.7
```
**Critical Differences**:
- Populate payload with actual device settings (not zeros)
- **Omit the `modeSetid` field** (this causes 403 errors)
- Set `modeType=2` when `onSpead > 0` to activate the fan (or `modeType=0` to turn off)
- Only change the `onSpead` and `modeType` parameters
- Keep all other values as retrieved from current settings
## Key API Fields
### Fan Speed Control
- **onSpead**: Target fan speed (0-10)
- **speak**: Current fan power level (0-10, read-only)
- **onSelfSpead**: Self-regulating speed setting
### Device Information
- **devId**: Device identifier
- **externalPort**: Port number (1-8 depending on controller)
- **devType**: Controller type (11=UIS 69 PRO, 20=UIS 89 AI+, 18=UIS 69 PRO+)
- **newFrameworkDevice**: Boolean indicating API approach needed
### Environmental Data
- **temperature**: Temperature (×100, e.g., 2366 = 23.66°C)
- **humidity**: Humidity (×100, e.g., 5118 = 51.18%)
- **vpdnums**: VPD value (×100, e.g., 143 = 1.43 kPa)
### Mode Detection
- **curMode**: Current operating mode (read-only status)
- `1` = OFF
- `2` = ON (Manual)
- `3` = AUTO
- `8` = VPD
- **modeType**: Mode to set when changing settings
- `0` = OFF
- `2` = ON (Manual)
- **Important**: Must set `modeType=2` when changing speed to activate the fan
### Port Status
- **online**: Port connection status (0/1)
- **loadState**: Load detection (0/1)
- **portResistance**: Port resistance reading
## Error Codes
- **200**: Success
- **403**: "Data saving failed" (rate limiting or invalid payload)
- **404**: Endpoint not found
- **500**: Invalid credentials
- **10001**: Authentication failed
- **100001**: Generic request error
- **999999**: Operation failed (usually unsupported controller)
## Common Issues
### Speed Changes Not Persisting
**Symptom**: API returns 200 success, but controller doesn't change speed. Speed reverts to 0 after a few seconds.
**Root Cause**: Controller is in OFF mode (`curMode: 1`). The API accepts speed changes but the controller ignores them when not activated.
**Solution**: Always set `modeType=2` (ON) when setting `onSpead > 0`. Set `modeType=0` when turning off.
**Example**:
```
// Wrong - speed won't persist if controller is OFF
onSpead=5&modeType=0
// Correct - activates controller and sets speed
onSpead=5&modeType=2
// Correct - turns off controller
onSpead=0&modeType=0
```
### "Data saving failed" (403)
**Symptom**: 403 error when trying to set fan speed on UIS 69 PRO.
**Root Cause**: Including `modeSetid` field or using wrong payload format.
**Solution**:
- For UIS 69 PRO: Use static payload with real device settings (NO `modeSetid`)
- For UIS 89 AI+: Use hardcoded static payload
## Rate Limiting
The API implements connection-based rate limiting:
- Multiple requests from different connections are treated as different clients
- Use persistent HTTP connections (keepalive) to avoid rate limits
- Implement request queuing with reasonable delays (500ms between requests)
## Network Analysis
This API documentation was created through:
1. **Charles Proxy Analysis**: Captured official AC Infinity iPhone app network traffic
2. **Home Assistant Integration**: Analyzed working Home Assistant plugin implementation
3. **Live Testing**: Tested with actual UIS 69 PRO and UIS 89 AI+ hardware
4. **Reverse Engineering**: Discovered controller-specific approaches through trial and error
## Security Notes
⚠️ **Important Security Considerations**:
- API credentials (email/password) are transmitted in plain text
- Authentication tokens have no visible expiration
- All API communication happens over HTTP (not HTTPS)
- This API is intended for local network use with AC Infinity controllers
- Never expose API credentials in public repositories or logs
## Implementation Notes
### HTTP Client Configuration
```javascript
const axios = axios.create({
baseURL: 'http://www.acinfinityserver.com',
timeout: 15000,
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',
},
httpAgent: new Agent({ keepAlive: true, maxSockets: 1 }),
httpsAgent: new HttpsAgent({ keepAlive: true, maxSockets: 1 }),
maxRedirects: 3,
validateStatus: (status) => status < 500,
});
```
### Controller Detection
```javascript
function isNewFrameworkDevice(deviceType, deviceData) {
// AI+ controllers use hardcoded static payload
if (deviceType === 20) return true;
// Check explicit framework flag
if (deviceData?.newFrameworkDevice === true) return true;
if (deviceData?.newFrameworkDevice === false) return false;
// Default: older controllers use real settings approach
return false;
}
```
### Error Handling
```javascript
if (response.data.code === 403) {
// Rate limited or invalid payload
// For UIS 69 PRO: try iPhone app approach
// For UIS 89 AI+: verify static payload format
}
if (response.data.code === 999999) {
// Unsupported controller or wrong API approach
// Switch between hardcoded vs real settings method
}
```
## Testing
A comprehensive test CLI application is included (`test-api.js`) that:
- Auto-detects controller types
- Uses appropriate API approach for each controller
- Includes fallback logic for unknown devices
- Provides detailed logging for debugging
Usage:
```bash
node test-api.js http://www.acinfinityserver.com email@example.com password123 devices
node test-api.js http://www.acinfinityserver.com email@example.com password123 speed DEVICE_ID PORT_ID SPEED
```
---
*This documentation reflects the current understanding of the AC Infinity API as of December 2025. The API may change without notice as it's not officially documented by AC Infinity.*

168
daemon.js Normal file
View File

@@ -0,0 +1,168 @@
import 'dotenv/config';
import Database from 'better-sqlite3';
// Configuration
const BASE_URL = 'http://www.acinfinityserver.com';
const USER_AGENT = 'ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2';
const POLL_INTERVAL_MS = 60000; // 60 seconds
const DB_FILE = 'ac_data.db';
// Database Setup
const db = new Database(DB_FILE);
db.exec(`
CREATE TABLE IF NOT EXISTS readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
dev_id TEXT,
dev_name TEXT,
temp_c REAL,
humidity REAL,
vpd REAL,
fan_speed INTEGER,
on_speed INTEGER,
off_speed INTEGER
)
`);
const insertStmt = db.prepare(`
INSERT INTO readings (dev_id, dev_name, temp_c, humidity, vpd, fan_speed, on_speed, off_speed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
// State
let token = null;
// Helper to check credentials
if (!process.env.AC_EMAIL || !process.env.AC_PASSWORD) {
console.error('Error: AC_EMAIL and AC_PASSWORD must be set in .env file');
process.exit(1);
}
/**
* Login to AC Infinity API
*/
async function login() {
console.log('Logging in...');
const params = new URLSearchParams();
params.append('appEmail', process.env.AC_EMAIL);
params.append('appPasswordl', process.env.AC_PASSWORD);
try {
const response = await fetch(`${BASE_URL}/api/user/appUserLogin`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT
},
body: params
});
const data = await response.json();
if (data.code === 200) {
console.log('Login successful.');
return data.data.appId;
} else {
throw new Error(`Login failed: ${data.msg} (${data.code})`);
}
} catch (error) {
console.error('Login error:', error.message);
throw error;
}
}
/**
* Get All Devices
*/
async function getDeviceList(authToken) {
const params = new URLSearchParams();
params.append('userId', authToken);
const response = await fetch(`${BASE_URL}/api/user/devInfoListAll`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
'token': authToken,
'phoneType': '1',
'appVersion': '1.9.7'
},
body: params
});
const data = await response.json();
if (data.code === 200) return data.data || [];
throw new Error(`Get device list failed: ${data.msg}`);
}
/**
* Get Settings
*/
async function getDeviceModeSettings(authToken, devId, port) {
const params = new URLSearchParams();
params.append('devId', devId);
params.append('port', port.toString());
const response = await fetch(`${BASE_URL}/api/dev/getdevModeSettingList`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
'token': authToken,
'phoneType': '1',
'appVersion': '1.9.7',
'minversion': '3.5'
},
body: params
});
const data = await response.json();
if (data.code === 200) return data.data;
console.warn(`Failed to get settings for ${devId}: ${data.msg}`);
return null;
}
/**
* Poll Function
*/
async function poll() {
try {
if (!token) {
token = await login();
}
const devices = await getDeviceList(token);
console.log(`[${new Date().toISOString()}] Found ${devices.length} devices.`);
for (const device of devices) {
const settings = await getDeviceModeSettings(token, device.devId, device.externalPort || 1);
if (settings) {
const tempC = settings.temperature ? settings.temperature / 100 : null;
const hum = settings.humidity ? settings.humidity / 100 : null;
const vpd = settings.vpdnums ? settings.vpdnums / 100 : null;
insertStmt.run(
device.devId,
device.devName,
tempC,
hum,
vpd,
settings.speak,
settings.onSpead,
settings.offSpead
);
console.log(`Saved reading for ${device.devName || device.devId}: ${tempC}°C, ${hum}%, Fan: ${settings.speak}/10`);
}
}
} catch (error) {
console.error('Polling error:', error.message);
// Reset token on error to force re-login next time if needed
token = null;
}
}
// Start Daemon
console.log(`Starting AC Infinity Data Daemon (Interval: ${POLL_INTERVAL_MS}ms)`);
poll(); // Initial run
setInterval(poll, POLL_INTERVAL_MS);

479
package-lock.json generated Normal file
View File

@@ -0,0 +1,479 @@
{
"name": "ac-infinity-test",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ac-infinity-test",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"better-sqlite3": "^12.5.0",
"dotenv": "^16.4.5"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "12.5.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
"integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.85.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "ac-infinity-test",
"version": "1.0.0",
"description": "Test script for AC Infinity API",
"main": "test-ac-api.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node test-ac-api.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"better-sqlite3": "^12.5.0",
"dotenv": "^16.4.5"
}
}

184
test-ac-api.js Normal file
View File

@@ -0,0 +1,184 @@
import 'dotenv/config';
// Configuration
const BASE_URL = 'http://www.acinfinityserver.com';
const USER_AGENT = 'ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2';
// Helper to check credentials
if (!process.env.AC_EMAIL || !process.env.AC_PASSWORD) {
console.error('Error: AC_EMAIL and AC_PASSWORD must be set in .env file');
process.exit(1);
}
/**
* Login to AC Infinity API
* @returns {Promise<string>} userId (token)
*/
async function login() {
console.log('Attempting login...');
const params = new URLSearchParams();
params.append('appEmail', process.env.AC_EMAIL);
params.append('appPasswordl', process.env.AC_PASSWORD); // Note: appPasswordl with 'l'
try {
const response = await fetch(`${BASE_URL}/api/user/appUserLogin`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT
},
body: params
});
const data = await response.json();
if (data.code === 200) {
console.log('Login successful!');
return data.data.appId; // This is the token
} else {
throw new Error(`Login failed: ${data.msg || 'Unknown error'} (Code: ${data.code})`);
}
} catch (error) {
console.error('Login error:', error.message);
throw error;
}
}
/**
* Get All Devices
* @param {string} token
* @returns {Promise<Array>} device list
*/
async function getDeviceList(token) {
console.log('Fetching device list...');
const params = new URLSearchParams();
params.append('userId', token);
try {
const response = await fetch(`${BASE_URL}/api/user/devInfoListAll`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
'token': token,
'phoneType': '1',
'appVersion': '1.9.7'
},
body: params
});
const data = await response.json();
if (data.code === 200) {
const devices = data.data || [];
console.log(`Found ${devices.length} devices.`);
return devices;
} else {
throw new Error(`Get device list failed: ${data.msg} (Code: ${data.code})`);
}
} catch (error) {
console.error('Get device list error:', error.message);
throw error;
}
}
/**
* Get Device Mode Settings
* @param {string} token
* @param {string} devId
* @param {number} port
*/
async function getDeviceModeSettings(token, devId, port = 1) {
console.log(`Fetching settings for device ${devId}, port ${port}...`);
const params = new URLSearchParams();
params.append('devId', devId);
params.append('port', port.toString());
try {
const response = await fetch(`${BASE_URL}/api/dev/getdevModeSettingList`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
'token': token,
'phoneType': '1',
'appVersion': '1.9.7',
'minversion': '3.5'
},
body: params
});
const data = await response.json();
if (data.code === 200) {
console.log('Settings retrieved successfully.');
return data.data;
} else {
// 403 or other errors might happen
console.warn(`Get settings warning: ${data.msg} (Code: ${data.code})`);
return null;
}
} catch (error) {
console.error('Get settings error:', error.message);
return null;
}
}
// Main execution
async function main() {
try {
// 1. Login
const token = await login();
// 2. Get Devices
const devices = await getDeviceList(token);
if (devices.length === 0) {
console.log("No devices found on this account.");
return;
}
// 3. Inspect first device
const firstDevice = devices[0];
console.log('\n--- First Device Details ---');
console.log(`Name: ${firstDevice.devName}`);
console.log(`ID: ${firstDevice.devId}`);
console.log(`Mac Address: ${firstDevice.devMacAddr || 'N/A'}`);
console.log(`Type: ${firstDevice.devType}`);
console.log(`WiFi SSID: ${firstDevice.wifiName || 'N/A'}`);
console.log(`Firmware: ${firstDevice.firmwareVersion || 'N/A'}`);
console.log(`Hardware: ${firstDevice.hardwareVersion || 'N/A'}`);
// 4. Get Settings for first device
const settings = await getDeviceModeSettings(
token,
firstDevice.devId,
firstDevice.externalPort || 1
);
if (settings) {
console.log('\n--- Device Settings (First Port) ---');
const tempC = settings.temperature ? settings.temperature / 100 : 'N/A';
const tempF = settings.temperatureF ? settings.temperatureF / 100 : 'N/A';
const hum = settings.humidity ? settings.humidity / 100 : 'N/A';
const vpd = settings.vpdnums ? settings.vpdnums / 100 : 'N/A';
console.log(`Temperature: ${tempC}°C / ${tempF}°F`);
console.log(`Humidity: ${hum}%`);
console.log(`VPD: ${vpd} kPa`);
console.log(`Current Fan Speed: ${settings.speak}/10`);
console.log(`On Speed: ${settings.onSpead}/10`);
console.log(`Off Speed: ${settings.offSpead}/10`);
console.log('-----------------------------------');
}
} catch (error) {
console.error('Script failed:', error);
}
}
main();