diff --git a/CO2_SENSOR_INTEGRATION.md b/CO2_SENSOR_INTEGRATION.md new file mode 100644 index 0000000..73f738c --- /dev/null +++ b/CO2_SENSOR_INTEGRATION.md @@ -0,0 +1,153 @@ +# CO2 Sensor Integration Guide + +This document explains how to write CO2 sensor data directly to the `ac_data.db` SQLite database so it appears under the "Wall" device on the dashboard. + +## Database Location + +``` +/home/seb/src/actest/ac_data.db +``` + +## Current Schema + +```sql +CREATE TABLE readings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + dev_id TEXT, + dev_name TEXT, + port INTEGER, + port_name TEXT, + temp_c REAL, + humidity REAL, + vpd REAL, + fan_speed INTEGER, + on_speed INTEGER, + off_speed INTEGER +); +``` + +## Inserting CO2 Data + +Add CO2 readings as a **new port** on the "Wall" device. Use **port 2** with `port_name = 'CO2'`. + +### SQL Insert Statement + +```sql +INSERT INTO readings (dev_name, port, port_name, temp_c, humidity, vpd, fan_speed) +VALUES ('Wall', 2, 'CO2', NULL, NULL, NULL, ); +``` + +> **Note**: The `fan_speed` column stores the CO2 ppm value. This reuses the existing integer column for sensor readings. + +### Example: Insert 850 ppm + +```sql +INSERT INTO readings (dev_name, port, port_name, temp_c, humidity, vpd, fan_speed) +VALUES ('Wall', 2, 'CO2', NULL, NULL, NULL, 850); +``` + +### From Command Line (bash) + +```bash +sqlite3 /home/seb/src/actest/ac_data.db \ + "INSERT INTO readings (dev_name, port, port_name, fan_speed) VALUES ('Wall', 2, 'CO2', 850);" +``` + +### From Node.js + +```javascript +import Database from 'better-sqlite3'; + +const db = new Database('/home/seb/src/actest/ac_data.db'); + +function insertCO2Reading(ppm) { + const stmt = db.prepare(` + INSERT INTO readings (dev_name, port, port_name, fan_speed) + VALUES ('Wall', 2, 'CO2', ?) + `); + stmt.run(ppm); +} + +// Example: Insert reading +insertCO2Reading(850); +``` + +### From Python + +```python +import sqlite3 + +DB_PATH = '/home/seb/src/actest/ac_data.db' + +def insert_co2_reading(ppm): + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO readings (dev_name, port, port_name, fan_speed) + VALUES ('Wall', 2, 'CO2', ?) + ''', (ppm,)) + conn.commit() + conn.close() + +# Example: Insert reading +insert_co2_reading(850) +``` + +## Dashboard Display + +Once data is inserted, the dashboard will automatically: + +1. Detect "Wall" device now has **2 ports**: Fan (port 1) and CO2 (port 2) +2. Display a new chart card labeled "CO2" under the Wall controller + +### Current Limitation + +The `LevelChart` component currently: +- Uses `fan_speed` for the Y-axis data ✅ (works for CO2) +- Labels chart as "Fan Speed" for non-light ports ⚠️ +- Has Y-axis scale 0-10 ⚠️ (too small for CO2 ppm) + +**The chart will display CO2 data, but the label and scale won't be ideal.** + +## Recommended Dashboard Enhancements + +To properly display CO2 data, modify `src/client/LevelChart.js`: + +```javascript +// Detect CO2 sensor type +const isCO2 = portName?.toLowerCase().includes('co2'); + +// Set appropriate label +const levelLabel = isLight ? 'Brightness' : (isCO2 ? 'CO2 (ppm)' : 'Fan Speed'); + +// Set appropriate Y-axis scale +const yScale = isCO2 + ? { suggestedMin: 400, suggestedMax: 2000 } + : { suggestedMin: 0, suggestedMax: 10, ticks: { stepSize: 1 } }; +``` + +## Testing + +After inserting test data, verify it appears: + +```bash +# Check if data was inserted +sqlite3 /home/seb/src/actest/ac_data.db \ + "SELECT * FROM readings WHERE port_name = 'CO2' ORDER BY timestamp DESC LIMIT 5;" + +# View all ports for Wall device +sqlite3 /home/seb/src/actest/ac_data.db \ + "SELECT DISTINCT dev_name, port, port_name FROM readings WHERE dev_name = 'Wall';" +``` + +## Cron Job Example + +To log CO2 every minute from another script: + +```bash +# crontab -e +* * * * * /path/to/your/co2_logger.sh >> /var/log/co2_logger.log 2>&1 +``` + +Where `co2_logger.sh` reads your sensor and inserts to the database. diff --git a/README.md b/README.md index 7f25ad6..4d689c6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AC Infinity Dashboard & Logger +# Dashboard & Logger This project logs data from AC Infinity controllers (via the Cloud API) and visualizes it on a local web dashboard. diff --git a/android_app_integration.md b/android_app_integration.md new file mode 100644 index 0000000..019a307 --- /dev/null +++ b/android_app_integration.md @@ -0,0 +1,129 @@ +# Android App Integration Guide Dashboard Proxy + +This document outlines how to build an Android application that consumes the data provided by your AC Infinity Proxy API. + +## Base Configuration + +* **Base URL**: `https://dev.seedheads.de/ac/` +* **Protocol**: HTTPS + +## API Endpoints + +The proxy server exposes two main endpoints that return JSON data. + +### 1. Get All Devices +Retrieves a list of all devices and their ports currently stored in the database. + +* **Endpoint**: `GET /api/devices` +* **Full URL**: `https://dev.seedheads.de/ac/api/devices` +* **Response Format**: JSON Array +* **Example Response**: + ```json + [ + { + "dev_name": "Tent Controller", + "port": 1, + "port_name": "Fan Inline" + }, + { + "dev_name": "Tent Controller", + "port": 2, + "port_name": "Light Top" + } + ] + ``` + +### 2. Get Historical Data +Retrieves historical sensor readings for a specific device and port. + +* **Endpoint**: `GET /api/history` +* **Full URL**: `https://dev.seedheads.de/ac/api/history` +* **Query Parameters**: + * `devName` (string, required): The name of the controller (e.g., "Tent Controller"). URL-encoded. + * `port` (integer, required): The port number (e.g., `1`). + * `range` (string, optional): The time range. Defaults to `day`. + * `day`: Last 24 hours. + * `week`: Last 7 days. + * `month`: Last 30 days. + +* **Example Request**: + `GET https://dev.seedheads.de/ac/api/history?devName=Tent%20Controller&port=1&range=day` + +* **Response Format**: JSON Array of objects +* **Example Response**: + ```json + [ + { + "timestamp": "2023-10-27T10:00:00Z", + "temp_c": 24.5, + "humidity": 60.2, + "vpd": 1.1, + "fan_speed": 5, + "on_speed": 0, + "off_speed": 0 + }, + ... + ] + ``` + *Note: Timestamps are returned in UTC (ending in 'Z'). You must convert them to the local timezone within the Android app.* + +## Android Implementation Recommendations + +### Networking Library +Use **Retrofit** with **OkHttp** for robust networking. It simplifies URL handling, parameter encoding, and JSON parsing. + +**Dependencies (`build.gradle`):** +```groovy +implementation 'com.squareup.retrofit2:retrofit:2.9.0' +implementation 'com.squareup.retrofit2:converter-gson:2.9.0' +``` + +### Data Models (Kotlin) + +```kotlin +data class Device( + @SerializedName("dev_name") val devName: String, + @SerializedName("port") val port: Int, + @SerializedName("port_name") val portName: String +) + +data class Reading( + val timestamp: String, + @SerializedName("temp_c") val tempC: Float, + @SerializedName("humidity") val humidity: Float, + @SerializedName("vpd") val vpd: Float, + @SerializedName("fan_speed") val fanSpeed: Int +) +``` + +### Retrofit Service Interface + +```kotlin +interface AcApi { + @GET("api/devices") + suspend fun getDevices(): List + + @GET("api/history") + suspend fun getHistory( + @Query("devName") devName: String, + @Query("port") port: Int, + @Query("range") range: String = "day" + ): List +} +``` + +### Handling Timezones +Since the API returns UTC timestamps (e.g., `2023-10-27T10:00:00Z`), use `java.time` (API 26+) to format them for the user's local device time. + +```kotlin +val instant = Instant.parse(reading.timestamp) +val localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()) +``` + +## Security Considerations + +* **Authentication**: Currently, the proxy at `https://dev.seedheads.de/ac/` is publicly accessible (if not protected by Basic Auth in Nginx). If you add Basic Auth to Nginx, you will need to add an `Authorization` header to your Retrofit client. +* **HTTPS**: Ensure your Android app allows HTTPS connections (default behavior). + +## Future Enhancements +* **Push Notifications**: The current API is poll-based. For real-time alerts (e.g., high temp), you would need to implement a background worker in Android (`WorkManager`) to poll the API periodically, or implement Firebase Cloud Messaging (FCM) on the Node.js server. diff --git a/server.js b/server.js index b527825..027aef7 100644 --- a/server.js +++ b/server.js @@ -232,7 +232,7 @@ app.get('/api/history', (req, res) => { } const stmt = db.prepare(` - SELECT timestamp, temp_c, humidity, vpd, fan_speed, on_speed + SELECT timestamp || 'Z' as timestamp, temp_c, humidity, vpd, fan_speed, on_speed FROM readings WHERE dev_name = ? AND port = ? AND timestamp >= datetime('now', ?) ORDER BY timestamp ASC @@ -252,6 +252,12 @@ app.get('/api/history', (req, res) => { const devMiddleware = webpackDevMiddleware(compiler, { publicPath: '/', writeToDisk: false, + headers: (req, res, context) => { + // Set cache headers for hashed bundle files (immutable) + if (req.url && req.url.match(/\.[a-f0-9]{8,}\.(js|css)$/i)) { + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + } + } }); app.use(devMiddleware); diff --git a/src/client/App.js b/src/client/App.js index 842f23e..5468ffa 100644 --- a/src/client/App.js +++ b/src/client/App.js @@ -22,7 +22,7 @@ function App() { - AC Infinity Dashboard + Tischlerei Dashboard diff --git a/src/client/ControllerCard.js b/src/client/ControllerCard.js index a20b994..555bfad 100644 --- a/src/client/ControllerCard.js +++ b/src/client/ControllerCard.js @@ -67,18 +67,18 @@ export default function ControllerCard({ controllerName, ports, range }) { {ports.map((port) => { const isLight = port.port_name && port.port_name.toLowerCase().includes('light'); - const levelTitle = isLight ? 'Brightness' : 'Fan Speed'; + const isCO2 = port.port_name && port.port_name.toLowerCase().includes('co2'); const pData = portData[port.port] || []; return ( - + {port.port_name || `Port ${port.port}`} - + diff --git a/src/client/LevelChart.js b/src/client/LevelChart.js index 2cfd8b8..a08935e 100644 --- a/src/client/LevelChart.js +++ b/src/client/LevelChart.js @@ -21,7 +21,7 @@ ChartJS.register( Legend ); -export default function LevelChart({ data, isLight, range }) { +export default function LevelChart({ data, isLight, isCO2, range }) { if (!data || data.length === 0) return null; const formatDateLabel = (timestamp) => { @@ -34,8 +34,9 @@ export default function LevelChart({ data, isLight, range }) { } }; - const levelLabel = isLight ? 'Brightness' : 'Fan Speed'; - const levelColor = isLight ? '#ffcd56' : '#9966ff'; + // Determine label and color based on sensor type + const levelLabel = isCO2 ? 'CO2 (ppm)' : (isLight ? 'Brightness' : 'Fan Speed'); + const levelColor = isCO2 ? '#4caf50' : (isLight ? '#ffcd56' : '#9966ff'); const chartData = { labels: data.map(d => formatDateLabel(d.timestamp)), @@ -45,13 +46,18 @@ export default function LevelChart({ data, isLight, range }) { data: data.map(d => d.fan_speed), borderColor: levelColor, backgroundColor: levelColor, - stepped: true, + stepped: !isCO2, // CO2 uses smooth lines borderWidth: 2, pointRadius: 1 }, ], }; + // CO2 needs different Y-axis scale (ppm range) + const yScale = isCO2 + ? { suggestedMin: 400, suggestedMax: 2000 } + : { suggestedMin: 0, suggestedMax: 10, ticks: { stepSize: 1 } }; + const options = { responsive: true, maintainAspectRatio: false, @@ -67,11 +73,7 @@ export default function LevelChart({ data, isLight, range }) { maxTicksLimit: 8 } }, - y: { - suggestedMin: 0, - suggestedMax: 10, - ticks: { stepSize: 1 } - } + y: yScale }, }; diff --git a/src/client/index.html b/src/client/index.html index 841e0c7..fe6f62b 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -4,7 +4,7 @@ - AC Infinity Dashboard + Tischlerei Dashboard diff --git a/webpack.config.js b/webpack.config.js index 1728850..09e3593 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,8 +10,9 @@ export default { entry: './src/client/index.js', output: { path: path.resolve(__dirname, 'dist'), - filename: 'bundle.js', - publicPath: '/ac/' + filename: 'bundle.[contenthash].js', + publicPath: '/ac/', + clean: true // Clean dist folder on rebuild }, module: { rules: [