diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..b9bba15 --- /dev/null +++ b/server/README.md @@ -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 \ No newline at end of file diff --git a/server/cmd/import_history/main.go b/server/cmd/import_history/main.go new file mode 100644 index 0000000..1c8700f --- /dev/null +++ b/server/cmd/import_history/main.go @@ -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) +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..131f67f --- /dev/null +++ b/server/go.mod @@ -0,0 +1,5 @@ +module chickencoop + +go 1.24.4 + +require github.com/mattn/go-sqlite3 v1.14.28 diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..42e5bac --- /dev/null +++ b/server/go.sum @@ -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= diff --git a/server/ldr_readings.db b/server/ldr_readings.db new file mode 100644 index 0000000..8a75146 Binary files /dev/null and b/server/ldr_readings.db differ diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..e83f5a4 --- /dev/null +++ b/server/main.go @@ -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)) +} diff --git a/server/web/templates/index.html b/server/web/templates/index.html new file mode 100644 index 0000000..512f947 --- /dev/null +++ b/server/web/templates/index.html @@ -0,0 +1,125 @@ + + + + + + Chicken Coop LDR Monitor + + + + + +
+

Chicken Coop LDR Monitor

+
+ +
+ +
+ + + + \ No newline at end of file