This commit is contained in:
sebseb7
2025-12-20 21:09:44 +01:00
parent eeaaac1153
commit 3da9be5c1b
8 changed files with 1423 additions and 170 deletions

253
server.js Normal file
View File

@@ -0,0 +1,253 @@
import 'dotenv/config';
import express from 'express';
import Database from 'better-sqlite3';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// --- 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';
const PORT = 3905;
// Device Type Mapping
const DEVICE_TYPES = {
1: 'Outlet',
3: 'Fan',
7: 'Light'
};
// 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);
}
// --- DATABASE SETUP ---
// Note: Opened in Read/Write mode (default)
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,
port INTEGER,
port_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, port, port_name, temp_c, humidity, vpd, fan_speed, on_speed, off_speed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
// --- AC INFINITY API LOGIC ---
let token = null;
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;
}
}
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}`);
}
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;
}
async function poll() {
try {
if (!token) {
token = await login();
}
const devices = await getDeviceList(token);
console.log(`[${new Date().toISOString()}] Data Fetch: Found ${devices.length} controllers.`);
for (const device of devices) {
const ports = device.deviceInfo && device.deviceInfo.ports ? device.deviceInfo.ports : [];
if (ports.length === 0) {
console.warn(`Device ${device.devName} has no ports info.`);
continue;
}
for (const portInfo of ports) {
// Filter by online status
if (portInfo.online === 1) {
const port = portInfo.port;
const settings = await getDeviceModeSettings(token, device.devId, port);
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;
// Determine Port Name
let portName = portInfo.portName;
if (!portName || portName.startsWith('Port ')) {
const typeName = DEVICE_TYPES[settings.atType];
if (typeName) {
portName = typeName;
}
}
insertStmt.run(
device.devId,
device.devName,
port,
portName,
tempC,
hum,
vpd,
settings.speak,
settings.onSpead,
settings.offSpead
);
let label = 'Level';
if (portName === 'Fan') label = 'Fan Speed';
if (portName === 'Light') label = 'Brightness';
console.log(`Saved reading for ${device.devName} (${portName}): ${tempC}°C, ${hum}%, ${label}: ${settings.speak}/10`);
}
}
}
}
} catch (error) {
console.error('Polling error:', error.message);
token = null; // Reset token to force re-login
}
}
// --- EXPRESS SERVER ---
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
// API: Devices
app.get('/api/devices', (req, res) => {
try {
const stmt = db.prepare(`
SELECT DISTINCT dev_name, port, port_name
FROM readings
ORDER BY dev_name, port
`);
const rows = stmt.all();
res.json(rows);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// API: History
app.get('/api/history', (req, res) => {
try {
const { devName, port, range } = req.query;
if (!devName || !port) return res.status(400).json({ error: 'Missing devName or port' });
let timeFilter;
switch (range) {
case 'week': timeFilter = "-7 days"; break;
case 'month': timeFilter = "-30 days"; break;
case 'day': default: timeFilter = "-24 hours"; break;
}
const stmt = db.prepare(`
SELECT timestamp, temp_c, humidity, vpd, fan_speed, on_speed
FROM readings
WHERE dev_name = ? AND port = ? AND timestamp >= datetime('now', ?)
ORDER BY timestamp ASC
`);
const rows = stmt.all(devName, parseInt(port, 10), timeFilter);
res.json(rows);
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
});
// Start Server & Daemon
app.listen(PORT, '127.0.0.1', () => {
console.log(`Dashboard Server running at http://127.0.0.1:${PORT}`);
// Start Polling Loop
console.log(`Starting AC Infinity Poll Loop (Interval: ${POLL_INTERVAL_MS}ms)`);
poll(); // Initial run
setInterval(poll, POLL_INTERVAL_MS);
});