Add Go server with POST LDR to Sqlite3 and GET graphs of historic data
This commit is contained in:
parent
a780ffc918
commit
7599b3f652
72
server/README.md
Normal file
72
server/README.md
Normal 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
|
118
server/cmd/import_history/main.go
Normal file
118
server/cmd/import_history/main.go
Normal 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
5
server/go.mod
Normal 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
2
server/go.sum
Normal 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
BIN
server/ldr_readings.db
Normal file
Binary file not shown.
123
server/main.go
Normal file
123
server/main.go
Normal 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))
|
||||
}
|
125
server/web/templates/index.html
Normal file
125
server/web/templates/index.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user