forked from massivebox/ecodash
220 lines
6.1 KiB
Go
220 lines
6.1 KiB
Go
|
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 <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", 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)
|
||
|
}
|