forked from massivebox/ecodash
e9125b783c
This is the first, most basic implementation of a SQLite database for caching. Future commits will make it much more optimized and able to efficiently store data for periods longer than 8 days.
236 lines
6.7 KiB
Go
236 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/gofiber/fiber/v2"
|
|
"html/template"
|
|
"math"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
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: 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 && 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: 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 {
|
|
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")
|
|
|
|
}
|
|
|
|
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 errors.New("Required field is missing: " + 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: 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 = 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 reflect.DeepEqual(form, config) {
|
|
return errors.New("No changes from previous config.")
|
|
}
|
|
|
|
// in order to test if ha base URL, API key and entity IDs are correct we try fetching the devices history
|
|
_, err = form.queryHistory(form.Sensors.FossilPercentage, time.Now().Add(-5*time.Minute), time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = form.queryHistory(form.Sensors.PolledSmartEnergySummation, time.Now().Add(-5*time.Minute), time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
js, err := json.Marshal(form)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile("config.json", js, 0666)
|
|
|
|
}
|
|
|
|
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
|
|
}
|
|
var 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.readCache()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
labels []string
|
|
greenEnergyConsumptionAbsolute []float32
|
|
greenEnergyPercents []float32
|
|
energyConsumptions []float32
|
|
)
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
func templateDivide(num1, num2 float32) template.HTML {
|
|
|
|
division := float64(num1 / num2)
|
|
|
|
powerOfTen := int(math.Floor(math.Log10(division)))
|
|
if powerOfTen >= -2 && powerOfTen <= 2 {
|
|
return template.HTML(fmt.Sprintf("%s", strconv.FormatFloat(math.Round(division*100)/100, 'f', -1, 64)))
|
|
}
|
|
|
|
preComma := division / math.Pow10(powerOfTen)
|
|
return template.HTML(fmt.Sprintf("%s * 10<sup>%d</sup>", strconv.FormatFloat(math.Round(preComma*100)/100, 'f', -1, 64), powerOfTen))
|
|
|
|
}
|
|
|
|
func templateHTMLDateFormat(date time.Time) template.HTML {
|
|
if date.IsZero() {
|
|
return ""
|
|
}
|
|
return template.HTML(date.Format("2006-01-02"))
|
|
}
|