Skip to content

Fleet health alerts

Find out about the tablet with a dead battery before the morning shift does. This program checks every device and posts one message listing the ones that need attention. Run it every hour.

Needs: a service account with ViewDevices, a Slack incoming webhook or Microsoft Teams workflow webhook, and .NET 10.

Terminal window
OHR_TOKEN=ohrs_… ALERT_WEBHOOK_URL=https://hooks.slack.com/services/… dotnet run fleet-health-alerts.cs
3 device alert(s):
• A0000A00043 (Airport kiosk) has not checked in for 11 hours
• A0000A00070 (Downtown store) is at 9% battery and not charging
• A0000A00081 (Downtown store) could not apply 1 setting(s)
fleet-health-alerts.cs
#:package OhrTech.Management@*
// Posts a summary of devices that need attention to a Slack or Microsoft Teams channel.
// Run it on a schedule (cron, a scheduled task, a GitHub Actions schedule).
// OHR_TOKEN=ohrs_… ALERT_WEBHOOK_URL=https://hooks.slack.com/… dotnet run fleet-health-alerts.cs
// Needs ViewDevices. Without ALERT_WEBHOOK_URL it prints the summary instead.
using System.Text;
using System.Text.Json.Nodes;
using OhrTech.Management;
var offlineAfter = TimeSpan.FromHours(6);
const int lowBattery = 15;
using var api = OhrManagementClient.ForServiceAccount(
Environment.GetEnvironmentVariable("OHR_TOKEN") ?? throw new InvalidOperationException("Set OHR_TOKEN to a service account token."));
var alerts = new List<string>();
foreach (var d in await api.ListDevicesAsync())
{
var name = $"{d.Serial ?? $"device {d.DeviceId}"} ({d.FleetName ?? "no fleet"})";
if (d.LastCheckIn is { } seen && DateTimeOffset.UtcNow - seen is var away && away > offlineAfter)
alerts.Add($"{name} has not checked in for {(away.TotalDays >= 2 ? $"{away.TotalDays:0} days" : $"{away.TotalHours:0} hours")}");
if (d.BatteryLevel is < lowBattery && !d.Charging)
alerts.Add($"{name} is at {d.BatteryLevel}% battery and not charging");
if (d.SettingsErrors > 0)
alerts.Add($"{name} could not apply {d.SettingsErrors} setting(s)");
if (d.AgentOutdated)
alerts.Add($"{name} is waiting to update the OhrTech Agent");
}
if (alerts.Count == 0) return;
var text = $"{alerts.Count} device alert(s):\n• " + string.Join("\n• ", alerts);
var webhook = Environment.GetEnvironmentVariable("ALERT_WEBHOOK_URL");
if (string.IsNullOrEmpty(webhook))
{
Console.WriteLine(text);
return;
}
// Slack incoming webhooks and Teams workflow webhooks both accept {"text": "…"}.
using var http = new HttpClient();
var body = new JsonObject { ["text"] = text }.ToJsonString();
(await http.PostAsync(webhook, new StringContent(body, Encoding.UTF8, "application/json"))).EnsureSuccessStatusCode();

One call, GET /v1/devices, returns every device’s last check-in, battery and charging state, settings errors and whether the OhrTech Agent is behind. The program applies your thresholds and posts only when something needs attention.

crontab
0 * * * * OHR_TOKEN=ohrs_… ALERT_WEBHOOK_URL=https://hooks.slack.com/… dotnet run /opt/ohr/fleet-health-alerts.cs

Or as a GitHub Actions schedule, a Windows scheduled task or a Kubernetes CronJob. Pin the service account to the networks it runs from.

  • Change offlineAfter and lowBattery, or alert per fleet to the team that owns it.
  • Before alerting that a device is offline, ask it to check in (POST /v1/devices/{id}/check-in) and look again a minute later.
  • Open a ticket in your help desk instead of posting a message.