diff --git a/agents/ac-infinity/src/ac-client.js b/agents/ac-infinity/src/ac-client.js index 3b21046..3ba31a5 100644 --- a/agents/ac-infinity/src/ac-client.js +++ b/agents/ac-infinity/src/ac-client.js @@ -229,6 +229,115 @@ export class ACInfinityClient { 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; diff --git a/agents/ac-infinity/src/index.js b/agents/ac-infinity/src/index.js index b998f92..c58ee2d 100644 --- a/agents/ac-infinity/src/index.js +++ b/agents/ac-infinity/src/index.js @@ -60,6 +60,68 @@ async function main() { // Connect to WebSocket server 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 console.log(`[Main] Starting polling every ${config.pollIntervalMs / 1000}s`); diff --git a/agents/ac-infinity/src/ws-client.js b/agents/ac-infinity/src/ws-client.js index c71d344..d603f51 100644 --- a/agents/ac-infinity/src/ws-client.js +++ b/agents/ac-infinity/src/ws-client.js @@ -22,6 +22,11 @@ export class WSClient { this.pingTimer = null; this.messageQueue = []; 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); break; + case 'command': + if (this.onCommandCallback) { + this.onCommandCallback(message); + } + break; + default: console.log('[WS] Unknown message type:', message.type); } diff --git a/uiserver/webpack.config.js b/uiserver/webpack.config.js index 1dd17ec..4389447 100644 --- a/uiserver/webpack.config.js +++ b/uiserver/webpack.config.js @@ -28,6 +28,7 @@ const OUTPUT_BINDINGS = { 'BigDehumid': { device: 'tapo', channel: 'r0', type: 'switch' }, 'CO2Valve': { device: 'tapo', channel: 'c', 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) { const now = new Date().toISOString(); - // Get last value for this channel const lastStmt = db.prepare(` SELECT id, value FROM output_events WHERE channel = ? @@ -747,6 +747,10 @@ module.exports = { `); 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; if (!valueChanged) { @@ -765,10 +769,17 @@ module.exports = { // Send command to bound physical device const binding = OUTPUT_BINDINGS[channel]; 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}:`, { device: binding.channel, action: 'set_state', - value: value > 0 ? 1 : 0 + value: commandValue }); } }