This commit is contained in:
sebseb7
2025-12-26 01:44:21 +01:00
parent 758684c598
commit e9a66cd1f4
6 changed files with 0 additions and 917 deletions

139
README.md
View File

@@ -1,139 +0,0 @@
# TischlerCtrl - Sensor Data Collection System
A Node.js server that collects sensor data from multiple agents via WebSocket, stores it in SQLite with automatic data summarization and retention policies.
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Central Server (Node.js) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ WebSocket │ │ SQLite DB │ │ Aggregation & │ │
│ │ Server │──│ sensor_data │ │ Cleanup Jobs │ │
│ │ :8080 │ │ sensor_10m │ │ (10m, 1h) │ │
│ └─────────────┘ │ sensor_1h │ └──────────────────┘ │
└────────┬─────────┴──────────────────────────────────────────┘
┌────┴────┬──────────────┐
│ │ │
┌───▼───┐ ┌───▼───┐ ┌─────▼─────┐
│ AC │ │ Tapo │ │ CLI │
│Infinity│ │ Agent │ │ Agent │
│ Agent │ │(Rust) │ │ (bash) │
└───────┘ └───────┘ └───────────┘
```
## Quick Start
### 1. Start the Server
```bash
cd server
cp .env.example .env
npm install
npm start
```
### 2. Generate API Keys
```bash
cd server
node src/cli/generate-key.js "ac-infinity-agent" "ac:"
node src/cli/generate-key.js "tapo-agent" "tapo:"
node src/cli/generate-key.js "custom" "custom:"
```
### 3. Configure and Start AC Infinity Agent
```bash
cd agents/ac-infinity
cp .env.example .env
# Edit .env with your AC Infinity credentials and API key
npm install
npm start
```
### 4. Build and Deploy Tapo Agent (Rust)
```bash
cd agents/tapo
cp config.toml.example config.toml
# Edit config.toml with your Tapo devices and API key
# Build for local machine
cargo build --release
# Or cross-compile for Raspberry Pi (requires cross)
# cargo install cross
# cross build --release --target armv7-unknown-linux-gnueabihf
# Run
./target/release/tapo-agent
# Or: RUST_LOG=info ./target/release/tapo-agent
```
### 5. Use CLI Agent
```bash
# Install websocat (one-time)
cargo install websocat
# Or: sudo apt install websocat
# Send data
export SENSOR_API_KEY="your-custom-api-key"
export SENSOR_SERVER="ws://localhost:8080"
./agents/cli/sensor-send mydevice temperature 24.5
```
## Data Retention Policy
| Resolution | Retention | Source |
|------------|-----------|--------|
| Raw (1 min) | 7 days | `sensor_data` |
| 10 minutes | 30 days | `sensor_data_10m` |
| 1 hour | Forever | `sensor_data_1h` |
Data is averaged when aggregating to higher resolutions.
## WebSocket Protocol
### Authentication
```json
{"type": "auth", "apiKey": "your-api-key"}
{"type": "auth", "success": true, "devicePrefix": "ac:"}
```
### Send Data
```json
{"type": "data", "readings": [
{"device": "ctrl1", "channel": "temperature", "value": 24.5},
{"device": "ctrl1", "channel": "humidity", "value": 65.0}
]}
{"type": "ack", "count": 2}
```
## Project Structure
```
tischlerctrl/
├── server/ # Central data collection server
│ ├── src/
│ │ ├── index.js # Entry point
│ │ ├── config.js # Configuration
│ │ ├── db/ # Database schema & queries
│ │ ├── websocket/ # WebSocket server
│ │ ├── jobs/ # Aggregation & cleanup jobs
│ │ └── cli/ # CLI tools (generate-key)
│ └── data/ # SQLite database files
├── agents/
│ ├── ac-infinity/ # Node.js AC Infinity agent
│ ├── tapo/ # Rust Tapo smart plug agent
│ └── cli/ # Bash CLI tool
└── README.md
```
## License
MIT

View File

@@ -1,17 +0,0 @@
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.resolve(__dirname, 'server/data/sensors.db');
const db = new Database(dbPath, { readonly: true });
console.log('--- RULES ---');
const rules = db.prepare('SELECT * FROM rules').all();
console.log(JSON.stringify(rules, null, 2));
console.log('\n--- OUTPUT CHANNELS ---');
const outputs = db.prepare('SELECT * FROM output_events WHERE channel = "CircFanLevel" ORDER BY timestamp DESC LIMIT 10').all();
console.table(outputs);
console.log('\n--- SENSOR DATA (ac:tent:temperature) ---');
const sensors = db.prepare('SELECT * FROM sensor_events WHERE device = "ac" AND channel = "tent:temperature" ORDER BY timestamp DESC LIMIT 5').all();
console.table(sensors);

View File

@@ -1,335 +0,0 @@
# Sensor Data Collection System
A Node.js server that collects sensor data from multiple agents via WebSocket, stores it in SQLite with automatic data summarization and retention policies.
## Architecture Overview
```mermaid
graph TB
subgraph "Central Server (Node.js)"
WS[WebSocket Server :8080]
DB[(SQLite Database)]
AGG[Aggregation Job]
WS --> DB
AGG --> DB
end
subgraph "AC Infinity Agent (Node.js)"
AC[AC Infinity Client]
AC -->|polls every 60s| ACAPI[AC Infinity Cloud API]
AC -->|WebSocket| WS
end
subgraph "Tapo Agent (Rust)"
TAPO[Tapo Client]
TAPO -->|polls every 60s| PLUG[Tapo P100/P110]
TAPO -->|WebSocket| WS
end
subgraph "Custom CLI Agent"
CLI[Shell Script]
CLI -->|WebSocket| WS
end
```
---
## User Review Required
> [!IMPORTANT]
> **Tapo Agent Language Choice**: I recommend **Rust** for the Tapo agent because:
> - Compiles to a single ~2MB static binary
> - Uses ~5-10MB RAM at runtime
> - Excellent [tapo crate](https://crates.io/crates/tapo) already exists
> - Easy cross-compilation for Raspberry Pi
>
> Alternatively, I could write it in **Go** (would need to implement protocol from scratch) or as a **Node.js** agent (but you mentioned wanting it lightweight).
> [!IMPORTANT]
> **AC Infinity Credentials**: The AC Infinity API requires email/password authentication to their cloud service. These will need to be stored in configuration.
---
## Project Structure
```
tischlerctrl/
├── server/
│ ├── package.json
│ ├── src/
│ │ ├── index.js # Entry point
│ │ ├── config.js # Configuration loader
│ │ ├── db/
│ │ │ ├── schema.js # SQLite schema + migrations
│ │ │ └── queries.js # Database operations
│ │ ├── websocket/
│ │ │ ├── server.js # WebSocket server
│ │ │ └── handlers.js # Message handlers
│ │ └── jobs/
│ │ ├── aggregator.js # Data summarization job
│ │ └── cleanup.js # Data retention cleanup
│ └── data/
│ └── sensors.db # SQLite database file
├── agents/
│ ├── ac-infinity/
│ │ ├── package.json
│ │ └── src/
│ │ ├── index.js # Entry point
│ │ ├── config.js # Configuration
│ │ ├── ac-client.js # AC Infinity API client
│ │ └── ws-client.js # WebSocket client with reconnect
│ │
│ ├── tapo/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs # Rust Tapo agent
│ │
│ └── cli/
│ └── sensor-send # Shell script CLI tool
├── .env.example # Example environment variables
└── README.md
```
---
## Proposed Changes
### Server - Database Schema
#### [NEW] [schema.js](file:///home/seb/src/tischlerctrl/server/src/db/schema.js)
SQLite tables:
```sql
-- API keys for agent authentication
CREATE TABLE api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
name TEXT NOT NULL, -- e.g., "ac-infinity-agent"
device_prefix TEXT NOT NULL, -- e.g., "ac:" or "tapo:"
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_used_at DATETIME
);
-- Raw sensor data (1-minute resolution, kept for 1 week)
CREATE TABLE sensor_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
device TEXT NOT NULL, -- e.g., "ac:controller-69-grow"
channel TEXT NOT NULL, -- e.g., "temperature", "humidity", "power"
value REAL NOT NULL,
INDEX idx_sensor_data_time (timestamp),
INDEX idx_sensor_data_device (device, channel)
);
-- 10-minute aggregated data (kept for 1 month)
CREATE TABLE sensor_data_10m (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL, -- Rounded to 10-min boundary
device TEXT NOT NULL,
channel TEXT NOT NULL,
value REAL NOT NULL, -- Averaged value
sample_count INTEGER NOT NULL,
UNIQUE(timestamp, device, channel)
);
-- 1-hour aggregated data (kept forever)
CREATE TABLE sensor_data_1h (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL, -- Rounded to 1-hour boundary
device TEXT NOT NULL,
channel TEXT NOT NULL,
value REAL NOT NULL, -- Averaged value
sample_count INTEGER NOT NULL,
UNIQUE(timestamp, device, channel)
);
```
---
### Server - WebSocket Protocol
#### [NEW] [server.js](file:///home/seb/src/tischlerctrl/server/src/websocket/server.js)
**Authentication Flow:**
1. Client connects to `ws://server:8080`
2. Client sends: `{ "type": "auth", "apiKey": "xxx" }`
3. Server validates API key, responds: `{ "type": "auth", "success": true, "devicePrefix": "ac:" }`
4. Client is now authenticated and can send data
**Data Ingestion Message:**
```json
{
"type": "data",
"readings": [
{ "device": "controller-69-grow", "channel": "temperature", "value": 24.5 },
{ "device": "controller-69-grow", "channel": "humidity", "value": 65.2 }
]
}
```
Server prepends `devicePrefix` to device names and adds timestamp.
**Keepalive:**
- Server sends `ping` every 30 seconds
- Client responds with `pong`
- Connection closed after 90 seconds of no response
---
### Server - Aggregation Jobs
#### [NEW] [aggregator.js](file:///home/seb/src/tischlerctrl/server/src/jobs/aggregator.js)
Runs every 10 minutes:
1. **10-minute aggregation**:
- Select data from `sensor_data` older than 10 minutes
- Group by device, channel, and 10-minute bucket
- Calculate average, insert into `sensor_data_10m`
2. **1-hour aggregation**:
- Select data from `sensor_data_10m` older than 1 hour
- Group by device, channel, and 1-hour bucket
- Calculate weighted average, insert into `sensor_data_1h`
#### [NEW] [cleanup.js](file:///home/seb/src/tischlerctrl/server/src/jobs/cleanup.js)
Runs every hour:
- Delete from `sensor_data` where timestamp < NOW - 7 days
- Delete from `sensor_data_10m` where timestamp < NOW - 30 days
---
### AC Infinity Agent
#### [NEW] [ac-client.js](file:///home/seb/src/tischlerctrl/agents/ac-infinity/src/ac-client.js)
Port of the TypeScript AC Infinity client to JavaScript ES modules:
- `login(email, password)` Returns userId token
- `getDevicesListAll()` Returns all controllers with sensor readings
- Polling interval: 60 seconds
- Extracts: temperature, humidity, VPD (if available) per controller
**Data extraction from API response:**
```javascript
// Each device in response has:
// - devId, devName
// - devSettings.temperature (°C * 100)
// - devSettings.humidity (% * 100)
// We normalize and send to server
```
#### [NEW] [ws-client.js](file:///home/seb/src/tischlerctrl/agents/ac-infinity/src/ws-client.js)
WebSocket client with:
- Auto-reconnect with exponential backoff (1s 2s 4s ... 60s max)
- Authentication on connect
- Heartbeat response
- Message queue during disconnection
---
### Tapo Agent (Rust)
#### [NEW] [main.rs](file:///home/seb/src/tischlerctrl/agents/tapo/src/main.rs)
Uses [tapo crate](https://crates.io/crates/tapo) for P100/P110 communication.
**Features:**
- Configuration via environment variables or TOML file
- WebSocket client with tungstenite crate
- Auto-reconnect with backoff
- Polls devices every 60 seconds
**Data collected:**
| Device | Channel | Description |
|--------|---------|-------------|
| P100 | `state` | 0 = off, 1 = on |
| P110 | `state` | 0 = off, 1 = on |
| P110 | `power` | Current power in watts |
| P110 | `energy_today` | Energy used today in Wh |
**Build for Raspberry Pi:**
```bash
# Cross-compile for ARM
cross build --release --target armv7-unknown-linux-gnueabihf
# Binary: ~2MB, runs with ~8MB RAM
```
---
### Custom CLI Agent
#### [NEW] [sensor-send](file:///home/seb/src/tischlerctrl/agents/cli/sensor-send)
A shell script using `websocat` (lightweight WebSocket CLI tool):
```bash
#!/bin/bash
# Usage: sensor-send --device=mydevice --channel=temp --value=23.5
API_KEY="${SENSOR_API_KEY:-}"
SERVER="${SENSOR_SERVER:-ws://localhost:8080}"
sensor-send mydevice temperature 23.5
```
Requires: `websocat` (single binary, ~3MB, available via cargo or apt)
---
## Configuration Examples
### Server `.env`
```bash
PORT=8080
DB_PATH=./data/sensors.db
# Generate API keys via CLI: node src/cli/generate-key.js "ac-infinity" "ac:"
```
### AC Infinity Agent `.env`
```bash
SERVER_URL=ws://192.168.1.100:8080
API_KEY=your-api-key-here
AC_EMAIL=your@email.com
AC_PASSWORD=your-password
POLL_INTERVAL_MS=60000
```
### Tapo Agent `config.toml`
```toml
server_url = "ws://192.168.1.100:8080"
api_key = "your-api-key-here"
poll_interval_secs = 60
[[devices]]
ip = "192.168.1.50"
name = "grow-light-plug"
type = "P110" # or "P100"
tapo_email = "your@email.com"
tapo_password = "your-tapo-password"
```
---
## Verification Plan
### Automated Tests
1. **Server unit tests**: Database operations, aggregation logic
2. **Integration test**: Start server, connect mock agent, verify data flow
3. **Run commands**:
```bash
cd server && npm test
cd agents/ac-infinity && npm test
```
### Manual Verification
1. Start server, verify WebSocket accepts connections
2. Send test data via CLI agent, verify it appears in database
3. Wait 10+ minutes, verify aggregation runs and data appears in `sensor_data_10m`
4. Connect AC Infinity agent with real credentials, verify sensor readings
5. Deploy Tapo agent to Raspberry Pi, verify plug data collection

View File

@@ -1,120 +0,0 @@
# Setting up Nginx as a Reverse Proxy
This guide explains how to configure Nginx to act as a reverse proxy for the TischlerCtrl server. This allows you to host the application on standard HTTP/HTTPS ports (80/443) and adds a layer of security.
## Prerequisites
- A Linux server (Debian/Ubuntu/Raspberry Pi OS).
- Root or sudo access.
- TischlerCtrl server running on localhost (default port: `8080`).
## 1. Install Nginx
If Nginx is not already installed:
```bash
sudo apt update
sudo apt install nginx
```
## 2. Create Configuration File
Create a new configuration file for the site in `/etc/nginx/sites-available/`. We'll name it `tischlerctrl`.
```bash
sudo nano /etc/nginx/sites-available/tischlerctrl
```
Paste the following configuration using your actual domain name or IP address:
```nginx
server {
listen 80;
server_name your-domain.com; # Replace with your domain or IP address
# Access logs
access_log /var/log/nginx/tischlerctrl.access.log;
error_log /var/log/nginx/tischlerctrl.error.log;
location /agentapi/ {
proxy_pass http://localhost:8080/; # Trailing slash strips /agentapi/
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Forwarding real client IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### Key Configuration Explained
- **proxy_pass**: Forwards requests to your Node.js application running on port 8080.
- **WebSocket Support**: These lines are **critical** for TischlerCtrl as it relies on WebSockets for real-time sensor data:
```nginx
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
```
## 3. Enable the Site
Create a symbolic link to the `sites-enabled` directory to activate the configuration:
```bash
sudo ln -s /etc/nginx/sites-available/tischlerctrl /etc/nginx/sites-enabled/
```
## 4. Test and Reload Nginx
Test the configuration for syntax errors:
```bash
sudo nginx -t
```
If the test is successful (returns `syntax is ok`), reload Nginx:
```bash
sudo systemctl reload nginx
```
## 5. SSL Configuration (Recommended)
To secure your connection with HTTPS (especially important for authentication), use Certbot to automatically configure a free specific Let's Encrypt SSL certificate.
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
```
Certbot will automatically modify your Nginx configuration to force HTTPS redirection and manage the SSL certificates.
## 6. Update Client Configurations
Since you are serving the API under `/agentapi/`, you must update your agents' configuration to point to the new URL path.
### WebSocket URL Format
- **Old (Direct):** `ws://server-ip:8080`
- **New (Proxy):** `ws://your-domain.com/agentapi/` (or `wss://` if using SSL)
### Example for Tapo Agent (`config.toml`)
```toml
server_url = "ws://your-domain.com/agentapi/"
# Or with SSL:
# server_url = "wss://your-domain.com/agentapi/"
```
### Example for Environment Variables
For agents using `.env` files:
```bash
SENSOR_SERVER="ws://your-domain.com/agentapi/"
```

View File

@@ -1,258 +0,0 @@
Sensor Data Collection System
A Node.js server that collects sensor data from multiple agents via WebSocket, stores it in SQLite with automatic data summarization and retention policies.
Architecture Overview
Custom CLI Agent
Tapo Agent (Rust)
AC Infinity Agent (Node.js)
Central Server (Node.js)
polls every 60s
WebSocket
polls every 60s
WebSocket
WebSocket
WebSocket Server :8080
SQLite Database
Aggregation Job
AC Infinity Client
AC Infinity Cloud API
Tapo Client
Tapo P100/P110
Shell Script
User Review Required
IMPORTANT
Tapo Agent Language Choice: I recommend Rust for the Tapo agent because:
Compiles to a single ~2MB static binary
Uses ~5-10MB RAM at runtime
Excellent tapo crate already exists
Easy cross-compilation for Raspberry Pi
Alternatively, I could write it in Go (would need to implement protocol from scratch) or as a Node.js agent (but you mentioned wanting it lightweight).
IMPORTANT
AC Infinity Credentials: The AC Infinity API requires email/password authentication to their cloud service. These will need to be stored in configuration.
Project Structure
tischlerctrl/
├── server/
│ ├── package.json
│ ├── src/
│ │ ├── index.js # Entry point
│ │ ├── config.js # Configuration loader
│ │ ├── db/
│ │ │ ├── schema.js # SQLite schema + migrations
│ │ │ └── queries.js # Database operations
│ │ ├── websocket/
│ │ │ ├── server.js # WebSocket server
│ │ │ └── handlers.js # Message handlers
│ │ └── jobs/
│ │ ├── aggregator.js # Data summarization job
│ │ └── cleanup.js # Data retention cleanup
│ └── data/
│ └── sensors.db # SQLite database file
├── agents/
│ ├── ac-infinity/
│ │ ├── package.json
│ │ └── src/
│ │ ├── index.js # Entry point
│ │ ├── config.js # Configuration
│ │ ├── ac-client.js # AC Infinity API client
│ │ └── ws-client.js # WebSocket client with reconnect
│ │
│ ├── tapo/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs # Rust Tapo agent
│ │
│ └── cli/
│ └── sensor-send # Shell script CLI tool
├── .env.example # Example environment variables
└── README.md
Proposed Changes
Server - Database Schema
[NEW]
schema.js
SQLite tables:
-- API keys for agent authentication
CREATE TABLE api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
name TEXT NOT NULL, -- e.g., "ac-infinity-agent"
device_prefix TEXT NOT NULL, -- e.g., "ac:" or "tapo:"
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_used_at DATETIME
);
-- Raw sensor data (1-minute resolution, kept for 1 week)
CREATE TABLE sensor_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
device TEXT NOT NULL, -- e.g., "ac:controller-69-grow"
channel TEXT NOT NULL, -- e.g., "temperature", "humidity", "power"
value REAL NOT NULL,
INDEX idx_sensor_data_time (timestamp),
INDEX idx_sensor_data_device (device, channel)
);
-- 10-minute aggregated data (kept for 1 month)
CREATE TABLE sensor_data_10m (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL, -- Rounded to 10-min boundary
device TEXT NOT NULL,
channel TEXT NOT NULL,
value REAL NOT NULL, -- Averaged value
sample_count INTEGER NOT NULL,
UNIQUE(timestamp, device, channel)
);
-- 1-hour aggregated data (kept forever)
CREATE TABLE sensor_data_1h (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL, -- Rounded to 1-hour boundary
device TEXT NOT NULL,
channel TEXT NOT NULL,
value REAL NOT NULL, -- Averaged value
sample_count INTEGER NOT NULL,
UNIQUE(timestamp, device, channel)
);
Server - WebSocket Protocol
[NEW]
server.js
Authentication Flow:
Client connects to ws://server:8080
Client sends: { "type": "auth", "apiKey": "xxx" }
Server validates API key, responds: { "type": "auth", "success": true, "devicePrefix": "ac:" }
Client is now authenticated and can send data
Data Ingestion Message:
{
"type": "data",
"readings": [
{ "device": "controller-69-grow", "channel": "temperature", "value": 24.5 },
{ "device": "controller-69-grow", "channel": "humidity", "value": 65.2 }
]
}
Server prepends devicePrefix to device names and adds timestamp.
Keepalive:
Server sends ping every 30 seconds
Client responds with pong
Connection closed after 90 seconds of no response
Server - Aggregation Jobs
[NEW]
aggregator.js
Runs every 10 minutes:
10-minute aggregation:
Select data from sensor_data older than 10 minutes
Group by device, channel, and 10-minute bucket
Calculate average, insert into sensor_data_10m
1-hour aggregation:
Select data from sensor_data_10m older than 1 hour
Group by device, channel, and 1-hour bucket
Calculate weighted average, insert into sensor_data_1h
[NEW]
cleanup.js
Runs every hour:
Delete from sensor_data where timestamp < NOW - 7 days
Delete from sensor_data_10m where timestamp < NOW - 30 days
AC Infinity Agent
[NEW]
ac-client.js
Port of the TypeScript AC Infinity client to JavaScript ES modules:
login(email, password) → Returns userId token
getDevicesListAll() → Returns all controllers with sensor readings
Polling interval: 60 seconds
Extracts: temperature, humidity, VPD (if available) per controller
Data extraction from API response:
// Each device in response has:
// - devId, devName
// - devSettings.temperature (°C * 100)
// - devSettings.humidity (% * 100)
// We normalize and send to server
[NEW]
ws-client.js
WebSocket client with:
Auto-reconnect with exponential backoff (1s → 2s → 4s → ... → 60s max)
Authentication on connect
Heartbeat response
Message queue during disconnection
Tapo Agent (Rust)
[NEW]
main.rs
Uses tapo crate for P100/P110 communication.
Features:
Configuration via environment variables or TOML file
WebSocket client with tungstenite crate
Auto-reconnect with backoff
Polls devices every 60 seconds
Data collected:
Device Channel Description
P100 state 0 = off, 1 = on
P110 state 0 = off, 1 = on
P110 power Current power in watts
P110 energy_today Energy used today in Wh
Build for Raspberry Pi:
# Cross-compile for ARM
cross build --release --target armv7-unknown-linux-gnueabihf
# Binary: ~2MB, runs with ~8MB RAM
Custom CLI Agent
[NEW]
sensor-send
A shell script using websocat (lightweight WebSocket CLI tool):
#!/bin/bash
# Usage: sensor-send --device=mydevice --channel=temp --value=23.5
API_KEY="${SENSOR_API_KEY:-}"
SERVER="${SENSOR_SERVER:-ws://localhost:8080}"
sensor-send mydevice temperature 23.5
Requires: websocat (single binary, ~3MB, available via cargo or apt)
Configuration Examples
Server .env
PORT=8080
DB_PATH=./data/sensors.db
# Generate API keys via CLI: node src/cli/generate-key.js "ac-infinity" "ac:"
AC Infinity Agent .env
SERVER_URL=ws://192.168.1.100:8080
API_KEY=your-api-key-here
AC_EMAIL=your@email.com
AC_PASSWORD=your-password
POLL_INTERVAL_MS=60000
Tapo Agent config.toml
server_url = "ws://192.168.1.100:8080"
api_key = "your-api-key-here"
poll_interval_secs = 60
[[devices]]
ip = "192.168.1.50"
name = "grow-light-plug"
type = "P110" # or "P100"
tapo_email = "your@email.com"
tapo_password = "your-tapo-password"
Verification Plan
Automated Tests
Server unit tests: Database operations, aggregation logic
Integration test: Start server, connect mock agent, verify data flow
Run commands:
cd server && npm test
cd agents/ac-infinity && npm test
Manual Verification
Start server, verify WebSocket accepts connections
Send test data via CLI agent, verify it appears in database
Wait 10+ minutes, verify aggregation runs and data appears in sensor_data_10m
Connect AC Infinity agent with real credentials, verify sensor readings
Deploy Tapo agent to Raspberry Pi, verify plug data collection

