From eeaaac1153a86c4b7c59e80c17f57108cefcd358 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sat, 20 Dec 2025 17:52:57 +0100 Subject: [PATCH] Genesis --- .gitignore | 3 + API_REFERENCE.md | 348 +++++++++++++++++++++++++++++++++ daemon.js | 168 ++++++++++++++++ package-lock.json | 479 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 17 ++ test-ac-api.js | 184 ++++++++++++++++++ 6 files changed, 1199 insertions(+) create mode 100644 .gitignore create mode 100644 API_REFERENCE.md create mode 100644 daemon.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 test-ac-api.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..837f7f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +.DS_Store diff --git a/API_REFERENCE.md b/API_REFERENCE.md new file mode 100644 index 0000000..fb4192a --- /dev/null +++ b/API_REFERENCE.md @@ -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.* \ No newline at end of file diff --git a/daemon.js b/daemon.js new file mode 100644 index 0000000..df34eab --- /dev/null +++ b/daemon.js @@ -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); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d9459ed --- /dev/null +++ b/package-lock.json @@ -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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e6b629 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/test-ac-api.js b/test-ac-api.js new file mode 100644 index 0000000..a59b14b --- /dev/null +++ b/test-ac-api.js @@ -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} 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} 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();