Move towards selectable time ranges in the dashboard

- Add the installation date to frontend and backend
- Add an error page to help with the upgrade from the previous version
- Avoid querying history if installation date is not set
- Make the fillMissing function work for periods of different lenght than than 8 days
This commit is contained in:
MassiveBox 2022-12-04 12:50:11 +01:00
parent 66e2a2de1a
commit 6dc8fa3750
8 changed files with 61 additions and 21 deletions

View file

@ -52,6 +52,7 @@ As soon as you navigate to the container's exposed port, you will see the admin
- **HomeAssistant's base URL**: the base URL which you use to access HomeAssistant on your server. It should be something like `http://INTERNAL_IP_ADDRESS:8123/` or `https://homeassistant.youdomain.com/`. - **HomeAssistant's base URL**: the base URL which you use to access HomeAssistant on your server. It should be something like `http://INTERNAL_IP_ADDRESS:8123/` or `https://homeassistant.youdomain.com/`.
- **HomeAssistant's API Key:** Get it by going into your HomeAssistant profile settings (at `http://HOMEASSISTANT-BASE-URL/profile`) -> Create Long Lived Access Token (at the very bottom of the page) -> Insert a name -> Copy the string it gives you - **HomeAssistant's API Key:** Get it by going into your HomeAssistant profile settings (at `http://HOMEASSISTANT-BASE-URL/profile`) -> Create Long Lived Access Token (at the very bottom of the page) -> Insert a name -> Copy the string it gives you
- **Installation Date**: Select the date of the first day in which your server's consumption was logged in its entirety. Users won't be able to see consumption data before this date.
- **Polled Smart Energy Summation entity ID:** After your plug is added in HomeAssistant, get it in Overview -> look for an entity called like "[Name of your plug] Polledsmartenergysummation" -> Settings -> Copy the Entity ID. Check that the unit of measurement in the "Info" tab is kWh. - **Polled Smart Energy Summation entity ID:** After your plug is added in HomeAssistant, get it in Overview -> look for an entity called like "[Name of your plug] Polledsmartenergysummation" -> Settings -> Copy the Entity ID. Check that the unit of measurement in the "Info" tab is kWh.
- **CO2 signal Grid fossil fuel percentage entity ID**: Get it in Settings -> Devices and Integrations -> Add Integration -> CO2 Signal -> Get your token from the website -> CO2 signal Grid fossil fuel percentage -> Settings -> Copy the Entity ID. Check that the unit of measurement in the "Info" tab is %. - **CO2 signal Grid fossil fuel percentage entity ID**: Get it in Settings -> Devices and Integrations -> Add Integration -> CO2 Signal -> Get your token from the website -> CO2 signal Grid fossil fuel percentage -> Settings -> Copy the Entity ID. Check that the unit of measurement in the "Info" tab is %.
- **Admin username and password** don't need to be the credentials to HomeAssistant! They are the credentials to log into the admin panel. - **Admin username and password** don't need to be the credentials to HomeAssistant! They are the credentials to log into the admin panel.

14
api.go
View file

