|
|
package hue
import ( "bufio" "bytes" "context" "crypto/tls" "encoding/json" "errors" "fmt" lucifer3 "git.aiterp.net/lucifer3/server" "io" "log" "net" "net/http" "strings" "time" )
func NewClient(host, token string) *Client { ch := make(chan struct{}, 5) for i := 0; i < 2; i++ { ch <- struct{}{} }
return &Client{ host: host, token: token, ch: ch, } }
type Client struct { host string token string ch chan struct{} }
func (c *Client) Register(ctx context.Context) (string, error) { result := make([]CreateUserResponse, 0, 1) err := c.post(ctx, "api/", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result) if err != nil { return "", err }
if len(result) == 0 || result[0].Error != nil { select { case <-ctx.Done(): return "", ctx.Err() case <-time.After(time.Second): return c.Register(ctx) } } if result[0].Success == nil { return "", lucifer3.ErrUnexpectedResponse }
c.token = result[0].Success.Username
return result[0].Success.Username, nil }
func (c *Client) AllResources(ctx context.Context) ([]ResourceData, error) { res := struct { Error interface{} Data []ResourceData }{}
err := c.get(ctx, "clip/v2/resource", &res) if err != nil { return nil, err }
return res.Data, nil }
func (c *Client) Resources(ctx context.Context, kind string) ([]ResourceData, error) { res := struct { Error interface{} Data []ResourceData }{}
err := c.get(ctx, "clip/v2/resource/"+kind, &res) if err != nil { return nil, err }
return res.Data, nil }
func (c *Client) UpdateResource(ctx context.Context, link ResourceLink, update ResourceUpdate) error { return c.put(ctx, link.Path(), update, nil) }
func (c *Client) LegacyDiscover(ctx context.Context, kind string) error { return c.legacyPost(ctx, kind, nil, nil) }
func (c *Client) SSE(ctx context.Context) <-chan []SSEUpdate { ch := make(chan []SSEUpdate, 4) go func() { defer close(ch)
reader, err := c.getReader(ctx, "/eventstream/clip/v2", map[string]string{ "Accept": "text/event-stream", }) if err != nil { log.Println("SSE Connect error:", err) return }
defer reader.Close()
br := bufio.NewReader(reader)
for { line, err := br.ReadString('\n') if err != nil { log.Println("SSE Read error:", err) return } line = strings.Trim(line, " \t\r\n") kv := strings.SplitN(line, ": ", 2) if len(kv) < 2 { continue }
switch kv[0] { case "data": var data []SSEUpdate err := json.Unmarshal([]byte(kv[1]), &data) if err != nil { log.Println("Parsing SSE event failed:", err) log.Println(" json:", kv[1]) return }
if len(data) > 0 { ch <- data } } } }()
return ch }
func (c *Client) getReader(ctx context.Context, path string, headers map[string]string) (io.ReadCloser, error) { select { case <-ctx.Done(): return nil, ctx.Err() case <-c.ch: defer func() { c.ch <- struct{}{} }() }
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s", c.host, path), nil) if err != nil { return nil, err }
for key, value := range headers { req.Header.Set(key, value) } req.Header.Set("hue-application-key", c.token)
client := httpClient if headers["Accept"] == "text/event-stream" { client = sseClient }
res, err := client.Do(req.WithContext(ctx)) if err != nil { return nil, err } if res.StatusCode != 200 { _ = res.Body.Close() return nil, errors.New(res.Status) }
return res.Body, nil }
func (c *Client) get(ctx context.Context, path string, target interface{}) error { body, err := c.getReader(ctx, path, map[string]string{}) if err != nil { return err }
defer body.Close()
if target == nil { return nil } return json.NewDecoder(body).Decode(&target) }
func (c *Client) put(ctx context.Context, path string, body interface{}, target interface{}) error { select { case <-ctx.Done(): return ctx.Err() case <-c.ch: defer func() { c.ch <- struct{}{} }() }
rb, err := reqBody(body) if err != nil { return err }
req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s", c.host, path), rb) if err != nil { return err }
req.Header.Set("hue-application-key", c.token)
res, err := httpClient.Do(req.WithContext(ctx)) if err != nil { return err } defer res.Body.Close()
if target == nil { return nil }
return json.NewDecoder(res.Body).Decode(&target) }
func (c *Client) post(ctx context.Context, path string, body interface{}, target interface{}) error { select { case <-ctx.Done(): return ctx.Err() case <-c.ch: defer func() { c.ch <- struct{}{} }() }
rb, err := reqBody(body) if err != nil { return err }
req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/%s", c.host, path), rb) if err != nil { return err }
if c.token != "" { req.Header.Set("hue-application-key", c.token) }
res, err := httpClient.Do(req.WithContext(ctx)) if err != nil { return err } defer res.Body.Close()
if target == nil { return nil }
return json.NewDecoder(res.Body).Decode(&target) }
func (c *Client) legacyPost(ctx context.Context, resource string, body interface{}, target interface{}) error { select { case <-ctx.Done(): return ctx.Err() case <-c.ch: defer func() { c.ch <- struct{}{} }() }
rb, err := reqBody(body) if err != nil { return err }
if c.token != "" { resource = c.token + "/" + resource }
req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", c.host, resource), rb) if err != nil { return err }
res, err := httpClient.Do(req.WithContext(ctx)) if err != nil { return err } defer res.Body.Close()
if target == nil { return nil }
return json.NewDecoder(res.Body).Decode(target) }
func reqBody(body interface{}) (io.Reader, error) { if body == nil { return nil, nil }
switch v := body.(type) { case []byte: return bytes.NewReader(v), nil case string: return strings.NewReader(v), nil case io.Reader: return v, nil default: jsonData, err := json.Marshal(v) if err != nil { return nil, err }
return bytes.NewReader(jsonData), nil } }
var sseClient = &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: time.Hour * 1000000, KeepAlive: 30 * time.Second, }).DialContext, MaxIdleConns: 5, MaxIdleConnsPerHost: 1, IdleConnTimeout: 0, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, Timeout: time.Hour * 1000000, }
var httpClient = &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, MaxIdleConns: 256, MaxIdleConnsPerHost: 16, IdleConnTimeout: 10 * time.Minute, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, Timeout: time.Minute, }
|