WellNuo/backend/src/services/legacyAPI.js
Sergei d453126c89 feat: Room location picker + robster credentials
- 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>
2026-01-24 15:22:40 -08:00

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
};