forked from massivebox/ecodash
Refactor
This commit is contained in:
parent
08be78f907
commit
9bd3f35cc7
259
.golangci.yml
Normal file
259
.golangci.yml
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
# options for analysis running
|
||||||
|
run:
|
||||||
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
|
timeout: 5m
|
||||||
|
|
||||||
|
# output configuration options
|
||||||
|
output:
|
||||||
|
# sorts results by: filepath, line and column
|
||||||
|
sort-results: true
|
||||||
|
|
||||||
|
# all available settings of specific linters
|
||||||
|
linters-settings:
|
||||||
|
cyclop:
|
||||||
|
# the maximal code complexity to report
|
||||||
|
max-complexity: 30
|
||||||
|
# the maximal average package complexity. If it's higher than 0.0 (float) the check is enabled (default 0.0)
|
||||||
|
package-average: 0.0
|
||||||
|
# should ignore tests (default false)
|
||||||
|
skip-tests: true
|
||||||
|
|
||||||
|
dogsled:
|
||||||
|
# checks assignments with too many blank identifiers; default is 2
|
||||||
|
max-blank-identifiers: 2
|
||||||
|
|
||||||
|
dupl:
|
||||||
|
# tokens count to trigger issue, 150 by default
|
||||||
|
threshold: 100
|
||||||
|
|
||||||
|
errcheck:
|
||||||
|
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
|
||||||
|
# default is false: such cases aren't reported by default.
|
||||||
|
check-type-assertions: true
|
||||||
|
|
||||||
|
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||||
|
# default is false: such cases aren't reported by default.
|
||||||
|
check-blank: true
|
||||||
|
|
||||||
|
errorlint:
|
||||||
|
# Check whether fmt.Errorf uses the %w verb for formatting errors. See the readme for caveats
|
||||||
|
errorf: true
|
||||||
|
# Check for plain type assertions and type switches
|
||||||
|
asserts: true
|
||||||
|
# Check for plain error comparisons
|
||||||
|
comparison: true
|
||||||
|
|
||||||
|
exhaustive:
|
||||||
|
# indicates that switch statements are to be considered exhaustive if a
|
||||||
|
# 'default' case is present, even if all enum members aren't listed in the
|
||||||
|
# switch
|
||||||
|
default-signifies-exhaustive: true
|
||||||
|
|
||||||
|
exhaustivestruct:
|
||||||
|
# Struct Patterns is list of expressions to match struct packages and names
|
||||||
|
# The struct packages have the form example.com/package.ExampleStruct
|
||||||
|
# The matching patterns can use matching syntax from https://pkg.go.dev/path#Match
|
||||||
|
# If this list is empty, all structs are tested.
|
||||||
|
struct-patterns:
|
||||||
|
|
||||||
|
gocognit:
|
||||||
|
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||||
|
min-complexity: 30
|
||||||
|
|
||||||
|
nestif:
|
||||||
|
# minimal complexity of if statements to report, 5 by default
|
||||||
|
min-complexity: 4
|
||||||
|
|
||||||
|
goconst:
|
||||||
|
# minimal length of string constant, 3 by default
|
||||||
|
min-len: 3
|
||||||
|
# minimal occurrences count to trigger, 3 by default
|
||||||
|
min-occurrences: 3
|
||||||
|
|
||||||
|
gocritic:
|
||||||
|
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
|
||||||
|
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
|
||||||
|
enabled-tags:
|
||||||
|
- diagnostic
|
||||||
|
- style
|
||||||
|
- performance
|
||||||
|
disabled-checks:
|
||||||
|
- paramTypeCombine
|
||||||
|
- commentedOutCode
|
||||||
|
- ifElseChain
|
||||||
|
|
||||||
|
# Settings passed to gocritic.
|
||||||
|
# The settings key is the name of a supported gocritic checker.
|
||||||
|
# The list of supported checkers can be find in https://go-critic.github.io/overview.
|
||||||
|
settings:
|
||||||
|
unnamedResult:
|
||||||
|
# whether to check exported functions
|
||||||
|
checkExported: true
|
||||||
|
|
||||||
|
gocyclo:
|
||||||
|
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||||
|
min-complexity: 30
|
||||||
|
|
||||||
|
godot:
|
||||||
|
# comments to be checked: `declarations`, `toplevel`, or `all`
|
||||||
|
scope: declarations
|
||||||
|
|
||||||
|
# check that each sentence starts with a capital letter
|
||||||
|
capital: true
|
||||||
|
|
||||||
|
gofmt:
|
||||||
|
# simplify code: gofmt with `-s` option, true by default
|
||||||
|
simplify: true
|
||||||
|
|
||||||
|
gofumpt:
|
||||||
|
# Choose whether or not to use the extra rules that are disabled
|
||||||
|
# by default
|
||||||
|
extra-rules: false
|
||||||
|
|
||||||
|
golint:
|
||||||
|
# minimal confidence for issues, default is 0.8
|
||||||
|
min-confidence: 0.8
|
||||||
|
|
||||||
|
gosimple:
|
||||||
|
# Select the Go version to target. The default is '1.13'.
|
||||||
|
go: "1.20"
|
||||||
|
# https://staticcheck.io/docs/options#checks
|
||||||
|
checks: ["all"]
|
||||||
|
|
||||||
|
govet:
|
||||||
|
# report about shadowed variables
|
||||||
|
check-shadowing: true
|
||||||
|
enable-all: true
|
||||||
|
|
||||||
|
maligned:
|
||||||
|
# print struct with more effective memory layout or not, false by default
|
||||||
|
suggest-new: true
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
# Correct spellings using locale preferences for US or UK.
|
||||||
|
# Default is to use a neutral variety of English.
|
||||||
|
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||||
|
locale: US
|
||||||
|
|
||||||
|
prealloc:
|
||||||
|
# XXX: we don't recommend using this linter before doing performance profiling.
|
||||||
|
# For most programs usage of prealloc will be a premature optimization.
|
||||||
|
|
||||||
|
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
|
||||||
|
# True by default.
|
||||||
|
simple: true
|
||||||
|
range-loops: true
|
||||||
|
for-loops: false
|
||||||
|
|
||||||
|
nolintlint:
|
||||||
|
# Enable to ensure that nolint directives are all used. Default is true.
|
||||||
|
allow-unused: true
|
||||||
|
# Disable to ensure that nolint directives don't have a leading space. Default is true.
|
||||||
|
allow-leading-space: true
|
||||||
|
# Exclude following linters from requiring an explanation. Default is [].
|
||||||
|
allow-no-explanation: []
|
||||||
|
# Enable to require an explanation of nonzero length after each nolint directive. Default is false.
|
||||||
|
require-explanation: true
|
||||||
|
# Enable to require nolint directives to mention the specific linter being suppressed. Default is false.
|
||||||
|
require-specific: true
|
||||||
|
|
||||||
|
staticcheck:
|
||||||
|
# Select the Go version to target. The default is '1.13'.
|
||||||
|
go: "1.20"
|
||||||
|
# https://staticcheck.io/docs/options#checks
|
||||||
|
checks: ["all"]
|
||||||
|
|
||||||
|
stylecheck:
|
||||||
|
# Select the Go version to target. The default is '1.13'.
|
||||||
|
go: "1.20"
|
||||||
|
# https://staticcheck.io/docs/options#checks
|
||||||
|
checks: ["all"]
|
||||||
|
|
||||||
|
unparam:
|
||||||
|
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
|
||||||
|
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
|
||||||
|
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
|
||||||
|
# with golangci-lint call it on a directory with the changed file.
|
||||||
|
check-exported: false
|
||||||
|
|
||||||
|
unused:
|
||||||
|
# Select the Go version to target. The default is '1.13'.
|
||||||
|
go: "1.20"
|
||||||
|
|
||||||
|
whitespace:
|
||||||
|
multi-if: false # Enforces newlines (or comments) after every multi-line if statement
|
||||||
|
multi-func: false # Enforces newlines (or comments) after every multi-line function signature
|
||||||
|
|
||||||
|
wrapcheck:
|
||||||
|
# An array of strings that specify substrings of signatures to ignore.
|
||||||
|
# If this set, it will override the default set of ignored signatures.
|
||||||
|
# See https://github.com/tomarrell/wrapcheck#configuration for more information.
|
||||||
|
ignoreSigs:
|
||||||
|
- .Errorf(
|
||||||
|
- errors.New(
|
||||||
|
- errors.Unwrap(
|
||||||
|
- .Wrap(
|
||||||
|
- .Wrapf(
|
||||||
|
- .WithMessage(
|
||||||
|
|
||||||
|
linters:
|
||||||
|
disable:
|
||||||
|
# Don't check for newlines before return
|
||||||
|
- nlreturn
|
||||||
|
|
||||||
|
# Don't check line length
|
||||||
|
- lll
|
||||||
|
- funlen
|
||||||
|
- exhaustivestruct
|
||||||
|
|
||||||
|
# Don't check nested ifs
|
||||||
|
- nestif
|
||||||
|
|
||||||
|
# Don't check var length
|
||||||
|
- varnamelen
|
||||||
|
|
||||||
|
# Don't check excessive blank identifiers
|
||||||
|
- dogsled
|
||||||
|
|
||||||
|
# Don't check json struct field tags
|
||||||
|
- tagliatelle
|
||||||
|
|
||||||
|
# Absolutely useless whitespace linting
|
||||||
|
- wsl
|
||||||
|
|
||||||
|
# Cognitive complexity ;)
|
||||||
|
- gocognit
|
||||||
|
|
||||||
|
# Deprecated
|
||||||
|
- golint
|
||||||
|
- interfacer
|
||||||
|
- scopelint
|
||||||
|
|
||||||
|
# Don't care about this (for now)
|
||||||
|
- exhaustruct
|
||||||
|
- wrapcheck
|
||||||
|
- nonamedreturns
|
||||||
|
- gomnd
|
||||||
|
|
||||||
|
enable-all: true
|
||||||
|
fast: false
|
||||||
|
|
||||||
|
issues:
|
||||||
|
# Fix found issues (if it's supported by the linter)
|
||||||
|
fix: true
|
||||||
|
|
||||||
|
|
||||||
|
severity:
|
||||||
|
# Default value is empty string.
|
||||||
|
# Set the default severity for issues. If severity rules are defined and the issues
|
||||||
|
# do not match or no severity is provided to the rule this will be the default
|
||||||
|
# severity applied. Severities should match the supported severity names of the
|
||||||
|
# selected out format.
|
||||||
|
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
|
||||||
|
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity
|
||||||
|
# - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
|
||||||
|
default-severity: error
|
||||||
|
|
||||||
|
# The default value is false.
|
||||||
|
# If set to true severity-rules regular expressions become case sensitive.
|
||||||
|
case-sensitive: false
|
25
Dockerfile
25
Dockerfile
|
@ -1,14 +1,23 @@
|
||||||
FROM golang:1.19
|
FROM golangci/golangci-lint:latest-alpine
|
||||||
|
FROM golang:alpine
|
||||||
|
|
||||||
RUN mkdir -p /app
|
COPY --from=0 /usr/bin/golangci-lint /usr/bin/golangci-lint
|
||||||
|
RUN apk add --no-cache gcc libc-dev
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD . /app
|
COPY src /app/
|
||||||
|
|
||||||
RUN rm -rf go.mod go.sum config.json cache.json; \
|
RUN golangci-lint run
|
||||||
touch config.json; \
|
RUN go test ./src/...
|
||||||
go mod init ecodash; \
|
|
||||||
go mod tidy
|
RUN CGO_ENABLED=1 go build -o app src/main.go
|
||||||
RUN go build -o app .
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=1 /app/app .
|
||||||
|
COPY ./templates /app/templates
|
||||||
|
RUN touch config.json
|
||||||
|
|
||||||
CMD ["./app"]
|
CMD ["./app"]
|
4
go.mod
4
go.mod
|
@ -1,6 +1,6 @@
|
||||||
module ecodash
|
module git.massivebox.net/ecodash/ecodash
|
||||||
|
|
||||||
go 1.17
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gofiber/fiber/v2 v2.37.1
|
github.com/gofiber/fiber/v2 v2.37.1
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
@ -13,8 +15,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type HistoryResult []struct {
|
type HistoryResult []struct {
|
||||||
State string `json:"state"`
|
|
||||||
LastUpdated time.Time `json:"last_updated"`
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
State string `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func dayStart(t time.Time) time.Time {
|
func dayStart(t time.Time) time.Time {
|
||||||
|
@ -22,13 +24,20 @@ func dayStart(t time.Time) time.Time {
|
||||||
return t.Add(-(time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second))
|
return t.Add(-(time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) queryHistory(entityID string, startTime, endTime time.Time) (HistoryResult, error) {
|
var errNon200 = errors.New("got a non-200 status code. Check the correctness of sensors IDs")
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", config.HomeAssistant.BaseURL+
|
func (config *Config) queryHistory(entityID string, startTime, endTime time.Time) (HistoryResult, error) {
|
||||||
"/api/history/period/"+url.QueryEscape(startTime.Format(time.RFC3339))+
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
"?filter_entity_id="+entityID+
|
req, err := http.NewRequestWithContext(
|
||||||
"&end_time="+url.QueryEscape(endTime.Format(time.RFC3339)), /*+
|
ctx,
|
||||||
"&minimal_response",*/nil)
|
http.MethodGet,
|
||||||
|
config.HomeAssistant.BaseURL+
|
||||||
|
"/api/history/period/"+url.QueryEscape(startTime.Format(time.RFC3339))+
|
||||||
|
"?filter_entity_id="+entityID+
|
||||||
|
"&end_time="+url.QueryEscape(endTime.Format(time.RFC3339)),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return HistoryResult{}, err
|
return HistoryResult{}, err
|
||||||
}
|
}
|
||||||
|
@ -42,8 +51,8 @@ func (config Config) queryHistory(entityID string, startTime, endTime time.Time)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return HistoryResult{}, errors.New("got a non-200 status code. Check the correctness of sensors IDs -" + resp.Status)
|
return HistoryResult{}, fmt.Errorf("%w - %s", errNon200, resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
@ -62,30 +71,26 @@ func (config Config) queryHistory(entityID string, startTime, endTime time.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result[0], nil
|
return result[0], nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// t can be any time during the desired day.
|
// t can be any time during the desired day.
|
||||||
func (config Config) getDayHistory(entityID string, t time.Time) (HistoryResult, error) {
|
func (config *Config) getDayHistory(entityID string, t time.Time) (HistoryResult, error) {
|
||||||
|
|
||||||
hours, minutes, seconds := t.Clock()
|
hours, minutes, seconds := t.Clock()
|
||||||
endTime := t.Add(time.Duration(23-hours)*time.Hour + time.Duration(59-minutes)*time.Minute + time.Duration(59-seconds)*time.Second)
|
endTime := t.Add(time.Duration(23-hours)*time.Hour + time.Duration(59-minutes)*time.Minute + time.Duration(59-seconds)*time.Second)
|
||||||
|
|
||||||
return config.queryHistory(entityID, dayStart(t), endTime)
|
return config.queryHistory(entityID, dayStart(t), endTime)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DayData struct {
|
type DayData struct {
|
||||||
DayNumber int
|
|
||||||
DayTime time.Time
|
DayTime time.Time
|
||||||
|
DayNumber int
|
||||||
Measurements int
|
Measurements int
|
||||||
Value float32
|
Value float32
|
||||||
High float32
|
High float32
|
||||||
Low float32
|
Low float32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) historyAverageAndConvertToGreen(entityID string, t time.Time) (DayData, error) {
|
func (config *Config) historyAverageAndConvertToGreen(entityID string, t time.Time) (DayData, error) {
|
||||||
|
|
||||||
history, err := config.getDayHistory(entityID, t)
|
history, err := config.getDayHistory(entityID, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return DayData{}, err
|
return DayData{}, err
|
||||||
|
@ -94,7 +99,6 @@ func (config Config) historyAverageAndConvertToGreen(entityID string, t time.Tim
|
||||||
var day DayData
|
var day DayData
|
||||||
|
|
||||||
for _, historyChange := range history {
|
for _, historyChange := range history {
|
||||||
|
|
||||||
val, err := strconv.ParseFloat(historyChange.State, 32)
|
val, err := strconv.ParseFloat(historyChange.State, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
@ -102,16 +106,13 @@ func (config Config) historyAverageAndConvertToGreen(entityID string, t time.Tim
|
||||||
|
|
||||||
day.Value += float32(val)
|
day.Value += float32(val)
|
||||||
day.Measurements++
|
day.Measurements++
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
day.Value = 100 - (day.Value / float32(day.Measurements))
|
day.Value = 100 - (day.Value / float32(day.Measurements))
|
||||||
return day, nil
|
return day, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) historyBulkAverageAndConvertToGreen(entityID string, startTime, endTime time.Time) ([]DayData, error) {
|
func (config *Config) historyBulkAverageAndConvertToGreen(entityID string, startTime, endTime time.Time) ([]DayData, error) {
|
||||||
|
|
||||||
history, err := config.queryHistory(entityID, startTime, endTime)
|
history, err := config.queryHistory(entityID, startTime, endTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -154,11 +155,9 @@ func (config Config) historyBulkAverageAndConvertToGreen(entityID string, startT
|
||||||
days = fillMissing(days, startTime, endTime)
|
days = fillMissing(days, startTime, endTime)
|
||||||
|
|
||||||
return days, nil
|
return days, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) historyDelta(entityID string, t time.Time) (DayData, error) {
|
func (config *Config) historyDelta(entityID string, t time.Time) (DayData, error) {
|
||||||
|
|
||||||
history, err := config.getDayHistory(entityID, t)
|
history, err := config.getDayHistory(entityID, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return DayData{}, err
|
return DayData{}, err
|
||||||
|
@ -167,7 +166,6 @@ func (config Config) historyDelta(entityID string, t time.Time) (DayData, error)
|
||||||
var day DayData
|
var day DayData
|
||||||
|
|
||||||
for _, historyChange := range history {
|
for _, historyChange := range history {
|
||||||
|
|
||||||
val, err := strconv.ParseFloat(historyChange.State, 32)
|
val, err := strconv.ParseFloat(historyChange.State, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
@ -180,16 +178,13 @@ func (config Config) historyDelta(entityID string, t time.Time) (DayData, error)
|
||||||
if value < day.Low || day.Low == 0 {
|
if value < day.Low || day.Low == 0 {
|
||||||
day.Low = value
|
day.Low = value
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
day.Value = day.High - day.Low
|
day.Value = day.High - day.Low
|
||||||
return day, nil
|
return day, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) historyBulkDelta(entityID string, startTime, endTime time.Time) ([]DayData, error) {
|
func (config *Config) historyBulkDelta(entityID string, startTime, endTime time.Time) ([]DayData, error) {
|
||||||
|
|
||||||
history, err := config.queryHistory(entityID, startTime, endTime)
|
history, err := config.queryHistory(entityID, startTime, endTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -198,33 +193,34 @@ func (config Config) historyBulkDelta(entityID string, startTime, endTime time.T
|
||||||
var days []DayData
|
var days []DayData
|
||||||
|
|
||||||
for _, historyChange := range history {
|
for _, historyChange := range history {
|
||||||
if historyChange.State != "off" {
|
if historyChange.State == "off" {
|
||||||
val, err := strconv.ParseFloat(historyChange.State, 32)
|
continue
|
||||||
if err != nil {
|
}
|
||||||
continue
|
val, err := strconv.ParseFloat(historyChange.State, 32)
|
||||||
}
|
if err != nil {
|
||||||
value := float32(val)
|
continue
|
||||||
var found bool
|
}
|
||||||
dayNo := dayStart(historyChange.LastUpdated.Local()).Day()
|
value := float32(val)
|
||||||
for key, day := range days {
|
var found bool
|
||||||
if dayNo == day.DayNumber {
|
dayNo := dayStart(historyChange.LastUpdated.Local()).Day()
|
||||||
found = true
|
for key, day := range days {
|
||||||
if value > day.High {
|
if dayNo == day.DayNumber {
|
||||||
day.High = value
|
found = true
|
||||||
}
|
if value > day.High {
|
||||||
if value < day.Low || day.Low == 0 {
|
day.High = value
|
||||||
day.Low = value
|
|
||||||
}
|
|
||||||
days[key] = day
|
|
||||||
}
|
}
|
||||||
|
if value < day.Low || day.Low == 0 {
|
||||||
|
day.Low = value
|
||||||
|
}
|
||||||
|
days[key] = day
|
||||||
}
|
}
|
||||||
if !found {
|
}
|
||||||
days = append(days, DayData{
|
if !found {
|
||||||
DayNumber: dayNo,
|
days = append(days, DayData{
|
||||||
DayTime: dayStart(historyChange.LastUpdated.Local()),
|
DayNumber: dayNo,
|
||||||
Value: value,
|
DayTime: dayStart(historyChange.LastUpdated.Local()),
|
||||||
})
|
Value: value,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,11 +232,9 @@ func (config Config) historyBulkDelta(entityID string, startTime, endTime time.T
|
||||||
days = fillMissing(days, startTime, endTime)
|
days = fillMissing(days, startTime, endTime)
|
||||||
|
|
||||||
return days, nil
|
return days, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillMissing(days []DayData, startTime, endTime time.Time) []DayData {
|
func fillMissing(days []DayData, startTime, endTime time.Time) []DayData {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
previousDay time.Time
|
previousDay time.Time
|
||||||
defaultDay time.Time
|
defaultDay time.Time
|
||||||
|
@ -252,9 +246,7 @@ func fillMissing(days []DayData, startTime, endTime time.Time) []DayData {
|
||||||
expectedDaysDiff := int(math.Trunc(endTime.Sub(startTime).Hours()/24) + 1)
|
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 {
|
||||||
|
|
||||||
if day.DayTime.Day() != previousDay.Add(24*time.Hour).Day() {
|
if day.DayTime.Day() != previousDay.Add(24*time.Hour).Day() {
|
||||||
daysDiff := math.Trunc(day.DayTime.Sub(previousDay).Hours() / 24)
|
daysDiff := math.Trunc(day.DayTime.Sub(previousDay).Hours() / 24)
|
||||||
for i := 1; float64(i) < daysDiff; i++ {
|
for i := 1; float64(i) < daysDiff; i++ {
|
||||||
|
@ -266,13 +258,11 @@ func fillMissing(days []DayData, startTime, endTime time.Time) []DayData {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = append(ret, day)
|
ret = append(ret, day)
|
||||||
previousValue = day.Value
|
previousValue = day.Value
|
||||||
previousDay = day.DayTime
|
previousDay = day.DayTime
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// note that here previousDay is the last logged day
|
// note that here previousDay is the last logged day
|
||||||
|
@ -313,5 +303,4 @@ func fillMissing(days []DayData, startTime, endTime time.Time) []DayData {
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
}
|
}
|
|
@ -6,12 +6,14 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -23,9 +25,9 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type HomeAssistant struct {
|
type HomeAssistant struct {
|
||||||
|
InstallationDate time.Time `json:"installation_date"`
|
||||||
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"`
|
||||||
|
@ -42,31 +44,28 @@ type Dashboard struct {
|
||||||
HeaderLinks []Link `json:"header_links"`
|
HeaderLinks []Link `json:"header_links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatURL(url string) (string, error) {
|
var errBadHAFormat = errors.New("HomeAssistant base URL is badly formatted")
|
||||||
|
|
||||||
|
func formatURL(url string) (string, error) {
|
||||||
// the URL we want is: protocol://hostname[:port] without a final /
|
// the URL we want is: protocol://hostname[:port] without a final /
|
||||||
|
|
||||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
url = "http://" + url
|
url = "http://" + url
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(url, "/") {
|
url = strings.TrimSuffix(url, "/")
|
||||||
url = url[0 : len(url)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
test := regexp.MustCompile(`(?m)https?:\/\/[^/]*`).ReplaceAllString(url, "")
|
test := regexp.MustCompile(`(?m)https?:\/\/[^/]*`).ReplaceAllString(url, "")
|
||||||
if test != "" {
|
if test != "" {
|
||||||
return "", errors.New("HomeAssistant base URL is badly formatted")
|
return "", errBadHAFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
return url, nil
|
return url, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() (config Config, err error, isFirstRun bool) {
|
func loadConfig() (config *Config, isFirstRun bool, err error) {
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3", "./database.db")
|
db, err := sql.Open("sqlite3", "./database.db")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err, false
|
return &Config{}, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS "cache" (
|
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS "cache" (
|
||||||
|
@ -76,10 +75,10 @@ func loadConfig() (config Config, err error, isFirstRun bool) {
|
||||||
PRIMARY KEY("time")
|
PRIMARY KEY("time")
|
||||||
);`)
|
);`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err, false
|
return &Config{}, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultConfig = Config{}
|
defaultConfig := &Config{}
|
||||||
defaultConfig.Dashboard.Theme = "default"
|
defaultConfig.Dashboard.Theme = "default"
|
||||||
defaultConfig.Dashboard.Name = "EcoDash"
|
defaultConfig.Dashboard.Name = "EcoDash"
|
||||||
defaultConfig.Dashboard.HeaderLinks = append(defaultConfig.Dashboard.HeaderLinks, Link{
|
defaultConfig.Dashboard.HeaderLinks = append(defaultConfig.Dashboard.HeaderLinks, Link{
|
||||||
|
@ -97,35 +96,41 @@ func loadConfig() (config Config, err error, isFirstRun bool) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if the data file doesn't exist, we consider it a first run
|
// if the data file doesn't exist, we consider it a first run
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return defaultConfig, nil, true
|
return defaultConfig, true, nil
|
||||||
}
|
}
|
||||||
return Config{}, err, false
|
return &Config{}, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the data file is empty, we consider it as a first run
|
// if the data file is empty, we consider it as a first run
|
||||||
if string(data) == "" {
|
if len(data) == 0 {
|
||||||
return defaultConfig, nil, true
|
return defaultConfig, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var conf Config
|
conf := &Config{}
|
||||||
err = json.Unmarshal(data, &conf)
|
err = json.Unmarshal(data, &conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err, false
|
return &Config{}, false, err
|
||||||
}
|
}
|
||||||
conf.db = db
|
conf.db = db
|
||||||
|
|
||||||
return conf, nil, false
|
return conf, false, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// just a little utility function to SHA256 strings (for hashing passwords)
|
// just a little utility function to SHA256 strings (for hashing passwords).
|
||||||
func hash(toHash string) string {
|
func hash(toHash string) string {
|
||||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(toHash)))
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(toHash)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) isAuthorized(c *fiber.Ctx) bool {
|
func (config *Config) isAuthorized(c *fiber.Ctx) bool {
|
||||||
if config.Administrator.PasswordHash == "" {
|
if config.Administrator.PasswordHash == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return c.Cookies("admin_username") == config.Administrator.Username && c.Cookies("admin_password_hash") == config.Administrator.PasswordHash
|
return c.Cookies("admin_username") == config.Administrator.Username && c.Cookies("admin_password_hash") == config.Administrator.PasswordHash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) equals(new *Config) bool {
|
||||||
|
return reflect.DeepEqual(new.HomeAssistant, config.HomeAssistant) &&
|
||||||
|
reflect.DeepEqual(new.Sensors, config.Sensors) &&
|
||||||
|
reflect.DeepEqual(new.Administrator, config.Administrator) &&
|
||||||
|
reflect.DeepEqual(new.Dashboard, config.Dashboard)
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,8 +13,7 @@ type HistoryEntry struct {
|
||||||
}
|
}
|
||||||
type History []HistoryEntry
|
type History []HistoryEntry
|
||||||
|
|
||||||
func (config Config) updateHistory() {
|
func (config *Config) updateHistory() {
|
||||||
|
|
||||||
greenEnergyPercentage, err := config.historyAverageAndConvertToGreen(config.Sensors.FossilPercentage, time.Now())
|
greenEnergyPercentage, err := config.historyAverageAndConvertToGreen(config.Sensors.FossilPercentage, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -24,31 +23,34 @@ func (config Config) updateHistory() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config.db.Exec("INSERT OR REPLACE INTO cache(time,green_energy_percentage,energy_consumption) VALUES (?,?,?);", dayStart(time.Now()).Unix(), greenEnergyPercentage.Value, historyPolledSmartEnergySummation.Value)
|
_, err = config.db.Exec("INSERT OR REPLACE INTO cache(time,green_energy_percentage,energy_consumption) VALUES (?,?,?);", dayStart(time.Now()).Unix(), greenEnergyPercentage.Value, historyPolledSmartEnergySummation.Value)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error inserting into cache", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
cached, err := config.readHistory()
|
cached, err := config.readHistory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(cached) != 8 && time.Now().Sub(config.HomeAssistant.InstallationDate) > 8*time.Hour*24 {
|
if len(cached) != 8 && time.Since(config.HomeAssistant.InstallationDate) > 8*time.Hour*24 {
|
||||||
err := config.refreshCacheFromPast(time.Now().Add(-8 * time.Hour * 24))
|
err := config.refreshCacheFromPast(time.Now().Add(-8 * time.Hour * 24))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error refreshing cache", err.Error())
|
log.Println("Error refreshing cache", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) refreshCacheFromInstall() error {
|
func (config *Config) refreshCacheFromInstall() error {
|
||||||
return config.refreshCacheFromPast(config.HomeAssistant.InstallationDate)
|
return config.refreshCacheFromPast(config.HomeAssistant.InstallationDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) refreshCacheFromPast(pastTime time.Time) error {
|
var errNoInstallDate = errors.New("installation date not set")
|
||||||
|
|
||||||
|
func (config *Config) refreshCacheFromPast(pastTime time.Time) error {
|
||||||
// in order to avoid querying and storing each day's data from 0001-01-01 in future versions
|
// in order to avoid querying and storing each day's data from 0001-01-01 in future versions
|
||||||
if config.HomeAssistant.InstallationDate.IsZero() {
|
if config.HomeAssistant.InstallationDate.IsZero() {
|
||||||
return errors.New("installation date not set")
|
return errNoInstallDate
|
||||||
}
|
}
|
||||||
|
|
||||||
greenEnergyPercentage, err := config.historyBulkAverageAndConvertToGreen(config.Sensors.FossilPercentage, pastTime, time.Now())
|
greenEnergyPercentage, err := config.historyBulkAverageAndConvertToGreen(config.Sensors.FossilPercentage, pastTime, time.Now())
|
||||||
|
@ -61,7 +63,6 @@ func (config Config) refreshCacheFromPast(pastTime time.Time) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, day := range greenEnergyPercentage {
|
for key, day := range greenEnergyPercentage {
|
||||||
|
|
||||||
var action2 string
|
var action2 string
|
||||||
if greenEnergyPercentage[key].Value != 0 && historyPolledSmartEnergySummation[key].Value != 0 {
|
if greenEnergyPercentage[key].Value != 0 && historyPolledSmartEnergySummation[key].Value != 0 {
|
||||||
action2 = "REPLACE"
|
action2 = "REPLACE"
|
||||||
|
@ -77,15 +78,12 @@ func (config Config) refreshCacheFromPast(pastTime time.Time) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) readHistory() (History, error) {
|
func (config *Config) readHistory() (History, error) {
|
||||||
|
|
||||||
start := dayStart(time.Now()).AddDate(0, 0, -8)
|
start := dayStart(time.Now()).AddDate(0, 0, -8)
|
||||||
|
|
||||||
rows, err := config.db.Query("SELECT time, green_energy_percentage, energy_consumption FROM cache WHERE time > ?", start.Unix())
|
rows, err := config.db.Query("SELECT time, green_energy_percentage, energy_consumption FROM cache WHERE time > ?", start.Unix())
|
||||||
|
@ -102,9 +100,11 @@ func (config Config) readHistory() (History, error) {
|
||||||
polledSmartEnergyConsumption float32
|
polledSmartEnergyConsumption float32
|
||||||
)
|
)
|
||||||
err = rows.Scan(&date, &greenEnergyPercentage, &polledSmartEnergyConsumption)
|
err = rows.Scan(&date, &greenEnergyPercentage, &polledSmartEnergyConsumption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
ret = append(ret, HistoryEntry{date, greenEnergyPercentage, polledSmartEnergyConsumption})
|
ret = append(ret, HistoryEntry{date, greenEnergyPercentage, polledSmartEnergyConsumption})
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,13 +4,13 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
|
@ -27,7 +27,7 @@ type Warning struct {
|
||||||
IsSuccess bool
|
IsSuccess bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) getTemplateDefaults() fiber.Map {
|
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,16 +35,14 @@ func (config Config) getTemplateDefaults() fiber.Map {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) templateDefaultsMap() fiber.Map {
|
func (config *Config) templateDefaultsMap() fiber.Map {
|
||||||
return fiber.Map{
|
return fiber.Map{
|
||||||
"Default": config.getTemplateDefaults(),
|
"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" {
|
||||||
|
|
||||||
if config.isAuthorized(c) { // here the user is submitting the form to change configuration
|
if config.isAuthorized(c) { // here the user is submitting the form to change configuration
|
||||||
err := config.saveAdminForm(c)
|
err := config.saveAdminForm(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -68,18 +66,15 @@ func (config Config) adminEndpoint(c *fiber.Ctx) error {
|
||||||
return config.renderAdminPanel(c)
|
return config.renderAdminPanel(c)
|
||||||
}
|
}
|
||||||
return c.Render("login", fiber.Map{"Defaults": config.getTemplateDefaults(), "Failed": true}, "base")
|
return c.Render("login", fiber.Map{"Defaults": config.getTemplateDefaults(), "Failed": true}, "base")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.isAuthorized(c) {
|
if config.isAuthorized(c) {
|
||||||
return config.renderAdminPanel(c)
|
return config.renderAdminPanel(c)
|
||||||
}
|
}
|
||||||
return c.Render("login", config.templateDefaultsMap(), "base")
|
return c.Render("login", config.templateDefaultsMap(), "base")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) renderAdminPanel(c *fiber.Ctx, warning ...Warning) error {
|
func (config *Config) renderAdminPanel(c *fiber.Ctx, warning ...Warning) error {
|
||||||
|
|
||||||
dirs, err := os.ReadDir("./templates")
|
dirs, err := os.ReadDir("./templates")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -100,15 +95,18 @@ func (config Config) renderAdminPanel(c *fiber.Ctx, warning ...Warning) error {
|
||||||
"Themes": dirs,
|
"Themes": dirs,
|
||||||
"Config": config,
|
"Config": config,
|
||||||
}, "base")
|
}, "base")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) saveAdminForm(c *fiber.Ctx) error {
|
var (
|
||||||
|
errNoChanges = errors.New("no changes from previous config")
|
||||||
|
errMissingField = errors.New("required field is missing")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (config *Config) saveAdminForm(c *fiber.Ctx) error {
|
||||||
requiredFields := []string{"base_url", "api_key", "polled_smart_energy_summation", "fossil_percentage", "username", "theme", "name", "installation_date"}
|
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 fmt.Errorf("%w: %s", errMissingField, requiredField)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +115,7 @@ func (config Config) saveAdminForm(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
form := Config{
|
form := &Config{
|
||||||
HomeAssistant: HomeAssistant{ /*BaseURL to be filled later*/ ApiKey: c.FormValue("api_key"), InstallationDate: dayStart(parsedTime)},
|
HomeAssistant: HomeAssistant{ /*BaseURL to be filled later*/ ApiKey: c.FormValue("api_key"), InstallationDate: dayStart(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*/},
|
||||||
|
@ -136,8 +134,8 @@ func (config Config) saveAdminForm(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
form.HomeAssistant.BaseURL = fmtURL
|
form.HomeAssistant.BaseURL = fmtURL
|
||||||
|
|
||||||
if reflect.DeepEqual(form, config) {
|
if form.equals(config) {
|
||||||
return errors.New("No changes from previous config.")
|
return errNoChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
form.db = config.db
|
form.db = config.db
|
||||||
|
@ -151,8 +149,7 @@ func (config Config) saveAdminForm(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile("config.json", js, 0666)
|
return os.WriteFile("config.json", js, 0o666)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func averageExcludingCurrentDay(data []float32) float32 {
|
func averageExcludingCurrentDay(data []float32) float32 {
|
||||||
|
@ -164,12 +161,11 @@ func averageExcludingCurrentDay(data []float32) float32 {
|
||||||
for _, num := range data {
|
for _, num := range data {
|
||||||
sum += num
|
sum += num
|
||||||
}
|
}
|
||||||
var avg = sum / float32(len(data))
|
avg := sum / float32(len(data))
|
||||||
return float32(math.Floor(float64(avg)*100)) / 100
|
return float32(math.Floor(float64(avg)*100)) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config Config) renderIndex(c *fiber.Ctx) error {
|
func (config *Config) renderIndex(c *fiber.Ctx) error {
|
||||||
|
|
||||||
if config.HomeAssistant.InstallationDate.IsZero() {
|
if config.HomeAssistant.InstallationDate.IsZero() {
|
||||||
return c.Render("config-error", fiber.Map{
|
return c.Render("config-error", fiber.Map{
|
||||||
"Defaults": config.getTemplateDefaults(),
|
"Defaults": config.getTemplateDefaults(),
|
||||||
|
@ -182,12 +178,10 @@ func (config Config) renderIndex(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
labels := make([]string, 0, len(data))
|
||||||
labels []string
|
greenEnergyConsumptionAbsolute := make([]float32, 0, len(data))
|
||||||
greenEnergyConsumptionAbsolute []float32
|
greenEnergyPercents := make([]float32, 0, len(data))
|
||||||
greenEnergyPercents []float32
|
energyConsumptions := make([]float32, 0, len(data))
|
||||||
energyConsumptions []float32
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, datum := range data {
|
for _, datum := range data {
|
||||||
labels = append(labels, time.Unix(datum.Date, 0).Format("02/01"))
|
labels = append(labels, time.Unix(datum.Date, 0).Format("02/01"))
|
||||||
|
@ -206,21 +200,18 @@ func (config Config) renderIndex(c *fiber.Ctx) error {
|
||||||
"GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents),
|
"GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents),
|
||||||
"PerDayUsage": perDayUsage,
|
"PerDayUsage": perDayUsage,
|
||||||
}, "base")
|
}, "base")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func templateDivide(num1, num2 float32) template.HTML {
|
func templateDivide(num1, num2 float32) template.HTML {
|
||||||
|
|
||||||
division := float64(num1 / num2)
|
division := float64(num1 / num2)
|
||||||
|
|
||||||
powerOfTen := int(math.Floor(math.Log10(division)))
|
powerOfTen := int(math.Floor(math.Log10(division)))
|
||||||
if powerOfTen >= -2 && powerOfTen <= 2 {
|
if powerOfTen >= -2 && powerOfTen <= 2 {
|
||||||
return template.HTML(fmt.Sprintf("%s", strconv.FormatFloat(math.Round(division*100)/100, 'f', -1, 64)))
|
return template.HTML(strconv.FormatFloat(math.Round(division*100)/100, 'f', -1, 64))
|
||||||
}
|
}
|
||||||
|
|
||||||
preComma := division / math.Pow10(powerOfTen)
|
preComma := division / math.Pow10(powerOfTen)
|
||||||
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 {
|
func templateHTMLDateFormat(date time.Time) template.HTML {
|
|
@ -1,17 +1,18 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/template/html"
|
"github.com/gofiber/template/html"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
config, isFirstRun, err := loadConfig()
|
||||||
config, err, isFirstRun := loadConfig()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +60,7 @@ func main() {
|
||||||
}()
|
}()
|
||||||
return c.Render("restart", config.templateDefaultsMap(), "base")
|
return c.Render("restart", config.templateDefaultsMap(), "base")
|
||||||
}
|
}
|
||||||
return c.Redirect("./", 307)
|
return c.Redirect("./", http.StatusTemporaryRedirect)
|
||||||
})
|
})
|
||||||
|
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
|
@ -67,5 +68,4 @@ func main() {
|
||||||
port = "80"
|
port = "80"
|
||||||
}
|
}
|
||||||
log.Fatal(app.Listen(":" + port))
|
log.Fatal(app.Listen(":" + port))
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue