This commit is contained in:
sebseb7
2025-12-25 05:30:06 +01:00
parent 8f01f35470
commit ab89cbc97f
4 changed files with 195 additions and 2 deletions

View File

@@ -229,6 +229,115 @@ export class ACInfinityClient {
return readings; return readings;
} }
async getDeviceModeSettings(devId, port) {
if (!this.isLoggedIn()) {
await this.login();
}
try {
const response = await fetch(`${this.host}/api/dev/getdevModeSettingList`, {
method: 'POST',
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',
...this.getAuthHeaders(),
},
body: new URLSearchParams({
devId: devId,
port: port.toString()
})
});
const data = await response.json();
return data.data;
} catch (err) {
console.error('[AC] Error getting device settings:', err);
throw err;
}
}
/**
* Set device port mode/level
* @param {string} devId - Device ID
* @param {number} port - Port number (1-4)
* @param {number} level - Level 0-10
*/
async setDevicePort(devId, port, level) {
if (!this.isLoggedIn()) {
await this.login();
}
try {
// 1. Get existing settings
const settings = await this.getDeviceModeSettings(devId, port);
if (!settings) throw new Error('Could not fetch existing settings');
// 2. Prepare updates
// Constrain level 0-10
const safeLevel = Math.max(0, Math.min(10, Math.round(level)));
// Mode 1 = ON (Manual), 0 = OFF
const mode = safeLevel === 0 ? 0 : 1;
// Merge with existing settings
// We need to send back mostly specific keys.
// Based on reference usage, we can try merging into a new object using existing keys
// but 'mode' and 'speak' are overrides.
const params = new URLSearchParams();
// Add required base params
params.append('userId', this.userId);
params.append('devId', devId);
params.append('port', port.toString());
// Add mode/speak
params.append('mode', mode.toString());
params.append('speak', safeLevel.toString());
// Copy other relevant fields from settings if they exist to maintain state
// Common fields seen in other implementations:
// transitionType, surplus, backup, trigger related fields...
// For addDevMode, usually just the basics + what we want to change is enough IF the server merges?
// But the error 999999 suggests missing fields.
// Let's copy everything from settings that looks like a config parameter
const keyBlocklist = ['devId', 'port', 'mode', 'speak', 'devName', 'deviceInfo', 'devType', 'macAddr'];
for (const [key, val] of Object.entries(settings)) {
if (!keyBlocklist.includes(key) && typeof val !== 'object') {
params.append(key, val);
}
}
// Ensure defaults if missing
if (!params.has('surplus')) params.append('surplus', '0');
if (!params.has('backup')) params.append('backup', '0');
if (!params.has('transitionType')) params.append('transitionType', '0');
// 3. Send update
const response = await fetch(`${this.host}/api/dev/addDevMode?${params.toString()}`, {
method: 'POST',
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',
...this.getAuthHeaders(),
},
});
const data = await response.json();
if (data.code !== 200) {
throw new ACInfinityClientError(`Set mode failed: ${JSON.stringify(data)}`);
}
console.log(`[AC] Set device ${devId} port ${port} to level ${safeLevel} (mode ${mode})`);
return true;
} catch (error) {
console.error('[AC] Error setting device port:', error);
return false;
}
}
} }
export default ACInfinityClient; export default ACInfinityClient;

View File

