import logger from '../../utils/logger'; /** * TTS API configuration */ interface TtsApiConfig { apiUrl: string; apiKey: string; applicationId: string; enabled: boolean; } /** * Device registration payload for TTS */ export interface TtsDeviceRegistration { devEui: string; joinEui: string; deviceId: string; name: string; description?: string; appKey: string; nwkKey?: string; lorawanVersion?: string; lorawanPhyVersion?: string; frequencyPlanId?: string; supportsClassC?: boolean; supportsJoin?: boolean; } /** * Downlink message payload */ export interface TtsDownlinkPayload { fPort: number; frmPayload: string; // Base64 encoded confirmed?: boolean; priority?: 'LOWEST' | 'LOW' | 'BELOW_NORMAL' | 'NORMAL' | 'ABOVE_NORMAL' | 'HIGH' | 'HIGHEST'; classBC?: { absoluteTime?: string; }; } /** * Device status from TTS API */ export interface TtsDeviceStatus { devEui: string; deviceId: string; name: string; description?: string; createdAt?: string; updatedAt?: string; lastSeenAt?: string; session?: { devAddr: string; startedAt: string; }; macState?: { lastDevStatusReceivedAt?: string; batteryPercentage?: number; margin?: number; }; } /** * Result of TTS API operations */ export interface TtsApiResult { success: boolean; data?: T; error?: string; statusCode?: number; } /** * Get TTS API configuration from environment */ function getTtsConfig(): TtsApiConfig { return { apiUrl: process.env.TTS_API_URL || '', apiKey: process.env.TTS_API_KEY || '', applicationId: process.env.TTS_APPLICATION_ID || '', enabled: process.env.TTS_ENABLED === 'true', }; } /** * Check if TTS integration is enabled */ export function isTtsEnabled(): boolean { const config = getTtsConfig(); return config.enabled && !!config.apiUrl && !!config.apiKey; } /** * Make an authenticated request to the TTS API */ async function ttsApiRequest( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: unknown ): Promise> { const config = getTtsConfig(); if (!config.enabled) { logger.debug('TTS API is disabled, skipping request', { path }); return { success: false, error: 'TTS integration is disabled', }; } if (!config.apiUrl || !config.apiKey) { logger.warn('TTS API configuration missing', { hasApiUrl: !!config.apiUrl, hasApiKey: !!config.apiKey, }); return { success: false, error: 'TTS API configuration is incomplete', }; } const url = `${config.apiUrl}${path}`; try { logger.debug('Making TTS API request', { method, path }); const response = await fetch(url, { method, headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); const responseText = await response.text(); let responseData: T | undefined; try { responseData = responseText ? JSON.parse(responseText) : undefined; } catch { // Response is not JSON logger.debug('TTS API response is not JSON', { responseText: responseText.substring(0, 200) }); } if (!response.ok) { logger.warn('TTS API request failed', { method, path, status: response.status, statusText: response.statusText, response: responseText.substring(0, 500), }); return { success: false, error: `TTS API error: ${response.status} ${response.statusText}`, statusCode: response.status, data: responseData, }; } logger.debug('TTS API request successful', { method, path, status: response.status }); return { success: true, data: responseData, statusCode: response.status, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('TTS API request error', { method, path, error: errorMessage, }); return { success: false, error: `TTS API request failed: ${errorMessage}`, }; } } /** * Register a device in The Things Stack * * @param device - Device registration details * @returns Result of the registration */ export async function registerDevice( device: TtsDeviceRegistration ): Promise> { const config = getTtsConfig(); if (!isTtsEnabled()) { logger.info('TTS disabled, skipping device registration', { devEui: device.devEui }); return { success: true, data: { devEui: device.devEui, deviceId: device.deviceId, name: device.name, }, }; } logger.info('Registering device in TTS', { devEui: device.devEui, deviceId: device.deviceId, }); const path = `/api/v3/applications/${config.applicationId}/devices`; const body = { end_device: { ids: { device_id: device.deviceId, dev_eui: device.devEui.toUpperCase(), join_eui: device.joinEui.toUpperCase(), application_ids: { application_id: config.applicationId, }, }, name: device.name, description: device.description || '', lorawan_version: device.lorawanVersion || 'MAC_V1_0_3', lorawan_phy_version: device.lorawanPhyVersion || 'PHY_V1_0_3_REV_A', frequency_plan_id: device.frequencyPlanId || 'US_902_928_FSB_2', supports_join: device.supportsJoin !== false, supports_class_c: device.supportsClassC || false, root_keys: { app_key: { key: device.appKey.toUpperCase(), }, ...(device.nwkKey && { nwk_key: { key: device.nwkKey.toUpperCase(), }, }), }, }, field_mask: { paths: [ 'name', 'description', 'lorawan_version', 'lorawan_phy_version', 'frequency_plan_id', 'supports_join', 'supports_class_c', 'root_keys.app_key.key', ...(device.nwkKey ? ['root_keys.nwk_key.key'] : []), ], }, }; const result = await ttsApiRequest('POST', path, body); if (result.success) { logger.info('Device registered in TTS successfully', { devEui: device.devEui, deviceId: device.deviceId, }); } else { logger.error('Failed to register device in TTS', { devEui: device.devEui, error: result.error, }); } return result; } /** * Delete a device from The Things Stack * * @param devEui - Device EUI * @param deviceId - Device ID in TTS (optional, will use devEui if not provided) * @returns Result of the deletion */ export async function deleteDevice( devEui: string, deviceId?: string ): Promise> { const config = getTtsConfig(); if (!isTtsEnabled()) { logger.info('TTS disabled, skipping device deletion', { devEui }); return { success: true }; } const id = deviceId || devEui.toLowerCase(); logger.info('Deleting device from TTS', { devEui, deviceId: id }); const path = `/api/v3/applications/${config.applicationId}/devices/${id}`; const result = await ttsApiRequest('DELETE', path); if (result.success) { logger.info('Device deleted from TTS successfully', { devEui }); } else if (result.statusCode === 404) { // Device doesn't exist, consider it a success logger.info('Device not found in TTS, considering deletion successful', { devEui }); return { success: true }; } else { logger.error('Failed to delete device from TTS', { devEui, error: result.error, }); } return result; } /** * Queue a downlink message to a device * * @param devEui - Device EUI * @param payload - Downlink payload * @param deviceId - Device ID in TTS (optional) * @returns Result of the operation */ export async function sendDownlink( devEui: string, payload: TtsDownlinkPayload, deviceId?: string ): Promise> { const config = getTtsConfig(); if (!isTtsEnabled()) { logger.info('TTS disabled, skipping downlink', { devEui }); return { success: false, error: 'TTS integration is disabled', }; } const id = deviceId || devEui.toLowerCase(); logger.info('Sending downlink to device via TTS', { devEui, deviceId: id, fPort: payload.fPort, confirmed: payload.confirmed, }); const path = `/api/v3/as/applications/${config.applicationId}/devices/${id}/down/push`; const body = { downlinks: [ { f_port: payload.fPort, frm_payload: payload.frmPayload, confirmed: payload.confirmed || false, priority: payload.priority || 'NORMAL', ...(payload.classBC && { class_b_c: payload.classBC, }), }, ], }; const result = await ttsApiRequest<{ correlation_ids?: string[] }>('POST', path, body); if (result.success) { logger.info('Downlink queued successfully', { devEui, correlationIds: result.data?.correlation_ids, }); return { success: true, data: { correlationIds: result.data?.correlation_ids, }, }; } else { logger.error('Failed to send downlink', { devEui, error: result.error, }); return { success: false, error: result.error, statusCode: result.statusCode, }; } } /** * Get device status from The Things Stack * * @param devEui - Device EUI * @param deviceId - Device ID in TTS (optional) * @returns Device status information */ export async function getDeviceStatus( devEui: string, deviceId?: string ): Promise> { const config = getTtsConfig(); if (!isTtsEnabled()) { logger.info('TTS disabled, skipping device status request', { devEui }); return { success: false, error: 'TTS integration is disabled', }; } const id = deviceId || devEui.toLowerCase(); logger.debug('Getting device status from TTS', { devEui, deviceId: id }); const path = `/api/v3/applications/${config.applicationId}/devices/${id}?field_mask=name,description,created_at,updated_at,session,mac_state`; const result = await ttsApiRequest<{ ids: { device_id: string; dev_eui: string; }; name: string; description?: string; created_at?: string; updated_at?: string; session?: { dev_addr: string; started_at: string; }; mac_state?: { last_dev_status_received_at?: string; battery_percentage?: number; margin?: number; }; }>('GET', path); if (result.success && result.data) { const data = result.data; const status: TtsDeviceStatus = { devEui: data.ids.dev_eui, deviceId: data.ids.device_id, name: data.name, description: data.description, createdAt: data.created_at, updatedAt: data.updated_at, session: data.session ? { devAddr: data.session.dev_addr, startedAt: data.session.started_at, } : undefined, macState: data.mac_state ? { lastDevStatusReceivedAt: data.mac_state.last_dev_status_received_at, batteryPercentage: data.mac_state.battery_percentage, margin: data.mac_state.margin, } : undefined, }; logger.debug('Device status retrieved successfully', { devEui, hasSession: !!status.session, }); return { success: true, data: status, }; } if (result.statusCode === 404) { logger.info('Device not found in TTS', { devEui }); return { success: false, error: 'Device not found in TTS', statusCode: 404, }; } return { success: false, error: result.error, statusCode: result.statusCode, }; } /** * Encode a hex string to base64 for downlink payloads */ export function hexToBase64(hex: string): string { const cleanHex = hex.replace(/\s/g, ''); const buffer = Buffer.from(cleanHex, 'hex'); return buffer.toString('base64'); } /** * Decode a base64 string to hex for debugging */ export function base64ToHex(base64: string): string { const buffer = Buffer.from(base64, 'base64'); return buffer.toString('hex'); } export default { isTtsEnabled, registerDevice, deleteDevice, sendDownlink, getDeviceStatus, hexToBase64, base64ToHex, };