package main
import (
"encoding/json"
"errors"
"fmt"
"github.com/gofiber/fiber/v2"
"html/template"
"math"
"os"
"reflect"
"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() map[string]interface{} {
return fiber.Map{
"DashboardName": config.Dashboard.Name,
"HeaderLinks": config.Dashboard.HeaderLinks,
"FooterLinks": config.Dashboard.FooterLinks,
}
}
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 restart EcoDash.
" +
"If you're running via Docker, click here 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", fiber.Map{"Defaults": config.getTemplateDefaults()}, "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"}
for _, requiredField := range requiredFields {
if c.FormValue(requiredField) == "" {
return errors.New("Required field is missing: " + requiredField)
}
}
form := Config{
HomeAssistant: HomeAssistant{ /*BaseURL to be filled later*/ ApiKey: c.FormValue("api_key")},
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 sevenDaysAverageExcludingCurrentDay(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 {
data, err := readCache()
if err != nil {
return err
}
var (
labels []string
greenEnergyConsumptionAbsolute []float32
greenEnergyPercents []float32
energyConsumptions []float32
)
for _, datum := range data {
labels = append(labels, datum.Date)
greenEnergyPercents = append(greenEnergyPercents, datum.GreenEnergyPercentage)
greenEnergyConsumptionAbsolute = append(greenEnergyConsumptionAbsolute, datum.GreenEnergyPercentage/100*datum.PolledSmartEnergySummation)
energyConsumptions = append(energyConsumptions, datum.PolledSmartEnergySummation)
}
perDayUsage := sevenDaysAverageExcludingCurrentDay(energyConsumptions)
return c.Render("index", fiber.Map{
"Defaults": config.getTemplateDefaults(),
"Labels": labels,
"GreenEnergyPercents": greenEnergyConsumptionAbsolute,
"EnergyConsumptions": energyConsumptions,
"GreenEnergyPercent": sevenDaysAverageExcludingCurrentDay(greenEnergyPercents),
"PerDayUsage": perDayUsage,
}, "base")
}
func templateDivide(num1, num2 float32) template.HTML {
var (
isInDecimals bool
hasHitMeaningful bool
meaningfulDigits int
rounded []byte
num = num1 / num2
)
for _, char := range []byte(fmt.Sprintf("%v", num)) {
if (isInDecimals && char != '0') || hasHitMeaningful {
hasHitMeaningful = true
meaningfulDigits++
}
if char == '.' {
isInDecimals = true
}
rounded = append(rounded, char)
if meaningfulDigits == 3 {
break
}
}
return template.HTML(rounded)
}