u
This commit is contained in:
139
README.md
139
README.md
@@ -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
|
|
||||||
17
debug_db.js
17
debug_db.js
@@ -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);
|
|
||||||
@@ -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
|
|
||||||
120
nginx_proxy.md
120
nginx_proxy.md
@@ -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/"
|
|
||||||
```
|
|
||||||
258
promptlog.txt
258
promptlog.txt
@@ -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
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user