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