From 9bd3f35cc77171437a6eb48ea87f3e226664fa6e Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 1 May 2023 18:33:15 +0200 Subject: [PATCH] Refactor --- .golangci.yml | 259 +++++++++++++++++++++++++++++++++ Dockerfile | 25 +++- go.mod | 4 +- api.go => src/api.go | 111 +++++++------- config.go => src/config.go | 53 ++++--- database.go => src/database.go | 32 ++-- http.go => src/http.go | 55 +++---- main.go => src/main.go | 14 +- 8 files changed, 403 insertions(+), 150 deletions(-) create mode 100644 .golangci.yml rename api.go => src/api.go (74%) rename config.go => src/config.go (75%) rename database.go => src/database.go (75%) rename http.go => src/http.go (84%) rename main.go => src/main.go (92%) diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8664e09 --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index 3a5f7dd..d7be2c4 100644 --- a/Dockerfile +++ b/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 -ADD . /app +COPY src /app/ -RUN rm -rf go.mod go.sum config.json cache.json; \ - touch config.json; \ - go mod init ecodash; \ - go mod tidy -RUN go build -o app . +RUN golangci-lint run +RUN go test ./src/... + +RUN CGO_ENABLED=1 go build -o app src/main.go + +FROM alpine:latest + +WORKDIR /app + +COPY --from=1 /app/app . +COPY ./templates /app/templates +RUN touch config.json CMD ["./app"] \ No newline at end of file diff --git a/go.mod b/go.mod index e54d368..ade3cef 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module ecodash +module git.massivebox.net/ecodash/ecodash -go 1.17 +go 1.20 require ( github.com/gofiber/fiber/v2 v2.37.1 diff --git a/api.go b/src/api.go similarity index 74% rename from api.go rename to src/api.go index 60e781a..81b79a1 100644 --- a/api.go +++ b/src/api.go @@ -1,8 +1,10 @@ package main import ( + "context" "encoding/json" "errors" + "fmt" "io" "log" "math" @@ -13,8 +15,8 @@ import ( ) type HistoryResult []struct { - State string `json:"state"` LastUpdated time.Time `json:"last_updated"` + State string `json:"state"` } 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)) } -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+ - "/api/history/period/"+url.QueryEscape(startTime.Format(time.RFC3339))+ - "?filter_entity_id="+entityID+ - "&end_time="+url.QueryEscape(endTime.Format(time.RFC3339)), /*+ - "&minimal_response",*/nil) +func (config *Config) queryHistory(entityID string, startTime, endTime time.Time) (HistoryResult, error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + req, err := http.NewRequestWithContext( + ctx, + 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 { return HistoryResult{}, err } @@ -42,8 +51,8 @@ func (config Config) queryHistory(entityID string, startTime, endTime time.Time) } defer resp.Body.Close() - if resp.StatusCode != 200 { - return HistoryResult{}, errors.New("got a non-200 status code. Check the correctness of sensors IDs -" + resp.Status) + if resp.StatusCode != http.StatusOK { + return HistoryResult{}, fmt.Errorf("%w - %s", errNon200, resp.Status) } body, err := io.ReadAll(resp.Body) @@ -62,30 +71,26 @@ func (config Config) queryHistory(entityID string, startTime, endTime time.Time) } return result[0], nil - } // 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() 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) - } type DayData struct { - DayNumber int DayTime time.Time + DayNumber int Measurements int Value float32 High 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) if err != nil { return DayData{}, err @@ -94,7 +99,6 @@ func (config Config) historyAverageAndConvertToGreen(entityID string, t time.Tim var day DayData for _, historyChange := range history { - val, err := strconv.ParseFloat(historyChange.State, 32) if err != nil { continue @@ -102,16 +106,13 @@ func (config Config) historyAverageAndConvertToGreen(entityID string, t time.Tim day.Value += float32(val) day.Measurements++ - } day.Value = 100 - (day.Value / float32(day.Measurements)) 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) if err != nil { return nil, err @@ -154,11 +155,9 @@ func (config Config) historyBulkAverageAndConvertToGreen(entityID string, startT days = fillMissing(days, startTime, endTime) 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) if err != nil { return DayData{}, err @@ -167,7 +166,6 @@ func (config Config) historyDelta(entityID string, t time.Time) (DayData, error) var day DayData for _, historyChange := range history { - val, err := strconv.ParseFloat(historyChange.State, 32) if err != nil { continue @@ -180,16 +178,13 @@ func (config Config) historyDelta(entityID string, t time.Time) (DayData, error) if value < day.Low || day.Low == 0 { day.Low = value } - } day.Value = day.High - day.Low 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) if err != nil { return nil, err @@ -198,33 +193,34 @@ func (config Config) historyBulkDelta(entityID string, startTime, endTime time.T var days []DayData for _, historyChange := range history { - if historyChange.State != "off" { - val, err := strconv.ParseFloat(historyChange.State, 32) - if err != nil { - continue - } - value := float32(val) - var found bool - dayNo := dayStart(historyChange.LastUpdated.Local()).Day() - for key, day := range days { - if dayNo == day.DayNumber { - found = true - if value > day.High { - day.High = value - } - if value < day.Low || day.Low == 0 { - day.Low = value - } - days[key] = day + if historyChange.State == "off" { + continue + } + val, err := strconv.ParseFloat(historyChange.State, 32) + if err != nil { + continue + } + value := float32(val) + var found bool + dayNo := dayStart(historyChange.LastUpdated.Local()).Day() + for key, day := range days { + if dayNo == day.DayNumber { + found = true + if value > day.High { + day.High = value } + if value < day.Low || day.Low == 0 { + day.Low = value + } + days[key] = day } - if !found { - days = append(days, DayData{ - DayNumber: dayNo, - DayTime: dayStart(historyChange.LastUpdated.Local()), - Value: value, - }) - } + } + if !found { + days = append(days, DayData{ + DayNumber: dayNo, + 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) return days, nil - } func fillMissing(days []DayData, startTime, endTime time.Time) []DayData { - var ( previousDay 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) for key, day := range days { - if key != 0 { - if day.DayTime.Day() != previousDay.Add(24*time.Hour).Day() { daysDiff := math.Trunc(day.DayTime.Sub(previousDay).Hours() / 24) for i := 1; float64(i) < daysDiff; i++ { @@ -266,13 +258,11 @@ func fillMissing(days []DayData, startTime, endTime time.Time) []DayData { }) } } - } ret = append(ret, day) previousValue = day.Value previousDay = day.DayTime - } // note that here previousDay is the last logged day @@ -313,5 +303,4 @@ func fillMissing(days []DayData, startTime, endTime time.Time) []DayData { } return ret - } diff --git a/config.go b/src/config.go similarity index 75% rename from config.go rename to src/config.go index d63fcd4..6b7011f 100644 --- a/config.go +++ b/src/config.go @@ -6,12 +6,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gofiber/fiber/v2" - _ "github.com/mattn/go-sqlite3" "os" + "reflect" "regexp" "strings" "time" + + "github.com/gofiber/fiber/v2" + _ "github.com/mattn/go-sqlite3" ) type Config struct { @@ -23,9 +25,9 @@ type Config struct { } type HomeAssistant struct { + InstallationDate time.Time `json:"installation_date"` 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"` @@ -42,31 +44,28 @@ type Dashboard struct { 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 / if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { url = "http://" + url } - if strings.HasSuffix(url, "/") { - url = url[0 : len(url)-1] - } + url = strings.TrimSuffix(url, "/") test := regexp.MustCompile(`(?m)https?:\/\/[^/]*`).ReplaceAllString(url, "") if test != "" { - return "", errors.New("HomeAssistant base URL is badly formatted") + return "", errBadHAFormat } 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") if err != nil { - return Config{}, err, false + return &Config{}, false, err } _, err = db.Exec(`CREATE TABLE IF NOT EXISTS "cache" ( @@ -76,10 +75,10 @@ func loadConfig() (config Config, err error, isFirstRun bool) { PRIMARY KEY("time") );`) if err != nil { - return Config{}, err, false + return &Config{}, false, err } - var defaultConfig = Config{} + defaultConfig := &Config{} defaultConfig.Dashboard.Theme = "default" defaultConfig.Dashboard.Name = "EcoDash" defaultConfig.Dashboard.HeaderLinks = append(defaultConfig.Dashboard.HeaderLinks, Link{ @@ -97,35 +96,41 @@ func loadConfig() (config Config, err error, isFirstRun bool) { if err != nil { // if the data file doesn't exist, we consider it a first run 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 string(data) == "" { - return defaultConfig, nil, true + if len(data) == 0 { + return defaultConfig, true, nil } - var conf Config + conf := &Config{} err = json.Unmarshal(data, &conf) if err != nil { - return Config{}, err, false + return &Config{}, false, err } 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 { 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 == "" { return true } 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) +} diff --git a/database.go b/src/database.go similarity index 75% rename from database.go rename to src/database.go index 50f8616..d489bc0 100644 --- a/database.go +++ b/src/database.go @@ -2,7 +2,7 @@ package main import ( "errors" - "fmt" + "log" "time" ) @@ -13,8 +13,7 @@ type HistoryEntry struct { } type History []HistoryEntry -func (config Config) updateHistory() { - +func (config *Config) updateHistory() { greenEnergyPercentage, err := config.historyAverageAndConvertToGreen(config.Sensors.FossilPercentage, time.Now()) if err != nil { return @@ -24,31 +23,34 @@ func (config Config) updateHistory() { 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() if err != nil { 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)) if err != nil { - fmt.Println("Error refreshing cache", err.Error()) + log.Println("Error refreshing cache", err.Error()) return } } - } -func (config Config) refreshCacheFromInstall() error { +func (config *Config) refreshCacheFromInstall() error { 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 if config.HomeAssistant.InstallationDate.IsZero() { - return errors.New("installation date not set") + return errNoInstallDate } 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 { - var action2 string if greenEnergyPercentage[key].Value != 0 && historyPolledSmartEnergySummation[key].Value != 0 { action2 = "REPLACE" @@ -77,15 +78,12 @@ func (config Config) refreshCacheFromPast(pastTime time.Time) error { if err != nil { return err } - } return nil - } -func (config Config) readHistory() (History, error) { - +func (config *Config) readHistory() (History, error) { 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()) @@ -102,9 +100,11 @@ func (config Config) readHistory() (History, error) { polledSmartEnergyConsumption float32 ) err = rows.Scan(&date, &greenEnergyPercentage, &polledSmartEnergyConsumption) + if err != nil { + return nil, err + } ret = append(ret, HistoryEntry{date, greenEnergyPercentage, polledSmartEnergyConsumption}) } return ret, nil - } diff --git a/http.go b/src/http.go similarity index 84% rename from http.go rename to src/http.go index cb50ef0..4b50505 100644 --- a/http.go +++ b/src/http.go @@ -4,13 +4,13 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gofiber/fiber/v2" "html/template" "math" "os" - "reflect" "strconv" "time" + + "github.com/gofiber/fiber/v2" ) type Link struct { @@ -27,7 +27,7 @@ type Warning struct { IsSuccess bool } -func (config Config) getTemplateDefaults() fiber.Map { +func (config *Config) getTemplateDefaults() fiber.Map { return fiber.Map{ "DashboardName": config.Dashboard.Name, "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{ "Default": config.getTemplateDefaults(), } } -func (config Config) adminEndpoint(c *fiber.Ctx) error { - +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 { @@ -68,18 +66,15 @@ func (config Config) adminEndpoint(c *fiber.Ctx) error { 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", 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") if err != nil { return err @@ -100,15 +95,18 @@ func (config Config) renderAdminPanel(c *fiber.Ctx, warning ...Warning) error { "Themes": dirs, "Config": config, }, "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"} for _, requiredField := range requiredFields { 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 } - form := Config{ + form := &Config{ 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")}, 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 - if reflect.DeepEqual(form, config) { - return errors.New("No changes from previous config.") + if form.equals(config) { + return errNoChanges } form.db = config.db @@ -151,8 +149,7 @@ func (config Config) saveAdminForm(c *fiber.Ctx) error { return err } - return os.WriteFile("config.json", js, 0666) - + return os.WriteFile("config.json", js, 0o666) } func averageExcludingCurrentDay(data []float32) float32 { @@ -164,12 +161,11 @@ func averageExcludingCurrentDay(data []float32) float32 { for _, num := range data { sum += num } - var avg = sum / float32(len(data)) + avg := sum / float32(len(data)) 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() { return c.Render("config-error", fiber.Map{ "Defaults": config.getTemplateDefaults(), @@ -182,12 +178,10 @@ func (config Config) renderIndex(c *fiber.Ctx) error { return err } - var ( - labels []string - greenEnergyConsumptionAbsolute []float32 - greenEnergyPercents []float32 - energyConsumptions []float32 - ) + labels := make([]string, 0, len(data)) + greenEnergyConsumptionAbsolute := make([]float32, 0, len(data)) + greenEnergyPercents := make([]float32, 0, len(data)) + energyConsumptions := make([]float32, 0, len(data)) for _, datum := range data { 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), "PerDayUsage": perDayUsage, }, "base") - } func templateDivide(num1, num2 float32) template.HTML { - division := float64(num1 / num2) powerOfTen := int(math.Floor(math.Log10(division))) 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) return template.HTML(fmt.Sprintf("%s * 10%d", strconv.FormatFloat(math.Round(preComma*100)/100, 'f', -1, 64), powerOfTen)) - } func templateHTMLDateFormat(date time.Time) template.HTML { diff --git a/main.go b/src/main.go similarity index 92% rename from main.go rename to src/main.go index e1f5cc6..838c21c 100644 --- a/main.go +++ b/src/main.go @@ -1,17 +1,18 @@ package main import ( + "log" + "net/http" + "os" + "time" + "github.com/gofiber/fiber/v2" "github.com/gofiber/template/html" "github.com/robfig/cron/v3" - "log" - "os" - "time" ) func main() { - - config, err, isFirstRun := loadConfig() + config, isFirstRun, err := loadConfig() if err != nil { log.Fatal(err) } @@ -59,7 +60,7 @@ func main() { }() return c.Render("restart", config.templateDefaultsMap(), "base") } - return c.Redirect("./", 307) + return c.Redirect("./", http.StatusTemporaryRedirect) }) port := os.Getenv("PORT") @@ -67,5 +68,4 @@ func main() { port = "80" } log.Fatal(app.Listen(":" + port)) - }