chore: add Claude Code configuration and sensor updates
This commit is contained in:
parent
3847d2cf26
commit
4094654a5b
4 changed files with 235 additions and 28 deletions
172
.claude/claude.md
Normal file
172
.claude/claude.md
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
# Project Overview
|
||||||
|
|
||||||
|
**Name**: Veridian Edge Agent
|
||||||
|
**Type**: Edge computing service (sensor data sync)
|
||||||
|
**Primary Languages**: TypeScript (Bun runtime)
|
||||||
|
**Status**: v1.0.0, Production
|
||||||
|
|
||||||
|
## High-Level Description
|
||||||
|
|
||||||
|
Veridian Edge is a SensorPush integration agent that runs on Raspberry Pi or Ubuntu. It polls the SensorPush Cloud API for environmental readings and syncs them to the Veridian backend API.
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
```
|
||||||
|
SensorPush Cloud API → Veridian Edge Agent → Veridian Backend API
|
||||||
|
↓
|
||||||
|
SQLite (offline buffer)
|
||||||
|
```
|
||||||
|
|
||||||
|
**External Systems:**
|
||||||
|
- SensorPush Cloud API (OAuth 2.0, automatic token refresh)
|
||||||
|
- Veridian Backend API (HTTP/JSON webhooks)
|
||||||
|
- Local SQLite database (offline resilience)
|
||||||
|
- Systemd service management on Linux
|
||||||
|
|
||||||
|
## Multi-Repo Architecture
|
||||||
|
|
||||||
|
This repo works in conjunction with **veridian** as a single codebase:
|
||||||
|
- **veridian-edge** (this repo): Edge agent that polls SensorPush and syncs to backend
|
||||||
|
- **veridian**: Main platform backend that receives data from edge agent
|
||||||
|
|
||||||
|
When making changes, consider:
|
||||||
|
- API contract: Edge agent must send data in format expected by Veridian backend
|
||||||
|
- Authentication: API key authentication with Veridian backend
|
||||||
|
- Error handling: Network failures must be buffered to SQLite
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
veridian-edge/
|
||||||
|
├── src/ # Source code
|
||||||
|
│ ├── index.ts # Main entry point
|
||||||
|
│ ├── sensorpush.ts # SensorPush API client
|
||||||
|
│ ├── sync.ts # Sync logic to Veridian backend
|
||||||
|
│ └── db.ts # SQLite database operations
|
||||||
|
├── scripts/ # Installation and maintenance scripts
|
||||||
|
│ └── install.sh # Systemd service installer
|
||||||
|
├── config.json # Configuration (after installation)
|
||||||
|
└── package.json # Bun dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### Language/Style
|
||||||
|
- **TypeScript strict mode** enabled
|
||||||
|
- **Bun runtime** (not Node.js)
|
||||||
|
- **Hono framework** for HTTP endpoints
|
||||||
|
- **Better-sqlite3** for local database
|
||||||
|
- **Systemd service** for process management
|
||||||
|
|
||||||
|
### Package Manager
|
||||||
|
- **Use `bun` exclusively** (this is a Bun project)
|
||||||
|
- Commands: `bun install`, `bun run`, `bun test`, `bun add`
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Graceful degradation: Network failures buffer to SQLite
|
||||||
|
- Retry logic with exponential backoff for API calls
|
||||||
|
- Clear error messages in logs
|
||||||
|
- Never expose credentials in logs
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Structured logging to systemd journal
|
||||||
|
- Log levels: error, warn, info, debug
|
||||||
|
- Include timestamp, severity, and context
|
||||||
|
|
||||||
|
### Config
|
||||||
|
- Configuration file: `/opt/veridian-edge/config.json`
|
||||||
|
- Sensitive data: email, password, API keys
|
||||||
|
- Use `config.demo.json` or `config.example.json` as templates
|
||||||
|
|
||||||
|
## Patterns & Playbooks
|
||||||
|
|
||||||
|
### How to Add New Sensor Types
|
||||||
|
1. Update SensorPush API client to handle new sensor data
|
||||||
|
2. Add database schema migration if needed
|
||||||
|
3. Update sync logic to include new data type in API payload
|
||||||
|
4. Add tests for new sensor type
|
||||||
|
5. Update configuration schema
|
||||||
|
|
||||||
|
### How to Modify API Payload Format
|
||||||
|
**WARNING**: This affects the Veridian backend integration.
|
||||||
|
1. Check Veridian backend API contract in `veridian/` repo
|
||||||
|
2. Update this repo's sync logic
|
||||||
|
3. Coordinate changes with Veridian backend deployment
|
||||||
|
4. Test with actual Veridian backend before deploying
|
||||||
|
5. Consider backward compatibility during transition
|
||||||
|
|
||||||
|
### How to Run Locally (Development)
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
cp config.demo.json config.json
|
||||||
|
# Edit config.json with your credentials
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to Run in Production (Raspberry Pi/Ubuntu)
|
||||||
|
```bash
|
||||||
|
# Run installer (sets up systemd service)
|
||||||
|
sudo ./scripts/install.sh
|
||||||
|
|
||||||
|
# Service management
|
||||||
|
sudo systemctl start veridian-edge
|
||||||
|
sudo systemctl status veridian-edge
|
||||||
|
sudo systemctl stop veridian-edge
|
||||||
|
journalctl -u veridian-edge -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important Configuration Fields
|
||||||
|
- `facilityId`: UUID of facility in Veridian backend
|
||||||
|
- `backendUrl`: Veridian backend API URL
|
||||||
|
- `backendApiKey`: API key for Veridian authentication
|
||||||
|
- `sensorpush.email`: SensorPush account email
|
||||||
|
- `sensorpush.password`: SensorPush account password
|
||||||
|
- `sensorMappings`: Array mapping sensor IDs to room IDs
|
||||||
|
|
||||||
|
## PR & Learning Workflow
|
||||||
|
|
||||||
|
- When a PR introduces a new pattern or fixes a subtle issue:
|
||||||
|
1. Summarize the lesson in 1-2 bullets
|
||||||
|
2. Append under "Patterns & Playbooks" above
|
||||||
|
3. Consider updating README.md if user-facing
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
- **Unit tests**: Bun test for pure functions
|
||||||
|
- **Integration tests**: Mock SensorPush API and Veridian backend
|
||||||
|
- **VPS verification**: Deploy to edge device and verify:
|
||||||
|
- Service is running: `systemctl status veridian-edge`
|
||||||
|
- Logs show successful sync: `journalctl -u veridian-edge -f`
|
||||||
|
- Data appears in Veridian backend
|
||||||
|
|
||||||
|
## Deployment Considerations
|
||||||
|
|
||||||
|
- **Target devices**: Raspberry Pi 4 (2GB+ RAM) or Ubuntu 22.04+
|
||||||
|
- **Installer script**: `scripts/install.sh` handles systemd setup
|
||||||
|
- **Service runs as**: Dedicated user (created by installer)
|
||||||
|
- **Offline resilience**: SQLite buffer survives network outages
|
||||||
|
- **Health monitoring**: HTTP endpoint `http://localhost:3030/health`
|
||||||
|
- **Prometheus metrics**: Available at `http://localhost:3030/metrics`
|
||||||
|
|
||||||
|
## Integration with Veridian Backend
|
||||||
|
|
||||||
|
**Webhook/POST endpoint format** (must match Veridian backend expectations):
|
||||||
|
```
|
||||||
|
POST /api/v1/sensor-readings
|
||||||
|
Authorization: Bearer {backendApiKey}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"facilityId": "uuid",
|
||||||
|
"readings": [
|
||||||
|
{
|
||||||
|
"sensorId": "string",
|
||||||
|
"roomId": "uuid",
|
||||||
|
"timestamp": "ISO8601",
|
||||||
|
"temperature": number,
|
||||||
|
"humidity": number
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Before changing this format, ALWAYS check the Veridian backend API specification.
|
||||||
42
.claude/commands/deploy-edge.md
Normal file
42
.claude/commands/deploy-edge.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
You are the edge device deployment assistant for Veridian Edge.
|
||||||
|
|
||||||
|
## Edge Device Information
|
||||||
|
- Target: Raspberry Pi or Ubuntu device
|
||||||
|
- Service: veridian-edge (systemd)
|
||||||
|
- Default deployment path: /opt/veridian-edge
|
||||||
|
|
||||||
|
## Deployment Process
|
||||||
|
|
||||||
|
1. **Pre-deployment checks**:
|
||||||
|
- `git status` to check for uncommitted changes
|
||||||
|
- `git log -1 --oneline` to show what will be deployed
|
||||||
|
- Run tests: `bun test`
|
||||||
|
|
||||||
|
2. **Deploy to edge device** (replace EDGE_IP with actual device IP):
|
||||||
|
```bash
|
||||||
|
# Copy files to edge device
|
||||||
|
scp -r ./* admin@EDGE_IP:/tmp/veridian-edge/
|
||||||
|
|
||||||
|
# SSH to device and deploy
|
||||||
|
ssh admin@EDGE_IP << 'EOF'
|
||||||
|
cd /opt/veridian-edge
|
||||||
|
systemctl stop veridian-edge
|
||||||
|
cp -r /tmp/veridian-edge/* .
|
||||||
|
bun install --production
|
||||||
|
systemctl start veridian-edge
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Post-deployment verification**:
|
||||||
|
- Check service status: `ssh admin@EDGE_IP 'systemctl status veridian-edge'`
|
||||||
|
- View logs: `ssh admin@EDGE_IP 'journalctl -u veridian-edge -n 50'`
|
||||||
|
- Health check: `ssh admin@EDGE_IP 'curl http://localhost:3030/health'`
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Report:
|
||||||
|
- What was deployed (commit hash, message)
|
||||||
|
- Service status (running/not running)
|
||||||
|
- Any errors or warnings in logs
|
||||||
|
- Sync status (is it sending data to Veridian backend?)
|
||||||
|
- Suggested manual verification steps
|
||||||
24
src/kasa.ts
24
src/kasa.ts
|
|
@ -12,28 +12,30 @@ export class KasaController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discover the Kasa device on local network.
|
* Discover the Kasa device on local network.
|
||||||
* Prioritizes EP10 model as requested.
|
* Filters for device named "Veridian test plug" or similar.
|
||||||
*/
|
*/
|
||||||
async discover(): Promise<string | null> {
|
async discover(): Promise<string | null> {
|
||||||
// Prevent spamming discovery
|
// Prevent spamming discovery
|
||||||
if (Date.now() - this.lastDiscovery < 10000 && !this.deviceIp) {
|
if (Date.now() - this.lastDiscovery < 10000 && this.deviceIp) {
|
||||||
return null;
|
return this.deviceIp;
|
||||||
}
|
}
|
||||||
this.lastDiscovery = Date.now();
|
this.lastDiscovery = Date.now();
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
console.log('🔌 Kasa: Starting discovery...');
|
console.log('🔌 Kasa: Starting discovery (looking for "Veridian" device)...');
|
||||||
let found = false;
|
let found = false;
|
||||||
|
|
||||||
this.client.startDiscovery({
|
this.client.startDiscovery({
|
||||||
discoveryInterval: 500,
|
discoveryInterval: 500,
|
||||||
discoveryTimeout: 2000
|
discoveryTimeout: 3000
|
||||||
}).on('device-new', (device) => {
|
}).on('device-new', (device) => {
|
||||||
|
const alias = device.alias?.toLowerCase() || '';
|
||||||
const model = device.model;
|
const model = device.model;
|
||||||
console.log(`🔌 Kasa: Found device ${device.alias} (${model}) at ${device.host}`);
|
console.log(`🔌 Kasa: Found device "${device.alias}" (${model}) at ${device.host}`);
|
||||||
|
|
||||||
// If we found an EP10, grab it immediately
|
// Filter: Only accept devices with "veridian" in the name
|
||||||
if (model.toLowerCase().includes('ep10')) {
|
if (alias.includes('veridian')) {
|
||||||
|
console.log(`✅ Kasa: Matched target device "${device.alias}"`);
|
||||||
this.deviceIp = device.host;
|
this.deviceIp = device.host;
|
||||||
found = true;
|
found = true;
|
||||||
this.client.stopDiscovery();
|
this.client.stopDiscovery();
|
||||||
|
|
@ -41,15 +43,15 @@ export class KasaController {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Timeout after 3s
|
// Timeout after 4s
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.client.stopDiscovery();
|
this.client.stopDiscovery();
|
||||||
if (!found) {
|
if (!found) {
|
||||||
if (this.deviceIp) console.log(`🔌 Kasa: Using previously known IP: ${this.deviceIp}`);
|
if (this.deviceIp) console.log(`🔌 Kasa: Using previously known IP: ${this.deviceIp}`);
|
||||||
else console.warn('🔌 Kasa: Discovery timed out, no device found');
|
else console.warn('🔌 Kasa: No device named "Veridian" found');
|
||||||
resolve(this.deviceIp);
|
resolve(this.deviceIp);
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 4000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,13 @@ export class SensorPushClient {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!authRes.ok) {
|
if (!authRes.ok) {
|
||||||
// DEGRADE GRACEFULLY: If auth fails with 400/403 (likely invalid/missing creds), switch to Mock Mode
|
// DEGRADE GRACEFULLY: If auth fails, switch to SKIP MODE (no sensor polling)
|
||||||
|
// Edge Agent will still run for heartbeats and Kasa control
|
||||||
if (authRes.status === 400 || authRes.status === 403 || authRes.status === 401) {
|
if (authRes.status === 400 || authRes.status === 403 || authRes.status === 401) {
|
||||||
console.warn('⚠️ SensorPush auth failed. Switching to DEMO MODE (Mock Data Generator).');
|
console.warn('⚠️ SensorPush auth failed. SKIPPING sensor polling (Pulse sensors are primary).');
|
||||||
this.tokens = {
|
this.tokens = {
|
||||||
accessToken: 'MOCK_TOKEN',
|
accessToken: 'SKIP_MODE',
|
||||||
authorization: 'MOCK_AUTH',
|
authorization: 'SKIP_AUTH',
|
||||||
expiresAt: Date.now() + 24 * 60 * 60 * 1000 // 24h
|
expiresAt: Date.now() + 24 * 60 * 60 * 1000 // 24h
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
|
|
@ -106,19 +107,9 @@ export class SensorPushClient {
|
||||||
async getSamples(minutes: number = 5): Promise<SensorReading[]> {
|
async getSamples(minutes: number = 5): Promise<SensorReading[]> {
|
||||||
await this.ensureAuthenticated();
|
await this.ensureAuthenticated();
|
||||||
|
|
||||||
// Check for Demo Mode
|
// Check for Skip Mode - no sensor polling, Pulse is primary
|
||||||
if (this.tokens?.accessToken === 'MOCK_TOKEN') {
|
if (this.tokens?.accessToken === 'SKIP_MODE') {
|
||||||
const now = Date.now();
|
return []; // No SensorPush data - Pulse handles sensors
|
||||||
const sineWave = Math.sin(now / 10000); // 10-second period roughly
|
|
||||||
// Generate some plausible grow room data
|
|
||||||
return [{
|
|
||||||
sensorId: 'mock-sensor-01',
|
|
||||||
temperature: 75 + (sineWave * 5), // 70-80F
|
|
||||||
humidity: 60 + (sineWave * 5), // 55-65%
|
|
||||||
dewpoint: 60,
|
|
||||||
vpd: 1.2 + (sineWave * 0.2), // 1.0-1.4 kPa
|
|
||||||
observed: new Date().toISOString()
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// First get list of sensors
|
// First get list of sensors
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue