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 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.
|
||||
- **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.
|
||||
|
|
14
api.go
14
api.go
|
@ -106,7 +106,7 @@ func (config Config) historyAverageAndConvertToGreen(entityID string, startTime,
|
|||
days[key] = day
|
||||
}
|
||||
|
||||
days = fillMissing(days)
|
||||
days = fillMissing(days, startTime, endTime)
|
||||
|
||||
return days, nil
|
||||
|
||||
|
@ -157,13 +157,13 @@ func (config Config) historyDelta(entityID string, startTime, endTime time.Time)
|
|||
days[key] = day
|
||||
}
|
||||
|
||||
days = fillMissing(days)
|
||||
days = fillMissing(days, startTime, endTime)
|
||||
|
||||
return days, nil
|
||||
|
||||
}
|
||||
|
||||
func fillMissing(days []DayData) []DayData {
|
||||
func fillMissing(days []DayData, startTime, endTime time.Time) []DayData {
|
||||
|
||||
var (
|
||||
previousDay time.Time
|
||||
|
@ -173,6 +173,8 @@ func fillMissing(days []DayData) []DayData {
|
|||
currentTime = time.Now()
|
||||
)
|
||||
|
||||
expectedDaysDiff := int(math.Trunc(endTime.Sub(startTime).Hours()/24) + 1)
|
||||
|
||||
for key, day := range days {
|
||||
|
||||
if key != 0 {
|
||||
|
@ -214,8 +216,8 @@ func fillMissing(days []DayData) []DayData {
|
|||
}
|
||||
}
|
||||
|
||||
if len(ret) < 8 {
|
||||
shouldAdd := 8 - len(ret)
|
||||
if len(ret) < expectedDaysDiff {
|
||||
shouldAdd := expectedDaysDiff - len(ret)
|
||||
startDay := currentTime.Add(-time.Duration(24*len(ret)) * time.Hour)
|
||||
for i := 0; i < shouldAdd; i++ {
|
||||
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
|
||||
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() {
|
||||
|
||||
// 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()
|
||||
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))
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
@ -19,8 +20,9 @@ type Config struct {
|
|||
}
|
||||
|
||||
type HomeAssistant struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
ApiKey string `json:"api_key"`
|
||||
BaseURL string `json:"base_url"`
|
||||
ApiKey string `json:"api_key"`
|
||||
InstallationDate time.Time `json:"installation_date"`
|
||||
}
|
||||
type Sensors struct {
|
||||
PolledSmartEnergySummation string `json:"polled_smart_energy_summation"`
|
||||
|
|
39
http.go
39
http.go
|
@ -27,7 +27,7 @@ type Warning struct {
|
|||
IsSuccess bool
|
||||
}
|
||||
|
||||
func (config Config) getTemplateDefaults() map[string]interface{} {
|
||||
func (config Config) getTemplateDefaults() fiber.Map {
|
||||
return fiber.Map{
|
||||
"DashboardName": config.Dashboard.Name,
|
||||
"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 {
|
||||
|
||||
if c.Method() == "POST" {
|
||||
|
@ -68,7 +74,7 @@ func (config Config) adminEndpoint(c *fiber.Ctx) error {
|
|||
if config.isAuthorized(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 {
|
||||
|
||||
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 {
|
||||
if c.FormValue(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{
|
||||
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")},
|
||||
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},
|
||||
|
@ -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 {
|
||||
return 0
|
||||
}
|
||||
|
@ -163,6 +174,13 @@ func sevenDaysAverageExcludingCurrentDay(data []float32) float32 {
|
|||
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -182,14 +200,14 @@ func (config Config) renderIndex(c *fiber.Ctx) error {
|
|||
energyConsumptions = append(energyConsumptions, datum.PolledSmartEnergySummation)
|
||||
}
|
||||
|
||||
perDayUsage := sevenDaysAverageExcludingCurrentDay(energyConsumptions)
|
||||
perDayUsage := averageExcludingCurrentDay(energyConsumptions)
|
||||
|
||||
return c.Render("index", fiber.Map{
|
||||
"Defaults": config.getTemplateDefaults(),
|
||||
"Labels": labels,
|
||||
"GreenEnergyPercents": greenEnergyConsumptionAbsolute,
|
||||
"EnergyConsumptions": energyConsumptions,
|
||||
"GreenEnergyPercent": sevenDaysAverageExcludingCurrentDay(greenEnergyPercents),
|
||||
"GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents),
|
||||
"PerDayUsage": perDayUsage,
|
||||
}, "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))
|
||||
|
||||
}
|
||||
|
||||
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.AddFunc("divide", templateDivide)
|
||||
engine.AddFunc("HTMLDateFormat", templateHTMLDateFormat)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
Views: engine,
|
||||
|
@ -45,9 +46,7 @@ func main() {
|
|||
})
|
||||
|
||||
app.Get("/accuracy-notice", func(c *fiber.Ctx) error {
|
||||
return c.Render("accuracy-notice", fiber.Map{
|
||||
"Defaults": config.getTemplateDefaults(),
|
||||
}, "base")
|
||||
return c.Render("accuracy-notice", config.templateDefaultsMap(), "base")
|
||||
})
|
||||
|
||||
app.All("/admin", config.adminEndpoint)
|
||||
|
@ -58,9 +57,7 @@ func main() {
|
|||
time.Sleep(time.Second)
|
||||
os.Exit(1)
|
||||
}()
|
||||
return c.Render("restart", fiber.Map{
|
||||
"Defaults": config.getTemplateDefaults(),
|
||||
}, "base")
|
||||
return c.Render("restart", config.templateDefaultsMap(), "base")
|
||||
}
|
||||
return c.Redirect("./", 307)
|
||||
})
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<h3>HomeAssistant</h3>
|
||||
<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>
|
||||
<lablel>Installation date<input type="date" name="installation_date" value="{{HTMLDateFormat .Config.HomeAssistant.InstallationDate}}" required></lablel>
|
||||
|
||||
<h3>Sensors</h3>
|
||||
<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