u
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user