View File

@@ -1,48 +0,0 @@
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.resolve(__dirname, 'server/data/sensors.db');
console.log(`Connecting to database at ${dbPath}`);
const db = new Database(dbPath);
// 1. Verify Table Creation
console.log('Creating changelog table...');
try {
db.exec(`
CREATE TABLE IF NOT EXISTS changelog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
user TEXT,
text TEXT NOT NULL
)
`);
console.log('PASS: Table creation successful (or already exists)');
} catch (err) {
console.error('FAIL: Table creation failed:', err.message);
process.exit(1);
}
// 2. Verify Insert
console.log('Inserting test entry...');
try {
const stmt = db.prepare('INSERT INTO changelog (date, user, text) VALUES (?, ?, ?)');
const info = stmt.run(new Date().toISOString(), 'test_user', 'Test changelog entry');
console.log(`PASS: Insert successful, ID: ${info.lastInsertRowid}`);
} catch (err) {
console.error('FAIL: Insert failed:', err.message);
process.exit(1);
}
// 3. Verify Read
console.log('Reading entries...');
try {
const rows = db.prepare('SELECT * FROM changelog ORDER BY id DESC LIMIT 5').all();
console.table(rows);
if (rows.length > 0 && rows[0].user === 'test_user') {
console.log('PASS: Read verification successful');
} else {
console.error('FAIL: Read verification failed or data mismatch');
}
} catch (err) {
console.error('FAIL: Read failed:', err.message);
}