systant/index.ts
ryan ae778ebdab Initial systant implementation in Bun/TypeScript
A lightweight system monitoring agent that:
- Collects metrics via configurable shell commands
- Publishes to MQTT with Home Assistant auto-discovery
- Supports entity types: sensor, binary_sensor, light, switch, button
- Responds to commands over MQTT for controllable entities

Architecture:
- src/config.ts: TOML config loading and validation
- src/mqtt.ts: MQTT client with HA discovery
- src/entities.ts: Entity state polling and command handling
- index.ts: CLI entry point (run, check, once commands)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 19:52:47 -08:00

131 lines
3.4 KiB
TypeScript

import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { loadConfig } from "./src/config";
import { connect } from "./src/mqtt";
import { createEntityManager } from "./src/entities";
const DEFAULT_CONFIG_PATH = "./systant.toml";
yargs(hideBin(process.argv))
.scriptName("systant")
.usage("$0 <cmd> [args]")
.command(
"run",
"Start the systant daemon",
(yargs) => {
return yargs.option("config", {
alias: "c",
type: "string",
default: DEFAULT_CONFIG_PATH,
describe: "Path to config file",
});
},
async (argv) => {
await run(argv.config);
}
)
.command(
"check",
"Check config and connectivity",
(yargs) => {
return yargs.option("config", {
alias: "c",
type: "string",
default: DEFAULT_CONFIG_PATH,
describe: "Path to config file",
});
},
async (argv) => {
await check(argv.config);
}
)
.command(
"once",
"Poll all entity states once, then exit",
(yargs) => {
return yargs.option("config", {
alias: "c",
type: "string",
default: DEFAULT_CONFIG_PATH,
describe: "Path to config file",
});
},
async (argv) => {
await once(argv.config);
}
)
.demandCommand(1, "\nError: You need to specify a command!")
.help()
.parse();
async function run(configPath: string): Promise<void> {
const config = await loadConfig(configPath);
console.log(`Starting systant on ${config.systant.hostname}`);
const mqtt = await connect(config, config.systant.hostname);
const entities = createEntityManager(config, mqtt);
await entities.start();
// Handle shutdown
const shutdown = async () => {
console.log("\nShutting down...");
entities.stop();
await mqtt.disconnect();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
console.log("Systant running. Press Ctrl+C to stop.");
}
async function check(configPath: string): Promise<void> {
console.log(`Checking config: ${configPath}`);
try {
const config = await loadConfig(configPath);
const entityCount = Object.keys(config.entities).length;
console.log("Config loaded successfully");
console.log(` MQTT broker: ${config.mqtt.broker}`);
console.log(` Entities: ${entityCount} configured (default interval: ${config.systant.defaultInterval}s)`);
console.log(` HA discovery: ${config.homeassistant.discovery}`);
console.log("\nTesting MQTT connection...");
const hostname = config.systant.hostname;
const mqtt = await connect(config, hostname);
console.log("MQTT connection successful");
await mqtt.disconnect();
console.log("\nAll checks passed!");
} catch (err) {
console.error("Check failed:", err instanceof Error ? err.message : err);
process.exit(1);
}
}
async function once(configPath: string): Promise<void> {
const config = await loadConfig(configPath);
const hostname = config.systant.hostname;
const mqtt = await connect(config, hostname);
const entities = createEntityManager(config, mqtt);
// Start will do initial poll of all entities
await entities.start();
// Wait a moment for the initial polls to complete
await new Promise((resolve) => setTimeout(resolve, 1000));
entities.stop();
await mqtt.disconnect();
console.log("Entity states published successfully");
}