Rewrite HomeKit pairing API
This commit is contained in:
+18
-2
@@ -50,7 +50,8 @@ func Init() {
|
||||
HandleFunc("api/exit", exitHandler)
|
||||
|
||||
// ensure we can listen without errors
|
||||
listener, err := net.Listen("tcp", cfg.Mod.Listen)
|
||||
var err error
|
||||
ln, err = net.Listen("tcp", cfg.Mod.Listen)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] listen")
|
||||
return
|
||||
@@ -75,7 +76,7 @@ func Init() {
|
||||
go func() {
|
||||
s := http.Server{}
|
||||
s.Handler = Handler
|
||||
if err = s.Serve(listener); err != nil {
|
||||
if err = s.Serve(ln); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] serve")
|
||||
}
|
||||
}()
|
||||
@@ -111,6 +112,13 @@ func Init() {
|
||||
}
|
||||
}
|
||||
|
||||
func Port() int {
|
||||
if ln == nil {
|
||||
return 0
|
||||
}
|
||||
return ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
const (
|
||||
MimeJSON = "application/json"
|
||||
MimeText = "text/plain"
|
||||
@@ -192,6 +200,7 @@ func middlewareCORS(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
var ln net.Listener
|
||||
var mu sync.Mutex
|
||||
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -216,6 +225,7 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type Source struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Info string `json:"info,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
}
|
||||
@@ -233,3 +243,9 @@ func ResponseSources(w http.ResponseWriter, sources []*Source) {
|
||||
}
|
||||
ResponseJSON(w, response)
|
||||
}
|
||||
|
||||
func Error(w http.ResponseWriter, err error) {
|
||||
log.Error().Err(err).Caller(1).Send()
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInsufficientStorage)
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -11,9 +12,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var Version = "1.6.2"
|
||||
@@ -81,6 +82,8 @@ func Init() {
|
||||
modules = cfg.Mod
|
||||
|
||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
migrateStore()
|
||||
}
|
||||
|
||||
func NewLogger(format string, level string) zerolog.Logger {
|
||||
@@ -123,6 +126,22 @@ func GetLogger(module string) zerolog.Logger {
|
||||
return log.Logger
|
||||
}
|
||||
|
||||
func PatchConfig(key string, value any, path ...string) error {
|
||||
if ConfigPath == "" {
|
||||
return errors.New("config file disabled")
|
||||
}
|
||||
|
||||
// empty config is OK
|
||||
b, _ := os.ReadFile(ConfigPath)
|
||||
|
||||
b, err := yaml.Patch(b, key, value, path...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(ConfigPath, b, 0644)
|
||||
}
|
||||
|
||||
// internal
|
||||
|
||||
type Config []string
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func migrateStore() {
|
||||
const name = "go2rtc.json"
|
||||
|
||||
data, _ := os.ReadFile(name)
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var store struct {
|
||||
Streams map[string]string `json:"streams"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &store); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
for id, url := range store.Streams {
|
||||
if err := PatchConfig(id, url, "streams"); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = os.Remove(name)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/rs/zerolog/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
const name = "go2rtc.json"
|
||||
|
||||
var store map[string]any
|
||||
|
||||
func load() {
|
||||
data, _ := os.ReadFile(name)
|
||||
if data != nil {
|
||||
if err := json.Unmarshal(data, &store); err != nil {
|
||||
// TODO: log
|
||||
log.Warn().Err(err).Msg("[app] read storage")
|
||||
}
|
||||
}
|
||||
|
||||
if store == nil {
|
||||
store = make(map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
func save() error {
|
||||
data, err := json.Marshal(store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(name, data, 0644)
|
||||
}
|
||||
|
||||
func GetRaw(key string) any {
|
||||
if store == nil {
|
||||
load()
|
||||
}
|
||||
|
||||
return store[key]
|
||||
}
|
||||
|
||||
func GetDict(key string) map[string]any {
|
||||
raw := GetRaw(key)
|
||||
if raw != nil {
|
||||
return raw.(map[string]any)
|
||||
}
|
||||
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
func Set(key string, v any) error {
|
||||
if store == nil {
|
||||
load()
|
||||
}
|
||||
|
||||
store[key] = v
|
||||
|
||||
return save()
|
||||
}
|
||||
+98
-93
@@ -1,12 +1,14 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app/store"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||
@@ -15,119 +17,122 @@ import (
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
items := make([]any, 0)
|
||||
|
||||
for name, src := range store.GetDict("streams") {
|
||||
if src := src.(string); strings.HasPrefix(src, "homekit") {
|
||||
u, err := url.Parse(src)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
device := Device{
|
||||
Name: name,
|
||||
Addr: u.Host,
|
||||
Paired: true,
|
||||
}
|
||||
items = append(items, device)
|
||||
}
|
||||
}
|
||||
|
||||
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||
log.Trace().Msgf("[homekit] mdns=%s", entry)
|
||||
|
||||
if entry.Complete() {
|
||||
device := Device{
|
||||
Name: entry.Name,
|
||||
Addr: entry.Addr(),
|
||||
ID: entry.Info[hap.TXTDeviceID],
|
||||
Model: entry.Info[hap.TXTModel],
|
||||
Paired: entry.Info[hap.TXTStatusFlags] == "0",
|
||||
}
|
||||
items = append(items, device)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
sources, err := discovery()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.ResponseJSON(w, items)
|
||||
urls := findHomeKitURLs()
|
||||
for id, u := range urls {
|
||||
deviceID := u.Query().Get("device_id")
|
||||
for _, source := range sources {
|
||||
if strings.Contains(source.URL, deviceID) {
|
||||
source.Location = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
if source.Location == "" {
|
||||
source.Location = " "
|
||||
}
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
|
||||
case "POST":
|
||||
// TODO: post params...
|
||||
if err := r.ParseMultipartForm(1024); err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.URL.Query().Get("id")
|
||||
pin := r.URL.Query().Get("pin")
|
||||
name := r.URL.Query().Get("name")
|
||||
if err := hkPair(id, pin, name); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
|
||||
api.Error(w, err)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
src := r.URL.Query().Get("src")
|
||||
if err := hkDelete(src); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
if err := r.ParseMultipartForm(1024); err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := apiUnpair(r.Form.Get("id")); err != nil {
|
||||
api.Error(w, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hkPair(deviceID, pin, name string) (err error) {
|
||||
var conn *hap.Client
|
||||
func discovery() ([]*api.Source, error) {
|
||||
var sources []*api.Source
|
||||
|
||||
if conn, err = hap.Pair(deviceID, pin); err != nil {
|
||||
return
|
||||
// 1. Get streams from Discovery
|
||||
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||
log.Trace().Msgf("[homekit] mdns=%s", entry)
|
||||
|
||||
if entry.Complete() && entry.Info[hap.TXTCategory] == hap.CategoryCamera {
|
||||
source := &api.Source{
|
||||
Name: entry.Name,
|
||||
Info: entry.Info[hap.TXTModel],
|
||||
URL: fmt.Sprintf(
|
||||
"homekit://%s:%d?device_id=%s&feature=%s&status=%s",
|
||||
entry.IP, entry.Port, entry.Info[hap.TXTDeviceID],
|
||||
entry.Info[hap.TXTFeatureFlags], entry.Info[hap.TXTStatusFlags],
|
||||
),
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streams.New(name, conn.URL())
|
||||
|
||||
dict := store.GetDict("streams")
|
||||
dict[name] = conn.URL()
|
||||
|
||||
return store.Set("streams", dict)
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func hkDelete(name string) (err error) {
|
||||
dict := store.GetDict("streams")
|
||||
for key, rawURL := range dict {
|
||||
if key != name {
|
||||
continue
|
||||
}
|
||||
|
||||
var conn *hap.Client
|
||||
|
||||
if conn, err = hap.NewClient(rawURL.(string)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = conn.Dial(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = conn.ListPairings(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = conn.DeletePairing(conn.ClientID); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
delete(dict, name)
|
||||
|
||||
return store.Set("streams", dict)
|
||||
func apiPair(id, url string) error {
|
||||
conn, err := hap.Pair(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
streams.New(id, conn.URL())
|
||||
|
||||
return app.PatchConfig(id, conn.URL(), "streams")
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Addr string `json:"addr"`
|
||||
Model string `json:"model"`
|
||||
Paired bool `json:"paired"`
|
||||
//Type string `json:"type"`
|
||||
func apiUnpair(id string) error {
|
||||
stream := streams.Get(id)
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
rawURL := findHomeKitURL(stream)
|
||||
if rawURL == "" {
|
||||
return errors.New("not homekit source")
|
||||
}
|
||||
|
||||
if err := hap.Unpair(rawURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streams.Delete(id)
|
||||
|
||||
return app.PatchConfig(id, nil, "streams")
|
||||
}
|
||||
|
||||
func findHomeKitURLs() map[string]*url.URL {
|
||||
urls := map[string]*url.URL{}
|
||||
for id, stream := range streams.Streams() {
|
||||
if rawURL := findHomeKitURL(stream); rawURL != "" {
|
||||
if u, err := url.Parse(rawURL); err == nil {
|
||||
urls[id] = u
|
||||
}
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/srtp"
|
||||
@@ -23,3 +25,24 @@ var log zerolog.Logger
|
||||
func streamHandler(url string) (core.Producer, error) {
|
||||
return homekit.Dial(url, srtp.Server)
|
||||
}
|
||||
|
||||
func findHomeKitURL(stream *streams.Stream) string {
|
||||
sources := stream.Sources()
|
||||
if len(sources) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
url := sources[0]
|
||||
if strings.HasPrefix(url, "homekit") {
|
||||
return url
|
||||
}
|
||||
|
||||
if strings.HasPrefix(url, "hass") {
|
||||
location, _ := streams.Location(url)
|
||||
if strings.HasPrefix(location, "homekit") {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -38,6 +38,13 @@ func NewStream(source any) *Stream {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) Sources() (sources []string) {
|
||||
for _, prod := range s.producers {
|
||||
sources = append(sources, prod.url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Stream) SetSource(source string) {
|
||||
for _, prod := range s.producers {
|
||||
prod.SetSource(source)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/app/store"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -25,10 +24,6 @@ func Init() {
|
||||
streams[name] = NewStream(item)
|
||||
}
|
||||
|
||||
for name, item := range store.GetDict("streams") {
|
||||
streams[name] = NewStream(item)
|
||||
}
|
||||
|
||||
api.HandleFunc("api/streams", streamsHandler)
|
||||
}
|
||||
|
||||
@@ -118,6 +113,14 @@ func GetAll() (names []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func Streams() map[string]*Stream {
|
||||
return streams
|
||||
}
|
||||
|
||||
func Delete(id string) {
|
||||
delete(streams, id)
|
||||
}
|
||||
|
||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
||||
@@ -141,6 +144,11 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if New(name, src) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig(name, src, "streams"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "PATCH":
|
||||
@@ -173,6 +181,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
case "DELETE":
|
||||
delete(streams, src)
|
||||
|
||||
if err := app.PatchConfig(src, nil, "streams"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+54
-23
@@ -5,43 +5,74 @@ import (
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||
"github.com/tadglines/go-pkgs/crypto/srp"
|
||||
)
|
||||
|
||||
func Pair(deviceID, pin string) (*Client, error) {
|
||||
var addr string
|
||||
var mfi bool
|
||||
|
||||
_ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||
if entry.Complete() && entry.Info[TXTDeviceID] == deviceID {
|
||||
addr = entry.Addr()
|
||||
mfi = entry.Info[TXTFeatureFlags] == "1"
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if addr == "" {
|
||||
return nil, errors.New("hap: mdns.Discovery")
|
||||
// Pair homekit
|
||||
func Pair(rawURL string) (*Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
|
||||
c := &Client{
|
||||
DeviceAddress: addr,
|
||||
DeviceID: deviceID,
|
||||
ClientID: GenerateUUID(),
|
||||
ClientPrivate: GenerateKey(),
|
||||
DeviceAddress: u.Host,
|
||||
DeviceID: query.Get("device_id"),
|
||||
ClientID: query.Get("client_id"),
|
||||
ClientPrivate: DecodeKey(query.Get("client_private")),
|
||||
}
|
||||
|
||||
return c, c.Pair(mfi, pin)
|
||||
if c.ClientID == "" {
|
||||
c.ClientID = GenerateUUID()
|
||||
}
|
||||
if c.ClientPrivate == nil {
|
||||
c.ClientPrivate = GenerateKey()
|
||||
}
|
||||
|
||||
if err = c.Pair(query.Get("feature"), query.Get("pin")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
func Unpair(rawURL string) error {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
conn := &Client{
|
||||
DeviceAddress: u.Host,
|
||||
DeviceID: query.Get("device_id"),
|
||||
DevicePublic: DecodeKey(query.Get("device_public")),
|
||||
ClientID: query.Get("client_id"),
|
||||
ClientPrivate: DecodeKey(query.Get("client_private")),
|
||||
}
|
||||
|
||||
if err = conn.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
if err = conn.ListPairings(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return conn.DeletePairing(conn.ClientID)
|
||||
}
|
||||
|
||||
func (c *Client) Pair(feature, pin string) (err error) {
|
||||
if pin, err = SanitizePin(pin); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,7 +92,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
Method: MethodPair,
|
||||
State: StateM1,
|
||||
}
|
||||
if mfi {
|
||||
if feature == "1" {
|
||||
plainM1.Method = MethodPairMFi // ff=1 => method=1, ff=2 => method=0
|
||||
}
|
||||
res, err := c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM1))
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
func TestPatch(t *testing.T) {
|
||||
b := []byte(`# prefix`)
|
||||
|
||||
// 1. Add first
|
||||
b, err := Patch(b, "camera1", "url1", "streams")
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -17,6 +18,7 @@ streams:
|
||||
camera1: url1
|
||||
`, string(b))
|
||||
|
||||
// 2. Add second
|
||||
b, err = Patch(b, "camera2", []string{"url2", "url3"}, "streams")
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -28,6 +30,7 @@ streams:
|
||||
- url3
|
||||
`, string(b))
|
||||
|
||||
// 3. Replace first
|
||||
b, err = Patch(b, "camera1", "url4", "streams")
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -39,6 +42,7 @@ streams:
|
||||
- url3
|
||||
`, string(b))
|
||||
|
||||
// 4. Replace second
|
||||
b, err = Patch(b, "camera2", "url5", "streams")
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -48,6 +52,7 @@ streams:
|
||||
camera2: url5
|
||||
`, string(b))
|
||||
|
||||
// 5. Delete first
|
||||
b, err = Patch(b, "camera1", nil, "streams")
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -65,10 +70,8 @@ streams:
|
||||
camera1: url1
|
||||
`)
|
||||
|
||||
pairings := map[string]string{
|
||||
"client1": "public1",
|
||||
"client2": "public2",
|
||||
}
|
||||
// 1. Add new key
|
||||
pairings := []string{"client1", "client2"}
|
||||
|
||||
b, err := Patch(b, "pairings", pairings, "homekit", "camera1")
|
||||
require.Nil(t, err)
|
||||
@@ -77,8 +80,8 @@ streams:
|
||||
camera1:
|
||||
pin: 123-45-678
|
||||
pairings:
|
||||
client1: public1
|
||||
client2: public2
|
||||
- client1
|
||||
- client2
|
||||
streams:
|
||||
camera1: url1
|
||||
`, string(b))
|
||||
|
||||
+86
-84
@@ -59,7 +59,18 @@
|
||||
<body>
|
||||
<script src="main.js"></script>
|
||||
<script>
|
||||
async function getSources(url, tableID) {
|
||||
function drawTable(table, data) {
|
||||
const cols = ['id', 'name', 'info', 'url', 'location'];
|
||||
const th = (row) => cols.reduce((html, k) => k in row ? `${html}<th>${k}</th>` : html, '<tr>') + '</tr>';
|
||||
const td = (row) => cols.reduce((html, k) => k in row ? `${html}<td>${row[k]}</td>` : html, '<tr>') + '</tr>';
|
||||
|
||||
const thead = th(data.sources[0]);
|
||||
const tbody = data.sources.reduce((html, source) => `${html}${td(source)}`, '');
|
||||
|
||||
table.innerHTML = `<thead>${thead}</thead><tbody>${tbody}</tbody>`;
|
||||
}
|
||||
|
||||
async function getSources(tableID, url) {
|
||||
const table = document.getElementById(tableID);
|
||||
table.innerText = 'loading...';
|
||||
|
||||
@@ -69,16 +80,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const td = value => value ? `<td>${value}</td>` : '';
|
||||
const th = (name, value) => value ? `<th>${name}</th>` : '';
|
||||
|
||||
/** @type {{sources:Array<{id:string,name:string,url:string,location:string}>}} */
|
||||
const data = await r.json();
|
||||
const i0 = data.sources[0];
|
||||
const thead = `<tr>${th('ID', i0.id)}${th('Name', i0.name)}${th('URL', i0.url)}${th('Location', i0.location)}</tr>`;
|
||||
table.innerHTML = data.sources.reduce((html, item) => {
|
||||
return `${html}<tr>${td(item.id)}${td(item.name)}${td(item.url)}${td(item.location)}</tr>`;
|
||||
}, `<thead>${thead}</thead><tbody>`) + '</tbody>';
|
||||
drawTable(table, await r.json());
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -104,76 +106,79 @@
|
||||
url.searchParams.set('src', ev.target.elements['src'].value);
|
||||
|
||||
const r = await fetch(url, {method: 'PUT'});
|
||||
alert(r.ok ? 'OK' : 'ERROR');
|
||||
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<button id="homekit">Apple HomeKit</button>
|
||||
<div class="module">
|
||||
<div style="margin-bottom: 10px">
|
||||
<label for="pin">PIN</label>
|
||||
<input id="pin" type="text">
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Model</th>
|
||||
<th>Commands</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="homekit-body">
|
||||
</tbody>
|
||||
</table>
|
||||
<form id="homekit-pair" style="margin-bottom: 10px">
|
||||
<input type="text" name="id" placeholder="stream id" size="20">
|
||||
<input type="text" name="url" placeholder="url" size="40">
|
||||
<input type="text" name="pin" placeholder="pin" size="10">
|
||||
<input type="submit" value="Pair">
|
||||
</form>
|
||||
<form id="homekit-unpair" style="margin-bottom: 10px">
|
||||
<input type="text" name="id" placeholder="stream id" size="20">
|
||||
<input type="submit" value="Unpair">
|
||||
</form>
|
||||
<table id="homekit-table"></table>
|
||||
</div>
|
||||
<script>
|
||||
async function reloadHomeKit() {
|
||||
await getSources('homekit-table', 'api/homekit');
|
||||
|
||||
const rows = document.querySelectorAll('#homekit-table tr');
|
||||
rows.forEach((row) => {
|
||||
let commands = '';
|
||||
if (row.children[2].innerText.indexOf('status=1') > 0) {
|
||||
commands += '<a href="#">pair</a>';
|
||||
} else if (row.children[3].innerText) {
|
||||
commands += '<a href="#">unpair</a>';
|
||||
}
|
||||
row.innerHTML += `<td>${commands}</td>`;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('homekit').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
const r = await fetch('api/homekit', {cache: 'no-cache'});
|
||||
|
||||
/** @type {Array<{id:string,name:string,addr:string,model:string,paired:boolean}>} */
|
||||
const data = await r.json();
|
||||
|
||||
const tbody = document.getElementById('homekit-body');
|
||||
tbody.innerHTML =
|
||||
data.reduce((res, item) => {
|
||||
let commands = '';
|
||||
if (item.id === '') {
|
||||
commands = `<a href="#" onclick="unpair('${item.name}')">unpair</a>`;
|
||||
} else if (item.paired === false) {
|
||||
commands = `<a href="#" onclick="pair('${item.id}','${item.name}')">pair</a>`;
|
||||
}
|
||||
return res + `<tr>
|
||||
<td>${item.name}</td>
|
||||
<td>${item.addr}</td>
|
||||
<td>${item.model}</td>
|
||||
<td>${commands}</td>
|
||||
</tr>`;
|
||||
}, '');
|
||||
await reloadHomeKit();
|
||||
});
|
||||
|
||||
function pair(id, name) {
|
||||
const pin = document.querySelector('#pin').value;
|
||||
fetch(`api/homekit?id=${id}&name=${name}&pin=${pin}`, {method: 'POST'})
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
if (data.length > 0) alert(data);
|
||||
else window.location.reload();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
document.getElementById('homekit-table').addEventListener('click', ev => {
|
||||
if (ev.target.innerText === 'pair') {
|
||||
const form = document.querySelector('#homekit-pair');
|
||||
const row = ev.target.closest('tr');
|
||||
form.children[0].value = row.children[0].innerText;
|
||||
form.children[1].value = row.children[2].innerText;
|
||||
} else if (ev.target.innerText === 'unpair') {
|
||||
const form = document.querySelector('#homekit-unpair');
|
||||
const row = ev.target.closest('tr');
|
||||
form.children[0].value = row.children[3].innerText;
|
||||
}
|
||||
});
|
||||
|
||||
function unpair(src) {
|
||||
fetch(`api/homekit?src=${src}`, {method: 'DELETE'})
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
if (data.length > 0) alert(data);
|
||||
else window.location.reload();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
document.getElementById('homekit-pair').addEventListener('submit', async ev => {
|
||||
ev.preventDefault();
|
||||
|
||||
const body = new FormData(ev.target);
|
||||
body.set('url', body.get('url') + '&pin=' + body.get('pin'));
|
||||
body.delete('pin');
|
||||
|
||||
const r = await fetch('api/homekit', {method: 'POST', body: body});
|
||||
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
|
||||
|
||||
await reloadHomeKit();
|
||||
});
|
||||
|
||||
document.getElementById('homekit-unpair').addEventListener('submit', async ev => {
|
||||
ev.preventDefault();
|
||||
const r = await fetch('api/homekit', {method: 'DELETE', body: new FormData(ev.target)});
|
||||
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
|
||||
|
||||
await reloadHomeKit();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -184,33 +189,31 @@
|
||||
<script>
|
||||
document.getElementById('dvrip').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
await getSources('api/dvrip', 'dvrip-table');
|
||||
await getSources('dvrip-table', 'api/dvrip');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<button id="devices">FFmpeg Devices (USB)</button>
|
||||
<div class="module">
|
||||
<table id="devices-table">
|
||||
</table>
|
||||
<table id="devices-table"></table>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('devices').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
await getSources('api/ffmpeg/devices', 'devices-table');
|
||||
await getSources('devices-table', 'api/ffmpeg/devices');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<button id="hardware">FFmpeg Hardware</button>
|
||||
<div class="module">
|
||||
<table id="hardware-table">
|
||||
</table>
|
||||
<table id="hardware-table"></table>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('hardware').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
await getSources('api/ffmpeg/hardware', 'hardware-table');
|
||||
await getSources('hardware-table', 'api/ffmpeg/hardware');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -224,8 +227,7 @@
|
||||
<input type="text" name="project_id" placeholder="project_id">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<table id="nest-table">
|
||||
</table>
|
||||
<table id="nest-table"></table>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('nest').addEventListener('click', async ev => {
|
||||
@@ -239,7 +241,7 @@
|
||||
const url = new URL('api/nest?' + query.toString(), location.href);
|
||||
|
||||
const r = await fetch(url, {cache: 'no-cache'});
|
||||
await getSources(r, 'nest-table');
|
||||
await getSources('nest-table', r);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -251,7 +253,7 @@
|
||||
<script>
|
||||
document.getElementById('hass').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
await getSources('api/hass', 'hass-table');
|
||||
await getSources('hass-table', 'api/hass');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -267,7 +269,7 @@
|
||||
<script>
|
||||
document.getElementById('onvif').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
await getSources('api/onvif', 'onvif-table');
|
||||
await getSources('onvif-table', 'api/onvif');
|
||||
});
|
||||
|
||||
document.getElementById('onvif-form').addEventListener('submit', async ev => {
|
||||
@@ -276,7 +278,7 @@
|
||||
const url = new URL('api/onvif', location.href);
|
||||
url.searchParams.set('src', ev.target.elements['src'].value);
|
||||
|
||||
await getSources(url.toString(), 'onvif-table');
|
||||
await getSources('onvif-table', url.toString());
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -294,13 +296,13 @@
|
||||
<script>
|
||||
document.getElementById('roborock').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
await getSources('api/roborock', 'roborock-table');
|
||||
await getSources('roborock-table', 'api/roborock');
|
||||
});
|
||||
|
||||
document.getElementById('roborock-form').addEventListener('submit', async ev => {
|
||||
ev.preventDefault();
|
||||
const r = await fetch('api/roborock', {method: 'POST', body: new FormData(ev.target)});
|
||||
await getSources(r, 'roborock-table');
|
||||
await getSources('roborock-table', r);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -312,7 +314,7 @@
|
||||
<script>
|
||||
document.getElementById('webtorrent').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
await getSources('api/webtorrent', 'webtorrent-table');
|
||||
await getSources('webtorrent-table', 'api/webtorrent');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user