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