initial commit

This commit is contained in:
V
2026-03-09 01:26:52 +01:00
commit 2198c20790
18 changed files with 1942 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/.env
/.venv/
/persist/
/.idea/
/.vscode/

173
api.go Normal file
View File

@@ -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)
}
})
}

20
config.yaml Normal file
View File

@@ -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

47
database.go Normal file
View File

@@ -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
}

31
db/db.go Normal file
View File

@@ -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,
}
}

49
db/models.go Normal file
View File

@@ -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
}

681
db/queries.sql.go Normal file
View File

@@ -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
}

27
go.mod Normal file
View File

@@ -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
)

67
go.sum Normal file
View File

@@ -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=

71
main.go Normal file
View File

@@ -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)
}

220
mqtt_client.go Normal file
View File

@@ -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)
}

BIN
public/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

45
public/index.css Normal file
View File

@@ -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;
}
}

73
public/index.html Normal file
View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/moment-timezone@0.6.0/moment-timezone.min.js"></script>-->
<script src="https://momentjs.com/downloads/moment-timezone-with-data.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment"></script>
<script defer src="index.js"></script>
<!-- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>-->
<!-- <script defer src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>-->
<!-- <script defer src="https://cdn.jsdelivr.net/npm/chartjs-plugin-colorschemes"></script>-->
<link rel="stylesheet" href="index.css">
</head>
<body>
<main class="container">
<h1>Luftfeuchtigkeitsdaten</h1>
<form class="select-form">
<div class="pickers">
<label for="tag-select">Daten von
<select name="tag-select" id="tag-select">
<option value="">--Lade--</option>
</select>
</label>
<label for="range-select">Wie lange?
<select name="range-select" id="range-select">
<option value="3" selected>3 Stunden</option>
<option value="6">6 Stunden</option>
<option value="12">12 Stunden</option>
<option value="24">1 Tag</option>
<option value="72">3 Tage</option>
<option value="72">3 Tage</option>
<option value="168">1 Woche</option>
</select>
</div>
</label>
<input type="submit" value="Anwenden" id="submit-select">
</form>
<div class="chart-container">
<canvas id="humChart" class="chart"> Lade...</canvas>
<canvas id="pwrChart" class="chart"> Lade...</canvas>
</div>
</main>
<!--<form>-->
<!-- <label for="range-select">Choose a range:</label>-->
<!-- <select name="range-select" id="table-select">-->
<!-- <option value="">&#45;&#45;Please choose an option&#45;&#45;</option>-->
<!-- <option value="humidity">Luftfeuchtigkeit</option>-->
<!-- <option value="power">Stromverbrauch</option>-->
<!-- </select>-->
<!-- <input type="submit" value="Submit" id="submit-table-select">-->
<!--</form>-->
</body>
</html>

244
public/index.js Normal file
View File

@@ -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());

136
queries.sql Normal file
View File

@@ -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;

43
schema.sql Normal file
View File

@@ -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
)

10
sqlc.yaml Normal file
View File

@@ -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