Genesis
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
348
API_REFERENCE.md
Normal file
348
API_REFERENCE.md
Normal 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
168
daemon.js
Normal 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
479
package-lock.json
generated
Normal 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
17
package.json
Normal 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
184
test-ac-api.js
Normal 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();
|
||||||
Reference in New Issue
Block a user