Add Go server with POST LDR to Sqlite3 and GET graphs of historic data

This commit is contained in:
bdnugget 2025-06-16 20:58:46 +02:00
parent a780ffc918
commit 7599b3f652
7 changed files with 445 additions and 0 deletions

72
server/README.md Normal file
View File

@ -0,0 +1,72 @@
# Chicken Coop LDR Monitor Server
This is a Go server that receives and stores LDR (Light Dependent Resistor) readings from the ESP8266 and provides a web interface to visualize the data.
## Prerequisites
- Go 1.16 or later
- SQLite3 development libraries
On Ubuntu/Debian, install SQLite3 development libraries with:
```bash
sudo apt-get install libsqlite3-dev
```
## Setup
1. Install Go dependencies:
```bash
go mod tidy
```
2. Update the ESP8266 code with your Go server's IP address:
- Open `ESP8266/ESP8266.ino`
- Change the `goServerHost` value to your Go server's IP address
## Running the Server
1. Start the server:
```bash
go run main.go
```
2. Access the web interface at `http://localhost:8080`
The server will:
- Listen for LDR readings on `/api/ldr` endpoint
- Store readings in a SQLite database
- Provide a web interface with charts showing LDR values over time
- Support different time ranges (day, week, month, all time)
## Importing Historical Data
If you have a Telegram export file (`tg_export.json`) with historical LDR readings, you can import them into the database:
1. Make sure the Telegram export file is in the root directory of the project
2. Run the import script:
```bash
cd cmd/import_history
go run main.go
```
The script will:
- Read the Telegram export file
- Extract LDR readings from messages
- Import them into the SQLite database
- Show the number of readings imported
## API Endpoints
- `POST /api/ldr` - Receive LDR readings
- Body: `{"value": 123, "timestamp": "2024-03-14T12:00:00Z"}`
- Note: timestamp is optional, server will use current time if not provided
- `GET /api/readings?period=day|week|month|all` - Get LDR readings
- Returns JSON array of readings with timestamps
## Web Interface
The web interface provides:
- Line chart of LDR values over time
- Time range selector (24h, week, month, all time)
- Auto-refresh every 5 minutes

View File

