forked from massivebox/ecodash
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:
parent
66e2a2de1a
commit
6dc8fa3750
|
@ -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
14
api.go
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
5
cache.go
5
cache.go
|
@ -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))
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -19,8 +20,9 @@ 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
39
http.go
|
@ -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"))
|
||||||
|
}
|
||||||
|
|
9
main.go
9
main.go
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
|
7
templates/default/config-error.html
Normal file
7
templates/default/config-error.html
Normal 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>
|
Loading…
Reference in a new issue