Skip to content

Sort devices into fleets

Your inventory system already knows which store, vehicle or team each device belongs to. This program reads that as a CSV and moves each device into the matching fleet, so it gets that fleet’s settings at once. Run it after each delivery, or on a schedule.

Needs: a service account with ViewDevices and MoveDevices, and .NET 10.

inventory.csv
serial,fleet
A0000A00042,Downtown store
A0000A00043,Airport kiosk
Terminal window
OHR_TOKEN=ohrs_… dotnet run sort-into-fleets.cs -- inventory.csv # what would move
OHR_TOKEN=ohrs_… dotnet run sort-into-fleets.cs -- inventory.csv --apply # move them
A0000A00042: (no fleet) → Downtown store
A0000A00043: Warehouse → Airport kiosk
Would move 2, already in place 0, problems 0. Run again with --apply to move them.
sort-into-fleets.cs
#:package OhrTech.Management@*
// Puts devices into fleets from your own inventory: a CSV of serial number and fleet name.
// OHR_TOKEN=ohrs_… dotnet run sort-into-fleets.cs -- inventory.csv show what would move
// OHR_TOKEN=ohrs_… dotnet run sort-into-fleets.cs -- inventory.csv --apply move them
// Needs ViewDevices and MoveDevices.
using OhrTech.Management;
var file = args.FirstOrDefault(a => !a.StartsWith("--")) ?? "inventory.csv";
var apply = args.Contains("--apply");
using var api = OhrManagementClient.ForServiceAccount(
Environment.GetEnvironmentVariable("OHR_TOKEN") ?? throw new InvalidOperationException("Set OHR_TOKEN to a service account token."));
var fleets = (await api.ListFleetsAsync()).ToLookup(f => f.FleetName, StringComparer.OrdinalIgnoreCase);
var devices = (await api.ListDevicesAsync())
.Where(d => d.Serial != null)
.ToDictionary(d => d.Serial!, StringComparer.OrdinalIgnoreCase);
int moved = 0, unchanged = 0, problems = 0;
foreach (var line in File.ReadLines(file).Skip(1)) // header: serial,fleet
{
var columns = line.Split(',', 2, StringSplitOptions.TrimEntries);
if (columns.Length < 2 || columns[0].Length == 0) continue;
var (serial, fleetName) = (columns[0], columns[1]);
if (!devices.TryGetValue(serial, out var device))
{
Console.WriteLine($"{serial}: no such device in your organisation");
problems++;
continue;
}
var matches = fleets[fleetName].ToList();
if (matches.Count != 1)
{
Console.WriteLine($"{serial}: {(matches.Count == 0 ? "no fleet" : "more than one fleet")} named \"{fleetName}\"");
problems++;
continue;
}
var fleet = matches[0];
if (device.FleetId == fleet.FleetId)
{
unchanged++;
continue;
}
Console.WriteLine($"{serial}: {device.FleetName ?? "(no fleet)"} → {fleet.FleetName}");
if (apply) await api.MoveDeviceAsync(device.DeviceId, new MoveDeviceRequest { FleetId = fleet.FleetId });
moved++;
}
Console.WriteLine($"{(apply ? "Moved" : "Would move")} {moved}, already in place {unchanged}, problems {problems}." +
(apply || moved == 0 ? "" : " Run again with --apply to move them."));
  • Fleets are matched by name (ignoring case), devices by serial number. A name that matches no fleet, or more than one, is reported and skipped.
  • Devices already in the right fleet are left alone, so running it again changes nothing.
  • A service account scoped to some fleets can only move devices among them.

Read from your inventory system’s API instead of a CSV, or take the fleet from a field you already have, such as a store number.