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