139 lines
3.2 KiB
Go
139 lines
3.2 KiB
Go
package homeassistant
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"maps"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/evcc-io/evcc/server/service"
|
|
"github.com/evcc-io/evcc/util"
|
|
)
|
|
|
|
var log = util.NewLogger("homeassistant")
|
|
|
|
func init() {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /instances", getInstances)
|
|
mux.HandleFunc("GET /entities", getEntities)
|
|
mux.HandleFunc("GET /services", getServices)
|
|
|
|
service.Register("homeassistant", mux)
|
|
}
|
|
|
|
func getInstances(w http.ResponseWriter, req *http.Request) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
jsonWrite(w, slices.Sorted(maps.Values(instances)))
|
|
}
|
|
|
|
func connectionFromRequest(req *http.Request) (*Connection, error) {
|
|
uri := util.DefaultScheme(strings.TrimSuffix(req.URL.Query().Get("uri"), "/"), "http")
|
|
if uri == "" {
|
|
return nil, errors.New("missing uri")
|
|
}
|
|
return NewConnection(log, uri, "")
|
|
}
|
|
|
|
// domainsFromRequest parses the comma-separated "domain" query parameter.
|
|
func domainsFromRequest(req *http.Request) []string {
|
|
if domain := req.URL.Query().Get("domain"); domain != "" {
|
|
return strings.Split(domain, ",")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// matchesDomains reports whether entityID belongs to any of the given domains.
|
|
// If domains is empty, all entities match.
|
|
func matchesDomains(entityID string, domains []string) bool {
|
|
if len(domains) == 0 {
|
|
return true
|
|
}
|
|
for _, d := range domains {
|
|
if strings.HasPrefix(entityID, d+".") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getEntities(w http.ResponseWriter, req *http.Request) {
|
|
conn, err := connectionFromRequest(req)
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
states, err := conn.GetStates()
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
domains := domainsFromRequest(req)
|
|
|
|
var result []string
|
|
for _, e := range states {
|
|
if matchesDomains(e.EntityId, domains) {
|
|
result = append(result, e.EntityId)
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Cache-control", "max-age=300")
|
|
jsonWrite(w, result)
|
|
}
|
|
|
|
func getServices(w http.ResponseWriter, req *http.Request) {
|
|
conn, err := connectionFromRequest(req)
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
domains := domainsFromRequest(req)
|
|
|
|
seen := make(map[string]struct{})
|
|
|
|
// collect callable services from /api/services (e.g. notify.mobile_app_android)
|
|
svcRes, err := conn.GetServices()
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
for _, sd := range svcRes {
|
|
if len(domains) == 0 || slices.Contains(domains, sd.Domain) {
|
|
for svc := range sd.Services {
|
|
seen[sd.Domain+"."+svc] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// collect entity-based notifiers from /api/states (e.g. Telegram in HA 2024+)
|
|
if len(domains) > 0 {
|
|
if states, err := conn.GetStates(); err == nil {
|
|
for _, e := range states {
|
|
if matchesDomains(e.EntityId, domains) {
|
|
seen[e.EntityId] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Cache-control", "max-age=300")
|
|
jsonWrite(w, slices.Sorted(maps.Keys(seen)))
|
|
}
|
|
|
|
// jsonWrite writes a JSON response
|
|
func jsonWrite(w http.ResponseWriter, data any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
|
|
// jsonError writes an error response
|
|
func jsonError(w http.ResponseWriter, status int, err error) {
|
|
w.WriteHeader(status)
|
|
jsonWrite(w, util.ErrorAsJson(err))
|
|
}
|