diff --git a/.marketplace/devices/devices.yml b/.marketplace/devices/devices.yml index 9b526938..87ddb6b4 100644 --- a/.marketplace/devices/devices.yml +++ b/.marketplace/devices/devices.yml @@ -10,6 +10,19 @@ - blueprint: network_infrastructure/starlink_gen4 verification_level: verified +- id: unifi-udm + display_name: UniFi Dream Machine + description: > + Ubiquiti UniFi Dream Machine (UDM/UDM-Pro/UDM-SE) monitoring via the + UniFi Integration API. Reports throughput, clients, devices, and system + resource utilization. + icon: enapter-router-wifi + vendor: ubiquiti + category: network_infrastructure + blueprint_options: + - blueprint: network_infrastructure/unifi_udm + verification_level: ready_for_testing + - id: alarm-with-sonoff-433-rf-bridge display_name: Alarm sensor connected to Sonoff 433 RF Bridge description: Alarm sensor connected to Sonoff 433 RF Bridge via eWelink REST API Server. diff --git a/.marketplace/vendors/icons/ubiquiti.png b/.marketplace/vendors/icons/ubiquiti.png new file mode 100644 index 00000000..5f8781ca Binary files /dev/null and b/.marketplace/vendors/icons/ubiquiti.png differ diff --git a/.marketplace/vendors/vendors.yml b/.marketplace/vendors/vendors.yml index 67768b9b..e3e92674 100644 --- a/.marketplace/vendors/vendors.yml +++ b/.marketplace/vendors/vendors.yml @@ -297,3 +297,8 @@ display_name: MicroArt icon_url: https://raw.githubusercontent.com/Enapter/marketplace/main/.marketplace/vendors/icons/microart.png website: https://microart.ru + +- id: ubiquiti + display_name: Ubiquiti + icon_url: https://raw.githubusercontent.com/Enapter/marketplace/main/.marketplace/vendors/icons/ubiquiti.png + website: https://www.ui.com diff --git a/network_infrastructure/unifi_udm/README.md b/network_infrastructure/unifi_udm/README.md new file mode 100644 index 00000000..8bb671b0 --- /dev/null +++ b/network_infrastructure/unifi_udm/README.md @@ -0,0 +1,65 @@ +# Ubiquiti UniFi Dream Machine + +This [Enapter Blueprint](https://go.enapter.com/marketplace-readme) monitors +Ubiquiti UniFi Dream Machine (UDM, UDM-Pro, UDM-SE) via the UniFi Integration +API using API Key authentication. + +## Overview + +The blueprint connects to your UDM's Integration API and reports real-time +network health, client counts, throughput, and system resource utilization. + +## Telemetry + +- **Throughput**: WAN download and upload (Mbps) derived from uplink rates +- **Clients**: total, wired (LAN), and wireless (WLAN) client counts +- **Devices**: number of adopted UniFi devices +- **System**: CPU and memory utilization, uptime + +## Alerts + +- No data from UDM (unreachable or bad API key) +- High CPU usage (>90%) +- High memory usage (>90%) + +## Requirements + +- UniFi Dream Machine running UniFi OS with Integration API support +- An API Key generated from the UniFi application +- Network connectivity between the Enapter Virtual UCM and the UDM + +## Creating an API Key + +1. Open the UniFi application on your UDM (e.g. `https://192.168.1.1`) +2. Go to **Settings** (gear icon) +3. Navigate to **Integrations** (under System or Advanced, depending on version) +4. Click **Create New API Key** +5. Give the key a descriptive name (e.g. "Enapter Monitoring") +6. Copy the generated key immediately — it is shown only once + +> **Note**: API Keys are read-only. The blueprint does not modify any settings +> on your UDM. + +## Configuration + +- **IP Address** (`ip_address`): UDM IP address (e.g. `192.168.1.1`) +- **API Key** (`api_key`): The API key generated above +- **Site** (`site`): UniFi site name (default: `default`). Only needed if you + have multiple sites configured. + +## How It Works + +The blueprint authenticates using the `Authorization: Bearer ` header +and polls the following Integration API endpoints every 15 seconds: + +| Endpoint | Data | +| --------------------------------------------------------------- | ------------------------------------------- | +| `GET /integration/v1/sites` | Resolve site name to UUID | +| `GET /integration/v1/sites/{id}/devices` | Adopted device list, gateway identification | +| `GET /integration/v1/sites/{id}/devices/{id}/statistics/latest` | CPU, memory, uptime, uplink throughput | +| `GET /integration/v1/sites/{id}/clients` | Total, wired, and wireless client counts | + +## References + +- [UniFi Developer Portal](https://developer.ui.com/) +- [Enapter Blueprint SDK](https://developers.enapter.com) diff --git a/network_infrastructure/unifi_udm/firmware.lua b/network_infrastructure/unifi_udm/firmware.lua new file mode 100644 index 00000000..f37a14a2 --- /dev/null +++ b/network_infrastructure/unifi_udm/firmware.lua @@ -0,0 +1,256 @@ +-- Ubiquiti UniFi Dream Machine +-- Monitors UDM/UDM-Pro/UDM-SE via the UniFi Integration API. +-- Uses API Key authentication (X-API-Key header). +-- Generate a key in UniFi OS → Settings → Integrations. + +local config = require('enapter.ucm.config') +local json = require('json') + +local CONFIG_IP = 'ip_address' +local CONFIG_API_KEY = 'api_key' +local CONFIG_SITE = 'site' + +local SITE_DEFAULT = 'default' + +local api_client + +-- Resolved site ID (UUID, looked up from site name on first poll) +local resolved_site_id + +local telemetry_cache = {} +local properties_cache = {} +local active_alerts = {} + +function main() + -- config.init() automatically registers write_configuration and + -- read_configuration commands — do NOT define them manually. + config.init({ + [CONFIG_IP] = { type = 'string', required = true }, + [CONFIG_API_KEY] = { type = 'string', required = true }, + [CONFIG_SITE] = { type = 'string', required = false, default = SITE_DEFAULT }, + }) + + api_client = http.client({ timeout = 30, insecure_tls = true }) + + scheduler.add(1000, poll_data) + scheduler.add(30000, send_properties) + scheduler.add(1000, send_telemetry) +end + +-- Thin HTTP GET wrapper with API key auth. +local function api_get(path) + local values, err = config.read_all() + if err or not values[CONFIG_IP] or not values[CONFIG_API_KEY] then + return nil, 'not_configured' + end + + local url = 'https://' .. values[CONFIG_IP] .. '/proxy/network/integration/v1' .. path + local req = http.request('GET', url) + req:set_header('X-API-Key', values[CONFIG_API_KEY]) + req:set_header('Accept', 'application/json') + + local response, req_err = api_client:do_request(req) + if req_err then + enapter.log('API request failed: ' .. tostring(req_err), 'error') + return nil, 'request_failed' + end + if response.code ~= 200 then + enapter.log('API returned HTTP ' .. tostring(response.code) .. ' for ' .. path, 'error') + return nil, 'http_' .. tostring(response.code) + end + + local ok, decoded = pcall(json.decode, response.body) + if not ok then + return nil, 'json_decode_error' + end + return decoded, nil +end + +-- Resolve the configured site name to a UUID via /v1/sites. +local function resolve_site_id() + if resolved_site_id then + return resolved_site_id + end + + local result, err = api_get('/sites?limit=200') + if err or not result or not result.data then + return nil + end + + local values, _ = config.read_all() + local target = values and values[CONFIG_SITE] or SITE_DEFAULT + + for _, site in ipairs(result.data) do + if site.name == target or site.internalReference == target then + resolved_site_id = site.id + return resolved_site_id + end + end + + -- If only one site exists, use it regardless of name + if result.totalCount == 1 and result.data[1] then + resolved_site_id = result.data[1].id + return resolved_site_id + end + + enapter.log('site "' .. target .. '" not found', 'error') + return nil +end + +-- Paginate through all results for a given endpoint. +local function api_get_all(path) + local all_data = {} + local offset = 0 + local limit = 200 + + while true do + local sep = path:find('?') and '&' or '?' + local paged_path = path .. sep .. 'offset=' .. offset .. '&limit=' .. limit + local result, err = api_get(paged_path) + if err or not result or not result.data then + return all_data, err + end + for _, item in ipairs(result.data) do + all_data[#all_data + 1] = item + end + if #all_data >= (result.totalCount or 0) then + break + end + offset = offset + limit + end + + return all_data, nil +end + +function poll_data() + local alerts = {} + + local site_id = resolve_site_id() + if not site_id then + alerts['no_data'] = true + telemetry_cache = {} + active_alerts = alerts + return + end + + local telemetry = {} + + -- Fetch all adopted devices + local devices, dev_err = api_get_all('/sites/' .. site_id .. '/devices') + if dev_err then + alerts['no_data'] = true + telemetry_cache = {} + active_alerts = alerts + return + end + + local udm_device_id + local props = {} + + telemetry.num_devices = #devices + + for _, dev in ipairs(devices) do + -- Identify the UDM/gateway device by model name or matching IP + if not udm_device_id then + local is_udm = false + if dev.model and dev.model:find('Dream Machine') then + is_udm = true + end + local cfg_values, _ = config.read_all() + if not is_udm and cfg_values and dev.ipAddress == cfg_values[CONFIG_IP] then + is_udm = true + end + if is_udm then + udm_device_id = dev.id + props = { + model = dev.model or 'Unknown', + firmware_version = dev.firmwareVersion, + hostname = dev.name, + } + end + end + end + + -- Fetch UDM statistics (CPU, memory, uptime, uplink rates) + if udm_device_id then + local stats, stats_err = api_get('/sites/' .. site_id .. '/devices/' .. udm_device_id .. '/statistics/latest') + if not stats_err and stats then + telemetry.uptime_s = stats.uptimeSec + telemetry.cpu_util = stats.cpuUtilizationPct + telemetry.mem_util = stats.memoryUtilizationPct + if stats.uplink then + if stats.uplink.rxRateBps then + telemetry.wan_download_mbps = stats.uplink.rxRateBps * 8 / 1000000 + end + if stats.uplink.txRateBps then + telemetry.wan_upload_mbps = stats.uplink.txRateBps * 8 / 1000000 + end + end + telemetry.status = 'ok' + telemetry.wan_status = 'ok' + telemetry.lan_status = 'ok' + telemetry.wlan_status = 'ok' + end + end + + -- Fetch client counts + local clients_result, _ = api_get('/sites/' .. site_id .. '/clients?limit=1') + if clients_result then + telemetry.num_clients = clients_result.totalCount or 0 + end + + local wired_result, _ = api_get('/sites/' .. site_id .. "/clients?limit=1&filter=type.eq('WIRED')") + if wired_result then + telemetry.num_lan_clients = wired_result.totalCount or 0 + end + + local wireless_result, _ = api_get('/sites/' .. site_id .. "/clients?limit=1&filter=type.eq('WIRELESS')") + if wireless_result then + telemetry.num_wlan_clients = wireless_result.totalCount or 0 + end + + if not telemetry.status then + telemetry.status = 'unknown' + telemetry.wan_status = 'unknown' + telemetry.lan_status = 'unknown' + telemetry.wlan_status = 'unknown' + end + + if telemetry.cpu_util and telemetry.cpu_util > 90 then + alerts['high_cpu'] = true + end + if telemetry.mem_util and telemetry.mem_util > 90 then + alerts['high_memory'] = true + end + + telemetry_cache = telemetry + active_alerts = alerts + if props and next(props) then + properties_cache = props + end +end + +function send_telemetry() + local telemetry = telemetry_cache + if not telemetry.status then + telemetry = { status = 'unknown' } + end + + enapter.send_telemetry(telemetry) + + local alerts = active_alerts + for alert_key, _ in pairs(alerts) do + enapter.send_telemetry({ alerts = { alert_key } }) + end + if next(alerts) == nil then + enapter.send_telemetry({ alerts = {} }) + end +end + +function send_properties() + if properties_cache and next(properties_cache) then + enapter.send_properties(properties_cache) + end +end + +main() diff --git a/network_infrastructure/unifi_udm/manifest.yml b/network_infrastructure/unifi_udm/manifest.yml new file mode 100644 index 00000000..59a789b2 --- /dev/null +++ b/network_infrastructure/unifi_udm/manifest.yml @@ -0,0 +1,158 @@ +blueprint_spec: device/1.0 + +display_name: Ubiquiti UniFi Dream Machine +description: > + Ubiquiti UniFi Dream Machine (UDM/UDM-Pro/UDM-SE) monitoring via the + UniFi Integration API with API Key authentication. Reports throughput, + connected clients, adopted devices, and system resource utilization. +icon: enapter-router-wifi +vendor: ubiquiti +license: MIT +author: enapter +support: + url: https://go.enapter.com/enapter-blueprint-support + email: support@enapter.com + +communication_module: + product: ENP-VIRTUAL + lua: + file: firmware.lua + dependencies: + - enapter-ucm ~> 0.3.2 + +properties: + model: + display_name: Model + type: string + firmware_version: + display_name: Firmware Version + type: string + hostname: + display_name: Hostname + type: string + +telemetry: + status: + display_name: Status + type: string + enum: + - ok + - error + - unknown + wan_status: + display_name: WAN Status + type: string + enum: + - ok + - error + - unknown + wan_download_mbps: + display_name: WAN Download + type: float + unit: Mbps + wan_upload_mbps: + display_name: WAN Upload + type: float + unit: Mbps + lan_status: + display_name: LAN Status + type: string + enum: + - ok + - error + - unknown + wlan_status: + display_name: WLAN Status + type: string + enum: + - ok + - error + - unknown + num_devices: + display_name: Adopted Devices + type: integer + num_clients: + display_name: Total Clients + type: integer + num_lan_clients: + display_name: LAN Clients + type: integer + num_wlan_clients: + display_name: WLAN Clients + type: integer + uptime_s: + display_name: UDM Uptime + type: integer + unit: s + cpu_util: + display_name: CPU Utilization + type: float + unit: "%" + mem_util: + display_name: Memory Utilization + type: float + unit: "%" + +alerts: + no_data: + severity: error + display_name: No Data From UDM + description: > + Cannot reach the UniFi Dream Machine Integration API. Check that the + IP address, API key, and network connectivity are correct. + high_cpu: + severity: warning + display_name: High CPU Usage + description: UDM CPU utilization exceeds 90%. + high_memory: + severity: warning + display_name: High Memory Usage + description: UDM memory utilization exceeds 90%. + +command_groups: + config: + display_name: Configuration + info: + display_name: Information + +commands: + write_configuration: + populate_values_command: read_configuration + display_name: Main Configuration + group: config + ui: + icon: wrench-outline + arguments: + ip_address: + display_name: UDM IP Address + description: IP address of the UniFi Dream Machine (e.g. 192.168.1.1) + type: string + required: true + api_key: + display_name: API Key + description: > + API key generated in UniFi OS Settings → Integrations. + type: string + required: true + site: + display_name: Site Name + description: > + UniFi site name. Leave empty for the default site. + type: string + required: false + read_configuration: + display_name: Read Configuration + group: info + ui: + icon: wrench-outline + +.cloud: + category: network_infrastructure + mobile_main_chart: num_clients + mobile_charts: + - num_clients + - num_devices + - wan_download_mbps + - wan_upload_mbps + - cpu_util + - mem_util