@ -106,7 +106,7 @@ func (config Config) historyAverageAndConvertToGreen(entityID string, startTime,
days[key] = day days[key] = day
} }
days = fillMissing(days) days = fillMissing(days, startTime, endTime)
return days, nil return days, nil
@ -157,13 +157,13 @@ func (config Config) historyDelta(entityID string, startTime, endTime time.Time)
days[key] = day days[key] = day
} }
days = fillMissing(days) days = fillMissing(days, startTime, endTime)
return days, nil return days, nil
} }
func fillMissing(days []DayData) []DayData { func fillMissing(days []DayData, startTime, endTime time.Time) []DayData {
var ( var (
previousDay time.Time previousDay time.Time
@ -173,6 +173,8 @@ func fillMissing(days []DayData) []DayData {
currentTime = time.Now() currentTime = time.Now()
) )
expectedDaysDiff := int(math.Trunc(endTime.Sub(startTime).Hours()/24) + 1)
for key, day := range days { for key, day := range days {
if key != 0 { if key != 0 {
@ -214,8 +216,8 @@ func fillMissing(days []DayData) []DayData {
} }
} }
if len(ret) < 8 { if len(ret) < expectedDaysDiff {
shouldAdd := 8 - len(ret) shouldAdd := expectedDaysDiff - len(ret)
startDay := currentTime.Add(-time.Duration(24*len(ret)) * time.Hour) startDay := currentTime.Add(-time.Duration(24*len(ret)) * time.Hour)
for i := 0; i < shouldAdd; i++ { for i := 0; i < shouldAdd; i++ {
fakeTime := startDay.Add(-time.Duration(24*i) * time.Hour) fakeTime := startDay.Add(-time.Duration(24*i) * time.Hour)
@ -229,7 +231,7 @@ func fillMissing(days []DayData) []DayData {
} }
} }
if len(ret) != 8 { if len(ret) != expectedDaysDiff {
// oh shit // oh shit
log.Panicln("You've found a logic bug! Open a bug report ASAP.") log.Panicln("You've found a logic bug! Open a bug report ASAP.")
} }

View file

@ -16,6 +16,11 @@ type CacheFile []CacheEntry
func (config Config) updateCache() { func (config Config) updateCache() {
// in order to avoid querying and storing each day's data from 0001-01-01 in future versions
if config.HomeAssistant.InstallationDate.IsZero() {
return
}
now := time.Now() now := time.Now()
h, m, s := now.Clock() h, m, s := now.Clock()
start := now.AddDate(0, 0, -7).Add(-(time.Duration(h)*time.Hour + time.Duration(m)*time.Minute + time.Duration(s)*time.Second)) start := now.AddDate(0, 0, -7).Add(-(time.Duration(h)*time.Hour + time.Duration(m)*time.Minute + time.Duration(s)*time.Second))

View file

@ -9,6 +9,7 @@ import (
"os" "os"
"regexp" "regexp"
"strings" "strings"
"time"
) )
type Config struct { type Config struct {
@ -21,6 +22,7 @@ type Config struct {
type HomeAssistant struct { type HomeAssistant struct {
BaseURL string `json:"base_url"` BaseURL string `json:"base_url"`
ApiKey string `json:"api_key"` ApiKey string `json:"api_key"`
InstallationDate time.Time `json:"installation_date"`
} }
type Sensors struct { type Sensors struct {
PolledSmartEnergySummation string `json:"polled_smart_energy_summation"` PolledSmartEnergySummation string `json:"polled_smart_energy_summation"`

39
http.go
View file

@ -27,7 +27,7 @@ type Warning struct {
IsSuccess bool IsSuccess bool
} }
func (config Config) getTemplateDefaults() map[string]interface{} { func (config Config) getTemplateDefaults() fiber.Map {
return fiber.Map{ return fiber.Map{
"DashboardName": config.Dashboard.Name, "DashboardName": config.Dashboard.Name,
"HeaderLinks": config.Dashboard.HeaderLinks, "HeaderLinks": config.Dashboard.HeaderLinks,
@ -35,6 +35,12 @@ func (config Config) getTemplateDefaults() map[string]interface{} {
} }
} }
func (config Config) templateDefaultsMap() fiber.Map {
return fiber.Map{
"Default": config.getTemplateDefaults(),
}
}
func (config Config) adminEndpoint(c *fiber.Ctx) error { func (config Config) adminEndpoint(c *fiber.Ctx) error {
if c.Method() == "POST" { if c.Method() == "POST" {
@ -68,7 +74,7 @@ func (config Config) adminEndpoint(c *fiber.Ctx) error {
if config.isAuthorized(c) { if config.isAuthorized(c) {
return config.renderAdminPanel(c) return config.renderAdminPanel(c)
} }
return c.Render("login", fiber.Map{"Defaults": config.getTemplateDefaults()}, "base") return c.Render("login", config.templateDefaultsMap(), "base")
} }
@ -99,15 +105,20 @@ func (config Config) renderAdminPanel(c *fiber.Ctx, warning ...Warning) error {
func (config Config) saveAdminForm(c *fiber.Ctx) error { func (config Config) saveAdminForm(c *fiber.Ctx) error {
requiredFields := []string{"base_url", "api_key", "polled_smart_energy_summation", "fossil_percentage", "username", "theme", "name"} requiredFields := []string{"base_url", "api_key", "polled_smart_energy_summation", "fossil_percentage", "username", "theme", "name", "installation_date"}
for _, requiredField := range requiredFields { for _, requiredField := range requiredFields {
if c.FormValue(requiredField) == "" { if c.FormValue(requiredField) == "" {
return errors.New("Required field is missing: " + 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{ form := Config{
HomeAssistant: HomeAssistant{ /*BaseURL to be filled later*/ ApiKey: c.FormValue("api_key")}, 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")}, 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*/}, 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}, Dashboard: Dashboard{Theme: c.FormValue("theme"), Name: c.FormValue("name"), HeaderLinks: config.Dashboard.HeaderLinks, FooterLinks: config.Dashboard.FooterLinks},
@ -148,7 +159,7 @@ func (config Config) saveAdminForm(c *fiber.Ctx) error {
} }
func sevenDaysAverageExcludingCurrentDay(data []float32) float32 { func averageExcludingCurrentDay(data []float32) float32 {
if len(data) == 0 { if len(data) == 0 {
return 0 return 0
} }
@ -163,6 +174,13 @@ func sevenDaysAverageExcludingCurrentDay(data []float32) float32 {
func (config Config) renderIndex(c *fiber.Ctx) error { 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 := readCache() data, err := readCache()
if err != nil { if err != nil {
return err return err
@ -182,14 +200,14 @@ func (config Config) renderIndex(c *fiber.Ctx) error {
energyConsumptions = append(energyConsumptions, datum.PolledSmartEnergySummation) energyConsumptions = append(energyConsumptions, datum.PolledSmartEnergySummation)
} }
perDayUsage := sevenDaysAverageExcludingCurrentDay(energyConsumptions) perDayUsage := averageExcludingCurrentDay(energyConsumptions)
return c.Render("index", fiber.Map{ return c.Render("index", fiber.Map{
"Defaults": config.getTemplateDefaults(), "Defaults": config.getTemplateDefaults(),
"Labels": labels, "Labels": labels,
"GreenEnergyPercents": greenEnergyConsumptionAbsolute, "GreenEnergyPercents": greenEnergyConsumptionAbsolute,
"EnergyConsumptions": energyConsumptions, "EnergyConsumptions": energyConsumptions,
"GreenEnergyPercent": sevenDaysAverageExcludingCurrentDay(greenEnergyPercents), "GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents),
"PerDayUsage": perDayUsage, "PerDayUsage": perDayUsage,
}, "base") }, "base")
@ -208,3 +226,10 @@ func templateDivide(num1, num2 float32) template.HTML {
return template.HTML(fmt.Sprintf("%s * 10<sup>%d</sup>", strconv.FormatFloat(math.Round(preComma*100)/100, 'f', -1, 64), 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"))
}

View file

@ -28,6 +28,7 @@ func main() {
engine := html.New("./templates/"+config.Dashboard.Theme, ".html") engine := html.New("./templates/"+config.Dashboard.Theme, ".html")
engine.AddFunc("divide", templateDivide) engine.AddFunc("divide", templateDivide)
engine.AddFunc("HTMLDateFormat", templateHTMLDateFormat)
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
Views: engine, Views: engine,
@ -45,9 +46,7 @@ func main() {
}) })
app.Get("/accuracy-notice", func(c *fiber.Ctx) error { app.Get("/accuracy-notice", func(c *fiber.Ctx) error {
return c.Render("accuracy-notice", fiber.Map{ return c.Render("accuracy-notice", config.templateDefaultsMap(), "base")
"Defaults": config.getTemplateDefaults(),
}, "base")
}) })
app.All("/admin", config.adminEndpoint) app.All("/admin", config.adminEndpoint)
@ -58,9 +57,7 @@ func main() {
time.Sleep(time.Second) time.Sleep(time.Second)
os.Exit(1) os.Exit(1)
}() }()
return c.Render("restart", fiber.Map{ return c.Render("restart", config.templateDefaultsMap(), "base")
"Defaults": config.getTemplateDefaults(),
}, "base")
} }
return c.Redirect("./", 307) return c.Redirect("./", 307)
}) })

View file

@ -17,6 +17,7 @@
<h3>HomeAssistant</h3> <h3>HomeAssistant</h3>
<label>HomeAssistant's base URL <input type="text" name="base_url" value="{{.Config.HomeAssistant.BaseURL}}" required></label> <label>HomeAssistant's base URL <input type="text" name="base_url" value="{{.Config.HomeAssistant.BaseURL}}" required></label>
<label>HomeAssistant's API Key <input type="text" name="api_key" value="{{.Config.HomeAssistant.ApiKey}}" required></label> <label>HomeAssistant's API Key <input type="text" name="api_key" value="{{.Config.HomeAssistant.ApiKey}}" required></label>
<lablel>Installation date<input type="date" name="installation_date" value="{{HTMLDateFormat .Config.HomeAssistant.InstallationDate}}" required></lablel>
<h3>Sensors</h3> <h3>Sensors</h3>
<label>Polled Smart Energy Summation entity ID <input type="text" name="polled_smart_energy_summation" value="{{.Config.Sensors.PolledSmartEnergySummation}}" required></label> <label>Polled Smart Energy Summation entity ID <input type="text" name="polled_smart_energy_summation" value="{{.Config.Sensors.PolledSmartEnergySummation}}" required></label>

View file

@ -0,0 +1,7 @@
<h1>Configuration error</h1>
<p>We've detected an issue with your configuration that prevents EcoDash from working. Please check it below, and <a href="https://gitea.massivebox.net/massivebox/ecodash/issues" target="_blank" rel="noopener noreferrer">open an issue</a> if this problem persists.</p>
<pre><code>{{.Error}}</code></pre>
<a href="/admin" class="button">Admin panel</a>