@@ -60,6 +60,68 @@ async function main() {
// Connect to WebSocket server // Connect to WebSocket server
await wsClient.connect(); await wsClient.connect();
// Handle commands from server
wsClient.onCommand(async (cmd) => {
console.log('[Main] Received command:', cmd);
const { device, value } = cmd; // e.g. device="tent:fan"
if (!device) return;
try {
// Fetch latest device list to get IDs and port mapping
const devices = await acClient.getDevicesListAll();
// Parse "tent:fan" -> devName="tent", portName="fan"
// If just "tent", assume port 1 or device level
const parts = device.split(':');
const targetDevName = parts[0];
const targetPortName = parts[1];
// Find matching device by name
const dev = devices.find(d => {
const name = (d.devName || `device-${d.devId}`).toLowerCase();
return name.includes(targetDevName.toLowerCase());
});
if (!dev) {
console.error(`[Main] Device not found: ${targetDevName}`);
return;
}
// Find port index
// Structure varies: dev.deviceInfo.ports OR dev.devPortList
const info = dev.deviceInfo || dev;
const ports = info.ports || dev.devPortList || [];
let portId = 0; // 0 usually means "All" or "Device"? But setDevicePort expects 1-4.
// If explicit port set
if (targetPortName) {
const port = ports.find(p => {
const pName = (p.portName || `port${p.port || p.portId}`).toLowerCase();
return pName.includes(targetPortName.toLowerCase());
});
if (port) {
portId = port.port || port.portId;
} else {
// Check if it's a number
const pNum = parseInt(targetPortName);
if (!isNaN(pNum)) portId = pNum;
}
} else {
// Default to first port if available, or 0?
// Let's assume port 1 if no specific port requested but ports exist
if (ports.length > 0) portId = ports[0].port || ports[0].portId;
}
console.log(`[Main] Setting ${dev.devName} (${dev.devId}) port ${portId} to ${value}`);
await acClient.setDevicePort(dev.devId, portId, value);
} catch (err) {
console.error('[Main] Error handling command:', err);
}
});
// Start polling // Start polling
console.log(`[Main] Starting polling every ${config.pollIntervalMs / 1000}s`); console.log(`[Main] Starting polling every ${config.pollIntervalMs / 1000}s`);

View File

@@ -22,6 +22,11 @@ export class WSClient {
this.pingTimer = null; this.pingTimer = null;
this.messageQueue = []; this.messageQueue = [];
this.onReadyCallback = null; this.onReadyCallback = null;
this.onCommandCallback = null;
}
onCommand(callback) {
this.onCommandCallback = callback;
} }
/** /**
@@ -104,6 +109,12 @@ export class WSClient {
console.error('[WS] Server error:', message.error); console.error('[WS] Server error:', message.error);
break; break;
case 'command':
if (this.onCommandCallback) {
this.onCommandCallback(message);
}
break;
default: default:
console.log('[WS] Unknown message type:', message.type); console.log('[WS] Unknown message type:', message.type);
} }

View File

@@ -28,6 +28,7 @@ const OUTPUT_BINDINGS = {
'BigDehumid': { device: 'tapo', channel: 'r0', type: 'switch' }, 'BigDehumid': { device: 'tapo', channel: 'r0', type: 'switch' },
'CO2Valve': { device: 'tapo', channel: 'c', type: 'switch' }, 'CO2Valve': { device: 'tapo', channel: 'c', type: 'switch' },
'TentExhaust': { device: 'tapo', channel: 'fantent', type: 'switch' }, 'TentExhaust': { device: 'tapo', channel: 'fantent', type: 'switch' },
'CircFanLevel': { device: 'ac', channel: 'tent:fan', type: 'level' },
}; };
// ============================================= // =============================================
@@ -739,7 +740,6 @@ module.exports = {
function writeOutputValue(channel, value) { function writeOutputValue(channel, value) {
const now = new Date().toISOString(); const now = new Date().toISOString();
// Get last value for this channel
const lastStmt = db.prepare(` const lastStmt = db.prepare(`
SELECT id, value FROM output_events SELECT id, value FROM output_events
WHERE channel = ? WHERE channel = ?
@@ -747,6 +747,10 @@ module.exports = {
`); `);
const last = lastStmt.get(channel); const last = lastStmt.get(channel);
if (channel === 'CircFanLevel') {
console.log('[RuleRunner] Debug Bindings:', JSON.stringify(OUTPUT_BINDINGS['CircFanLevel']));
}
const valueChanged = !last || Math.abs(last.value - value) >= Number.EPSILON; const valueChanged = !last || Math.abs(last.value - value) >= Number.EPSILON;
if (!valueChanged) { if (!valueChanged) {
@@ -765,10 +769,17 @@ module.exports = {
// Send command to bound physical device // Send command to bound physical device
const binding = OUTPUT_BINDINGS[channel]; const binding = OUTPUT_BINDINGS[channel];
if (binding) { if (binding) {
let commandValue = value;
if (binding.type === 'switch') {
commandValue = value > 0 ? 1 : 0;
}
console.log(`[RuleRunner] Binding for ${channel}: type=${binding.type}, val=${value}, cmdVal=${commandValue}`);
sendCommandToDevicePrefix(`${binding.device}:`, { sendCommandToDevicePrefix(`${binding.device}:`, {
device: binding.channel, device: binding.channel,
action: 'set_state', action: 'set_state',
value: value > 0 ? 1 : 0 value: commandValue
}); });
} }
} }