From 2198c20790164bd26482e162935dfdd1829ed782 Mon Sep 17 00:00:00 2001 From: Rachel Volk Date: Mon, 9 Mar 2026 01:26:52 +0100 Subject: [PATCH] initial commit --- .gitignore | 5 + api.go | 173 ++++++++++++ config.yaml | 20 ++ database.go | 47 ++++ db/db.go | 31 +++ db/models.go | 49 ++++ db/queries.sql.go | 681 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 27 ++ go.sum | 67 +++++ main.go | 71 +++++ mqtt_client.go | 220 +++++++++++++++ public/favicon.ico | Bin 0 -> 11019 bytes public/index.css | 45 +++ public/index.html | 73 +++++ public/index.js | 244 ++++++++++++++++ queries.sql | 136 +++++++++ schema.sql | 43 +++ sqlc.yaml | 10 + 18 files changed, 1942 insertions(+) create mode 100644 .gitignore create mode 100644 api.go create mode 100644 config.yaml create mode 100644 database.go create mode 100644 db/db.go create mode 100644 db/models.go create mode 100644 db/queries.sql.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 mqtt_client.go create mode 100644 public/favicon.ico create mode 100644 public/index.css create mode 100644 public/index.html create mode 100644 public/index.js create mode 100644 queries.sql create mode 100644 schema.sql create mode 100644 sqlc.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56f293a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.env +/.venv/ +/persist/ +/.idea/ +/.vscode/ diff --git a/api.go b/api.go new file mode 100644 index 0000000..6dfabc7 --- /dev/null +++ b/api.go @@ -0,0 +1,173 @@ +package main + +import ( + "context" + "embed" + "encoding/json" + "errors" + "io/fs" + "log" + "net/http" + "os" + "sensor_dashboard/db" + "time" +) + +//go:embed public +var staticFiles embed.FS // Holds embedded static files + +func Api(queries db.Queries, config Config) (http.Handler, error) { + + mux := http.NewServeMux() + + var publicDir fs.FS + if config.ServeFromFS { + publicDir = os.DirFS("./public") + } else { + publicDir, _ = fs.Sub(staticFiles, "public") + } + + fileServer := http.FileServer(http.FS(publicDir)) + + mux.Handle("/humidity", GetHumidityLogByDate(queries, config)) + mux.Handle("/power", GetPowerLogByDate(queries, config)) + mux.Handle("/state", GetSwitchStateLogByDate(queries, config)) + mux.Handle("/tags", GetTags(queries, config)) + + mux.Handle("/", fileServer) + + return mux, nil +} + +func tagAndDateFromQuery(r *http.Request) (string, time.Time, error) { + q := r.URL.Query() + tag := q.Get("tag") + startDateStr := q.Get("from") + now := time.Now() + + if tag == "" { + return "", now, errors.New("no tag specified") + } + + var startDate time.Time + if startDateStr == "" || startDateStr == "null" { + startDate = now.AddDate(0, 0, -7) + } else { + startDate, _ = time.Parse("2006-01-02T15:04:05Z", startDateStr) + } + + return tag, startDate, nil +} + +func GetTags(queries db.Queries, config Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + rows, err := queries.ListDevices(context.Background()) + + if err != nil { + http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) + log.Printf("Error: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(rows); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("Error encoding JSON: %v", err) + } + + }) +} + +func GetPowerLogByDate(queries db.Queries, config Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tag, startDate, err := tagAndDateFromQuery(r) + + if err != nil { + http.Error(w, "400 Bad Request", http.StatusBadRequest) + log.Println(err) + return + } + + rows, err := queries.PowerLogForDeviceToDate(context.Background(), db.PowerLogForDeviceToDateParams{ + Tag: tag, + FromTime: startDate, + ToTime: time.Now(), + }) + + if err != nil { + http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) + log.Printf("Error: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(rows); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("Error encoding JSON: %v", err) + } + + }) +} + +func GetSwitchStateLogByDate(queries db.Queries, config Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tag, startDate, err := tagAndDateFromQuery(r) + + if err != nil { + http.Error(w, "400 Bad Request", http.StatusBadRequest) + log.Println(err) + return + } + + rows, err := queries.SwitchStateLogForDeviceToDate(context.Background(), db.SwitchStateLogForDeviceToDateParams{ + Tag: tag, + FromTime: startDate, + ToTime: time.Now(), + }) + + if err != nil { + http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) + log.Printf("Error: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(rows); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("Error encoding JSON: %v", err) + } + + }) +} + +func GetHumidityLogByDate(queries db.Queries, config Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tag, startDate, err := tagAndDateFromQuery(r) + + if err != nil { + http.Error(w, "400 Bad Request", http.StatusBadRequest) + log.Println(err) + return + } + + rows, err := queries.HumidityLogForDeviceToDate(context.Background(), db.HumidityLogForDeviceToDateParams{ + Tag: tag, + FromTime: startDate, + ToTime: time.Now(), + }) + + if err != nil { + http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) + log.Printf("Error: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(rows); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("Error encoding JSON: %v", err) + } + + }) +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..eafef01 --- /dev/null +++ b/config.yaml @@ -0,0 +1,20 @@ +base_url: "localhost:8080" +mqtt_url: "tcp://192.168.178.139:1883" +mqtt_devices: + - name: "plexi01" + topic: "tele/plexi01/SENSOR" + type: "humidity" + tag: "Bad Unten" + + - name: "mere01" + topic: "tele/mere01/SENSOR" + type: "power" + tag: "Bad Unten" + + - name: "mere01result" + topic: "stat/mere01/RESULT" + type: "switch" + tag: "Bad Unten" + +datasource_dir: "./persist" +serve_from_fs: true \ No newline at end of file diff --git a/database.go b/database.go new file mode 100644 index 0000000..0b3cc01 --- /dev/null +++ b/database.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "database/sql" + _ "embed" + "log" + "os" + "path" + + "sensor_dashboard/db" + + _ "github.com/mattn/go-sqlite3" + _ "modernc.org/sqlite" +) + +//go:embed schema.sql +var ddl string + +func InitDB(datasourceDir string) *db.Queries { + ctx := context.Background() + + _, err := os.ReadDir(datasourceDir) + if err != nil { + if os.IsNotExist(err) { + err := os.MkdirAll(datasourceDir, 0755) + if err != nil { + log.Fatal(err) + } + } else { + log.Fatal(err) + } + } + + sqliteDB, err := sql.Open("sqlite3", path.Join(datasourceDir, "sqlite.db")) + if err != nil { + log.Fatal(err) + } + _, err = sqliteDB.ExecContext(ctx, ddl) + if err != nil { + log.Fatal(err) + } + + queries := db.New(sqliteDB) + + return queries +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..cd5bbb8 --- /dev/null +++ b/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/db/models.go b/db/models.go new file mode 100644 index 0000000..dd9126a --- /dev/null +++ b/db/models.go @@ -0,0 +1,49 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type DeviceTag struct { + ID int64 + DeviceName string + Tag string +} + +type HumidityLog struct { + ID int64 + Time time.Time + Sensor string + Temperature float64 + Humidity float64 + DewPoint float64 +} + +type PowerLog struct { + ID int64 + Time time.Time + Sensor string + TotalStartTime time.Time + Total float64 + Yesterday float64 + Today float64 + Period float64 + Power float64 + ApparentPower float64 + ReactivePower float64 + Factor float64 + Voltage float64 + Current float64 + SensorTemperature float64 +} + +type SwitchStateLog struct { + ID int64 + Time time.Time + Sensor string + SwitchState string +} diff --git a/db/queries.sql.go b/db/queries.sql.go new file mode 100644 index 0000000..41182a5 --- /dev/null +++ b/db/queries.sql.go @@ -0,0 +1,681 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: queries.sql + +package db + +import ( + "context" + "time" +) + +const createDeviceTag = `-- name: CreateDeviceTag :one +insert +into device_tag (device_name, tag) +values (?, ?) returning id, device_name, tag +` + +type CreateDeviceTagParams struct { + DeviceName string + Tag string +} + +func (q *Queries) CreateDeviceTag(ctx context.Context, arg CreateDeviceTagParams) (DeviceTag, error) { + row := q.db.QueryRowContext(ctx, createDeviceTag, arg.DeviceName, arg.Tag) + var i DeviceTag + err := row.Scan(&i.ID, &i.DeviceName, &i.Tag) + return i, err +} + +const createHumidityLog = `-- name: CreateHumidityLog :one +insert +into humidity_log (time, sensor , temperature, humidity, dew_point) +values (?, ?, ?, ?, ?) returning id, time, sensor, temperature, humidity, dew_point +` + +type CreateHumidityLogParams struct { + Time time.Time + Sensor string + Temperature float64 + Humidity float64 + DewPoint float64 +} + +func (q *Queries) CreateHumidityLog(ctx context.Context, arg CreateHumidityLogParams) (HumidityLog, error) { + row := q.db.QueryRowContext(ctx, createHumidityLog, + arg.Time, + arg.Sensor, + arg.Temperature, + arg.Humidity, + arg.DewPoint, + ) + var i HumidityLog + err := row.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.Temperature, + &i.Humidity, + &i.DewPoint, + ) + return i, err +} + +const createPowerLog = `-- name: CreatePowerLog :one +insert +into power_log (time, sensor, total_start_time, total, yesterday, today, period, power, apparent_power, reactive_power, factor, voltage, current, sensor_temperature) +values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) returning id, time, sensor, total_start_time, total, yesterday, today, period, power, apparent_power, reactive_power, factor, voltage, "current", sensor_temperature +` + +type CreatePowerLogParams struct { + Time time.Time + Sensor string + TotalStartTime time.Time + Total float64 + Yesterday float64 + Today float64 + Period float64 + Power float64 + ApparentPower float64 + ReactivePower float64 + Factor float64 + Voltage float64 + Current float64 + SensorTemperature float64 +} + +func (q *Queries) CreatePowerLog(ctx context.Context, arg CreatePowerLogParams) (PowerLog, error) { + row := q.db.QueryRowContext(ctx, createPowerLog, + arg.Time, + arg.Sensor, + arg.TotalStartTime, + arg.Total, + arg.Yesterday, + arg.Today, + arg.Period, + arg.Power, + arg.ApparentPower, + arg.ReactivePower, + arg.Factor, + arg.Voltage, + arg.Current, + arg.SensorTemperature, + ) + var i PowerLog + err := row.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.TotalStartTime, + &i.Total, + &i.Yesterday, + &i.Today, + &i.Period, + &i.Power, + &i.ApparentPower, + &i.ReactivePower, + &i.Factor, + &i.Voltage, + &i.Current, + &i.SensorTemperature, + ) + return i, err +} + +const createSwitchStateLog = `-- name: CreateSwitchStateLog :one +insert +into switch_state_log (time, sensor , switch_state) +values (?, ?, ?) returning id, time, sensor, switch_state +` + +type CreateSwitchStateLogParams struct { + Time time.Time + Sensor string + SwitchState string +} + +func (q *Queries) CreateSwitchStateLog(ctx context.Context, arg CreateSwitchStateLogParams) (SwitchStateLog, error) { + row := q.db.QueryRowContext(ctx, createSwitchStateLog, arg.Time, arg.Sensor, arg.SwitchState) + var i SwitchStateLog + err := row.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.SwitchState, + ) + return i, err +} + +const deleteDeviceTag = `-- name: DeleteDeviceTag :exec +delete +from device_tag +where id = ? +` + +func (q *Queries) DeleteDeviceTag(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteDeviceTag, id) + return err +} + +const deleteHumidityLog = `-- name: DeleteHumidityLog :exec +delete +from humidity_log +where id = ? +` + +func (q *Queries) DeleteHumidityLog(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteHumidityLog, id) + return err +} + +const deletePowerLog = `-- name: DeletePowerLog :exec +delete +from power_log +where id = ? +` + +func (q *Queries) DeletePowerLog(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deletePowerLog, id) + return err +} + +const deleteSwitchStateLog = `-- name: DeleteSwitchStateLog :exec +delete +from switch_state_log +where id = ? +` + +func (q *Queries) DeleteSwitchStateLog(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteSwitchStateLog, id) + return err +} + +const getDeviceTag = `-- name: GetDeviceTag :one +select id, device_name, tag +from device_tag +where id = ? limit 1 +` + +func (q *Queries) GetDeviceTag(ctx context.Context, id int64) (DeviceTag, error) { + row := q.db.QueryRowContext(ctx, getDeviceTag, id) + var i DeviceTag + err := row.Scan(&i.ID, &i.DeviceName, &i.Tag) + return i, err +} + +const getHumidityLog = `-- name: GetHumidityLog :one +select id, time, sensor, temperature, humidity, dew_point +from humidity_log +where id = ? limit 1 +` + +func (q *Queries) GetHumidityLog(ctx context.Context, id int64) (HumidityLog, error) { + row := q.db.QueryRowContext(ctx, getHumidityLog, id) + var i HumidityLog + err := row.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.Temperature, + &i.Humidity, + &i.DewPoint, + ) + return i, err +} + +const getPowerLog = `-- name: GetPowerLog :one +select id, time, sensor, total_start_time, total, yesterday, today, period, power, apparent_power, reactive_power, factor, voltage, "current", sensor_temperature +from power_log +where id = ? limit 1 +` + +func (q *Queries) GetPowerLog(ctx context.Context, id int64) (PowerLog, error) { + row := q.db.QueryRowContext(ctx, getPowerLog, id) + var i PowerLog + err := row.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.TotalStartTime, + &i.Total, + &i.Yesterday, + &i.Today, + &i.Period, + &i.Power, + &i.ApparentPower, + &i.ReactivePower, + &i.Factor, + &i.Voltage, + &i.Current, + &i.SensorTemperature, + ) + return i, err +} + +const getSwitchStateLog = `-- name: GetSwitchStateLog :one +select id, time, sensor, switch_state +from switch_state_log +where id = ? limit 1 +` + +func (q *Queries) GetSwitchStateLog(ctx context.Context, id int64) (SwitchStateLog, error) { + row := q.db.QueryRowContext(ctx, getSwitchStateLog, id) + var i SwitchStateLog + err := row.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.SwitchState, + ) + return i, err +} + +const humidityLogForDeviceToDate = `-- name: HumidityLogForDeviceToDate :many +select humidity_log.id, time, sensor, temperature, humidity, dew_point, device_tag.id, device_name, tag +from humidity_log +join device_tag on device_name = humidity_log.sensor +where device_tag.tag = ? + and (time between ? and ?) +order by time desc +` + +type HumidityLogForDeviceToDateParams struct { + Tag string + FromTime time.Time + ToTime time.Time +} + +type HumidityLogForDeviceToDateRow struct { + ID int64 + Time time.Time + Sensor string + Temperature float64 + Humidity float64 + DewPoint float64 + ID_2 int64 + DeviceName string + Tag string +} + +func (q *Queries) HumidityLogForDeviceToDate(ctx context.Context, arg HumidityLogForDeviceToDateParams) ([]HumidityLogForDeviceToDateRow, error) { + rows, err := q.db.QueryContext(ctx, humidityLogForDeviceToDate, arg.Tag, arg.FromTime, arg.ToTime) + if err != nil { + return nil, err + } + defer rows.Close() + var items []HumidityLogForDeviceToDateRow + for rows.Next() { + var i HumidityLogForDeviceToDateRow + if err := rows.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.Temperature, + &i.Humidity, + &i.DewPoint, + &i.ID_2, + &i.DeviceName, + &i.Tag, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listDeviceTag = `-- name: ListDeviceTag :many +select id, device_name, tag +from device_tag +` + +func (q *Queries) ListDeviceTag(ctx context.Context) ([]DeviceTag, error) { + rows, err := q.db.QueryContext(ctx, listDeviceTag) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DeviceTag + for rows.Next() { + var i DeviceTag + if err := rows.Scan(&i.ID, &i.DeviceName, &i.Tag); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listDevices = `-- name: ListDevices :many +select tag from device_tag group by tag +` + +func (q *Queries) ListDevices(ctx context.Context) ([]string, error) { + rows, err := q.db.QueryContext(ctx, listDevices) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var tag string + if err := rows.Scan(&tag); err != nil { + return nil, err + } + items = append(items, tag) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listHumidityLog = `-- name: ListHumidityLog :many +select id, time, sensor, temperature, humidity, dew_point +from humidity_log +order by time desc limit ? offset ? +` + +type ListHumidityLogParams struct { + Limit int64 + Offset int64 +} + +func (q *Queries) ListHumidityLog(ctx context.Context, arg ListHumidityLogParams) ([]HumidityLog, error) { + rows, err := q.db.QueryContext(ctx, listHumidityLog, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []HumidityLog + for rows.Next() { + var i HumidityLog + if err := rows.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.Temperature, + &i.Humidity, + &i.DewPoint, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPowerLog = `-- name: ListPowerLog :many +select id, time, sensor, total_start_time, total, yesterday, today, period, power, apparent_power, reactive_power, factor, voltage, "current", sensor_temperature +from power_log +order by time desc limit ? offset ? +` + +type ListPowerLogParams struct { + Limit int64 + Offset int64 +} + +func (q *Queries) ListPowerLog(ctx context.Context, arg ListPowerLogParams) ([]PowerLog, error) { + rows, err := q.db.QueryContext(ctx, listPowerLog, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PowerLog + for rows.Next() { + var i PowerLog + if err := rows.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.TotalStartTime, + &i.Total, + &i.Yesterday, + &i.Today, + &i.Period, + &i.Power, + &i.ApparentPower, + &i.ReactivePower, + &i.Factor, + &i.Voltage, + &i.Current, + &i.SensorTemperature, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listSwitchStateLog = `-- name: ListSwitchStateLog :many +select id, time, sensor, switch_state +from switch_state_log +order by time desc limit ? offset ? +` + +type ListSwitchStateLogParams struct { + Limit int64 + Offset int64 +} + +func (q *Queries) ListSwitchStateLog(ctx context.Context, arg ListSwitchStateLogParams) ([]SwitchStateLog, error) { + rows, err := q.db.QueryContext(ctx, listSwitchStateLog, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SwitchStateLog + for rows.Next() { + var i SwitchStateLog + if err := rows.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.SwitchState, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const powerLogForDeviceToDate = `-- name: PowerLogForDeviceToDate :many +select power_log.id, time, sensor, total_start_time, total, yesterday, today, period, power, apparent_power, reactive_power, factor, voltage, "current", sensor_temperature, device_tag.id, device_name, tag +from power_log + join device_tag on device_name = power_log.sensor +where device_tag.tag = ? + and (time between ? and ?) +order by time desc +` + +type PowerLogForDeviceToDateParams struct { + Tag string + FromTime time.Time + ToTime time.Time +} + +type PowerLogForDeviceToDateRow struct { + ID int64 + Time time.Time + Sensor string + TotalStartTime time.Time + Total float64 + Yesterday float64 + Today float64 + Period float64 + Power float64 + ApparentPower float64 + ReactivePower float64 + Factor float64 + Voltage float64 + Current float64 + SensorTemperature float64 + ID_2 int64 + DeviceName string + Tag string +} + +func (q *Queries) PowerLogForDeviceToDate(ctx context.Context, arg PowerLogForDeviceToDateParams) ([]PowerLogForDeviceToDateRow, error) { + rows, err := q.db.QueryContext(ctx, powerLogForDeviceToDate, arg.Tag, arg.FromTime, arg.ToTime) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PowerLogForDeviceToDateRow + for rows.Next() { + var i PowerLogForDeviceToDateRow + if err := rows.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.TotalStartTime, + &i.Total, + &i.Yesterday, + &i.Today, + &i.Period, + &i.Power, + &i.ApparentPower, + &i.ReactivePower, + &i.Factor, + &i.Voltage, + &i.Current, + &i.SensorTemperature, + &i.ID_2, + &i.DeviceName, + &i.Tag, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const switchStateLogForDeviceToDate = `-- name: SwitchStateLogForDeviceToDate :many +select switch_state_log.id, time, sensor, switch_state, device_tag.id, device_name, tag +from switch_state_log + join device_tag on device_name = switch_state_log.sensor +where device_tag.tag = ? + and (time between ? and ?) +order by time desc +` + +type SwitchStateLogForDeviceToDateParams struct { + Tag string + FromTime time.Time + ToTime time.Time +} + +type SwitchStateLogForDeviceToDateRow struct { + ID int64 + Time time.Time + Sensor string + SwitchState string + ID_2 int64 + DeviceName string + Tag string +} + +func (q *Queries) SwitchStateLogForDeviceToDate(ctx context.Context, arg SwitchStateLogForDeviceToDateParams) ([]SwitchStateLogForDeviceToDateRow, error) { + rows, err := q.db.QueryContext(ctx, switchStateLogForDeviceToDate, arg.Tag, arg.FromTime, arg.ToTime) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SwitchStateLogForDeviceToDateRow + for rows.Next() { + var i SwitchStateLogForDeviceToDateRow + if err := rows.Scan( + &i.ID, + &i.Time, + &i.Sensor, + &i.SwitchState, + &i.ID_2, + &i.DeviceName, + &i.Tag, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const truncateDeviceTag = `-- name: TruncateDeviceTag :exec +delete from device_tag +` + +func (q *Queries) TruncateDeviceTag(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, truncateDeviceTag) + return err +} + +const updateDeviceTag = `-- name: UpdateDeviceTag :one +UPDATE device_tag +set device_name = ?, + tag = ? +WHERE id = ? +RETURNING id, device_name, tag +` + +type UpdateDeviceTagParams struct { + DeviceName string + Tag string + ID int64 +} + +func (q *Queries) UpdateDeviceTag(ctx context.Context, arg UpdateDeviceTagParams) (DeviceTag, error) { + row := q.db.QueryRowContext(ctx, updateDeviceTag, arg.DeviceName, arg.Tag, arg.ID) + var i DeviceTag + err := row.Scan(&i.ID, &i.DeviceName, &i.Tag) + return i, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c6a2de9 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module sensor_dashboard + +go 1.25 + +require ( + github.com/eclipse/paho.mqtt.golang v1.5.1 + github.com/joho/godotenv v1.5.1 + github.com/mattn/go-sqlite3 v1.14.34 + gopkg.in/yaml.v2 v2.4.0 + modernc.org/sqlite v1.46.1 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..686a701 --- /dev/null +++ b/go.sum @@ -0,0 +1,67 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= +github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..55854a2 --- /dev/null +++ b/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "log" + "net/http" + "os" + + "github.com/joho/godotenv" + "gopkg.in/yaml.v2" +) + +type Config struct { + BaseUrl string `yaml:"base_url"` + MqttUrl string `yaml:"mqtt_url"` + MqttDevices []MqttDevice `yaml:"mqtt_devices"` + DatasourceDir string `yaml:"datasource_dir"` + ServeFromFS bool `yaml:"serve_from_fs"` +} + +type MqttDevice struct { + Name string `yaml:"name"` + Topic string `yaml:"topic"` + Type string `yaml:"type"` + Tag string `yaml:"tag"` +} + +func main() { + + err := godotenv.Load() + if err != nil { + log.Println("No .env file found, all config coming from system env") + } + // + //dashboardHost := os.Getenv("SENSOR_DASHBOARD_WEB_BASE_URL") + //mqttServer := os.Getenv("SENSOR_DASHBOARD_MQTT_SERVER") + //mqttTopicsJson := os.Getenv("SENSOR_DASHBOARD_MQTT_TOPICS") + //datasourceDir := os.Getenv("SENSOR_DASHBOARD_DATASOURCE_DIR") + + configPath := os.Getenv("SENSOR_DASHBOARD_CONFIG_PATH") + if configPath == "" { + configPath = "./config.yaml" + } + f, err := os.Open(configPath) + + if err != nil { + log.Fatal(err) + } + defer f.Close() + + var config Config + decoder := yaml.NewDecoder(f) + err = decoder.Decode(&config) + if err != nil { + log.Fatal(err) + } + + queries := InitDB(config.DatasourceDir) + client, _ := InitMqtt(config.MqttUrl, config.MqttDevices, *queries) + + handler, err := Api(*queries, config) + + if err != nil { + log.Fatal(err) + } + + log.Println("Starting server on " + config.BaseUrl) + log.Fatal(http.ListenAndServe(config.BaseUrl, handler)) + + client.Disconnect(250) + +} diff --git a/mqtt_client.go b/mqtt_client.go new file mode 100644 index 0000000..465f99f --- /dev/null +++ b/mqtt_client.go @@ -0,0 +1,220 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "sensor_dashboard/db" + "strings" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +type MqttHumiditySensorMessage struct { + Time IsoTime `json:"Time"` + SensorData HumiditySensorData `json:"AM2301"` + TempUnit string `json:"TempUnit"` +} + +type MqttPowerSensorMessage struct { + Time IsoTime `json:"Time"` + SensorData PowerSensorData `json:"ENERGY"` + SensorTemperature SensorTemperature `json:"ESP32"` + TempUnit string `json:"TempUnit"` +} + +type MqttSwitchStateSensorMessage struct { + PowerState string `json:"POWER"` +} + +type HumiditySensorData struct { + Temperature float64 `json:"Temperature"` + Humidity float64 `json:"Humidity"` + DewPoint float64 `json:"DewPoint"` +} +type PowerSensorData struct { + TotalStartTime IsoTime `json:"TotalStartTime"` + Total float64 `json:"Total"` + Yesterday float64 `json:"Yesterday"` + Today float64 `json:"Today"` + Period float64 `json:"Period"` + Power float64 `json:"Power"` + ApparentPower float64 `json:"ApparentPower"` + ReactivePower float64 `json:"ReactivePower"` + Factor float64 `json:"Factor"` + Voltage float64 `json:"Voltage"` + Current float64 `json:"Current"` +} + +type SensorTemperature struct { + Temperature float64 `json:"Temperature"` +} + +type IsoTime struct { + time.Time +} + +const sensorTimeLayout = "2006-01-02T15:04:05" + +func (ct *IsoTime) UnmarshalJSON(b []byte) (err error) { + s := strings.Trim(string(b), "\"") + if s == "null" { + ct.Time = time.Time{} + return + } + ct.Time, err = time.Parse(sensorTimeLayout, s) + return +} + +var wildcardHandler = func(client mqtt.Client, msg mqtt.Message) { + log.Printf("got wildcard message on topic %s with payload: %s\n", msg.Topic(), msg.Payload()) +} + +func messageHandler(queries db.Queries, sensorName string, sensorType string) func(mqtt.Client, mqtt.Message) { + return func(client mqtt.Client, msg mqtt.Message) { + log.Printf("handling message for %s! topic: %s, payload: %s\n", sensorName, msg.Topic(), msg.Payload()) + + switch strings.ToLower(sensorType) { + case "humidity": + var message MqttHumiditySensorMessage + err := json.Unmarshal(msg.Payload(), &message) + if err != nil { + log.Printf("error unmarshalling mqtt message: %s\n", err) + return + } + + _, err = queries.CreateHumidityLog(context.Background(), db.CreateHumidityLogParams{ + Sensor: sensorName, + Time: message.Time.Time, + Temperature: message.SensorData.Temperature, + Humidity: message.SensorData.Humidity, + DewPoint: message.SensorData.DewPoint, + }) + + if err != nil { + log.Printf("error creating log: %s\n", err) + } + case "power": + var message MqttPowerSensorMessage + err := json.Unmarshal(msg.Payload(), &message) + if err != nil { + log.Printf("error unmarshalling mqtt message: %s\n", err) + } + + _, err = queries.CreatePowerLog(context.Background(), db.CreatePowerLogParams{ + Time: message.Time.Time, + Sensor: sensorName, + TotalStartTime: message.SensorData.TotalStartTime.Time, + Total: message.SensorData.Total, + Yesterday: message.SensorData.Yesterday, + Today: message.SensorData.Today, + Period: message.SensorData.Period, + Power: message.SensorData.Power, + ApparentPower: message.SensorData.ApparentPower, + ReactivePower: message.SensorData.ReactivePower, + Factor: message.SensorData.Factor, + Voltage: message.SensorData.Voltage, + Current: message.SensorData.Current, + SensorTemperature: message.SensorTemperature.Temperature, + }) + if err != nil { + log.Printf("error creating log: %s\n", err) + } + case "switch": + var message MqttSwitchStateSensorMessage + err := json.Unmarshal(msg.Payload(), &message) + if err != nil { + log.Printf("error unmarshalling mqtt message: %s\n", err) + return + } + + _, err = queries.CreateSwitchStateLog(context.Background(), db.CreateSwitchStateLogParams{ + Time: time.Now(), + Sensor: sensorName, + SwitchState: message.PowerState, + }) + + if err != nil { + log.Printf("error creating log: %s\n", err) + } + } + } +} + +var connectHandler mqtt.OnConnectHandler = func(client mqtt.Client) { + log.Printf("Connected to broker!") +} + +var connectionLostHandler mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) { + log.Printf("connection lost: %v", err) +} + +func createDeviceTags(queries db.Queries, sensors []MqttDevice) error { + + err := queries.TruncateDeviceTag(context.Background()) + + if err != nil { + return err + } + for _, sensor := range sensors { + _, err = queries.CreateDeviceTag(context.Background(), db.CreateDeviceTagParams{ + DeviceName: sensor.Name, + Tag: sensor.Tag, + }) + if err != nil { + return err + } + } + + return nil + +} + +func InitMqtt(brokerUrl string, sensors []MqttDevice, queries db.Queries) (mqtt.Client, error) { + + err := createDeviceTags(queries, sensors) + + if err != nil { + return nil, err + } + + log.Println("Listening for sensors:", sensors) + + opts := mqtt.NewClientOptions().AddBroker(brokerUrl) + opts.SetClientID("go-mqtt-dashboard") + opts.SetKeepAlive(2 * time.Second) + opts.SetPingTimeout(1 * time.Second) + opts.SetDefaultPublishHandler(wildcardHandler) + opts.OnConnect = connectHandler + opts.OnConnectionLost = connectionLostHandler + client := mqtt.NewClient(opts) + if token := client.Connect(); token.Wait() && token.Error() != nil { + panic(token.Error()) + } + + for _, sensor := range sensors { + sub(client, sensor, queries) + } + //subAll(client) + + return client, nil +} + +func subAll(client mqtt.Client) { + topic := "#" + token := client.Subscribe(topic, 1, wildcardHandler) + if token.Wait() && token.Error() != nil { + panic(token.Error()) + } +} + +func sub(client mqtt.Client, sensor MqttDevice, queries db.Queries) { + topic := sensor.Topic + token := client.Subscribe(topic, 1, messageHandler(queries, sensor.Name, sensor.Type)) + token.Wait() + if token.Error() != nil { + panic(token.Error()) + } + log.Printf("Subscribed to topic: %s\n", topic) +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..723184161f0157bca0b0171997247c2721eab8ee GIT binary patch literal 11019 zcmX9^1ymH@7amwT7Le`|=|;L!8UZPRC8Sd%L~7|4C6x}55Tv_dX^}3ak?vk#+5Pi7 z|Cuvq-poC7@4N5b?>qO+djNp;c>Z^w0ZhOZ7XVN^wqx~l)Clot@E)6l>MxY_|C9cA z;b1-9%)LtO|9kTA(lJCEFZ9a<0In)^Wd+0ce~v700}W?q8Dn^A*}nRpj6KCrrl2%0 zulIpeSNgbZz5Ahgpzl`c=C<55S9xdpTC>=>5?>#ExZPh^9VaUcN?VE(2j%$5gvSTv z3FBa5+L78>kxBu+iU|*EZS(HH?@?GvPqvpOc9sH;C3Zs4(J0bwMqezD^YHK>KbK>? zfH6%t&|z>b;#OC~mBL=)IDVIB?4YRs6bW7v^aXf$I*Db#p+j$W^9R@LVGRXlIJ*>5 z00l*eI3RuPLSeXZ5D23a3C`)H5cyBVdouzNuVpeRjvT8N-h1$VDK_AFkF4xkP}zgF#YuNFi;{Z)-=F^qOu?!f+N+?iR)Xb`nY%w6XR zFtUCGXA$qN4E^V1gCc#f_0j|PK>WCIITST4UY#*dH>{y zj;{N)8Nqcyg+j~;x{1jWXC|92kh-FDIMjotm()GG&VeyPdI&s991w3O&(v`AKk#F3 z>e{s0hkgHm{a(Q|oApDGMvK83&9w!aVO1;A##7WriJK1a!Oc}rQ844J;*C$ClTR#n zuIrY+oKemm(P$l|QQtg?Ds_uIV;98IeUc-hgIz1G(PC#7Pv!Pg$y|~1hro$GC$45I ze=C3pPA}{7hU0_`5>F}&X)E%8-vNemcU(46B$2rvcW4S_3~v3q>V?UUUNXJ129}ZhhCX zUX#?)AEz;Y>B-*(3bO?fOh#sz7#{K)gcvh-4u^vlqMEgK1fazWQ%wp3jim|f%tp@* zrnXu!qxa3b_jh}hjp#2eK!pAvengsFH5RGL=l7A8=KF<( z%&Tn>zy_oxq0e^#o#GEr%RoFUP7POD;}N%Bra+6Su;fmf+WaQCEMqRZMpiB)VrGF^ zv3F19PL;%ti6$3R_^?RESOzZ@V-$HblqNP80Ry^linlJ9l9}$0k^OTI_fU=miL--> z#y-IGmK|W}%qq8$YL{GrK^^j7sDN6$0y$}PZG9E!Q<@vh(I6u-S+>Gdk7Xphhf(8? zi~fj$W$c2!Jhr^`+;{_rK~znB#@V2LMu?h&>D(Q|261c6aZuJxnxV+Si^f&2z+-lN zM?{$bH_RZy|R!xqg7{zqfSs|Ayg9kos_tNjz#! z?-|&Rv;9}PYQo^{Lrn3r3={ISnf&+~jiejw2%5J+`_}w{+n!+kfKOI^h1QKsP`Fnqx-r69t~fs3z0+AkBAsNFY&K z_`HT!yGzD0V@zqq8%Xzb4?3lWIH_&wMw&VzKE%W-nc^|0-r=iY7HPj8F;jDV$${%% z9c*F1P8&E7e=&2F!R;6~F}^EvDD#;f&}3kHK5UfU@YB_x!ns!-4d76H!;8hCha-vThl*X*hLxzd-lkiRZu7+m}9MfuBWr85wM;8MjL8wuz* z$zbx?&8@H$kSz zm(pBLOEHR+MFS78f-UD~=(5w=5_76#cx3huf@C*pTWp%leoZFMeo~cbeLRQHRv462 zR4Dm8tCFV@+r)=osQ#m*w?b%Jr{h{b2HUBDc-;YmoWmw=!Pgrt_0r>gEyvq;v_E#? zDQ&1*>OX~lX)i5*Kk*aOq8^x{k+9N_JH2R&vBT7x>UWfWSk|KBQq4DIxiUUs`hOTK zzKEw5&(?%gzZCehWWv(>+tS02yIQf{HRUYH2D>|X^!J=H z+>{gRC7F(T-m?+c*6Jy9k?U6Kbq6)r16Tz4^ceT6UhgBHN~l*W2>$uv*kb|Y8;eMdJ2GvO z!ozTbSS4&E9{QGnvKwsG;tfLGejD|}IRg9-_%L5hYPA@2Q>ATzw1`D6(>nWfCPq}J@s+zfS}`K;A8@CknsrZS-uYpg#2`+1 zTSFXJ%<^=WYEWqsi%Hm0e%rg=%hVOCl5gsVFP-Rs+I-fJ6O`e^mAIp7rlLme;{TjMj66p=!l_wr5HRl*tGh!RSSO zo}x85*A6ZVfn)UfEONH&s$gc-oQO8$$vXm4f=+_VP^#f_In8fKXY+60Zk&?8SFE{2 zln_o*$GUjeXtp8E4E+5SgbuCGs#AwQ%H8FcRa_ZRa_$AR;)2S&Y9_4ZU;Na)@wHL* z#oIq?X%l{1I>;8o=$ZG$s=j5=4o@T1dR~R+tuUmT#&=m`3mLLI;UtHNUq?JR&B@f8 z5xSE4B52<}fH5PzK~{}ZL1Nug|Ma?~2xap5u5FnWPoub{>!mQk-{1?6VjQ`op4GM}dLS@CJdPbNVE0^CxBw4A z`Tm7^-ei%Us#^u9q)}TvmZk026Sv8my0engCM!A(5k9>0?y)Y|7K3g5ExKPxUX(Ml zfTP(#nn?${88cqx^detXA_tYQ@CLh~EQ2-n8lO5z@PJiy zX4B3qww$D0wSgtE=w9-X+v#ztT3*S4fn16r5{t+5S9t_p8ay=Z1-57WpzUieNCyku zcx8q~Qu$9g0|AzhktlZqf2R?F_#CL_x4*k&>ketUBW5KVf}Mur>kLquIK`Kr@9t@7 z{kH|;h0qGl4uK_-d$Gj()(P7(xwh1`q&i;n$xW6cHJ#c#4ZoU#znjl3ev(sSNg%iX zOAwpCv9kJ8gQNTT!jt|!^#w##^)npxW;Lo^7qQ{L)V}!m!)!(b2IF6*v@SBlRs9J1 z_HESDn6k#X+dfXOdT$#tI8W`mWV%^ajoSgoSi9WpT6*BTx`7gH0{I;)1SuzRVa`-ey@~kttER?<*7&@o& zw%^dhK0zw@qc^XVB#0_g`*KS32)!Gp@vSW5-w24=a-ERa1i`Fr;`HdH05x2a;PPII zI5)_7O|njYYCaX4QOA$uCyRDOkKoWOZhF)@m0 zqfg#f!Xtr|fu*?GG#i~NYzRDhtWuA0| z>9f?gPSv1qsgez)4sB+7!HHH|S}&uPwFZM(z_Ts}Zj&X&1kpIk&(!$v|&@vj4nTT-LZ+t!R={weD@ zg=KuZZJm41tszvk-j(NQ4`9$;c4dr(N2E>q4)q6GMu%BvGeo+lI@@hbTzdnCF_TOq zW~dR3Ar2N};9B)z2FJ_C+8slVS~>*?XLF-c5pi0C{B z(nahLTffTajWQF!W{RbbowO(5rth=12o8Q(^)q_rZCtyF2FNK0_21;vvyr+b&!OaN@FIC!3Z2)@Z86}Z16?P}sP9Y7kr z{jygZB*$kEPV@L)2gU-E|5Jf=%29*BAqnP6`a??ThsRCbw{8(;JV{ota&-ut8z81R zaH)JGzE~6fCwVVQ7G6vIsOaRJpDg+}thaZh-xFl_#l1(@3CE>6lU_ax7NhuB(lb4J zWgg)?7U3>gg7NqLt#PsNvOk6&nNc*f-8Z?-{=ERm+119@D5Iei-ap47*( z?jkgDVmJ*tF)m}DQe63M9v#CMWbA_mm5MR_Qc^bayNzxO@e9lu8i9E-m%e%!v67L3 zF8QIp_vRHy>fT#7QyP#w(qCr$neCD?SpG-!6Z2+l(^x9L0j%Y)lro-XmL4!uTM_Dk zvsX9MlV2V@x;JCubx8x&9RA<%9s7Vt2l4|0S`JqAk{Ex{! z<9LeBP1MHuKS9C!D{ zA!h{{L7sJ#y<&_a8pH@&lohcQZ4g4hxZ%R(U$WmF7ah0@+%5Ab>LciqfD&&#*zKQq_hk0Qb)WJNL}~4o-C7F|FWqc}$%}aqEHA_w4$Sa?z~mb?5?tV4IHC@PQa=OMU{D-f?2tH%(532Pb@7 ziEl8i1lHIHVgvAH3HZfuBgeJ1@AueE^x@$S0ytU3B`;sO`sYN`Ltkb|A|WFdAp`?_ za5WArw%iT^5rOOZiMBo5-Llb8TttB|)!2K`b5fD99~NN)?K8(Acs7I~YXk!f2?`}K zg-raGSo(3Sbd(k$NvF|cR`oGboLbk}AY%WJ#T#AF6WI--%pOOQ%?7H=sSa-3sX7OkkfbXk?}>hcasWumS4Up z5Kn!Aj~CKSpsKn=^Klgao!{t-Kqx7bo~WvLC`4eDG5j+*PtHEtQZ6{br0k=d&3#a& zoCy{ycY7GKyzaah7G@D~%u|e=Rh77h2sv{sR$<3%c<4_ew3s9Ir;9q@FQP}R-Xu26 zP_8*+=Rbe@koV)_iw72KlNJ=S>)YtXiu*5&iQ>HSyMv#?^K#*nG)r3j@7A68d;vJI z8k6i6QA*Dcb>F?1d~`J}Kf2Ox%ZG$eaOP9Q+J6H2Su?upIrAMro&w3lb;`Vbfjw*m ztl8r*lQP+$8N#;acTsM^_D^U+S`J1+?+LIM;iTd3ybGnbiSl{|h#vLs>`+X}T);d9vy?8j zri*g}rDP}!;zsMc0b30z`~T8q*PM``L%oRyuf0zlc6rDR1awN1x&OoaU0%IEvK&3qW zR;^P|5JY4JphapU^AW<&8MMSp2~9@l3G)a9b3>*0D}HCARgc&Ywg$;o88lkYbYnrC z@)j6Tl|=t!6!5#kk0*ka!WAr1(7%akbd16Wc50q96WP6l{%`;t)-To!Z0zOxWzTcb zI(!%rfi{Re&MajD%ZX+UA~Zn_PC^-@oniP98msjfQ&Ba zLuF&@&rl}yY&WSu$^?}F%qsB0qmHFn*XjOEekDtbyZayd_{l>!>OKRl@$g00M`Z#H zx=^P_sKQQQv65>-Tmmq(Ky=R%5Y>bgq^|SmwYrl9#nx5r>p_L)kHSLcNM18+&r^7P zo1FF}4a*_t{&!M&tptEE#v_n~JN%w3gY3LddoRuRibCD>@0@}+2!8%okW8tZ&pY%6zrqLjve{YE5w0};AOs2uiTnZq{-HK!I7&TA6m2vTAy z>A}d&?&y3_%uoQd=FyhMYZA7rDgL8rqZ#rcaQ@bn6nOq5hwyqp_f_bQj6;@ckY-&z zKRpWM*E)fQBMYS50!yXc8NX{+J2<4|m#z$yxE7pWJuy;u$f+A|B@r3 z%^v-ys;ljmr!MAN-^l{^$-gbCXESG*R+`#!#+^9Tf_?pr+DHkv)=JO=@tI65qg(ww zeK++l+}&Jw-jJ-LKx7VLRZ$d~&Zo1t+vV8t@Q37pKh4A#<~Dy?bB^JSXM?CRJ@x|( z!R=9LFnSHGw|kVU3#;Y^h;dB$Hc1_ud-cFix4XL1c&mAD+U~1_c*N6tcRVZO6GNI> zfD6-?OFgDHo>7;p?au*h(R1AVmNVnE)(1PZ?rr*O7qyI%t{{ocKQeJ4tFMGD+!4bc zCD0WVe-ZeKLqBc=Oe(x(E7r%T?h0YQ4R3dM4*q#<%;zOXsKtT84_KWXoBmTF0Lm z!D1y}LdRcKJgX1~t3Z~%=8ot!$=;(`hHIc#iKN!ukcGAEvI)b~TFJg+xnremR8bBJsw%N8M!Q*(zQ-t& z_W8L}BuO|ar{Tvh48ll5hkUlE2&7)d#n{3r9fr+SBHwU!=iRMV$fLnS`CMkFCc2`B{mbEC{Jncyz#p!#X`sTXAx0oC-C`=>gb03sJ(8Y6Xy{Rh+wGTo&SOjv`fuCPl372&2 zD-HYPgPK)g(`xO}smNb8o6(h00E3C^-_5+oGCLB^8k)a%{sC1HAGX0VfV&>v^$Tk^ zf$qA{=c_FW(($irIq&>xhFpF6lN?;AEhkSg{PT&15IOI=r4Ti7aE_w^L8hz#@*iiu zgJCv05{3Z!PfQuFEVnJ(_6L*a{^%BU4=+v%PYcRsov%Dq`EYd)+&eI2>3opG@Q1E zj>_p|p<}==)cY+5mKVgJ7TMsyDq5t9uWQQ@z+6itkU5Z=UFzcDtCgLE4@yTreH-U3U2K{xaFff(0USSyNpI$9b-$*Qli zV!388Q7tf-$WRKKM_P9;U@E{xr3j$|Fw?VI{luBY@cJg2sTW!1KDQWl6X`k=O| zW#5sg8JU!|@rxo2<2`vI>xgL=^za=JC0e6_dup}owEno;4XKbR6VBfyUvgq3I+wp@ zomHV!pZGtaR7^j(AjtJ5Dlvf&4BKsbw8+UfdH3(e!{ke&66K0g2i_z^SiXbg494u(VM{wX z`%3#f9qmk|jK~~Kx;N}FZo{y;HC{A6Egn5e3W7+&csy>qir+K(-H~45PB8m7^(Sj4 z0a0kG9iA9F?@Fxxehu2L8q-;ftq!9JT0;Jc)&|~R3p=LhxnW3*jaq(+lrcTNGaT|| zEYuYm99KgcNkYB$V;l4F`L>l@f3=+4c`y3(T-1i~(-!Q>hI~mOw~pM*d(gcw^J~eV zn^vL!EHZL0@6(sJ^;3>kx3aIdp|s8s4$AX8z<`u2VGkt$Ip|oWnNFg|JcLm9!(g8=wbJWNHS~O(mi|^Q1CUY)lxEt<>m7s)(Une*IpOPNj#&OmZ zu?B59m^^fJ`yY2UVCCMzHN|d-I?`I|s{AkTwtBfYSDMdCX3VedTc~B9-xGI8oH0|? zR3zQgk_aNil4eP(Gpx?2@i#6fj z$>+rt%C}Z)S%uYB4`+dpfs}EQk<&V!-NlIacs}Q@V_~_9awHE=7``rTHgDuhK7a8P ztAjY{owMgS86&j-=YtgFP1nUs54qyObMHfT)vTNB?o+LC#UOYMg=1@Mi{I_DTfdCo z`VV)DC-A9LTThaG+ZJZp1@zAtCwGxPz~px!>&vP8wF3fC#P`P8ld{IyL-tyTLW<82{j;EA?2XMmqAN!(ZzI_Ya9)UT)wFb843*l7|!> z8o45OM-KKf#h)t9uve3GZy@F+4>YND!dWy>A%t|tPLqm8()sOM!F~;C$ixPIXQ2cX zPue|Aon0R9x|5M`Rj)=?yKvZUo}E#_|7f~o=Srp{?!*BnNHl81Ke{YR2`TsL-L(P& z-I{qt!Ie8*cxdCkk+Y~u1wof*$LMVz;KVgBzv+6Zk`coufx*qYYc=$Tu zlTTTz<#KdM-+X34N#=o!?_W*(jW!I|2C8x~XY~0#U5Qt}BusMG)kJ^_6)0$v%2%@T z(;!i(-)#P9U#9)zdyLK7c#?1>+k%vWC@*3x1SNV$OT@eci!7C4BQp`smLz1$TBdC? zz9)rheB~6cySJf5tY*S)^;GO%q%lJx%0dLe7=`-L`q$O$b-xo6vzmDsyhRw9!1r-Z zuvz`6nV&_LuUcv0yLY6s#sJe#b{p^vo6H-9oj0yk1vSPqZ%LBsa-cL^Pg1p&D4Ar> zZMyCXnl~XyQF0DG+x148YQcpKh5B>SJMdM-Y)LBu3`GgL#Ii2dR5~o%GS5)29iItY zlzEv!RUpA{SQiSk0f{-={~4_qZ5N|U@`J?Xl|LLSl7B`A*&=jm*Q=m@o~Q}~?fd0^ z?8-wXa*4n@4DHojIc6$IVH3qREVP{g#~6mwpKM5JqD=OEi-@?KxmwG<)4Ge5~n<_nC7aJ&{?%JC!rY3J4ZOLe4kJEcT zJ4%o53_`JV(xg08%$Mm+#@v^9Ur5~B@2!ztZdYdp@$MNWvu$~Ayr}L8;HRtpMjA7; z$#w+PE&T&x%){WjmX^GuUaz?yCz>?xZ4kq@ar}$BLezvV)dJrz{F-aTwUjrB8R$n3 zB1%??$VCgvr|E*~l6U&Gxi~5Q$ss`cx>_8vZ^yyp zp3Rog9m%zG*nIa;my2c6QS39&wZRQVwG(Tlm`VlM((F z$2&;b9FP7x*QBV_Fq*lva8r$eRNDGisA@fHDQ4>+xNriSLjb4;@u%bBc4utd{w1(Xl4QrlsPs{jqyh6(tP}!hh1w^HW<#eVZqC zo65k$G=ZXpwRlk}7rRB$U(+M)bPH@xFD){4TAhVlJmXoy&5zLg*!eokVh&EfgaDl= zzOVx9yqzyjOAiCR$r2)tbN;GT7oIe*WEI9?fdv163+I5Z`{qe!#dk?!a9L)b z`Cp`$4P;*J&s4v92{|&XZ2X&g0oM@x8(27`y<%u+`quhT5MKODml<@r%7;HA^7brp z_P(sJ(ed|YT{W3yjM&E*!ApA)V?&h^wE|FZ<7_jv_ .pickers { + display: flex; + flex-direction: row; + gap: 1rem; + & > label { + padding: 0.5rem 0rem; + & > select { + padding: 0.5rem 1rem; + margin: 0.5rem; + font-size: 1rem; + } + } + } + & > input { + padding: 0.5rem 1rem; + font-size: 1rem; + } + +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..b6fa229 --- /dev/null +++ b/public/index.html @@ -0,0 +1,73 @@ + + + + + Hello + + + + + + + + + + + + + + + + +
+

