Lumi Gateway Technical Specifications
Comprehensive developer reference manual mapping codebase structure, main flows, code samples, and database schemas.
1. Project Overview
The Lumi Gateway is a lightweight integration engine built to link smart home setups managed by Home Assistant (HA) with Meta's WhatsApp Cloud API. This enables authenticated users to trigger home automation routines using WhatsApp-based One-Time Passwords (OTPs) and allows administrators to configure, manage, and monitor the entire gateway via a secure Web UI.
Its primary goal is providing secure, fast, and stateless triggers suitable for external integration tools (like Tasker, widgets, or custom scripts) while maintaining strict access control rules (such as IP rate limiting, JWT token-based admin scopes, and an administrative authorized mobile whitelist).
2. System Architecture
The system utilizes a decoupled architecture composed of a backend Node.js Express server and a frontend Vanilla Javascript Single Page Application (SPA). Data persistence is file-based (using structured JSON database files) for lightweight and rapid deployments.
+----------------------------------------------------------------------------------------------------------------+
| SYSTEM DATA FLOWS |
| |
| [WhatsApp App] === OTP Dispatched ===> (Meta Cloud API) === Outbound HTTP ===> [User Mobile Client] |
| || |
| || GET Request |
| \/ |
| +----------------------------------------------------------------------------------------------------------+ |
| | LUMI EXPRESS BACKEND SERVER | |
| | | |
| | [Router /api/device/:id/:action] | |
| | || | |
| | \/ | |
| | [authMiddleware] === Validate Mobile & OTP from sessions.json (Auto-Purges Expired Sessions) | |
| | || | |
| | \/ | |
| | [deviceController] === Execute Action on Target Device | |
| | || | |
| | \/ | |
| | [haService.callService] | |
| | || | |
| | +--- 1. HTTP GET /api/states/[entityId] ===> Validate entity exists in Home Assistant | |
| | || | |
| | +--- 2. HTTP POST /api/services/[domain]/[service] ===> Send command to Home Assistant | |
| | | |
| +----------------------------------------------------------------------------------------------------------+ |
| || |
| \/ |
| +-----------------------+ |
| | Home Assistant Server | |
| +-----------------------+ |
+----------------------------------------------------------------------------------------------------------------+
3. Detailed File Tree
Below is the complete and correctly formatted directory tree listing every codebase asset and its exact technical purpose:
E:/Whatsapp Meta/
├── .env # Environment configuration variables
├── package.json # Node.js project manifest & startup commands
├── data/ # Flat JSON File Databases directory
│ ├── users.json # Administrative portal user accounts (passwords hashed)
│ ├── authorized_numbers.json # Whitelisted 10-digit mobile numbers allowed for OTP auth
│ ├── rooms.json # Physical rooms list configuring name, color, and icons
│ ├── devices.json # Configured devices mapping local types and Home Assistant entities
│ ├── stats.json # Total/Today counters for API calls, OTPs, and success/failure rates
│ ├── sessions.json # Active verified client JWT session records (with expiresAt timestamps)
│ ├── otp.json # Pending OTP codes awaiting verification (expires in 5 minutes)
│ └── logs.json # System activity log database capped strictly to last 2000 entries
├── backend/ # Express.js Core Backend Server
│ ├── index.js # Entry point: initializes JSON database & starts Express server
│ ├── config/
│ │ └── config.js # Parses, validates environment variables, and exports file paths
│ ├── controllers/
│ │ ├── adminController.js # Handles Room, Device, User, Whitelist CRUDs, and synced stats
│ │ ├── authController.js # Coordinates OTP generation, verification, and admin JWT sign-in
│ │ ├── deviceController.js # Dynamic endpoint executor mapping HTTP requests to HA commands
│ │ └── systemController.js # Oversees system configurations backup and restore functions
│ ├── middlewares/
│ │ ├── authMiddleware.js # Decodes JWT tokens or validates stateless mobile/otp query parameters
│ │ ├── errorMiddleware.js # Centralized catch-all middleware mapping errors to clean responses
│ │ └── rateLimiter.js # Configures express-rate-limit bounds for security overrides
│ ├── routes/
│ │ ├── adminRoutes.js # Administrative REST API endpoints (scoped to Admin JWT)
│ │ ├── authRoutes.js # Authentication endpoint routes (Login, OTP request, OTP verify)
│ │ ├── deviceRoutes.js # Router for stateless client device execution
│ │ └── systemRoutes.js # Administration maintenance tools routes (Backup, Restore)
│ ├── services/
│ │ ├── cryptoService.js # Manages bcrypt hashing and AES-256 Long-Lived Access Token encryption
│ │ ├── haService.js # Communicates with Home Assistant (tests connection, syncs states)
│ │ ├── storageService.js # Low-level JSON read/write handlers and day-transition stats updater
│ │ └── whatsappService.js # Interacts with Meta Cloud WhatsApp API to dispatch OTP messages
│ └── utils/
│ └── logger.js # Winston setup for console logs and daily error log rotation
└── dashboard/ # Web UI Front-end SPA Assets
├── index.html # Dashboard skeleton containing main tabs and modular modal windows
├── developer_guide.html # This documentation guide (HTML page)
├── css/
│ └── style.css # Global stylesheets with responsive overrides
└── js/
└── app.js # SPA router: handles AJAX API calls, parameter generator, and page sync
4. Backend Main Flows
This section outlines the code flow logic for key system actions.
A. Request OTP Flow (authController.js)
When a client calls GET /api/auth/otp/[mobile]:
- Checks if the 10-digit number is in
authorized_numbers.json. If not, rejects with403 Forbidden. - Generates a cryptographically secure 6-digit numerical OTP.
- Saves the OTP to
otp.jsonalong with metadata (mobile, expiresAt: now + 5 minutes). - Calls
whatsappService.sendOTP(mobile, otp)to dispatch the WhatsApp message. - Increments
todaysOTPandtotalOtpSentinstats.json.
B. Verify OTP & Session Generation Flow (authController.js)
When a client calls GET /api/auth/verify/[mobile]/[otp]:
- Reads
otp.jsonand validates if an entry exists for the mobile number matching the OTP code. - Checks if the OTP has expired (compares
expiresAtagainst local system time). - On validation, deletes the OTP record from
otp.jsonto prevent reuse. - Generates a client JWT session token with properties
{ mobile, role: 'user' }valid for 5 minutes. - Inserts a session record into
sessions.json(storing mobile, token, ip, agent, createdAt, and expiresAt). - Returns a minimal payload containing
{ success: true, token, mobile }.
C. Device Control Execution Flow (deviceController.js)
When an endpoint like GET /api/device/[DEVICE_ID]/toggle?mobile=[MOBILE]&otp=[OTP] is invoked:
- Authentication parameters are parsed from query strings by the
verifyClientOrAdminmiddleware. - The active sessions database (
sessions.json) is queried. If no matching session is found or if the session expiry has passed, the request is aborted with a401 Unauthorizedstatus. - If expired, the database dynamically removes the record to save space.
- Once authenticated, the route executes the controller, invoking
haService.callService. - The execution is written to
logs.json, stats are incremented, and response feedback is returned.
5. File-By-File Walkthrough
This section outlines the code structure and workflow routines for every key file in the codebase.
A. entrypoint: backend/index.js
This is the server's starting point. It loads modules, initializes local JSON files, registers routes, and binds to the specified network port.
// backend/index.js
const express = require('express');
const cors = require('cors');
const path = require('path');
const config = require('./config/config');
const storageService = require('./services/storageService');
const errorMiddleware = require('./middlewares/errorMiddleware');
const logger = require('./utils/logger');
const app = express();
app.use(cors());
app.use(express.json());
// Serve Static Front-end Panel
app.use(express.static(path.join(__dirname, '../dashboard')));
// Route Mappings
app.use('/api/auth', require('./routes/authRoutes'));
app.use('/api/admin', require('./routes/adminRoutes'));
app.use('/api/device', require('./routes/deviceRoutes'));
app.use('/api/system', require('./routes/systemRoutes'));
app.use(errorMiddleware);
async function startServer() {
try {
await storageService.init(); // Create default databases if missing
app.listen(config.PORT, () => {
logger.info(`Server running on http://localhost:${config.PORT}`);
});
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
startServer();
B. configuration: backend/config/config.js
Handles environmental configs. Validates keys and sets up relative data paths.
// backend/config/config.js
require('dotenv').config();
const path = require('path');
module.exports = {
PORT: process.env.PORT || 5000,
JWT_SECRET: process.env.JWT_SECRET || 'lumi_default_secret_key_change_me',
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY || '1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p', // 32 chars
DB_PATHS: {
users: path.join(__dirname, '../../data/users.json'),
authorized_numbers: path.join(__dirname, '../../data/authorized_numbers.json'),
rooms: path.join(__dirname, '../../data/rooms.json'),
devices: path.join(__dirname, '../../data/devices.json'),
stats: path.join(__dirname, '../../data/stats.json'),
sessions: path.join(__dirname, '../../data/sessions.json'),
otp: path.join(__dirname, '../../data/otp.json'),
logs: path.join(__dirname, '../../data/logs.json')
},
homeAssistant: {
url: process.env.HA_URL || '',
token: process.env.HA_TOKEN || ''
},
whatsapp: {
apiUrl: process.env.WHATSAPP_API_URL || 'https://graph.facebook.com/v17.0',
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID || '',
accessToken: process.env.WHATSAPP_ACCESS_TOKEN || '',
templateName: process.env.WHATSAPP_TEMPLATE_NAME || 'lumi_otp_template'
}
};
C. persistence: backend/services/storageService.js
Executes asynchronous, thread-safe reads and writes to local database files. Also increments operational stats while handling calendar-day reset transitions.
// backend/services/storageService.js
const fs = require('fs').promises;
const config = require('../config/config');
const storageService = {
async init() {
for (const [key, filePath] of Object.entries(config.DB_PATHS)) {
try {
await fs.access(filePath);
} catch {
// Create initial empty structure if database file does not exist
const initialContent = key === 'stats'
? JSON.stringify({ totalApiCalls: 0, successfulRequests: 0, failedRequests: 0, totalOtpSent: 0, todaysApiCalls: 0, todaysOTP: 0, lastUpdated: new Date().toISOString() })
: JSON.stringify(key === 'users' ? [] : []);
await fs.writeFile(filePath, initialContent, 'utf8');
}
}
},
async read(key) {
const data = await fs.readFile(config.DB_PATHS[key], 'utf8');
return JSON.parse(data);
},
async write(key, content) {
await fs.writeFile(config.DB_PATHS[key], JSON.stringify(content, null, 2), 'utf8');
},
async incrementStat(metric) {
const stats = await this.read('stats');
const now = new Date();
const lastUpdate = new Date(stats.lastUpdated);
// Reset daily counters if the calendar day has changed
if (now.toDateString() !== lastUpdate.toDateString()) {
stats.todaysApiCalls = 0;
stats.todaysOTP = 0;
}
if (metric === 'api_call') {
stats.totalApiCalls++;
stats.todaysApiCalls++;
} else if (metric === 'otp_sent') {
stats.totalOtpSent++;
stats.todaysOTP++;
} else if (metric === 'success') {
stats.successfulRequests++;
} else if (metric === 'failed') {
stats.failedRequests++;
}
stats.lastUpdated = now.toISOString();
await this.write('stats', stats);
}
};
module.exports = storageService;
D. dispatching: backend/services/whatsappService.js
Sends outbound WhatsApp template messages to authorized clients via Meta's Cloud API endpoints.
// backend/services/whatsappService.js
const axios = require('axios');
const config = require('../config/config');
const logger = require('../utils/logger');
const whatsappService = {
async sendOTP(mobileNumber, otpCode) {
const { apiUrl, phoneNumberId, accessToken, templateName } = config.whatsapp;
if (!phoneNumberId || !accessToken) {
throw new Error('Meta WhatsApp credentials are not configured');
}
// Format number to contain country code if missing (assumes IN: +91)
const formattedNumber = mobileNumber.startsWith('91') && mobileNumber.length > 10
? mobileNumber
: `91${mobileNumber}`;
const url = `${apiUrl}/${phoneNumberId}/messages`;
const payload = {
messaging_product: 'whatsapp',
to: formattedNumber,
type: 'template',
template: {
name: templateName,
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: [
{ type: 'text', text: otpCode } // Maps to {{1}} in Meta template
]
}
]
}
};
try {
await axios.post(url, payload, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
timeout: 8000
});
logger.info(`OTP WhatsApp dispatch succeeded to: ${formattedNumber}`);
return true;
} catch (error) {
const errMsg = error.response ? JSON.stringify(error.response.data) : error.message;
logger.error(`WhatsApp dispatch failed: ${errMsg}`);
throw new Error(`WhatsApp Dispatch Error: ${errMsg}`);
}
}
};
module.exports = whatsappService;
E. home assistant: backend/services/haService.js
Queries HA to test credentials, syncs states, and verifies entities exist before dispatching service requests.
// backend/services/haService.js
const axios = require('axios');
const cryptoService = require('./cryptoService');
const logger = require('../utils/logger');
const haService = {
async syncDeviceStatuses(settings, devices) {
const { url, token } = settings.homeAssistant;
if (!url || !token) return devices;
try {
const decryptedToken = cryptoService.decrypt(token);
const baseUrl = url.replace(/\/$/, '');
// Single GET call to fetch all states from Home Assistant
const response = await axios.get(`${baseUrl}/api/states`, {
headers: {
Authorization: `Bearer ${decryptedToken}`,
'Content-Type': 'application/json'
},
timeout: 4000
});
const haStates = response.data;
if (!Array.isArray(haStates)) return devices;
const statesMap = {};
haStates.forEach(s => {
statesMap[s.entity_id] = s.state;
});
// Map dynamic status onto the local device schema configurations
return devices.map(d => {
if (d.entityId && statesMap[d.entityId] !== undefined) {
return { ...d, status: statesMap[d.entityId] };
}
return d;
});
} catch (error) {
logger.warn(`Failed to sync states from HA: ${error.message}`);
return devices; // Fallback to cache
}
}
};
module.exports = haService;
6. JSON Databases Schema
Here are the complete specifications for the JSON databases stored under data/:
A. Whitelist: authorized_numbers.json
[
{
"id": "auth-1783063204000",
"mobile": "9876543210",
"createdAt": "2026-07-04T05:00:00.000Z"
}
]
B. Client Sessions: sessions.json
[
{
"mobile": "9876543210",
"otp": "768777",
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresAt": "2026-07-04T06:34:51.445Z",
"ip": "::1",
"device": "Mozilla/5.0...",
"createdAt": "2026-07-04T05:54:51.446Z"
}
]
C. Integrated Metrics: stats.json
Daily stats reset automatically when a write occurs on a new calendar day:
{
"totalApiCalls": 120,
"successfulRequests": 105,
"failedRequests": 15,
"totalOtpSent": 30,
"todaysApiCalls": 8,
"todaysOTP": 2,
"lastUpdated": "2026-07-04T12:00:00.000Z"
}
7. HA & Entity Validation
Calling services on non-existent Home Assistant entity IDs returns status 200 with an empty body. To resolve this, our service validates the entity ID prior to firing any action:
// Inside backend/services/haService.js
async function callService(settings, device, action, bodyPayload = {}) {
const decryptedToken = cryptoService.decrypt(token);
const baseUrl = url.replace(/\/$/, '');
// 1. Verify that the entity ID exists in Home Assistant first
try {
await axios.get(`${baseUrl}/api/states/${device.entityId}`, {
headers: {
Authorization: `Bearer ${decryptedToken}`,
'Content-Type': 'application/json'
},
timeout: 4000
});
} catch (error) {
if (error.response && error.response.status === 404) {
throw new Error(`Entity "${device.entityId}" does not exist in Home Assistant.`);
}
throw new Error(`Failed to contact Home Assistant: ${error.message}`);
}
// 2. Assemble service parameters and execute the POST request
// Target: ${baseUrl}/api/services/[domain]/[service]
}
8. Security & Session Mgmt
| Security Domain | Implementation Logic | Configuration Bounds |
|---|---|---|
| Rate Limiting | Uses express-rate-limit. Differentiates general API requests from auth endpoints. |
General APIs: 1000 req / 15m. Auth (OTP request/verify, login): 100 attempts / 15m. |
| Admin Token Keys | Signed via JWT using JWT_SECRET. Tokens are valid for 2 hours. |
Decoded claims: { id, username, role: 'admin' }. Enforced by verifyAdmin. |
| Credentials Security | Admin passwords stored in users.json hashed using bcrypt (10 rounds). |
Encrypted Long-Lived Access Tokens are stored using AES-256-CBC via ENCRYPTION_KEY. |
| Session Expiry Check | Expired client sessions are cleaned up automatically upon query or check. | sessions.json deletes matches when `expiresAt` is less than current system time. |
9. Frontend SPA Workings
The dashboard at dashboard/index.html is designed as a vanilla JS Single Page Application (SPA):
A. Tab Routing & Views
All page panels are defined as separate <section class="dashboard-section"> blocks. The file dashboard/js/app.js listens for hashchange events on the window, toggles the active-section class on target panels, and runs the corresponding list loader (e.g. loadDevices(), loadStats()).
B. Parameter Binding & Copy Triggers
Inside the parameter generators on both the API Reference and Generated APIs tabs, changes are bound directly via keyup/input listeners:
// Updates all dynamic reference spans inside the HTML in real-time
document.getElementById('gen-param-mobile').addEventListener('input', (e) => {
const displayVal = e.target.value.trim() || 'YOUR_MOBILE';
document.querySelectorAll('.gen-mobile-val').forEach(el => el.innerText = displayVal);
});
10. Installation & Run
Follow these steps to deploy and start the Lumi Gateway application:
1. Setup Dependencies
Run npm install inside the project root folder to setup node modules:
npm install
2. Environment Settings
Create a file named .env in the project root folder and specify required configurations:
PORT=5000
JWT_SECRET=your_secure_jwt_secret_key
ENCRYPTION_KEY=32_character_hex_key_for_secrets
WHATSAPP_API_URL=https://graph.facebook.com/v17.0
WHATSAPP_PHONE_NUMBER_ID=your_meta_phone_number_id
WHATSAPP_ACCESS_TOKEN=your_meta_system_user_token
WHATSAPP_TEMPLATE_NAME=your_verified_otp_template_name
3. WhatsApp Template Setup
Your Meta Cloud WhatsApp account requires an OTP message template with a single body parameter {{1}} representing the OTP code. Example template structure:
"Your One-Time Verification Code is: {{1}}. Valid for 5 minutes."
4. Start the Server
npm start
Once started, access the Web UI at http://localhost:5000. Default credentials: admin / admin123.
5. Port Cleanup Commands (Address Already In Use)
If the terminal output indicates port 5000 is occupied, terminate the background processes:
- Windows PowerShell:
Stop-Process -Id (Get-NetTCPConnection -LocalPort 5000).OwningProcess -Force - Windows Command Prompt (CMD):
for /f "tokens=5" %a in ('netstat -aon ^| findstr 5000') do taskkill /f /pid %a