230 lines
5.6 KiB
Go
230 lines
5.6 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const envPrefix = "MQTT_SCRUBBER_"
|
|
|
|
type Config struct {
|
|
MQTT MQTTConfig `json:"mqtt"`
|
|
Influx InfluxConfig `json:"influx"`
|
|
App AppConfig `json:"app"`
|
|
}
|
|
|
|
type MQTTConfig struct {
|
|
Broker string `json:"broker"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
ClientID string `json:"client_id"`
|
|
Topics []string `json:"topics"`
|
|
QoS byte `json:"qos"`
|
|
}
|
|
|
|
type InfluxConfig struct {
|
|
URL string `json:"url"`
|
|
Database string `json:"database"`
|
|
Token string `json:"token"`
|
|
Precision string `json:"precision"`
|
|
}
|
|
|
|
type AppConfig struct {
|
|
BatchSize int `json:"batch_size"`
|
|
BufferSize int `json:"buffer_size"`
|
|
FlushInterval DurationValue `json:"flush_interval"`
|
|
FlushTimeout DurationValue `json:"flush_timeout"`
|
|
LogLevel string `json:"log_level"`
|
|
HealthAddress string `json:"health_address"`
|
|
}
|
|
|
|
type DurationValue struct {
|
|
time.Duration
|
|
}
|
|
|
|
func (value *DurationValue) UnmarshalJSON(data []byte) error {
|
|
var raw string
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return fmt.Errorf("duration must be a string: %w", err)
|
|
}
|
|
|
|
parsed, err := time.ParseDuration(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid duration %q: %w", raw, err)
|
|
}
|
|
|
|
value.Duration = parsed
|
|
return nil
|
|
}
|
|
|
|
func Load(path string) (Config, error) {
|
|
cfg := defaultConfig()
|
|
|
|
if path != "" {
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("read config file: %w", err)
|
|
}
|
|
|
|
if err := json.Unmarshal(contents, &cfg); err != nil {
|
|
return Config{}, fmt.Errorf("parse config file: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := applyEnvOverrides(&cfg); err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func (cfg Config) Validate() error {
|
|
if cfg.MQTT.Broker == "" {
|
|
return errors.New("mqtt broker is required")
|
|
}
|
|
if cfg.MQTT.ClientID == "" {
|
|
return errors.New("mqtt client_id is required")
|
|
}
|
|
if len(cfg.MQTT.Topics) == 0 {
|
|
return errors.New("at least one mqtt topic is required")
|
|
}
|
|
if cfg.Influx.URL == "" {
|
|
return errors.New("influx url is required")
|
|
}
|
|
if cfg.Influx.Database == "" {
|
|
return errors.New("influx database is required")
|
|
}
|
|
if cfg.Influx.Precision == "" {
|
|
return errors.New("influx precision is required")
|
|
}
|
|
if cfg.App.BatchSize <= 0 {
|
|
return errors.New("app batch_size must be greater than zero")
|
|
}
|
|
if cfg.App.BufferSize <= 0 {
|
|
return errors.New("app buffer_size must be greater than zero")
|
|
}
|
|
if cfg.App.FlushInterval.Duration <= 0 {
|
|
return errors.New("app flush_interval must be greater than zero")
|
|
}
|
|
if cfg.App.FlushTimeout.Duration <= 0 {
|
|
return errors.New("app flush_timeout must be greater than zero")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func defaultConfig() Config {
|
|
return Config{
|
|
MQTT: MQTTConfig{
|
|
Broker: "tcp://127.0.0.1:1883",
|
|
ClientID: "mqqt-scrubber",
|
|
Topics: []string{
|
|
"tele/+/SENSOR",
|
|
"tele/+/STATE",
|
|
},
|
|
QoS: 0,
|
|
},
|
|
Influx: InfluxConfig{
|
|
URL: "http://127.0.0.1:8181",
|
|
Database: "home",
|
|
Precision: "ns",
|
|
},
|
|
App: AppConfig{
|
|
BatchSize: 200,
|
|
BufferSize: 1000,
|
|
FlushInterval: DurationValue{Duration: 10 * time.Second},
|
|
FlushTimeout: DurationValue{Duration: 10 * time.Second},
|
|
LogLevel: "info",
|
|
HealthAddress: ":8080",
|
|
},
|
|
}
|
|
}
|
|
|
|
func applyEnvOverrides(cfg *Config) error {
|
|
setString(&cfg.MQTT.Broker, envPrefix+"MQTT_BROKER")
|
|
setString(&cfg.MQTT.Username, envPrefix+"MQTT_USERNAME")
|
|
setString(&cfg.MQTT.Password, envPrefix+"MQTT_PASSWORD")
|
|
setString(&cfg.MQTT.ClientID, envPrefix+"MQTT_CLIENT_ID")
|
|
setString(&cfg.Influx.URL, envPrefix+"INFLUX_URL")
|
|
setString(&cfg.Influx.Database, envPrefix+"INFLUX_DATABASE")
|
|
setString(&cfg.Influx.Token, envPrefix+"INFLUX_TOKEN")
|
|
setString(&cfg.Influx.Precision, envPrefix+"INFLUX_PRECISION")
|
|
setString(&cfg.App.LogLevel, envPrefix+"APP_LOG_LEVEL")
|
|
setString(&cfg.App.HealthAddress, envPrefix+"APP_HEALTH_ADDRESS")
|
|
|
|
if raw, ok := os.LookupEnv(envPrefix + "MQTT_TOPICS"); ok {
|
|
cfg.MQTT.Topics = splitAndTrim(raw)
|
|
}
|
|
|
|
if raw, ok := os.LookupEnv(envPrefix + "MQTT_QOS"); ok {
|
|
parsed, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("parse %sMQTT_QOS: %w", envPrefix, err)
|
|
}
|
|
cfg.MQTT.QoS = byte(parsed)
|
|
}
|
|
|
|
if raw, ok := os.LookupEnv(envPrefix + "APP_BATCH_SIZE"); ok {
|
|
parsed, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("parse %sAPP_BATCH_SIZE: %w", envPrefix, err)
|
|
}
|
|
cfg.App.BatchSize = parsed
|
|
}
|
|
|
|
if raw, ok := os.LookupEnv(envPrefix + "APP_BUFFER_SIZE"); ok {
|
|
parsed, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("parse %sAPP_BUFFER_SIZE: %w", envPrefix, err)
|
|
}
|
|
cfg.App.BufferSize = parsed
|
|
}
|
|
|
|
if raw, ok := os.LookupEnv(envPrefix + "APP_FLUSH_INTERVAL"); ok {
|
|
parsed, err := time.ParseDuration(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("parse %sAPP_FLUSH_INTERVAL: %w", envPrefix, err)
|
|
}
|
|
cfg.App.FlushInterval = DurationValue{Duration: parsed}
|
|
}
|
|
|
|
if raw, ok := os.LookupEnv(envPrefix + "APP_FLUSH_TIMEOUT"); ok {
|
|
parsed, err := time.ParseDuration(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("parse %sAPP_FLUSH_TIMEOUT: %w", envPrefix, err)
|
|
}
|
|
cfg.App.FlushTimeout = DurationValue{Duration: parsed}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setString(target *string, key string) {
|
|
if value, ok := os.LookupEnv(key); ok {
|
|
*target = value
|
|
}
|
|
}
|
|
|
|
func splitAndTrim(value string) []string {
|
|
parts := strings.Split(value, ",")
|
|
result := make([]string, 0, len(parts))
|
|
|
|
for _, part := range parts {
|
|
trimmed := strings.TrimSpace(part)
|
|
if trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|