diff --git a/bootserver/config/config.go b/bootserver/config/config.go index c78ead4..5f55e1e 100644 --- a/bootserver/config/config.go +++ b/bootserver/config/config.go @@ -60,21 +60,29 @@ type jsonConf struct { McastGroup string `json:"multicast_group"` SrcAddr string `json:"src_addr"` } `json:"boot_provider"` + HomeAssistant struct { + Enabled bool `json:"enabled"` + Host string `json:"host"` + APIToken string `json:"token"` + } `json:"home_assistant"` } type AppConfig struct { - LogLevel logrus.Level - ServerMode ListeningMode - DataFilepath string - StaticDir string - Host string - PublicHost string - Port int - SockPath string - UPDMcastGroup string - UDPPort int - UDPIface string - UDPSrcAddr string + LogLevel logrus.Level + ServerMode ListeningMode + DataFilepath string + StaticDir string + Host string + PublicHost string + Port int + SockPath string + UPDMcastGroup string + UDPPort int + UDPIface string + UDPSrcAddr string + HomeAssistantEnabled bool + HomeAssistantHost string + HomeAssistantToken string } func parseLevel(lvlStr string) logrus.Level { @@ -108,6 +116,9 @@ func (ac *AppConfig) UnmarshalJSON(data []byte) error { ac.UDPSrcAddr = jsonConf.BootProvider.SrcAddr ac.DataFilepath = jsonConf.Storage.Path ac.StaticDir = jsonConf.Storage.StaticDir + ac.HomeAssistantEnabled = jsonConf.HomeAssistant.Enabled + ac.HomeAssistantHost = jsonConf.HomeAssistant.Host + ac.HomeAssistantToken = jsonConf.HomeAssistant.APIToken return nil } diff --git a/bootserver/controllers/client/client.go b/bootserver/controllers/client/client.go index 019a409..64b73ac 100644 --- a/bootserver/controllers/client/client.go +++ b/bootserver/controllers/client/client.go @@ -7,7 +7,9 @@ import ( "io" "net/http" + "git.faercol.me/faercol/http-boot-server/bootserver/config" "git.faercol.me/faercol/http-boot-server/bootserver/helpers" + "git.faercol.me/faercol/http-boot-server/bootserver/homeassistant" "git.faercol.me/faercol/http-boot-server/bootserver/services" "github.com/google/uuid" "github.com/sirupsen/logrus" @@ -22,13 +24,15 @@ type setBootOptionPayload struct { type BootController struct { clientService *services.ClientHandlerService + appConf *config.AppConfig l *logrus.Logger } -func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController { +func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService, conf *config.AppConfig) *BootController { return &BootController{ clientService: service, l: logger, + appConf: conf, } } @@ -61,6 +65,18 @@ func (bc *BootController) setBootOption(w http.ResponseWriter, r *http.Request) return http.StatusInternalServerError, nil, fmt.Errorf("failed to set boot option for client: %w", err) } + if bc.appConf.HomeAssistantEnabled { + bc.l.Debug("Notifying HomeAssistant of change") + newConf, err := bc.clientService.GetClientConfig(clientID) + if err != nil { + bc.l.Errorf("Failed to get new config to send to HA: %s", err.Error()) + } else { + if err := homeassistant.New(bc.appConf).SendBootOption(r.Context(), newConf.Name, newConf.Options[newConf.SelectedOption].Name); err != nil { + bc.l.Errorf("Failed to notify HA: %s", err.Error()) + } + } + } + return http.StatusAccepted, nil, nil } diff --git a/bootserver/homeassistant/sender..go b/bootserver/homeassistant/sender..go new file mode 100644 index 0000000..9a06025 --- /dev/null +++ b/bootserver/homeassistant/sender..go @@ -0,0 +1,80 @@ +package homeassistant + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "git.faercol.me/faercol/http-boot-server/bootserver/config" + "git.faercol.me/faercol/http-boot-server/bootserver/logger" +) + +type Entity struct { + State string `json:"state"` + ID string `json:"-"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +func newBootOptionEntity(device, option string) Entity { + return Entity{ + State: option, + ID: "httpboot." + device, + Attributes: nil, + } +} + +type HomeAssistantExporter struct { + clt *http.Client + baseURL string + token string +} + +func New(conf *config.AppConfig) *HomeAssistantExporter { + clt := http.Client{} + return &HomeAssistantExporter{ + clt: &clt, + baseURL: conf.HomeAssistantHost, + token: conf.HomeAssistantToken, + } +} + +func (e *HomeAssistantExporter) SendBootOption(ctx context.Context, device string, option string) error { + subCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + entity := newBootOptionEntity(device, option) + + dat, err := json.Marshal(entity) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(subCtx, http.MethodPost, e.baseURL+"/api/states/"+entity.ID, bytes.NewBuffer(dat)) + if err != nil { + return err + } + req.Header.Add("Authorization", "Bearer "+e.token) + + resp, err := e.clt.Do(req) + if err != nil { + return err + } + switch resp.StatusCode { + case http.StatusOK: + logger.L.Debugf("Updated boot info for device %s to %s", device, option) + case http.StatusCreated: + logger.L.Debugf("Created boot info for device %s with value %s", device, option) + default: + respBod, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return fmt.Errorf("unexpected returncode %d (%s)", resp.StatusCode, string(respBod)) + } + + return nil +} diff --git a/bootserver/main.go b/bootserver/main.go index b5a5a4f..590f1dd 100644 --- a/bootserver/main.go +++ b/bootserver/main.go @@ -8,6 +8,7 @@ import ( "time" "git.faercol.me/faercol/http-boot-server/bootserver/config" + "git.faercol.me/faercol/http-boot-server/bootserver/homeassistant" "git.faercol.me/faercol/http-boot-server/bootserver/logger" "git.faercol.me/faercol/http-boot-server/bootserver/server" "git.faercol.me/faercol/http-boot-server/bootserver/services" @@ -61,6 +62,21 @@ func main() { logger.L.Fatalf("Failed to start UDP listener: %s", err.Error()) } + if conf.HomeAssistantEnabled { + logger.L.Info("Home assistant integration enabled, sending current configuration to the host") + haClt := homeassistant.New(conf) + cltSrv := services.NewClientHandlerService(conf.DataFilepath, logger.L) + clts, err := cltSrv.GetAllClientConfig() + if err != nil { + logger.L.Fatalf("Failed to get current clients from the storage: %s", err.Error()) + } + for _, c := range clts { + if err := haClt.SendBootOption(context.Background(), c.Name, c.Options[c.SelectedOption].Name); err != nil { + logger.L.Errorf("Failed to send config to homeassistant: %s", err.Error()) + } + } + } + go s.Run(mainCtx) go listener.Run(mainCtx) diff --git a/bootserver/server/server.go b/bootserver/server/server.go index 5830b4b..064bf36 100644 --- a/bootserver/server/server.go +++ b/bootserver/server/server.go @@ -69,7 +69,7 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) { controllers := map[string]http.Handler{ client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service, appConf.UDPPort, appConf.UPDMcastGroup), logger), client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger), - client.SetBootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger), + client.SetBootRoute: middlewares.WithLogger(client.NewBootController(logger, service, appConf), logger), ui.StaticRoute: middlewares.WithLogger(ui.NewStaticController(appConf.StaticDir), logger), ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service, appConf.StaticDir), logger), }