openapi: 3.0.3 info: title: 'M74 Labs API Documentation' description: '' version: 1.0.0 servers: - url: 'https://amarnathm74labs.com' tags: - name: Auth description: '' - name: Brokers description: '' - name: Calibrations description: '' - name: Capabilities description: '' - name: 'Cycle Readings' description: '' - name: Dashboard description: '' - name: 'Device Commands' description: '' - name: 'Device Telemetry' description: '' - name: Devices description: '' - name: Endpoints description: '' - name: 'Energy Aggregations' description: '' - name: 'Energy Readings' description: '' - name: Loads description: '' - name: Webhooks description: '' components: securitySchemes: default: type: http scheme: bearer description: 'You can retrieve your token by logging in.' security: - default: [] paths: /api/auth/login: post: summary: Login operationId: login description: 'Authenticate a user and issue a Sanctum access token.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: token: 1|abc123 token_type: Bearer properties: token: type: string example: 1|abc123 token_type: type: string example: Bearer tags: - Auth requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Registered email address.' example: user@example.com password: type: string description: 'Account password.' example: secret device_name: type: string description: 'Optional label for the generated token.' example: 'iPhone 15' required: - email - password - device_name security: [] /api/auth/logout: post: summary: Logout operationId: logout description: 'Revoke the access token used for this request.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Logged out.' properties: message: type: string example: 'Logged out.' tags: - Auth /api/brokers: get: summary: 'List brokers' operationId: listBrokers description: 'Retrieve all MQTT brokers configured by the authenticated user.' parameters: [] responses: 200: description: '' content: application/json: schema: type: array items: type: object properties: id: type: integer example: 1 name: type: string example: 'Primary broker' api_endpoint: type: string example: 'https://mqtt.example.com/api' app_id: type: string example: app-id created_at: type: string example: '2024-01-01T00:00:00Z' updated_at: type: string example: '2024-01-01T00:00:00Z' example: - id: 1 name: 'Primary broker' api_endpoint: 'https://mqtt.example.com/api' app_id: app-id created_at: '2024-01-01T00:00:00Z' updated_at: '2024-01-01T00:00:00Z' tags: - Brokers post: summary: 'Create broker' operationId: createBroker description: 'Register a new EMQX broker for the authenticated user.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: id: 1 name: 'Primary broker' api_endpoint: 'https://mqtt.example.com/api' app_id: app-id created_at: '2024-01-01T00:00:00Z' updated_at: '2024-01-01T00:00:00Z' properties: id: type: integer example: 1 name: type: string example: 'Primary broker' api_endpoint: type: string example: 'https://mqtt.example.com/api' app_id: type: string example: app-id created_at: type: string example: '2024-01-01T00:00:00Z' updated_at: type: string example: '2024-01-01T00:00:00Z' tags: - Brokers requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Friendly label for the broker.' example: 'Primary broker' api_endpoint: type: string description: 'Base URL for the EMQX REST API.' example: 'https://mqtt.example.com/api' app_id: type: string description: 'EMQX API App ID.' example: app-id app_secret: type: string description: 'EMQX API App Secret.' example: app-secret required: - name - api_endpoint - app_id - app_secret '/api/brokers/{id}': get: summary: 'View broker' operationId: viewBroker description: 'Retrieve configuration for a single EMQX broker.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 name: 'Primary broker' api_endpoint: 'https://mqtt.example.com/api' app_id: app-id created_at: '2024-01-01T00:00:00Z' updated_at: '2024-01-01T00:00:00Z' properties: id: type: integer example: 1 name: type: string example: 'Primary broker' api_endpoint: type: string example: 'https://mqtt.example.com/api' app_id: type: string example: app-id created_at: type: string example: '2024-01-01T00:00:00Z' updated_at: type: string example: '2024-01-01T00:00:00Z' tags: - Brokers put: summary: 'Update broker' operationId: updateBroker description: 'Modify the configuration for an EMQX broker.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 name: 'Primary broker' api_endpoint: 'https://mqtt.example.com/api' app_id: app-id created_at: '2024-01-01T00:00:00Z' updated_at: '2024-01-01T00:00:00Z' properties: id: type: integer example: 1 name: type: string example: 'Primary broker' api_endpoint: type: string example: 'https://mqtt.example.com/api' app_id: type: string example: app-id created_at: type: string example: '2024-01-01T00:00:00Z' updated_at: type: string example: '2024-01-01T00:00:00Z' tags: - Brokers requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Friendly label for the broker.' example: 'Primary broker' api_endpoint: type: string description: 'Base URL for the EMQX REST API.' example: 'https://mqtt.example.com/api' app_id: type: string description: 'EMQX API App ID.' example: app-id app_secret: type: string description: 'EMQX API App Secret.' example: app-secret required: - name - api_endpoint - app_id - app_secret delete: summary: 'Delete broker' operationId: deleteBroker description: 'Remove an EMQX broker and unlink its devices.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Broker deleted.' properties: message: type: string example: 'Broker deleted.' tags: - Brokers parameters: - in: path name: id description: 'The ID of the broker.' example: 1 required: true schema: type: integer - in: path name: broker description: 'ID of the broker.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/calibrations': get: summary: 'List calibrations' operationId: listCalibrations description: 'Retrieve paginated calibration history for a device.' parameters: - in: query name: page description: 'Page number for pagination.' example: 1 required: false schema: type: integer description: 'Page number for pagination.' example: 1 responses: 200: description: '' content: application/json: schema: type: object example: current_page: 1 data: - id: 1 device_id: 1 calibrated_at: '2024-01-01T12:00:00Z' mode: normal status: complete ai_gain: '1.000123' av_gain: '0.999876' error_message: null created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' total: 1 properties: current_page: type: integer example: 1 data: type: array example: - id: 1 device_id: 1 calibrated_at: '2024-01-01T12:00:00Z' mode: normal status: complete ai_gain: '1.000123' av_gain: '0.999876' error_message: null created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' items: type: object properties: id: type: integer example: 1 device_id: type: integer example: 1 calibrated_at: type: string example: '2024-01-01T12:00:00Z' mode: type: string example: normal status: type: string example: complete ai_gain: type: string example: '1.000123' av_gain: type: string example: '0.999876' error_message: type: string example: null nullable: true created_at: type: string example: '2024-01-01T12:00:00Z' updated_at: type: string example: '2024-01-01T12:00:00Z' total: type: integer example: 1 tags: - Calibrations parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer /api/capabilities: get: summary: 'List capabilities' operationId: listCapabilities description: 'Retrieve all available device capabilities.' parameters: [] responses: 200: description: '' content: application/json: schema: type: array items: type: object properties: value: type: string example: relay label: type: string example: Relay example: - value: relay label: Relay - value: energy_meter label: 'Energy Meter' tags: - Capabilities '/api/devices/{device_id}/cycles': get: summary: 'List cycle readings' operationId: listCycleReadings description: 'Retrieve paginated cycle/waveform readings for a device.' parameters: - in: query name: page description: 'Page number for pagination.' example: 1 required: false schema: type: integer description: 'Page number for pagination.' example: 1 responses: 200: description: '' content: application/json: schema: type: object example: current_page: 1 data: - id: 1 device_id: 1 recorded_at: '2024-01-01T12:00:00Z' frequency: '50.000' samples_per_cycle: 80 av_calibration_coeff: 1.342445739E-5 ai_calibration_coeff: 4.190951586E-7 samples: - - -16664835 - 73840 - - -18470656 - 71248 created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' total: 1 properties: current_page: type: integer example: 1 data: type: array example: - id: 1 device_id: 1 recorded_at: '2024-01-01T12:00:00Z' frequency: '50.000' samples_per_cycle: 80 av_calibration_coeff: 1.342445739E-5 ai_calibration_coeff: 4.190951586E-7 samples: - - -16664835 - 73840 - - -18470656 - 71248 created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' items: type: object properties: id: type: integer example: 1 device_id: type: integer example: 1 recorded_at: type: string example: '2024-01-01T12:00:00Z' frequency: type: string example: '50.000' samples_per_cycle: type: integer example: 80 av_calibration_coeff: type: number example: 1.342445739E-5 ai_calibration_coeff: type: number example: 4.190951586E-7 samples: type: array example: - - -16664835 - 73840 - - -18470656 - 71248 items: type: array created_at: type: string example: '2024-01-01T12:00:00Z' updated_at: type: string example: '2024-01-01T12:00:00Z' total: type: integer example: 1 tags: - 'Cycle Readings' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/cycles/latest': get: summary: 'Latest cycle reading' operationId: latestCycleReading description: 'Retrieve the most recent cycle/waveform reading for a device.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 device_id: 1 recorded_at: '2024-01-01T12:00:00Z' frequency: '50.000' samples_per_cycle: 80 av_calibration_coeff: 1.342445739E-5 ai_calibration_coeff: 4.190951586E-7 samples: - - -16664835 - 73840 - - -18470656 - 71248 created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' properties: id: type: integer example: 1 device_id: type: integer example: 1 recorded_at: type: string example: '2024-01-01T12:00:00Z' frequency: type: string example: '50.000' samples_per_cycle: type: integer example: 80 av_calibration_coeff: type: number example: 1.342445739E-5 ai_calibration_coeff: type: number example: 4.190951586E-7 samples: type: array example: - - -16664835 - 73840 - - -18470656 - 71248 items: type: array created_at: type: string example: '2024-01-01T12:00:00Z' updated_at: type: string example: '2024-01-01T12:00:00Z' 404: description: '' content: application/json: schema: type: object example: message: 'No cycle readings found.' properties: message: type: string example: 'No cycle readings found.' tags: - 'Cycle Readings' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/cycles/available': get: summary: 'Available waveform captures' operationId: availableWaveformCaptures description: 'List available waveform captures with metadata (no sample data) for date/time browsing.' parameters: - in: query name: from description: 'Filter from this datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' required: false schema: type: string description: 'Filter from this datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' - in: query name: to description: 'Filter to this datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' required: false schema: type: string description: 'Filter to this datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 recorded_at: '2024-01-01T12:00:00Z' frequency: '50.000' samples_per_cycle: 80 properties: data: type: array example: - id: 1 recorded_at: '2024-01-01T12:00:00Z' frequency: '50.000' samples_per_cycle: 80 items: type: object properties: id: type: integer example: 1 recorded_at: type: string example: '2024-01-01T12:00:00Z' frequency: type: string example: '50.000' samples_per_cycle: type: integer example: 80 tags: - 'Cycle Readings' requestBody: required: false content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true to: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/cycles/{cycle_id}': get: summary: 'Waveform detail with analysis' operationId: waveformDetailWithAnalysis description: 'Retrieve a specific waveform capture with computed analysis metrics.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 recorded_at: '2024-01-01T12:00:00Z' frequency: '50.000' samples_per_cycle: 80 waveform: voltage: - 230.5 - 228.1 current: - 5.12 - 4.98 power_instantaneous: - 1180.16 - 1135.9 time_ms: - 0 - 0.25 analysis: voltage_rms: 230.5 current_rms: 5.12 voltage_peak: 325.8 current_peak: 7.24 voltage_crest_factor: 1.414 current_crest_factor: 1.414 power_real: 1150 power_apparent: 1180 power_factor: 0.975 phase_angle_deg: 12.5 properties: id: type: integer example: 1 recorded_at: type: string example: '2024-01-01T12:00:00Z' frequency: type: string example: '50.000' samples_per_cycle: type: integer example: 80 waveform: type: object properties: voltage: type: array example: - 230.5 - 228.1 items: type: number current: type: array example: - 5.12 - 4.98 items: type: number power_instantaneous: type: array example: - 1180.16 - 1135.9 items: type: number time_ms: type: array example: - 0 - 0.25 items: type: integer analysis: type: object properties: voltage_rms: type: number example: 230.5 current_rms: type: number example: 5.12 voltage_peak: type: number example: 325.8 current_peak: type: number example: 7.24 voltage_crest_factor: type: number example: 1.414 current_crest_factor: type: number example: 1.414 power_real: type: integer example: 1150 power_apparent: type: integer example: 1180 power_factor: type: number example: 0.975 phase_angle_deg: type: number example: 12.5 404: description: '' content: application/json: schema: type: object example: message: 'Cycle reading not found.' properties: message: type: string example: 'Cycle reading not found.' tags: - 'Cycle Readings' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: cycle_id description: 'The ID of the cycle.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer - in: path name: cycle description: 'ID of the cycle reading.' example: 16 required: true schema: type: integer /api/dashboard/metrics: get: summary: 'Dashboard metrics' operationId: dashboardMetrics description: 'Retrieve device metrics for the authenticated user.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: devices_total: 3 devices_online: 1 recent_telemetry: - id: 42 device_id: 7 received_at: '2024-01-01T12:00:00Z' created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' event: message.delivered event_id: evt-123 event_timestamp: '2024-01-01T12:00:05Z' device: id: 7 name: 'Living room sensor' username: mqtt-user client_id: sensor-1 properties: devices_total: type: integer example: 3 devices_online: type: integer example: 1 recent_telemetry: type: array example: - id: 42 device_id: 7 received_at: '2024-01-01T12:00:00Z' created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' event: message.delivered event_id: evt-123 event_timestamp: '2024-01-01T12:00:05Z' device: id: 7 name: 'Living room sensor' username: mqtt-user client_id: sensor-1 items: type: object properties: id: type: integer example: 42 device_id: type: integer example: 7 received_at: type: string example: '2024-01-01T12:00:00Z' created_at: type: string example: '2024-01-01T12:00:00Z' updated_at: type: string example: '2024-01-01T12:00:00Z' event: type: string example: message.delivered event_id: type: string example: evt-123 event_timestamp: type: string example: '2024-01-01T12:00:05Z' device: type: object properties: id: type: integer example: 7 name: type: string example: 'Living room sensor' username: type: string example: mqtt-user client_id: type: string example: sensor-1 tags: - Dashboard '/api/devices/{device_id}/relay': post: summary: 'Control relay' operationId: controlRelay description: 'Send an on/off relay command to the device.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: relay state: 'on' properties: status: type: string example: sent command: type: string example: relay state: type: string example: 'on' tags: - 'Device Commands' requestBody: required: true content: application/json: schema: type: object properties: state: type: string description: 'Desired relay state.' example: 'on' enum: - 'on' - 'off' required: - state parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/calibrate': post: summary: 'Trigger calibration' operationId: triggerCalibration description: 'Start, stop, or check status of single-channel mSure calibration.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: calibrate op: start channel: AI mode: normal properties: status: type: string example: sent command: type: string example: calibrate op: type: string example: start channel: type: string example: AI mode: type: string example: normal tags: - 'Device Commands' requestBody: required: true content: application/json: schema: type: object properties: op: type: string description: 'Operation: start (default), stop, or status.' example: start enum: - start - stop - status channel: type: string description: 'Channel to calibrate (required for start/stop).' example: AI enum: - AI - AV mode: type: string description: 'Calibration mode (required for start).' example: normal enum: - normal - turbo required: - op - channel - mode parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/autocal': post: summary: 'Run auto-calibration' operationId: runAutoCalibration description: 'Start full AI + AV mSure calibration sequence (~45 seconds).' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: autocal properties: status: type: string example: sent command: type: string example: autocal tags: - 'Device Commands' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/mode': post: summary: 'Set operating mode' operationId: setOperatingMode description: 'Switch the device between normal, debug, and monitoring operating modes. Monitoring mode sends readings on every energy ready interrupt (~10s) and auto-returns to normal after 10 minutes.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: mode mode: normal properties: status: type: string example: sent command: type: string example: mode mode: type: string example: normal tags: - 'Device Commands' requestBody: required: true content: application/json: schema: type: object properties: mode: type: string description: 'Operating mode to set.' example: normal enum: - normal - debug - monitoring required: - mode parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/config': post: summary: 'Update device config' operationId: updateDeviceConfig description: 'Set the reading interval for energy measurements.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: config interval_ms: 5000 properties: status: type: string example: sent command: type: string example: config interval_ms: type: integer example: 5000 tags: - 'Device Commands' requestBody: required: true content: application/json: schema: type: object properties: interval_ms: type: integer description: 'Reading interval in milliseconds (100–60000).' example: 5000 required: - interval_ms parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/register': post: summary: 'Read/write ADE register' operationId: readwriteADERegister description: 'Perform a direct register read or write on the ADE9153A.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: register action: read address: '0x0000' properties: status: type: string example: sent command: type: string example: register action: type: string example: read address: type: string example: '0x0000' tags: - 'Device Commands' requestBody: required: true content: application/json: schema: type: object properties: action: type: string description: 'Register operation to perform.' example: read enum: - read - write address: type: string description: 'Register address (hex string).' example: '0x0000' value: type: integer description: 'Value to write (required when action is write).' example: 42 required: - action - address - value parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/diag': post: summary: 'Get diagnostics' operationId: getDiagnostics description: 'Request full system diagnostics from the device.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: diag properties: status: type: string example: sent command: type: string example: diag tags: - 'Device Commands' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/ade-hw-reset': post: summary: 'ADE hardware reset' operationId: aDEHardwareReset description: 'Perform hardware reset of the ADE chip (pulls RESET pin).' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: ade_hw_reset properties: status: type: string example: sent command: type: string example: ade_hw_reset tags: - 'Device Commands' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/ade-reset': post: summary: 'ADE software reset' operationId: aDESoftwareReset description: 'Deinitialize and re-initialize the full energy meter stack.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: ade_reset properties: status: type: string example: sent command: type: string example: ade_reset tags: - 'Device Commands' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/oc-config': post: summary: 'Configure overcurrent threshold' operationId: configureOvercurrentThreshold description: 'Set the overcurrent trip threshold in milliamps.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: oc_config threshold_ma: 12000 properties: status: type: string example: sent command: type: string example: oc_config threshold_ma: type: integer example: 12000 tags: - 'Device Commands' requestBody: required: true content: application/json: schema: type: object properties: threshold_ma: type: integer description: 'Overcurrent threshold in milliamps.' example: 12000 required: - threshold_ma parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/oc-reset': post: summary: 'Reset overcurrent trip' operationId: resetOvercurrentTrip description: 'Clear the overcurrent trip flag so relay can be turned ON again.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: oc_reset properties: status: type: string example: sent command: type: string example: oc_reset tags: - 'Device Commands' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/esp-reset': post: summary: 'Reset ESP32' operationId: resetESP32 description: 'Reboot the ESP32 device.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: esp_reset properties: status: type: string example: sent command: type: string example: esp_reset tags: - 'Device Commands' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/noload-config': post: summary: 'Configure no-load threshold' operationId: configureNoLoadThreshold description: 'Set the no-load power threshold in watts.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: noload_config threshold_w: 0.001 properties: status: type: string example: sent command: type: string example: noload_config threshold_w: type: number example: 0.001 tags: - 'Device Commands' requestBody: required: true content: application/json: schema: type: object properties: threshold_w: type: number description: 'No-load threshold in watts.' example: 0.001 required: - threshold_w parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/waveform': post: summary: 'Request waveform' operationId: requestWaveform description: 'Request a cycle/waveform capture from the device. Response arrives as type:cycle within 10-15 seconds.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: sent command: waveform properties: status: type: string example: sent command: type: string example: waveform tags: - 'Device Commands' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/telemetry': get: summary: 'List telemetry' operationId: listTelemetry description: 'Retrieve paginated telemetry records for a device.' parameters: - in: query name: page description: 'Page number for pagination.' example: 1 required: false schema: type: integer description: 'Page number for pagination.' example: 1 responses: 200: description: '' content: application/json: schema: type: object example: current_page: 1 data: - id: 10 device_id: 1 payload: '{"cmd":"ade_mode","status":"error","error":"ESP_ERR_INVALID_STATE"}' received_at: '2024-01-01T12:00:00Z' event: message.publish event_id: evt-123 event_timestamp: '2024-01-01T12:00:01Z' created_at: '2024-01-01T12:00:01Z' updated_at: '2024-01-01T12:00:01Z' total: 1 properties: current_page: type: integer example: 1 data: type: array example: - id: 10 device_id: 1 payload: '{"cmd":"ade_mode","status":"error","error":"ESP_ERR_INVALID_STATE"}' received_at: '2024-01-01T12:00:00Z' event: message.publish event_id: evt-123 event_timestamp: '2024-01-01T12:00:01Z' created_at: '2024-01-01T12:00:01Z' updated_at: '2024-01-01T12:00:01Z' items: type: object properties: id: type: integer example: 10 device_id: type: integer example: 1 payload: type: string example: '{"cmd":"ade_mode","status":"error","error":"ESP_ERR_INVALID_STATE"}' received_at: type: string example: '2024-01-01T12:00:00Z' event: type: string example: message.publish event_id: type: string example: evt-123 event_timestamp: type: string example: '2024-01-01T12:00:01Z' created_at: type: string example: '2024-01-01T12:00:01Z' updated_at: type: string example: '2024-01-01T12:00:01Z' total: type: integer example: 1 tags: - 'Device Telemetry' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer /api/devices: get: summary: 'List devices' operationId: listDevices description: 'Retrieve all devices owned by the authenticated user.' parameters: [] responses: 200: description: '' content: application/json: schema: type: array items: type: object properties: id: type: integer example: 1 name: type: string example: 'Living room sensor' broker_id: type: integer example: 1 username: type: string example: mqtt-user client_id: type: string example: sensor-1 notes: type: string example: 'Measures temperature' last_received_at: type: string example: '2024-01-01T12:00:00Z' status: type: string example: connected status_at: type: string example: '2024-01-01T12:05:00Z' telemetry: type: array example: [] created_at: type: string example: '2024-01-01T00:00:00Z' updated_at: type: string example: '2024-01-01T00:00:00Z' example: - id: 1 name: 'Living room sensor' broker_id: 1 username: mqtt-user client_id: sensor-1 notes: 'Measures temperature' last_received_at: '2024-01-01T12:00:00Z' status: connected status_at: '2024-01-01T12:05:00Z' telemetry: [] created_at: '2024-01-01T00:00:00Z' updated_at: '2024-01-01T00:00:00Z' tags: - Devices post: summary: 'Create device' operationId: createDevice description: 'Register a new device for the authenticated user.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: id: 1 name: 'Living room sensor' username: mqtt-user client_id: sensor-1 notes: 'Measures temperature' last_received_at: null status: null status_at: null capabilities: - capability: relay props: null created_at: '2024-01-01T00:00:00Z' updated_at: '2024-01-01T00:00:00Z' properties: id: type: integer example: 1 name: type: string example: 'Living room sensor' username: type: string example: mqtt-user client_id: type: string example: sensor-1 notes: type: string example: 'Measures temperature' last_received_at: type: string example: null nullable: true status: type: string example: null nullable: true status_at: type: string example: null nullable: true capabilities: type: array example: - capability: relay props: null items: type: object properties: capability: type: string example: relay props: type: string example: null nullable: true created_at: type: string example: '2024-01-01T00:00:00Z' updated_at: type: string example: '2024-01-01T00:00:00Z' tags: - Devices requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Friendly name for the device.' example: 'Living room sensor' broker_id: type: integer description: 'Identifier for the broker this device should use.' example: 1 username: type: string description: 'MQTT username associated with the device.' example: mqtt-user client_id: type: string description: 'MQTT client identifier.' example: sensor-1 notes: type: string description: 'Optional notes about the device.' example: architecto capabilities: type: array description: 'Device capabilities.' example: - relay - energy_meter items: type: string required: - name - broker_id - username - client_id - notes - capabilities '/api/devices/{id}': get: summary: 'View device' operationId: viewDevice description: 'Retrieve details for a single device.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 name: 'Living room sensor' broker_id: 1 username: mqtt-user client_id: sensor-1 notes: 'Measures temperature' last_received_at: '2024-01-01T12:00:00Z' status: connected status_at: '2024-01-01T12:05:00Z' telemetry: [] created_at: '2024-01-01T00:00:00Z' updated_at: '2024-01-01T00:00:00Z' properties: id: type: integer example: 1 name: type: string example: 'Living room sensor' broker_id: type: integer example: 1 username: type: string example: mqtt-user client_id: type: string example: sensor-1 notes: type: string example: 'Measures temperature' last_received_at: type: string example: '2024-01-01T12:00:00Z' status: type: string example: connected status_at: type: string example: '2024-01-01T12:05:00Z' telemetry: type: array example: [] created_at: type: string example: '2024-01-01T00:00:00Z' updated_at: type: string example: '2024-01-01T00:00:00Z' tags: - Devices put: summary: 'Update device' operationId: updateDevice description: 'Modify an existing device.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 name: 'Living room sensor' username: mqtt-user client_id: sensor-1 notes: 'Measures temperature' last_received_at: '2024-01-02T00:00:00Z' status: disconnected status_at: '2024-01-02T00:05:00Z' capabilities: - capability: relay props: null created_at: '2024-01-01T00:00:00Z' updated_at: '2024-01-02T00:00:00Z' properties: id: type: integer example: 1 name: type: string example: 'Living room sensor' username: type: string example: mqtt-user client_id: type: string example: sensor-1 notes: type: string example: 'Measures temperature' last_received_at: type: string example: '2024-01-02T00:00:00Z' status: type: string example: disconnected status_at: type: string example: '2024-01-02T00:05:00Z' capabilities: type: array example: - capability: relay props: null items: type: object properties: capability: type: string example: relay props: type: string example: null nullable: true created_at: type: string example: '2024-01-01T00:00:00Z' updated_at: type: string example: '2024-01-02T00:00:00Z' tags: - Devices requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Friendly name for the device.' example: 'Living room sensor' broker_id: type: integer description: 'Identifier for the broker this device should use.' example: 1 username: type: string description: 'MQTT username associated with the device.' example: mqtt-user client_id: type: string description: 'MQTT client identifier.' example: sensor-1 notes: type: string description: 'Optional notes about the device.' example: architecto capabilities: type: array description: 'Device capabilities.' example: - relay - energy_meter items: type: string required: - name - broker_id - username - client_id - notes - capabilities delete: summary: 'Delete device' operationId: deleteDevice description: 'Remove a device and its credentials.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Device deleted.' properties: message: type: string example: 'Device deleted.' tags: - Devices parameters: - in: path name: id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/publish': post: summary: 'Publish to device' operationId: publishToDevice description: 'Send a message to the device topic.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 00060563A10558877ACA0C006CFA0000 properties: id: type: string example: 00060563A10558877ACA0C006CFA0000 tags: - Devices requestBody: required: true content: application/json: schema: type: object properties: payload: type: string description: 'Message payload to deliver (CBOR planned; currently plain text).' example: device-online payload_encoding: type: string description: 'Encoding method for the payload.' example: plain qos: type: integer description: 'Quality of Service level (0-2).' example: 1 retain: type: boolean description: 'Whether to retain the message on the broker.' example: false required: - payload - payload_encoding - qos - retain parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer /api/user: get: summary: '' operationId: getApiUser description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints '/api/devices/{device_id}/aggregations': get: summary: 'List energy aggregations' operationId: listEnergyAggregations description: 'Retrieve paginated energy aggregations for a device.' parameters: - in: query name: period description: 'Filter by period type (hourly or daily).' example: hourly required: false schema: type: string description: 'Filter by period type (hourly or daily).' example: hourly - in: query name: from description: 'Filter aggregations from this date (Y-m-d format).' example: '2024-01-01' required: false schema: type: string description: 'Filter aggregations from this date (Y-m-d format).' example: '2024-01-01' - in: query name: to description: 'Filter aggregations to this date (Y-m-d format).' example: '2024-01-31' required: false schema: type: string description: 'Filter aggregations to this date (Y-m-d format).' example: '2024-01-31' - in: query name: page description: 'Page number for pagination.' example: 1 required: false schema: type: integer description: 'Page number for pagination.' example: 1 responses: 200: description: '' content: application/json: schema: type: object example: current_page: 1 data: - id: 1 device_id: 1 period: hourly period_start: '2024-01-01T12:00:00Z' energy_wh: '150.5000' reading_count: 60 avg_voltage: '120.5000' avg_current: '5.1234' avg_power_factor: '0.9800' max_power: '650.0000' min_power: '580.0000' created_at: '2024-01-01T13:00:00Z' updated_at: '2024-01-01T13:00:00Z' total: 1 properties: current_page: type: integer example: 1 data: type: array example: - id: 1 device_id: 1 period: hourly period_start: '2024-01-01T12:00:00Z' energy_wh: '150.5000' reading_count: 60 avg_voltage: '120.5000' avg_current: '5.1234' avg_power_factor: '0.9800' max_power: '650.0000' min_power: '580.0000' created_at: '2024-01-01T13:00:00Z' updated_at: '2024-01-01T13:00:00Z' items: type: object properties: id: type: integer example: 1 device_id: type: integer example: 1 period: type: string example: hourly period_start: type: string example: '2024-01-01T12:00:00Z' energy_wh: type: string example: '150.5000' reading_count: type: integer example: 60 avg_voltage: type: string example: '120.5000' avg_current: type: string example: '5.1234' avg_power_factor: type: string example: '0.9800' max_power: type: string example: '650.0000' min_power: type: string example: '580.0000' created_at: type: string example: '2024-01-01T13:00:00Z' updated_at: type: string example: '2024-01-01T13:00:00Z' total: type: integer example: 1 tags: - 'Energy Aggregations' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/aggregations/summary': get: summary: 'Energy aggregation summary' operationId: energyAggregationSummary description: 'Retrieve energy summary totals with quality metrics.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: today: '12.5000' this_week: '85.2500' this_month: '320.7500' all_time: '1500.0000' avg_daily_kwh: 0.43 projected_monthly_kwh: 12.9 voltage_quality: avg_voltage: 230.5 in_range_pct: 95.5 quality_score: good avg_power_factor: 0.92 properties: today: type: string example: '12.5000' this_week: type: string example: '85.2500' this_month: type: string example: '320.7500' all_time: type: string example: '1500.0000' avg_daily_kwh: type: number example: 0.43 projected_monthly_kwh: type: number example: 12.9 voltage_quality: type: object properties: avg_voltage: type: number example: 230.5 in_range_pct: type: number example: 95.5 quality_score: type: string example: good avg_power_factor: type: number example: 0.92 tags: - 'Energy Aggregations' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/readings': get: summary: 'List energy readings' operationId: listEnergyReadings description: 'Retrieve paginated energy readings for a device with optional time filtering.' parameters: - in: query name: page description: 'Page number for pagination.' example: 1 required: false schema: type: integer description: 'Page number for pagination.' example: 1 - in: query name: from description: 'Filter readings from this datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' required: false schema: type: string description: 'Filter readings from this datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' - in: query name: to description: 'Filter readings to this datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' required: false schema: type: string description: 'Filter readings to this datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' - in: query name: order description: 'Sort order by recorded_at (asc or desc).' example: desc required: false schema: type: string description: 'Sort order by recorded_at (asc or desc).' example: desc - in: query name: per_page description: 'Items per page (max 200).' example: 25 required: false schema: type: integer description: 'Items per page (max 200).' example: 25 responses: 200: description: '' content: application/json: schema: type: object example: current_page: 1 data: - id: 1 device_id: 1 recorded_at: '2024-01-01T12:00:00Z' voltage_rms: '120.5000' current_rms: '5.123456' power_active: '615.0000' power_fvar: '100.0000' power_apparent: '623.0000' power_factor: '0.9870' frequency: '60.000' phase_angle: '17.47' voltage_peak: '170.2500' current_peak: '7.234567' voltage_crest: '1.4142' current_crest: '1.4142' energy_wh: '1234.5678' energy_wh_delta: '0.1234' temp_c: '42.50' created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' total: 1 properties: current_page: type: integer example: 1 data: type: array example: - id: 1 device_id: 1 recorded_at: '2024-01-01T12:00:00Z' voltage_rms: '120.5000' current_rms: '5.123456' power_active: '615.0000' power_fvar: '100.0000' power_apparent: '623.0000' power_factor: '0.9870' frequency: '60.000' phase_angle: '17.47' voltage_peak: '170.2500' current_peak: '7.234567' voltage_crest: '1.4142' current_crest: '1.4142' energy_wh: '1234.5678' energy_wh_delta: '0.1234' temp_c: '42.50' created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' items: type: object properties: id: type: integer example: 1 device_id: type: integer example: 1 recorded_at: type: string example: '2024-01-01T12:00:00Z' voltage_rms: type: string example: '120.5000' current_rms: type: string example: '5.123456' power_active: type: string example: '615.0000' power_fvar: type: string example: '100.0000' power_apparent: type: string example: '623.0000' power_factor: type: string example: '0.9870' frequency: type: string example: '60.000' phase_angle: type: string example: '17.47' voltage_peak: type: string example: '170.2500' current_peak: type: string example: '7.234567' voltage_crest: type: string example: '1.4142' current_crest: type: string example: '1.4142' energy_wh: type: string example: '1234.5678' energy_wh_delta: type: string example: '0.1234' temp_c: type: string example: '42.50' created_at: type: string example: '2024-01-01T12:00:00Z' updated_at: type: string example: '2024-01-01T12:00:00Z' total: type: integer example: 1 tags: - 'Energy Readings' requestBody: required: false content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true to: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true order: type: string description: '' example: asc enum: - asc - desc nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 200.' example: 1 nullable: true parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/readings/latest': get: summary: 'Latest energy reading' operationId: latestEnergyReading description: 'Retrieve the most recent energy reading for a device.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 device_id: 1 recorded_at: '2024-01-01T12:00:00Z' voltage_rms: '120.5000' current_rms: '5.123456' power_active: '615.0000' power_fvar: '100.0000' power_apparent: '623.0000' power_factor: '0.9870' frequency: '60.000' phase_angle: '17.47' voltage_peak: '170.2500' current_peak: '7.234567' voltage_crest: '1.4142' current_crest: '1.4142' energy_wh: '1234.5678' energy_wh_delta: '0.1234' temp_c: '42.50' created_at: '2024-01-01T12:00:00Z' updated_at: '2024-01-01T12:00:00Z' properties: id: type: integer example: 1 device_id: type: integer example: 1 recorded_at: type: string example: '2024-01-01T12:00:00Z' voltage_rms: type: string example: '120.5000' current_rms: type: string example: '5.123456' power_active: type: string example: '615.0000' power_fvar: type: string example: '100.0000' power_apparent: type: string example: '623.0000' power_factor: type: string example: '0.9870' frequency: type: string example: '60.000' phase_angle: type: string example: '17.47' voltage_peak: type: string example: '170.2500' current_peak: type: string example: '7.234567' voltage_crest: type: string example: '1.4142' current_crest: type: string example: '1.4142' energy_wh: type: string example: '1234.5678' energy_wh_delta: type: string example: '0.1234' temp_c: type: string example: '42.50' created_at: type: string example: '2024-01-01T12:00:00Z' updated_at: type: string example: '2024-01-01T12:00:00Z' 404: description: '' content: application/json: schema: type: object example: message: 'No readings found.' properties: message: type: string example: 'No readings found.' tags: - 'Energy Readings' parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/readings/stats': get: summary: 'Energy reading statistics' operationId: energyReadingStatistics description: 'Retrieve aggregated statistics for energy readings within a time window.' parameters: - in: query name: from description: 'Start datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' required: false schema: type: string description: 'Start datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' - in: query name: to description: 'End datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' required: false schema: type: string description: 'End datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' responses: 200: description: '' content: application/json: schema: type: object example: period: from: '2024-01-01T00:00:00Z' to: '2024-01-31T23:59:59Z' reading_count: 1440 voltage: avg: 230.5 min: 218 max: 242 std_dev: 3.2 in_range_pct: 95.5 sag_count: 12 swell_count: 3 current: avg: 4.5 min: 0.1 max: 15.2 power: avg: 1035 min: 23 max: 3500 total_energy_wh: 14904 power_factor: avg: 0.92 min: 0.45 max: 0.99 below_085_pct: 8.5 frequency: avg: 50 min: 49.8 max: 50.2 std_dev: 0.05 temperature: avg: 42.5 min: 35 max: 55 properties: period: type: object properties: from: type: string example: '2024-01-01T00:00:00Z' to: type: string example: '2024-01-31T23:59:59Z' reading_count: type: integer example: 1440 voltage: type: object properties: avg: type: number example: 230.5 min: type: integer example: 218 max: type: integer example: 242 std_dev: type: number example: 3.2 in_range_pct: type: number example: 95.5 sag_count: type: integer example: 12 swell_count: type: integer example: 3 current: type: object properties: avg: type: number example: 4.5 min: type: number example: 0.1 max: type: number example: 15.2 power: type: object properties: avg: type: integer example: 1035 min: type: integer example: 23 max: type: integer example: 3500 total_energy_wh: type: integer example: 14904 power_factor: type: object properties: avg: type: number example: 0.92 min: type: number example: 0.45 max: type: number example: 0.99 below_085_pct: type: number example: 8.5 frequency: type: object properties: avg: type: integer example: 50 min: type: number example: 49.8 max: type: number example: 50.2 std_dev: type: number example: 0.05 temperature: type: object properties: avg: type: number example: 42.5 min: type: integer example: 35 max: type: integer example: 55 tags: - 'Energy Readings' requestBody: required: false content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true to: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/readings/timeseries': get: summary: 'Time series data' operationId: timeSeriesData description: 'Retrieve time-bucketed energy readings for charting.' parameters: - in: query name: from description: 'Start datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' required: true schema: type: string description: 'Start datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' - in: query name: to description: 'End datetime (ISO 8601).' example: '2024-01-02T00:00:00Z' required: true schema: type: string description: 'End datetime (ISO 8601).' example: '2024-01-02T00:00:00Z' - in: query name: metric description: 'Metric to aggregate: voltage_rms, current_rms, power_active, power_factor, frequency, temp_c, or all.' example: voltage_rms required: false schema: type: string description: 'Metric to aggregate: voltage_rms, current_rms, power_active, power_factor, frequency, temp_c, or all.' example: voltage_rms - in: query name: interval description: 'Bucket interval: 5m, 15m, 1h, 6h, 1d, 1w.' example: 1h required: false schema: type: string description: 'Bucket interval: 5m, 15m, 1h, 6h, 1d, 1w.' example: 1h responses: 200: description: '' content: application/json: schema: type: object example: interval: 1h metric: voltage_rms data: - bucket: '2024-01-01 00:00:00' avg: 230.5 min: 228 max: 233 count: 60 properties: interval: type: string example: 1h metric: type: string example: voltage_rms data: type: array example: - bucket: '2024-01-01 00:00:00' avg: 230.5 min: 228 max: 233 count: 60 items: type: object properties: bucket: type: string example: '2024-01-01 00:00:00' avg: type: number example: 230.5 min: type: integer example: 228 max: type: integer example: 233 count: type: integer example: 60 tags: - 'Energy Readings' requestBody: required: true content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' to: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' metric: type: string description: '' example: power_active enum: - voltage_rms - current_rms - power_active - power_factor - frequency - temp_c - all nullable: true interval: type: string description: '' example: 1h enum: - 5m - 15m - 1h - 6h - 1d - 1w nullable: true required: - from - to parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/readings/distribution': get: summary: 'Metric distribution' operationId: metricDistribution description: 'Get histogram-style distribution of a metric.' parameters: - in: query name: from description: 'Start datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' required: true schema: type: string description: 'Start datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' - in: query name: to description: 'End datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' required: true schema: type: string description: 'End datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' - in: query name: metric description: 'Metric to distribute: voltage_rms, current_rms, power_active, power_factor, frequency, temp_c.' example: voltage_rms required: true schema: type: string description: 'Metric to distribute: voltage_rms, current_rms, power_active, power_factor, frequency, temp_c.' example: voltage_rms - in: query name: buckets description: 'Number of histogram buckets (2-100).' example: 20 required: false schema: type: integer description: 'Number of histogram buckets (2-100).' example: 20 responses: 200: description: '' content: application/json: schema: type: object example: metric: voltage_rms buckets: - range_start: 218 range_end: 219.2 count: 15 percentage: 3.5 total_count: 430 properties: metric: type: string example: voltage_rms buckets: type: array example: - range_start: 218 range_end: 219.2 count: 15 percentage: 3.5 items: type: object properties: range_start: type: integer example: 218 range_end: type: number example: 219.2 count: type: integer example: 15 percentage: type: number example: 3.5 total_count: type: integer example: 430 tags: - 'Energy Readings' requestBody: required: true content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' to: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' metric: type: string description: '' example: current_rms enum: - voltage_rms - current_rms - power_active - power_factor - frequency - temp_c buckets: type: integer description: 'Must be at least 2. Must not be greater than 100.' example: 1 nullable: true required: - from - to - metric parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/readings/hourly-profile': get: summary: 'Hourly usage profile' operationId: hourlyUsageProfile description: 'Get average energy usage by hour of day.' parameters: - in: query name: from description: 'Start datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' required: false schema: type: string description: 'Start datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' - in: query name: to description: 'End datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' required: false schema: type: string description: 'End datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' responses: 200: description: '' content: application/json: schema: type: object example: data: - hour: 0 avg_power: 120.5 avg_voltage: 231.2 total_energy_wh: 240.5 reading_count: 120 properties: data: type: array example: - hour: 0 avg_power: 120.5 avg_voltage: 231.2 total_energy_wh: 240.5 reading_count: 120 items: type: object properties: hour: type: integer example: 0 avg_power: type: number example: 120.5 avg_voltage: type: number example: 231.2 total_energy_wh: type: number example: 240.5 reading_count: type: integer example: 120 tags: - 'Energy Readings' requestBody: required: false content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true to: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/loads': get: summary: 'List loads' operationId: listLoads description: 'Retrieve paginated loads for a device with optional filtering.' parameters: - in: query name: page description: 'Page number for pagination.' example: 1 required: false schema: type: integer description: 'Page number for pagination.' example: 1 - in: query name: from description: 'Filter loads started from this datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' required: false schema: type: string description: 'Filter loads started from this datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' - in: query name: to description: 'Filter loads started to this datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' required: false schema: type: string description: 'Filter loads started to this datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' - in: query name: sort description: 'Sort by: date, energy, or duration.' example: date required: false schema: type: string description: 'Sort by: date, energy, or duration.' example: date responses: 200: description: '' content: application/json: schema: type: object example: current_page: 1 data: - id: 1 device_id: 1 name: TV started_at: '2024-01-01T10:00:00Z' ended_at: '2024-01-01T12:00:00Z' start_energy_wh: '1234.5678' end_energy_wh: '1240.1234' energy_consumed_wh: '5.5556' created_at: '2024-01-01T10:00:00Z' updated_at: '2024-01-01T12:00:00Z' total: 1 properties: current_page: type: integer example: 1 data: type: array example: - id: 1 device_id: 1 name: TV started_at: '2024-01-01T10:00:00Z' ended_at: '2024-01-01T12:00:00Z' start_energy_wh: '1234.5678' end_energy_wh: '1240.1234' energy_consumed_wh: '5.5556' created_at: '2024-01-01T10:00:00Z' updated_at: '2024-01-01T12:00:00Z' items: type: object properties: id: type: integer example: 1 device_id: type: integer example: 1 name: type: string example: TV started_at: type: string example: '2024-01-01T10:00:00Z' ended_at: type: string example: '2024-01-01T12:00:00Z' start_energy_wh: type: string example: '1234.5678' end_energy_wh: type: string example: '1240.1234' energy_consumed_wh: type: string example: '5.5556' created_at: type: string example: '2024-01-01T10:00:00Z' updated_at: type: string example: '2024-01-01T12:00:00Z' total: type: integer example: 1 tags: - Loads requestBody: required: false content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true to: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true sort: type: string description: '' example: duration enum: - date - energy - duration nullable: true post: summary: 'Start a load' operationId: startALoad description: 'Start a new load for a device. Ends any existing active load.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: id: 1 device_id: 1 name: TV started_at: '2024-01-01T10:00:00Z' ended_at: null start_energy_wh: '1234.5678' end_energy_wh: null energy_consumed_wh: null created_at: '2024-01-01T10:00:00Z' updated_at: '2024-01-01T10:00:00Z' properties: id: type: integer example: 1 device_id: type: integer example: 1 name: type: string example: TV started_at: type: string example: '2024-01-01T10:00:00Z' ended_at: type: string example: null nullable: true start_energy_wh: type: string example: '1234.5678' end_energy_wh: type: string example: null nullable: true energy_consumed_wh: type: string example: null nullable: true created_at: type: string example: '2024-01-01T10:00:00Z' updated_at: type: string example: '2024-01-01T10:00:00Z' 422: description: '' content: application/json: schema: type: object example: message: 'No energy readings available to start a load.' properties: message: type: string example: 'No energy readings available to start a load.' tags: - Loads requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Name of the load/appliance.' example: TV required: - name parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/loads/active': get: summary: 'Get active load' operationId: getActiveLoad description: 'Retrieve the current active load for a device.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 device_id: 1 name: TV started_at: '2024-01-01T10:00:00Z' ended_at: null start_energy_wh: '1234.5678' end_energy_wh: null energy_consumed_wh: null created_at: '2024-01-01T10:00:00Z' updated_at: '2024-01-01T10:00:00Z' properties: id: type: integer example: 1 device_id: type: integer example: 1 name: type: string example: TV started_at: type: string example: '2024-01-01T10:00:00Z' ended_at: type: string example: null nullable: true start_energy_wh: type: string example: '1234.5678' end_energy_wh: type: string example: null nullable: true energy_consumed_wh: type: string example: null nullable: true created_at: type: string example: '2024-01-01T10:00:00Z' updated_at: type: string example: '2024-01-01T10:00:00Z' 404: description: '' content: application/json: schema: type: object example: message: 'No active load.' properties: message: type: string example: 'No active load.' tags: - Loads parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/loads/summary': get: summary: 'Load summary by appliance' operationId: loadSummaryByAppliance description: 'Aggregate load statistics grouped by appliance name.' parameters: - in: query name: from description: 'Filter loads started from this datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' required: false schema: type: string description: 'Filter loads started from this datetime (ISO 8601).' example: '2024-01-01T00:00:00Z' - in: query name: to description: 'Filter loads started to this datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' required: false schema: type: string description: 'Filter loads started to this datetime (ISO 8601).' example: '2024-01-31T23:59:59Z' responses: 200: description: '' content: application/json: schema: type: object example: data: - name: TV session_count: 15 total_energy_wh: 450.25 total_duration_minutes: 1800.5 avg_power_w: 15 projected_monthly_kwh: 0.54 properties: data: type: array example: - name: TV session_count: 15 total_energy_wh: 450.25 total_duration_minutes: 1800.5 avg_power_w: 15 projected_monthly_kwh: 0.54 items: type: object properties: name: type: string example: TV session_count: type: integer example: 15 total_energy_wh: type: number example: 450.25 total_duration_minutes: type: number example: 1800.5 avg_power_w: type: integer example: 15 projected_monthly_kwh: type: number example: 0.54 tags: - Loads requestBody: required: false content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true to: type: string description: 'Must be a valid date.' example: '2026-05-08T17:58:52' nullable: true parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer '/api/devices/{device_id}/loads/{load_id}/end': put: summary: 'End a load' operationId: endALoad description: 'End a specific load and calculate energy consumed.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: id: 1 device_id: 1 name: TV started_at: '2024-01-01T10:00:00Z' ended_at: '2024-01-01T12:00:00Z' start_energy_wh: '1234.5678' end_energy_wh: '1240.1234' energy_consumed_wh: '5.5556' created_at: '2024-01-01T10:00:00Z' updated_at: '2024-01-01T12:00:00Z' properties: id: type: integer example: 1 device_id: type: integer example: 1 name: type: string example: TV started_at: type: string example: '2024-01-01T10:00:00Z' ended_at: type: string example: '2024-01-01T12:00:00Z' start_energy_wh: type: string example: '1234.5678' end_energy_wh: type: string example: '1240.1234' energy_consumed_wh: type: string example: '5.5556' created_at: type: string example: '2024-01-01T10:00:00Z' updated_at: type: string example: '2024-01-01T12:00:00Z' 422: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'Load already ended.' properties: message: type: string example: 'Load already ended.' - description: '' type: object example: message: 'No energy readings available to end this load.' properties: message: type: string example: 'No energy readings available to end this load.' tags: - Loads parameters: - in: path name: device_id description: 'The ID of the device.' example: 1 required: true schema: type: integer - in: path name: load_id description: 'The ID of the load.' example: 1 required: true schema: type: integer - in: path name: device description: 'ID of the device.' example: 16 required: true schema: type: integer - in: path name: load description: 'ID of the load.' example: 16 required: true schema: type: integer '/api/emqx_endpoint/{broker_id}/{secret}': post: summary: 'Receive EMQX webhook' operationId: receiveEMQXWebhook description: 'Ingest messages forwarded from the EMQX HTTP connector.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: status: accepted properties: status: type: string example: accepted tags: - Webhooks requestBody: required: true content: application/json: schema: type: object properties: client: type: string description: 'MQTT client identifier provided by EMQX.' example: sensor-1 username: type: string description: 'MQTT username provided by EMQX.' example: mqtt-user payload: type: string description: 'Payload forwarded by EMQX (plain text, CBOR planned).' example: device-online received_at: type: string description: 'Timestamp reported by EMQX for published payloads (milliseconds Unix epoch).' example: '1704110400000' event: type: string description: 'Event emitted by EMQX ($events topic names normalised).' example: message.delivered timestamp: type: string description: 'Event timestamp provided by EMQX (milliseconds Unix epoch).' example: '1704110405000' id: type: string description: 'Event identifier supplied by EMQX.' example: 11f56100 required: - client - username - payload - received_at - event - timestamp - id parameters: - in: path name: broker_id description: 'The ID of the broker.' example: 1 required: true schema: type: integer - in: path name: secret description: 'Webhook secret appended to the route.' example: architecto required: true schema: type: string - in: path name: broker description: 'ID of the broker receiving webhook traffic.' example: 16 required: true schema: type: integer '/api/brokers/{broker_id}/webhook': get: summary: 'Get broker webhook URL' operationId: getBrokerWebhookURL description: 'Retrieve the full EMQX webhook endpoint for a broker.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: url: 'https://example.com/api/emqx_endpoint/1/secret123' properties: url: type: string example: 'https://example.com/api/emqx_endpoint/1/secret123' tags: - Webhooks parameters: - in: path name: broker_id description: 'The ID of the broker.' example: 1 required: true schema: type: integer '/api/brokers/{broker_id}/webhook/rotate': post: summary: 'Rotate broker webhook secret' operationId: rotateBrokerWebhookSecret description: 'Generate a new webhook secret for the broker.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: url: 'https://example.com/api/emqx_endpoint/1/newsecret456' properties: url: type: string example: 'https://example.com/api/emqx_endpoint/1/newsecret456' tags: - Webhooks parameters: - in: path name: broker_id description: 'The ID of the broker.' example: 1 required: true schema: type: integer