package main import ( _ "embed" "encoding/json" "fmt" "log" "net" "net/url" "regexp" "strconv" "time" ) const ( MQTT_BROKER_REGEX = `^tcp://127\.0\.0\.1:\d+$` MQTT_CLIENT_ID_REGEX = `^[0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]{1,23}$` MQTT_TOPIC_PREFIX_REGEX = `^[a-zA-Z0-9]{1,20}$` ) var ( mqttBrokerRegexp *regexp.Regexp mqttClientIDRegexp *regexp.Regexp mqttTopicPrefixRegexp *regexp.Regexp ) func init() { mqttBrokerRegexp = regexp.MustCompile(MQTT_BROKER_REGEX) mqttClientIDRegexp = regexp.MustCompile(MQTT_CLIENT_ID_REGEX) mqttTopicPrefixRegexp = regexp.MustCompile(MQTT_TOPIC_PREFIX_REGEX) } //go:embed configs/default.json var defaultConfig []byte type MQTTConfig struct { Broker string `json:"broker"` ClientID string `json:"client-id"` TopicPrefix string `json:"topic-prefix"` } type HomematicConfig struct { CCU string `json:"ccu"` PollingPeriod string `json:"polling-period"` } type StartupConfig struct { MQTT MQTTConfig `json:"mqtt"` Homematic HomematicConfig `json:"homematic"` } func (sc StartupConfig) Validate() error { if !mqttBrokerRegexp.MatchString(sc.MQTT.Broker) { return fmt.Errorf( "mqtt/broker configuration '%s' does not match regular expression '%s'.", sc.MQTT.Broker, MQTT_BROKER_REGEX, ) } if !mqttClientIDRegexp.MatchString(sc.MQTT.ClientID) { return fmt.Errorf( "mqtt/client-id configuration '%s' does not match regular expression '%s'.", sc.MQTT.ClientID, MQTT_CLIENT_ID_REGEX, ) } if !mqttTopicPrefixRegexp.MatchString(sc.MQTT.TopicPrefix) { return fmt.Errorf( "mqtt/topic-prefix configuration '%s' does not match regular expression '%s'.", sc.MQTT.TopicPrefix, MQTT_TOPIC_PREFIX_REGEX, ) } cu, err := url.Parse(sc.Homematic.CCU) if err != nil { return fmt.Errorf("homematic/ccu configuration '%s' cannot be parsed as URL: %v", sc.Homematic.CCU, err) } if cu.Scheme != `http` { return fmt.Errorf("homematic/ccu configuration '%s' must use scheme 'http'.", sc.Homematic.CCU) } host, portstring, err := net.SplitHostPort(cu.Host) if err != nil { return fmt.Errorf("homematic/ccu configuration '%s' can't be split into hostname and port: %v", sc.Homematic.CCU, err) } ip := net.ParseIP(host) if ip == nil { return fmt.Errorf("homematic/ccu configuration '%s' must use a plain IP address as host.", sc.Homematic.CCU) } _, err = strconv.Atoi(portstring) if err != nil { return fmt.Errorf("homematic/ccu configuration '%s' must use a numeric port: %v", sc.Homematic.CCU, err) } _, err = time.ParseDuration(sc.Homematic.PollingPeriod) if err != nil { return fmt.Errorf("homematic/polling-period configuration '%s' could not be parsed to duration: %v", sc.Homematic.PollingPeriod, err) } return nil } func (sc *StartupConfig) FromJSON(data []byte) error { err := json.Unmarshal(data, sc) if err != nil { return fmt.Errorf("Failed to unmarshal configuration: %w", err) } err = sc.Validate() if err != nil { return fmt.Errorf("Could not validate configuration: %w", err) } return nil } func GetStartupConfig() StartupConfig { config := StartupConfig{} err := config.FromJSON(defaultConfig) if err != nil { log.Fatalf("Could not parse default config: %v", err) } return config }