diff --git a/README.md b/README.md index 2cea3e3..6265a3b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,48 @@ Gome-Assistant is a new library, and I'm opening it up early to get some user fe go get saml.dev/gome-assistant ``` +### Generate Entity Constants + +You can generate type-safe constants for all your Home Assistant entities using `go generate`. This makes it easier to reference entities in your code. + +1. Create a `gen.yaml` file in your project root: + +```yaml +url: "http://192.168.1.123:8123" +ha_auth_token: "your_auth_token" # Or set HA_AUTH_TOKEN env var +home_zone_entity_id: "zone.home" +``` + +2. Add a `//go:generate` comment in your project: + +```go +//go:generate go run saml.dev/gome-assistant/cmd/generate +``` + +Optionally use the `-config` flag to customize the file path of the config file. + +3. Run the generator: + +``` +go generate +``` + +This will create an `entities` package with type-safe constants for all your Home Assistant entities, organized by domain. For example: + +```go +import "your_project/entities" + +// Instead of writing "light.living_room" as a string: +entities.Light.LivingRoom // Type-safe constant + +// All your entities are organized by domain +entities.Switch.Kitchen +entities.Climate.Bedroom +entities.MediaPlayer.TVRoom +``` + +The constants are based on the entity ID itself, not the name of the entity in Home Assistant. + ### Write your automations Check out [`example/example.go`](./example/example.go) for an example of the 3 types of automations — schedules, entity listeners, and event listeners. diff --git a/cmd/generate/main.go b/cmd/generate/main.go new file mode 100644 index 0000000..6573adc --- /dev/null +++ b/cmd/generate/main.go @@ -0,0 +1,192 @@ +// Package main provides the generate command for generating Home Assistant entity constants +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "gopkg.in/yaml.v3" + ga "saml.dev/gome-assistant" +) + +type Config struct { + URL string `yaml:"url"` + HAAuthToken string `yaml:"ha_auth_token"` + HomeZoneEntityId string `yaml:"home_zone_entity_id"` +} + +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() +} + +// generate creates the entities.go file with constants for all Home Assistant entities +func generate(config Config) error { + app, err := ga.NewApp(ga.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() + + 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] + 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 == "" || config.HomeZoneEntityId == "" { + fmt.Println("Error: url, ha_auth_token and home_zone_entity_id 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") +} diff --git a/example/example.go b/example/example.go index f4e2f76..7d68e18 100644 --- a/example/example.go +++ b/example/example.go @@ -6,12 +6,16 @@ import ( "os" "time" + "example/entities" // Import generated entities + ga "saml.dev/gome-assistant" ) +//go:generate go run saml.dev/gome-assistant/cmd/generate + func main() { app, err := ga.NewApp(ga.NewAppRequest{ - URL: "http://192.168.86.67:8123", // Replace with your Home Assistant IP Address + URL: "http://192.168.86.67:8123", // Replace with your Home Assistant URL HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), HomeZoneEntityId: "zone.home", }) @@ -24,7 +28,7 @@ func main() { pantryDoor := ga. NewEntityListener(). - EntityIds("binary_sensor.pantry_door"). + EntityIds(entities.BinarySensor.PantryDoor). // Use generated entity constant Call(pantryLights). Build() @@ -55,6 +59,7 @@ func main() { func pantryLights(service *ga.Service, state ga.State, sensor ga.EntityData) { l := "light.pantry" + // l := entities.Light.Pantry // Or use generated entity constant if sensor.ToState == "on" { service.HomeAssistant.TurnOn(l) } else { @@ -75,8 +80,8 @@ func onEvent(service *ga.Service, state ga.State, data ga.EventData) { func lightsOut(service *ga.Service, state ga.State) { // always turn off outside lights - service.Light.TurnOff("light.outside_lights") - s, err := state.Get("binary_sensor.living_room_motion") + service.Light.TurnOff(entities.Light.OutsideLights) + s, err := state.Get(entities.BinarySensor.LivingRoomMotion) if err != nil { slog.Warn("couldnt get living room motion state, doing nothing") return @@ -84,11 +89,11 @@ func lightsOut(service *ga.Service, state ga.State) { // if no motion detected in living room for 30mins if s.State == "off" && time.Since(s.LastChanged).Minutes() > 30 { - service.Light.TurnOff("light.main_lights") + service.Light.TurnOff(entities.Light.MainLights) } } func sunriseSched(service *ga.Service, state ga.State) { - service.Light.TurnOn("light.living_room_lamps") - service.Light.TurnOff("light.christmas_lights") + service.Light.TurnOn(entities.Light.LivingRoomLamps) + service.Light.TurnOff(entities.Light.ChristmasLights) } diff --git a/example/gen.yaml b/example/gen.yaml new file mode 100644 index 0000000..729c0e9 --- /dev/null +++ b/example/gen.yaml @@ -0,0 +1,3 @@ +url: "http://192.168.4.67:8123" # Replace with your Home Assistant URL +ha_auth_token: "" # Your auth token +home_zone_entity_id: "zone.home"