commit 2198c20790164bd26482e162935dfdd1829ed782 Author: Rachel Volk Date: Mon Mar 9 01:26:52 2026 +0100 initial commit 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 0000000..7231841 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.css b/public/index.css new file mode 100644 index 0000000..86c0fdd --- /dev/null +++ b/public/index.css @@ -0,0 +1,45 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; +} + +body { + font-size: 16px; +} + +.container { + padding: 1rem; + margin: 0 auto; + max-width: 1200px; +} + +.select-form { + display: flex; + flex-direction: row; + justify-content: space-between; + border: 1px lightgray solid; + gap: 2rem; + margin: 1rem 0; + padding: 1rem; + + & > .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