Skip to main content

Check heartbeat health

Verify that a gateway has accumulated enough valid heartbeats in the last day by first looking up its radios (GET /v0/gateways/{address}/radios) and then calling GET /v0/radios/{cbsdId}/heartbeats/count. The script aggregates hourly buckets, counts valid records, and flags gateways with insufficient heartbeat points.

Prerequisites

  • Node.js 18 or newer.
  • An API key exported as HELIUMGEEK_API_KEY.
  • A gateway address provided via GATEWAY_ADDRESS or the first CLI argument.

Run the example

  1. Download mobile-heartbeat-check.js and save it locally.

  2. Export the environment variables and run:

    export HELIUMGEEK_API_KEY="your-api-key"
    export GATEWAY_ADDRESS="your-gateway-address"
    node mobile-heartbeat-check.js

Script

mobile-heartbeat-check.js
#!/usr/bin/env node

const BASE_URL = (process.env.HELIUMGEEK_API_BASE_URL ?? 'https://api.heliumgeek.com/v0').replace(/\/$/, '');
const API_KEY = process.env.HELIUMGEEK_API_KEY;
const GATEWAY_ADDRESS =
(process.argv[2] ?? process.env.GATEWAY_ADDRESS ?? '').trim();
const REQUIRED_POINTS = 12;
const WINDOW_HOURS = 24;

if (!API_KEY) {
console.error('Set HELIUMGEEK_API_KEY before running this script.');
process.exit(1);
}

if (!GATEWAY_ADDRESS) {
console.error(
'Provide the gateway address as an argument or set GATEWAY_ADDRESS.'
);
process.exit(1);
}

async function main() {
const radio = await fetchFirstRadio();
if (!radio) {
console.log(
`Gateway ${GATEWAY_ADDRESS} has no radios, skipping heartbeat check.`
);
return;
}

const now = startOfUtcHour(new Date());
const minDate = new Date(now.getTime());
minDate.setUTCHours(minDate.getUTCHours() - WINDOW_HOURS);

const response = await fetch(
buildUrl(`/radios/${encodeURIComponent(radio.cbsdId)}/heartbeats/count`, {
min_time: minDate.toISOString(),
max_time: now.toISOString(),
bucket: 'hour',
validity: 'valid',
}),
{
headers: {
'x-api-key': API_KEY,
},
}
);

if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}

const heartbeats = (await response.json()) ?? [];
const heartbeatPoints = heartbeats.filter((item) => Number(item.count ?? 0) > 0).length;

console.log(`Heartbeat summary for ${GATEWAY_ADDRESS}`);
console.log(` Window: ${minDate.toISOString()}${now.toISOString()}`);
console.log(` Valid heartbeat points: ${heartbeatPoints}`);
console.log(` Expected minimum: ${REQUIRED_POINTS}`);

if (heartbeatPoints >= REQUIRED_POINTS) {
console.log('Status: OK — heartbeat coverage meets the requirement.');
} else {
console.log('Status: Attention required');
console.log(
` - Heartbeat points below threshold (${heartbeatPoints}/${REQUIRED_POINTS}). Gateways need at least 12 hourly heartbeats per epoch.`
);
}
}

function buildUrl(path, query = {}) {
const url = new URL(path.replace(/^\//, ''), `${BASE_URL}/`);
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
url.searchParams.set(key, String(value));
}
});
return url.toString();
}

function startOfUtcHour(date) {
return new Date(
Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours()
)
);
}

async function fetchFirstRadio() {
const response = await fetch(
buildUrl(`/gateways/${encodeURIComponent(GATEWAY_ADDRESS)}/radios`),
{
headers: {
'x-api-key': API_KEY,
},
}
);

if (!response.ok) {
throw new Error(`Failed to fetch radios: ${response.status} ${response.statusText}`);
}

const payload = await response.json();
const radios = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
const radio = radios.find((item) => item?.cbsdId);
return radio;
}

main().catch((error) => {
console.error(error.message);
process.exit(1);
});