- Update architecture section to reflect actual file structure - Document entity system (sensor, binary_sensor, switch, light, button) - Add MQTT topic documentation - Add NixOS/home-manager integration section - Update commands section - Replace metrics-specialist with entity-specialist - Replace events-specialist with nix-specialist - Update mqtt-specialist context for current topic structure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
166 lines
4.8 KiB
Markdown
166 lines
4.8 KiB
Markdown
# Systant
|
|
|
|
A system monitoring agent that collects metrics, monitors services, and reports to MQTT/Home Assistant; and responds to events over MQTT to trigger commands or other behavior.
|
|
|
|
## Project Overview
|
|
|
|
Systant is a lightweight CLI tool written in Bun/TypeScript that:
|
|
- Collects system metrics (CPU, memory, disk, network)
|
|
- Monitors service health
|
|
- Publishes data to MQTT brokers
|
|
- Supports Home Assistant auto-discovery
|
|
- **Listens for MQTT commands** to trigger actions (run scripts, restart services, etc.)
|
|
- **Responds to events** with configurable handlers
|
|
- Runs as a daemon or one-shot command
|
|
|
|
### Architecture
|
|
|
|
```
|
|
index.ts # CLI entry point
|
|
src/
|
|
config.ts # TOML configuration loading
|
|
mqtt.ts # MQTT client, publishing, and HA discovery
|
|
entities.ts # Entity management (state polling, command handling)
|
|
```
|
|
|
|
### Entity System
|
|
|
|
Systant uses a unified "entity" concept that combines state monitoring and command handling. Entity types:
|
|
|
|
- **sensor**: Read-only numeric/string values (CPU usage, temperature, etc.)
|
|
- **binary_sensor**: Read-only on/off states (service running, etc.)
|
|
- **switch**: Controllable on/off with `on_command` and `off_command`
|
|
- **light**: Same as switch, for display/monitor control
|
|
- **button**: Press-only actions with `press_command`
|
|
|
|
Each entity is defined in TOML with shell commands:
|
|
|
|
```toml
|
|
[entities.cpu_usage]
|
|
type = "sensor"
|
|
state_command = "awk '/^cpu / {print int(($2+$4)*100/($2+$4+$5))}' /proc/stat"
|
|
unit = "%"
|
|
icon = "mdi:chip"
|
|
name = "CPU Usage"
|
|
|
|
[entities.headphones]
|
|
type = "switch"
|
|
state_command = "pactl get-default-sink | grep -q usb && echo ON || echo OFF"
|
|
on_command = "pactl set-default-sink alsa_output.usb-..."
|
|
off_command = "pactl set-default-sink alsa_output.pci-..."
|
|
```
|
|
|
|
### MQTT Topics
|
|
|
|
```
|
|
systant/{hostname}/{entity_id}/state # State updates
|
|
systant/{hostname}/{entity_id}/set # Commands (for switch/light)
|
|
systant/{hostname}/{entity_id}/press # Button presses
|
|
systant/{hostname}/availability # Online/offline status
|
|
homeassistant/{type}/{hostname}_{id}/config # HA auto-discovery
|
|
```
|
|
|
|
### Key Design Decisions
|
|
|
|
- **Single binary**: Compiles to standalone executable via `bun build --compile`
|
|
- **No external services**: Uses Bun built-ins (sqlite, file, etc.)
|
|
- **Config-driven**: TOML configuration for flexibility
|
|
- **Typed throughout**: Full TypeScript with strict mode
|
|
|
|
## Tech Stack
|
|
|
|
- **Runtime**: Bun (not Node.js)
|
|
- **Config**: TOML (smol-toml)
|
|
- **MQTT**: mqtt.js
|
|
- **Packaging**: Nix flake with home-manager module
|
|
|
|
### NixOS/Home Manager Integration
|
|
|
|
```nix
|
|
# flake.nix inputs
|
|
inputs.systant.url = "git+ssh://...";
|
|
|
|
# Add overlay for pkgs.systant
|
|
nixpkgs.overlays = [ inputs.systant.overlays.default ];
|
|
|
|
# Import home-manager module
|
|
home-manager.sharedModules = [ inputs.systant.homeManagerModules.default ];
|
|
|
|
# In user config
|
|
services.systant = {
|
|
enable = true;
|
|
settings = { /* TOML as Nix attrset */ };
|
|
# or: configFile = ./systant.toml;
|
|
};
|
|
```
|
|
|
|
## Bun Conventions
|
|
|
|
Default to using Bun instead of Node.js.
|
|
|
|
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
- Use `bun test` instead of `jest` or `vitest`
|
|
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
- Use `bun run <script>` instead of `npm run <script>`
|
|
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
- Bun automatically loads .env, so don't use dotenv.
|
|
|
|
### Bun APIs
|
|
|
|
- `Bun.serve()` for HTTP/WebSocket servers
|
|
- `bun:sqlite` for SQLite (not better-sqlite3)
|
|
- `Bun.file()` for file I/O (not node:fs readFile/writeFile)
|
|
- `Bun.$\`cmd\`` for shell commands (not execa)
|
|
- Native `WebSocket` (not ws)
|
|
|
|
### Testing
|
|
|
|
```ts
|
|
import { test, expect, describe, beforeEach } from "bun:test";
|
|
|
|
describe("MetricCollector", () => {
|
|
test("collects CPU metrics", async () => {
|
|
const metrics = await collectCPU();
|
|
expect(metrics.usage).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
```
|
|
|
|
Run tests: `bun test`
|
|
Run specific: `bun test src/metrics`
|
|
Watch mode: `bun test --watch`
|
|
|
|
## Code Style
|
|
|
|
- Prefer `async/await` over callbacks
|
|
- Use explicit return types on public functions
|
|
- Prefer `interface` over `type` for object shapes
|
|
- Use `const` by default, `let` only when reassignment needed
|
|
- No classes unless state encapsulation is genuinely needed
|
|
- Prefer pure functions and composition
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Development
|
|
bun run index.ts run --config systant.toml
|
|
|
|
# Build standalone binary
|
|
bun build index.ts --compile --outfile systant
|
|
|
|
# Nix build
|
|
nix build .#systant
|
|
|
|
# Run tests
|
|
bun test
|
|
```
|
|
|
|
## Planning Protocol
|
|
|
|
When implementing features:
|
|
1. Discuss the approach before writing code
|
|
2. Start with types/interfaces
|
|
3. Write tests alongside implementation
|
|
4. Keep PRs focused and small
|