// Package main provides the generate command for generating Home Assistant entity constants package main import ( "flag" "fmt" "os" "path/filepath" "strings" "text/template" ha "github.com/Xevion/go-ha" "github.com/Xevion/go-ha/types" "gopkg.in/yaml.v3" ) type Config struct { URL string `yaml:"url"` HAAuthToken string `yaml:"ha_auth_token"` HomeZoneEntityId string `yaml:"home_zone_entity_id,omitempty"` // Now optional IncludeDomains []string `yaml:"include_domains,omitempty"` // Optional list of domains to include ExcludeDomains []string `yaml:"exclude_domains,omitempty"` // Optional list of domains to exclude } type Domain struct { Name string Entities []Entity } type Entity struct { FieldName string EntityID string } func toFieldName(entityID string) string { parts := strings.Split(entityID, ".") if len(parts) != 2 { return "" } return toCamelCase(parts[1]) } func toCamelCase(s string) string { if s == "" { return "" } parts := strings.Split(s, "_") var result strings.Builder // If first character is numeric firstChar := parts[0][0] if firstChar >= '0' && firstChar <= '9' { result.WriteString("_") } for _, part := range parts { if part == "" { continue } result.WriteString(strings.ToUpper(string(part[0]))) if len(part) > 1 { result.WriteString(part[1:]) } } return result.String() } // validateHomeZone verifies that the home zone entity exists and is valid func validateHomeZone(state ha.State, entityID string) error { entity, err := state.Get(entityID) if err != nil { return fmt.Errorf("home zone entity '%s' not found: %w", entityID, err) } // Ensure it's a zone entity if !strings.HasPrefix(entityID, "zone.") { return fmt.Errorf("entity '%s' is not a zone entity (must start with zone.)", entityID) } // Verify it has latitude and longitude if entity.Attributes == nil { return fmt.Errorf("home zone entity '%s' has no attributes", entityID) } if entity.Attributes["latitude"] == nil { return fmt.Errorf("home zone entity '%s' missing latitude attribute", entityID) } if entity.Attributes["longitude"] == nil { return fmt.Errorf("home zone entity '%s' missing longitude attribute", entityID) } return nil } // generate creates the entities.go file with constants for all Home Assistant entities func generate(config Config) error { if config.HomeZoneEntityId == "" { config.HomeZoneEntityId = "zone.home" } app, err := ha.NewApp(types.NewAppRequest{ URL: config.URL, HAAuthToken: config.HAAuthToken, HomeZoneEntityId: config.HomeZoneEntityId, }) if err != nil { return fmt.Errorf("failed to create app: %w", err) } defer app.Cleanup() // Validate that the home zone exists before proceeding if err := validateHomeZone(app.GetState(), config.HomeZoneEntityId); err != nil { return fmt.Errorf("invalid home zone: %w", err) } entities, err := app.GetState().ListEntities() if err != nil { return fmt.Errorf("failed to list entities: %w", err) } // Group entities by domain domainMap := make(map[string]*Domain) for _, entity := range entities { if entity.State == "unavailable" { continue } parts := strings.Split(entity.EntityID, ".") if len(parts) != 2 { continue } domain := parts[0] // Filter domains based on include/exclude lists if len(config.IncludeDomains) > 0 { // If include list is specified, only process listed domains found := false for _, includeDomain := range config.IncludeDomains { if domain == includeDomain { found = true break } } if !found { continue } } else { // If only exclude list is specified, skip excluded domains excluded := false for _, excludeDomain := range config.ExcludeDomains { if domain == excludeDomain { println("skipping excluded domain:", domain) excluded = true break } } if excluded { continue } } if _, exists := domainMap[domain]; !exists { domainMap[domain] = &Domain{ Name: toCamelCase(domain), } } domainMap[domain].Entities = append(domainMap[domain].Entities, Entity{ FieldName: toFieldName(entity.EntityID), EntityID: entity.EntityID, }) } // Map to slice for template domains := make([]Domain, 0) for _, domain := range domainMap { domains = append(domains, *domain) } // Create entities directory if it doesn't exist err = os.MkdirAll("entities", 0755) if err != nil { return fmt.Errorf("failed to create entities directory: %w", err) } // Create the file tmpl := template.Must(template.New("entities").Parse(`// Code generated by go generate; DO NOT EDIT. package entities {{ range .Domains }} type {{ .Name }}Domain struct { {{- range .Entities }} {{ .FieldName }} string {{- end }} } var {{ .Name }} = {{ .Name }}Domain{ {{- range .Entities }} {{ .FieldName }}: "{{ .EntityID }}", {{- end }} } {{ end }} `)) f, err := os.Create(filepath.Join("entities", "entities.go")) if err != nil { return fmt.Errorf("failed to create entities.go: %w", err) } defer f.Close() err = tmpl.Execute(f, struct{ Domains []Domain }{domains}) if err != nil { return fmt.Errorf("failed to execute template: %w", err) } return nil } func main() { println("Generating entities.go...") configFile := flag.String("config", "gen.yaml", "Path to config file") flag.Parse() absConfigPath, err := filepath.Abs(*configFile) if err != nil { fmt.Printf("Error resolving config path: %v\n", err) os.Exit(1) } configBytes, err := os.ReadFile(absConfigPath) if err != nil { fmt.Printf("Error reading config file: %v\n", err) os.Exit(1) } var config Config if err := yaml.Unmarshal(configBytes, &config); err != nil { fmt.Printf("Error parsing config file: %v\n", err) os.Exit(1) } if config.HAAuthToken == "" { config.HAAuthToken = os.Getenv("HA_AUTH_TOKEN") } if config.URL == "" || config.HAAuthToken == "" { fmt.Println("Error: url and ha_auth_token are required in config") os.Exit(1) } if err := generate(config); err != nil { fmt.Printf("Error generating entities: %v\n", err) os.Exit(1) } fmt.Println("Generated entities/entities.go") }