- Backend: Update Legacy API credentials to robster/rob2 - Frontend: ROOM_LOCATIONS with icons and legacyCode mapping - Device Settings: Modal picker for room selection - api.ts: Bidirectional conversion (code ↔ name) - Various UI/UX improvements across screens PRD-DEPLOYMENT.md completed (Score: 9/10) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
/**
|
|
* Legacy API Integration Service
|
|
*
|
|
* This service handles communication with the Legacy WellNuo API
|
|
* at eluxnetworks.net for device management and deployments.
|
|
*/
|
|
|
|
const axios = require('axios');
|
|
|
|
const LEGACY_API_BASE = 'https://eluxnetworks.net/function/well-api/api';
|
|
|
|
/**
|
|
* Room location codes mapping
|
|
* These numeric codes are used by Legacy API to identify room types
|
|
*/
|
|
const ROOM_LOCATIONS = {
|
|
'Bedroom': 102,
|
|
'Living Room': 103,
|
|
'Kitchen': 104,
|
|
'Bathroom': 105,
|
|
'Hallway': 106,
|
|
'Office': 107,
|
|
'Garage': 108,
|
|
'Dining Room': 109,
|
|
'Basement': 110,
|
|
'Other': 200
|
|
};
|
|
|
|
const LOCATION_NAMES = Object.fromEntries(
|
|
Object.entries(ROOM_LOCATIONS).map(([k, v]) => [v, k])
|
|
);
|
|
|
|
/**
|
|
* Get authentication token from Legacy API
|
|
* @param {string} username - Legacy API username
|
|
* @param {string} password - Legacy API password
|
|
* @returns {Promise<string>} Access token
|
|
*/
|
|
async function getLegacyToken(username, password) {
|
|
const formData = new URLSearchParams({
|
|
function: 'credentials',
|
|
user_name: username,
|
|
ps: password,
|
|
clientId: '001',
|
|
nonce: Date.now().toString()
|
|
});
|
|
|
|
const response = await axios.post(LEGACY_API_BASE, formData, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
});
|
|
|
|
if (response.data.status !== '200 OK') {
|
|
throw new Error('Legacy API authentication failed');
|
|
}
|
|
|
|
return response.data.access_token;
|
|
}
|
|
|
|
/**
|
|
* Create deployment in Legacy API
|
|
* @param {object} params - Deployment parameters
|
|
* @param {string} params.username - Installer username
|
|
* @param {string} params.token - Access token
|
|
* @param {string} params.beneficiaryName - Full name
|
|
* @param {string} params.beneficiaryEmail - Email
|
|
* @param {string} params.beneficiaryUsername - Login username
|
|
* @param {string} params.beneficiaryPassword - Password
|
|
* @param {string} params.address - Address
|
|
* @param {string} params.caretakerUsername - Caretaker username
|
|
* @param {string} params.caretakerEmail - Caretaker email
|
|
* @param {number} params.persons - Number of persons
|
|
* @param {number} params.pets - Number of pets
|
|
* @param {string} params.gender - Gender
|
|
* @param {number} params.race - Race index
|
|
* @param {number} params.born - Year born
|
|
* @param {number} params.lat - GPS latitude
|
|
* @param {number} params.lng - GPS longitude
|
|
* @param {Array<string>} params.wifis - WiFi credentials ["SSID|password", ...]
|
|
* @param {Array<number>} params.devices - Device well_ids [497, 523]
|
|
* @returns {Promise<number>} Created deployment_id
|
|
*/
|
|
async function createLegacyDeployment(params) {
|
|
const formData = new URLSearchParams({
|
|
function: 'set_deployment',
|
|
user_name: params.username,
|
|
token: params.token,
|
|
deployment: 'NEW',
|
|
beneficiary_name: params.beneficiaryName,
|
|
beneficiary_email: params.beneficiaryEmail,
|
|
beneficiary_user_name: params.beneficiaryUsername,
|
|
beneficiary_password: params.beneficiaryPassword,
|
|
beneficiary_address: params.address || 'Unknown', // Legacy API requires non-empty address
|
|
beneficiary_photo: params.beneficiaryPhoto || 'none', // Required by Legacy API, 'none' means no photo
|
|
phone_number: params.phoneNumber || '0000000000', // Required by Legacy API for email sending (must be non-empty)
|
|
caretaker_username: params.caretakerUsername || params.username,
|
|
caretaker_email: params.caretakerEmail || params.beneficiaryEmail,
|
|
persons: params.persons || 1,
|
|
pets: params.pets || 0,
|
|
gender: params.gender || 'Male', // Use 'Male' as default, 'Other' causes issues
|
|
race: params.race || 0,
|
|
born: params.born || new Date().getFullYear() - 65,
|
|
lat: params.lat || 40.7128, // Default to NYC coordinates
|
|
lng: params.lng || -74.0060,
|
|
gps_age: params.gpsAge || 0, // Required by Legacy API
|
|
wifis: JSON.stringify(params.wifis || []),
|
|
devices: JSON.stringify(params.devices || []),
|
|
reuse_existing_devices: params.devices && params.devices.length > 0 ? 1 : 0,
|
|
signature: 'wellnuo-api', // Required to avoid None error in SendWelcomeBeneficiaryEmail
|
|
skip_email: 1 // Skip welcome email to avoid Legacy API crash
|
|
});
|
|
|
|
console.log('[LEGACY API] set_deployment request params:', formData.toString());
|
|
|
|
const response = await axios.post(LEGACY_API_BASE, formData, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
});
|
|
|
|
console.log('[LEGACY API] set_deployment response:', JSON.stringify(response.data));
|
|
|
|
if (response.data.status !== '200 OK') {
|
|
throw new Error(`Failed to create deployment in Legacy API: ${response.data.status || JSON.stringify(response.data)}`);
|
|
}
|
|
|
|
// Extract deployment_id from response
|
|
// Response format varies, need to handle different cases
|
|
// Note: Current Legacy API returns only {ok: 1, status: "200 OK"} without deployment_id
|
|
const deploymentId = response.data.deployment_id || response.data.result || response.data.well_id;
|
|
|
|
if (!deploymentId) {
|
|
console.warn('[LEGACY API] Deployment created but no deployment_id returned. This is a known Legacy API limitation.');
|
|
}
|
|
|
|
return deploymentId;
|
|
}
|
|
|
|
/**
|
|
* Assign device to deployment (set well_id)
|
|
* @param {string} username - Username
|
|
* @param {string} token - Access token
|
|
* @param {number} deviceId - Internal device ID
|
|
* @param {number} wellId - Well ID to assign
|
|
* @param {string} mac - Device MAC address
|
|
* @returns {Promise<boolean>} Success status
|
|
*/
|
|
async function assignDeviceToDeployment(username, token, deviceId, wellId, mac) {
|
|
const formData = new URLSearchParams({
|
|
function: 'device_set_well_id',
|
|
user_name: username,
|
|
token: token,
|
|
device_id: deviceId,
|
|
well_id: wellId,
|
|
mac: mac.toUpperCase().replace(/:/g, '')
|
|
});
|
|
|
|
const response = await axios.post(LEGACY_API_BASE, formData, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
});
|
|
|
|
return response.data.success === true;
|
|
}
|
|
|
|
/**
|
|
* Update device location/room
|
|
* @param {string} username - Username
|
|
* @param {string} token - Access token
|
|
* @param {number} wellId - Device well_id
|
|
* @param {string} deviceMac - MAC address
|
|
* @param {string} roomName - Room name (Bedroom, Kitchen, etc.)
|
|
* @param {object} options - Additional options
|
|
* @param {string} options.description - Device description
|
|
* @param {string} options.closeTo - Position description
|
|
* @param {number} options.radarThreshold - Radar sensitivity (0-100)
|
|
* @param {number} options.group - Group ID
|
|
* @returns {Promise<boolean>} Success status
|
|
*/
|
|
async function updateDeviceLocation(username, token, wellId, deviceMac, roomName, options = {}) {
|
|
const locationCode = ROOM_LOCATIONS[roomName] || ROOM_LOCATIONS['Other'];
|
|
|
|
const formData = new URLSearchParams({
|
|
function: 'device_form',
|
|
user_name: username,
|
|
token: token,
|
|
well_id: wellId,
|
|
device_mac: deviceMac.toUpperCase().replace(/:/g, ''),
|
|
location: locationCode.toString(),
|
|
description: options.description || '',
|
|
close_to: options.closeTo || '',
|
|
radar_threshold: options.radarThreshold || 50,
|
|
group: options.group || 0
|
|
});
|
|
|
|
const response = await axios.post(LEGACY_API_BASE, formData, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
});
|
|
|
|
return response.data.status === '200 OK';
|
|
}
|
|
|
|
/**
|
|
* Get devices for deployment (only recently active)
|
|
* @param {string} username - Username
|
|
* @param {string} token - Access token
|
|
* @param {number} deploymentId - Deployment ID
|
|
* @param {boolean} onlineOnly - If true, return only online devices (fresh=true)
|
|
* @returns {Promise<Array>} List of devices
|
|
*/
|
|
async function getDeploymentDevices(username, token, deploymentId, onlineOnly = true) {
|
|
const formData = new URLSearchParams({
|
|
function: 'request_devices',
|
|
user_name: username,
|
|
token: token,
|
|
deployment_id: deploymentId,
|
|
group_id: 'All',
|
|
location: 'All',
|
|
fresh: onlineOnly ? 'true' : 'false'
|
|
});
|
|
|
|
const response = await axios.post(LEGACY_API_BASE, formData, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
});
|
|
|
|
if (response.data.status !== '200 OK') {
|
|
return [];
|
|
}
|
|
|
|
// Parse device array format:
|
|
// [device_id, well_id, MAC, timestamp, location, description, deployment_id]
|
|
const devices = response.data.result_list || [];
|
|
|
|
return devices.map(device => ({
|
|
deviceId: device[0],
|
|
wellId: device[1],
|
|
mac: device[2],
|
|
lastSeen: device[3],
|
|
locationCode: device[4],
|
|
locationName: LOCATION_NAMES[device[4]] || 'Unknown',
|
|
description: device[5],
|
|
deploymentId: device[6],
|
|
isOnline: true // If fresh=true was used, all returned devices are online
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Reboot device
|
|
* @param {string} username - Username
|
|
* @param {string} token - Access token
|
|
* @param {number} deviceId - Device ID
|
|
* @returns {Promise<boolean>} Success status
|
|
*/
|
|
async function rebootDevice(username, token, deviceId) {
|
|
const formData = new URLSearchParams({
|
|
function: 'device_reboot',
|
|
user_name: username,
|
|
token: token,
|
|
device_id: deviceId
|
|
});
|
|
|
|
const response = await axios.post(LEGACY_API_BASE, formData, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
});
|
|
|
|
return response.data.status === '200 OK';
|
|
}
|
|
|
|
/**
|
|
* Find deployment ID by beneficiary username
|
|
* Used to retrieve deployment_id after creation (since set_deployment doesn't return it)
|
|
* @param {string} adminUsername - Admin username
|
|
* @param {string} adminToken - Admin access token
|
|
* @param {string} beneficiaryUsername - Username of the beneficiary
|
|
* @returns {Promise<number|null>} Deployment ID or null if not found
|
|
*/
|
|
async function findDeploymentByUsername(adminUsername, adminToken, beneficiaryUsername) {
|
|
try {
|
|
// Try logging in as the beneficiary user to get their deployment
|
|
// Note: This requires knowing the beneficiary's password
|
|
console.log('[LEGACY API] Attempting to find deployment for username:', beneficiaryUsername);
|
|
|
|
// Alternative: Use get_user_deployments if available
|
|
const formData = new URLSearchParams({
|
|
function: 'get_user_deployments',
|
|
user_name: adminUsername,
|
|
token: adminToken,
|
|
target_username: beneficiaryUsername
|
|
});
|
|
|
|
const response = await axios.post(LEGACY_API_BASE, formData, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
});
|
|
|
|
console.log('[LEGACY API] get_user_deployments response:', JSON.stringify(response.data));
|
|
|
|
if (response.data.status === '200 OK' && response.data.result_list) {
|
|
// Return the first deployment ID
|
|
const deployments = response.data.result_list;
|
|
if (deployments.length > 0) {
|
|
return deployments[0].deployment_id || deployments[0][0]; // Handle both object and array format
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error('[LEGACY API] Error finding deployment:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getLegacyToken,
|
|
createLegacyDeployment,
|
|
findDeploymentByUsername,
|
|
assignDeviceToDeployment,
|
|
updateDeviceLocation,
|
|
getDeploymentDevices,
|
|
rebootDevice,
|
|
ROOM_LOCATIONS,
|
|
LOCATION_NAMES
|
|
};
|