ecodash/src/ecodash/http.go
2023-05-01 19:39:12 +02:00

206 lines
6.1 KiB
Go

package ecodash
import (
"encoding/json"
"errors"
"fmt"
"html"
"html/template"
"math"
"os"
"time"
"git.massivebox.net/ecodash/ecodash/src/tools"
"github.com/gofiber/fiber/v2"
)
type Link struct {
Label string `json:"label"`
Destination string `json:"destination"`
NewTab bool `json:"new_tab"`
Primary bool `json:"primary"`
}
type Warning struct {
Header string
Body string
BodyHTML template.HTML
IsSuccess bool
}
func (config *Config) getTemplateDefaults() fiber.Map {
return fiber.Map{
"DashboardName": config.Dashboard.Name,
"HeaderLinks": config.Dashboard.HeaderLinks,
"FooterLinks": config.Dashboard.FooterLinks,
}
}
func (config *Config) TemplateDefaultsMap() fiber.Map {
return fiber.Map{
"Default": config.getTemplateDefaults(),
}
}
func (config *Config) AdminEndpoint(c *fiber.Ctx) error {
if c.Method() == "POST" {
if config.IsAuthorized(c) { // here the user is submitting the form to change configuration
err := config.saveAdminForm(c)
if err != nil {
return config.RenderAdminPanel(c, Warning{
Header: "An error occurred!",
Body: html.EscapeString(err.Error()),
})
}
return config.RenderAdminPanel(c, Warning{
Header: "Restart needed",
Body: "In order to apply changes, please <b>restart EcoDash</b>.<br>" +
"If you're running via Docker, click <a href='./restart'>here</a> to restart automatically.",
IsSuccess: true,
})
}
// here the user is trying to authenticate
if c.FormValue("username") == config.Administrator.Username && tools.Hash(c.FormValue("password")) == config.Administrator.PasswordHash {
c.Cookie(&fiber.Cookie{Name: "admin_username", Value: c.FormValue("username")})
c.Cookie(&fiber.Cookie{Name: "admin_password_hash", Value: tools.Hash(c.FormValue("password"))})
return config.RenderAdminPanel(c)
}
return c.Render("login", fiber.Map{"Defaults": config.getTemplateDefaults(), "Failed": true}, "base")
}
if config.IsAuthorized(c) {
return config.RenderAdminPanel(c)
}
return c.Render("login", config.TemplateDefaultsMap(), "base")
}
func (config *Config) RenderAdminPanel(c *fiber.Ctx, warning ...Warning) error {
dirs, err := os.ReadDir("./templates")
if err != nil {
return err
}
if len(warning) > 0 {
// #nosec // TODO this is dangerous, even if we're escaping the only place where we're passing a non-literal
warning[0].BodyHTML = template.HTML(warning[0].Body)
return c.Render("admin", fiber.Map{
"Defaults": config.getTemplateDefaults(),
"Themes": dirs,
"Config": config,
"Warning": warning[0],
}, "base")
}
return c.Render("admin", fiber.Map{
"Defaults": config.getTemplateDefaults(),
"Themes": dirs,
"Config": config,
}, "base")
}
var (
errNoChanges = errors.New("no changes from previous config")
errMissingField = errors.New("required field is missing")
)
func (config *Config) saveAdminForm(c *fiber.Ctx) error {
requiredFields := []string{"base_url", "api_key", "polled_smart_energy_summation", "fossil_percentage", "username", "theme", "name", "installation_date"}
for _, requiredField := range requiredFields {
if c.FormValue(requiredField) == "" {
return fmt.Errorf("%w: %s", errMissingField, requiredField)
}
}
parsedTime, err := time.Parse("2006-01-02", c.FormValue("installation_date"))
if err != nil {
return err
}
form := &Config{
HomeAssistant: HomeAssistant{ /*BaseURL to be filled later*/ APIKey: c.FormValue("api_key"), InstallationDate: dayStart(parsedTime)},
Sensors: Sensors{PolledSmartEnergySummation: c.FormValue("polled_smart_energy_summation"), FossilPercentage: c.FormValue("fossil_percentage")},
Administrator: Administrator{Username: c.FormValue("username") /*PasswordHash to be filled later*/},
Dashboard: Dashboard{Theme: c.FormValue("theme"), Name: c.FormValue("name"), HeaderLinks: config.Dashboard.HeaderLinks, FooterLinks: config.Dashboard.FooterLinks},
}
if c.FormValue("keep_old_password") == "" {
form.Administrator.PasswordHash = tools.Hash(c.FormValue("password"))
} else {
form.Administrator.PasswordHash = config.Administrator.PasswordHash
}
fmtURL, err := formatURL(c.FormValue("base_url"))
if err != nil {
return err
}
form.HomeAssistant.BaseURL = fmtURL
if form.Equals(config) {
return errNoChanges
}
form.db = config.db
err = form.refreshCacheFromInstall()
if err != nil {
return err
}
js, err := json.Marshal(form)
if err != nil {
return err
}
return os.WriteFile("config.json", js, 0o600)
}
func averageExcludingCurrentDay(data []float32) float32 {
if len(data) == 0 {
return 0
}
data = data[:len(data)-1]
var sum float32
for _, num := range data {
sum += num
}
avg := sum / float32(len(data))
return float32(math.Floor(float64(avg)*100)) / 100
}
func (config *Config) RenderIndex(c *fiber.Ctx) error {
if config.HomeAssistant.InstallationDate.IsZero() {
return c.Render("config-error", fiber.Map{
"Defaults": config.getTemplateDefaults(),
"Error": "The installation date is not set! This is normal if you've just updated from v0.1 to v0.2.",
}, "base")
}
data, err := config.readHistory()
if err != nil {
return err
}
labels := make([]string, 0, len(data))
greenEnergyConsumptionAbsolute := make([]float32, 0, len(data))
greenEnergyPercents := make([]float32, 0, len(data))
energyConsumptions := make([]float32, 0, len(data))
for _, datum := range data {
labels = append(labels, time.Unix(datum.Date, 0).Format("02/01"))
greenEnergyPercents = append(greenEnergyPercents, datum.GreenEnergyPercentage)
greenEnergyConsumptionAbsolute = append(greenEnergyConsumptionAbsolute, datum.GreenEnergyPercentage/100*datum.PolledSmartEnergySummation)
energyConsumptions = append(energyConsumptions, datum.PolledSmartEnergySummation)
}
perDayUsage := averageExcludingCurrentDay(energyConsumptions)
return c.Render("index", fiber.Map{
"Defaults": config.getTemplateDefaults(),
"Labels": labels,
"GreenEnergyPercents": greenEnergyConsumptionAbsolute,
"EnergyConsumptions": energyConsumptions,
"GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents),
"PerDayUsage": perDayUsage,
}, "base")
}