Luftfeuchtigkeitsdaten

+ + +
+ +
+ + +
+ + + +
+ +
+ Lade... + Lade... +
+
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/index.js b/public/index.js new file mode 100644 index 0000000..3caf6bb --- /dev/null +++ b/public/index.js @@ -0,0 +1,244 @@ + +moment.tz('Europe/Berlin') +Chart.defaults.font.size = 24; + +let humChart, pwrChart + +function toMoment(time) { + + + let m = moment(time); + m = m.subtract({hours: 1}); + return m +} + +async function fetchTags() { + let res = await fetch("/tags"); + let tags = await res.json(); + return tags; +} + +async function fetchChartData(series, tag, from) { + + const queryParams = new URLSearchParams({tag, from}); + const url = `/${series}?${queryParams.toString()}` + let res = await fetch(url); + let data = await res.json(); + + return data; +} + +async function fetchTagData(tag, from) { + + let humidityDataPromise = fetchChartData("humidity",tag, from); + let powerDataPromise = fetchChartData("power",tag, from); + let stateDataPromise = fetchChartData("state",tag, from); + + let [humidityData, powerData, stateData] = await Promise.all([humidityDataPromise, powerDataPromise,stateDataPromise]) + return { + humidityData, powerData, stateData + } + +} + +function initHumChart({humidityData, powerData, stateData}) { + + // let humidityData = await fetchChartData("humidity",tag, new Date().getTime()); + // let powerData = await fetchChartData("power",tag, new Date().getTime()); + // let stateData = await fetchChartData("state",tag, new Date().getTime()); + + let humidityDataSet = { + label: 'Luftfeuchtigkeit', + yAxisID: 'humidity', + data: humidityData.map(v => ({x: toMoment(v.Time), y: v.Humidity})), + backgroundColor: 'rgba(8,62,236,0.2)', + borderColor: 'rgb(101,119,234)', + borderWidth: 1 + } + + + let powerDataSet = { + label: 'Apparent Power', + yAxisID: 'power', + data: powerData.map(v => ({x: toMoment(v.Time), y: v.ApparentPower})), + backgroundColor: 'rgba(193,151,91,0.2)', + borderColor: 'rgb(207,179,106)', + borderWidth: 1 + } + + // let stateDataSet = { + // label: 'An/Aus', + // data: stateData.map(v => ({x: v.Time, y: v.SwitchState})), + // backgroundColor: 'rgba(125,158,124,0.2)', + // borderColor: 'rgb(8,255,0)', + // borderWidth: 1 + // } + + chart = new Chart(document.getElementById('humChart').getContext('2d'), { + type: 'line', + data: { + datasets: [humidityDataSet, powerDataSet] + }, + options: { + scales:{ + x:{ + type: 'time', + ticks: { + autoSkip: true, + maxTicksLimit: 20 + }, + time: { + displayFormats: {minute: 'HH:mm'} + } + }, + + humidity: { + type: 'linear', + position: 'left', + ticks: + { + beginAtZero: true, + }, + grid: { display: false } + }, + power: { + type: 'linear', + position: 'right', + ticks: + { + beginAtZero: true, + }, + grid: { display: false } + }, + } + } + }); + return chart +} + +function initPwrChart({humidityData, powerData, stateData}) { + + // + // let humidityDataSet = { + // label: 'Luftfeuchtigkeit', + // yAxisID: 'humidity', + // data: humidityData.map(v => ({x: toMoment(v.Time), y: v.Humidity})), + // backgroundColor: 'rgba(8,62,236,0.2)', + // borderColor: 'rgb(101,119,234)', + // borderWidth: 1 + // } + + + let totalDataset = { + label: 'Total KWh', + yAxisID: 'total', + data: powerData.map(v => ({x: toMoment(v.Time), y: v.Total})), + backgroundColor: 'rgba(193,151,91,0.2)', + borderColor: 'rgb(207,179,106)', + borderWidth: 1 + } + + let yesterdayDataset = { + label: 'Gestern KWh', + yAxisID: 'yesterday', + data: powerData.map(v => ({x: toMoment(v.Time), y: v.Yesterday})), + backgroundColor: 'rgba(110,70,70,0.2)', + borderColor: 'rgb(207,106,114)', + borderWidth: 1 + } + + + + chart = new Chart(document.getElementById('pwrChart').getContext('2d'), { + type: 'line', + data: { + datasets: [totalDataset, yesterdayDataset] + }, + options: { + scales:{ + x:{ + type: 'time', + ticks: { + autoSkip: true, + maxTicksLimit: 20 + }, + time: { + displayFormats: {minute: 'HH:mm'} + } + }, + + total: { + type: 'linear', + position: 'left', + ticks: + { + beginAtZero: true, + }, + grid: { display: false } + }, + yesterday: { + type: 'linear', + position: 'right', + ticks: + { + beginAtZero: true, + }, + grid: { display: false } + }, + } + } + }); + return chart +} + +async function loadChartData(tag, date) { + console.log(date) + let data = await fetchTagData(tag, date); + + humChart = initHumChart(data); + pwrChart = initPwrChart(data); +} + + +async function populateOptions() { + + let tagSelectElement = document.getElementById("tag-select") + tagSelectElement.active = false + let tags = await fetchTags(); + tagSelectElement.innerHTML = ''; + for (let tag of tags) { + let option = document.createElement("option"); + option.value = tag; + option.text = tag; + tagSelectElement.appendChild(option); + } + tagSelectElement.active = true + let rangeSelectElement = document.getElementById("range-select") + document.getElementById("submit-select").addEventListener("click", function(event){ + event.preventDefault(); + console.log("submit clicked", rangeSelectElement.value, tagSelectElement.value); + let m = moment().subtract({hours: rangeSelectElement.value}); + console.log(m); + var fromDate = m.toDate(); + console.log(fromDate.toISOString()); + if (humChart !== undefined) { + humChart.destroy(); + } + if (pwrChart !== undefined) { + pwrChart.destroy(); + } + + loadChartData(tagSelectElement.value, fromDate.toISOString()); + }); + + +} + + + +populateOptions(); + +let m = moment().subtract({hours: 3}); +var fromDate = m.toDate(); + +loadChartData("Bad Unten", fromDate.toISOString()); \ No newline at end of file diff --git a/queries.sql b/queries.sql new file mode 100644 index 0000000..00aea24 --- /dev/null +++ b/queries.sql @@ -0,0 +1,136 @@ +-- name: ListHumidityLog :many +select * +from humidity_log +order by time desc +limit ? offset ?; + +-- name: HumidityLogForDeviceToDate :many +select * +from humidity_log + join device_tag on device_name = humidity_log.sensor +where device_tag.tag = ? + and (time between ? and ?) +order by time desc; + + +-- name: GetHumidityLog :one +select * +from humidity_log +where id = ? +limit 1; + +-- name: CreateHumidityLog :one +insert +into humidity_log (time, sensor, temperature, humidity, dew_point) +values (?, ?, ?, ?, ?) +returning *; + +-- name: DeleteHumidityLog :exec +delete +from humidity_log +where id = ?; + + +-- name: ListPowerLog :many +select * +from power_log +order by time desc +limit ? offset ?; + +-- name: PowerLogForDeviceToDate :many +select * +from power_log + join device_tag on device_name = power_log.sensor +where device_tag.tag = ? + and (time between ? and ?) +order by time desc; + + +-- name: GetPowerLog :one +select * +from power_log +where id = ? +limit 1; + +-- name: CreatePowerLog :one +insert +into power_log (time, sensor, total_start_time, total, yesterday, today, period, power, apparent_power, reactive_power, + factor, voltage, current, sensor_temperature) +values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +returning *; + +-- name: DeletePowerLog :exec +delete +from power_log +where id = ?; + +-- name: ListSwitchStateLog :many +select * +from switch_state_log +order by time desc +limit ? offset ?; + +-- name: SwitchStateLogForDeviceToDate :many +select * +from switch_state_log + join device_tag on device_name = switch_state_log.sensor +where device_tag.tag = ? + and (time between ? and ?) +order by time desc; + +-- name: GetSwitchStateLog :one +select * +from switch_state_log +where id = ? +limit 1; + +-- name: CreateSwitchStateLog :one +insert +into switch_state_log (time, sensor, switch_state) +values (?, ?, ?) +returning *; + +-- name: DeleteSwitchStateLog :exec +delete +from switch_state_log +where id = ?; + +-- name: ListDeviceTag :many +select * +from device_tag; + +-- name: ListDevices :many +select tag +from device_tag +group by tag; + +-- name: GetDeviceTag :one +select * +from device_tag +where id = ? +limit 1; + +-- name: CreateDeviceTag :one +insert +into device_tag (device_name, tag) +values (?, ?) +returning *; + +-- name: UpdateDeviceTag :one +UPDATE device_tag +set device_name = ?, + tag = ? +WHERE id = ? +RETURNING *; + +-- name: DeleteDeviceTag :exec +delete +from device_tag +where id = ?; + +-- name: TruncateDeviceTag :exec +delete +from device_tag; + + + diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..32a8e1d --- /dev/null +++ b/schema.sql @@ -0,0 +1,43 @@ +create table if not exists humidity_log +( + id integer primary key, + time datetime not null, + sensor text not null, + temperature real not null, + humidity real not null, + dew_point real not null +); + +create table if not exists power_log +( + id integer primary key, + time datetime not null, + sensor text not null, + total_start_time datetime not null, + total real not null, + yesterday real not null, + today real not null, + period real not null, + power real not null, + apparent_power real not null, + reactive_power real not null, + factor real not null, + voltage real not null, + current real not null, + sensor_temperature real not null +); + +create table if not exists switch_state_log +( + id integer primary key, + time datetime not null, + sensor text not null, + switch_state text not null +); + +create table if not exists device_tag +( + id integer primary key, + device_name text not null, + tag text not null +) \ No newline at end of file diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..d7e4d04 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: sqlite + queries: queries.sql + schema: schema.sql + gen: + go: + package: "db" + out: "db" + sql_package: database/sql \ No newline at end of file