@ -0,0 +1,118 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// Open the database
db, err := sql.Open("sqlite3", "../../ldr_readings.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create the table if it doesn't exist
createTable := `
CREATE TABLE IF NOT EXISTS ldr_readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value INTEGER NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);`
_, err = db.Exec(createTable)
if err != nil {
log.Fatal(err)
}
// Read the Telegram export file
data, err := os.ReadFile("../../../tg_export.json")
if err != nil {
log.Fatal(err)
}
// Parse the JSON into a map
var export map[string]interface{}
if err := json.Unmarshal(data, &export); err != nil {
log.Printf("Error unmarshaling JSON: %v", err)
log.Printf("First 100 bytes of data: %s", string(data[:100]))
log.Fatal(err)
}
// Get the messages array
messages, ok := export["messages"].([]interface{})
if !ok {
log.Fatal("Could not find messages array in export")
}
// Prepare the insert statement
stmt, err := db.Prepare("INSERT INTO ldr_readings (value, timestamp) VALUES (?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// Process each message
count := 0
for _, msgInterface := range messages {
msg, ok := msgInterface.(map[string]interface{})
if !ok {
continue
}
// Get the text field
text, ok := msg["text"].(string)
if !ok {
continue
}
// Only process messages containing LDR values
if !strings.HasPrefix(text, "Current LDR value: ") {
continue
}
// Extract the LDR value
valueStr := strings.TrimPrefix(text, "Current LDR value: ")
value, err := strconv.Atoi(valueStr)
if err != nil {
log.Printf("Error parsing LDR value from message: %s", text)
continue
}
// Get the date field
date, ok := msg["date"].(string)
if !ok {
continue
}
// Parse the timestamp
timestamp, err := time.Parse("2006-01-02T15:04:05", date)
if err != nil {
log.Printf("Error parsing timestamp from message: %s", date)
continue
}
// Insert into database
_, err = stmt.Exec(value, timestamp)
if err != nil {
log.Printf("Error inserting record: %v", err)
continue
}
count++
if count%100 == 0 {
fmt.Printf("Imported %d readings...\n", count)
}
}
fmt.Printf("Successfully imported %d LDR readings\n", count)
}

5
server/go.mod Normal file
View File

@ -0,0 +1,5 @@
module chickencoop
go 1.24.4
require github.com/mattn/go-sqlite3 v1.14.28

2
server/go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

BIN
server/ldr_readings.db Normal file

Binary file not shown.

123
server/main.go Normal file
View File

@ -0,0 +1,123 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"time"
_ "github.com/mattn/go-sqlite3"
)
type LDRReading struct {
Value int `json:"value"`
Timestamp time.Time `json:"timestamp"`
}
var db *sql.DB
func initDB() {
var err error
db, err = sql.Open("sqlite3", "./ldr_readings.db")
if err != nil {
log.Fatal(err)
}
createTable := `
CREATE TABLE IF NOT EXISTS ldr_readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value INTEGER NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);`
_, err = db.Exec(createTable)
if err != nil {
log.Fatal(err)
}
}
func handleLDRReading(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var reading LDRReading
if err := json.NewDecoder(r.Body).Decode(&reading); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, err := db.Exec("INSERT INTO ldr_readings (value, timestamp) VALUES (?, ?)",
reading.Value, reading.Timestamp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func handleGetReadings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
period := r.URL.Query().Get("period")
var query string
var args []interface{}
switch period {
case "day":
query = "SELECT value, timestamp FROM ldr_readings WHERE timestamp >= datetime('now', '-1 day') ORDER BY timestamp"
case "week":
query = "SELECT value, timestamp FROM ldr_readings WHERE timestamp >= datetime('now', '-7 day') ORDER BY timestamp"
case "month":
query = "SELECT value, timestamp FROM ldr_readings WHERE timestamp >= datetime('now', '-30 day') ORDER BY timestamp"
default:
query = "SELECT value, timestamp FROM ldr_readings ORDER BY timestamp"
}
rows, err := db.Query(query, args...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var readings []LDRReading
for rows.Next() {
var r LDRReading
if err := rows.Scan(&r.Value, &r.Timestamp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
readings = append(readings, r)
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
json.NewEncoder(w).Encode(readings)
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("web/templates/index.html"))
tmpl.Execute(w, nil)
}
func main() {
initDB()
defer db.Close()
http.HandleFunc("/", handleIndex)
http.HandleFunc("/api/ldr", handleLDRReading)
http.HandleFunc("/api/readings", handleGetReadings)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
fmt.Println("Server starting on :8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}

View File

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chicken Coop LDR Monitor</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.controls {
margin-bottom: 20px;
}
select {
padding: 8px;
font-size: 16px;
border-radius: 4px;
border: 1px solid #ddd;
}
canvas {
width: 100% !important;
height: 400px !important;
}
</style>
</head>
<body>
<div class="container">
<h1>Chicken Coop LDR Monitor</h1>
<div class="controls">
<select id="timeRange" onchange="updateChart()">
<option value="day">Last 24 Hours</option>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
<option value="all">All Time</option>
</select>
</div>
<canvas id="ldrChart"></canvas>
</div>
<script>
let chart = null;
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleString();
}
async function fetchData(period) {
const response = await fetch(`/api/readings?period=${period}`);
const data = await response.json();
return data.map(item => ({
x: new Date(item.timestamp),
y: item.value
}));
}
async function updateChart() {
const period = document.getElementById('timeRange').value;
const data = await fetchData(period);
if (chart) {
chart.destroy();
}
const ctx = document.getElementById('ldrChart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'LDR Value',
data: data,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time: {
unit: period === 'day' ? 'hour' : 'day',
displayFormats: {
hour: 'MMM d, HH:mm',
day: 'MMM d'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
title: {
display: true,
text: 'LDR Value'
}
}
}
}
});
}
// Initial load
updateChart();
// Update every 5 minutes
setInterval(updateChart, 5 * 60 * 1000);
</script>
</body>
</html>