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_ADDRESSor the first CLI argument.
Run the example
-
Download mobile-heartbeat-check.js and save it locally.
-
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);
});