install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Hass
|
||||
|
||||
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
|
||||
|
||||
- [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
|
||||
- [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
|
||||
- [ONVIF](https://www.home-assistant.io/integrations/onvif/)
|
||||
- [Roborock](https://github.com/humbertogontijo/homeassistant-roborock) vacuums with camera
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
hass:
|
||||
config: "/config" # skip this setting if you are a Home Assistant add-on user
|
||||
|
||||
streams:
|
||||
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
|
||||
aqara_g3: hass:Camera-Hub-G3-AB12
|
||||
```
|
||||
|
||||
### WebRTC Cameras
|
||||
|
||||
[`new in v1.6.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)
|
||||
|
||||
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this format.
|
||||
|
||||
**Important.** The Nest API only allows you to get a link to a stream for 5 minutes.
|
||||
Do not use this with Frigate! If the stream expires, Frigate will consume all available RAM on your machine within seconds.
|
||||
It's recommended to use [Nest source](../nest/README.md) - it supports extending the stream.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# link to Home Assistant Supervised
|
||||
hass-webrtc1: hass://supervisor?entity_id=camera.nest_doorbell
|
||||
# link to external Home Assistant with Long-Lived Access Tokens
|
||||
hass-webrtc2: hass://192.168.1.123:8123?entity_id=camera.nest_doorbell&token=eyXYZ...
|
||||
```
|
||||
|
||||
### RTSP Cameras
|
||||
|
||||
By default, the Home Assistant API does not allow you to get a dynamic RTSP link to a camera stream. [This method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-cameras-from-home-assistant-to-go2rtc-or-frigate) can work around it.
|
||||
@@ -0,0 +1,104 @@
|
||||
package hass
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
)
|
||||
|
||||
func apiOK(w http.ResponseWriter, r *http.Request) {
|
||||
api.Response(w, `{"status":1,"payload":{}}`, api.MimeJSON)
|
||||
}
|
||||
|
||||
func apiStream(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
// /stream/{id}/add
|
||||
case strings.HasSuffix(r.RequestURI, "/add"):
|
||||
var v addJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// we can get three types of links:
|
||||
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
|
||||
// 2. static link to Hass camera
|
||||
// 3. dynamic link to Hass camera
|
||||
if _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil {
|
||||
apiOK(w, r)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// /stream/{id}/channel/0/webrtc
|
||||
default:
|
||||
i := strings.IndexByte(r.RequestURI[8:], '/')
|
||||
if i <= 0 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name := r.RequestURI[8 : 8+i]
|
||||
stream := streams.Get(name)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s := r.FormValue("data")
|
||||
offer, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
s = base64.StdEncoding.EncodeToString([]byte(s))
|
||||
_, _ = w.Write([]byte(s))
|
||||
}
|
||||
}
|
||||
|
||||
func HassioAddr() string {
|
||||
ints, _ := net.Interfaces()
|
||||
|
||||
for _, i := range ints {
|
||||
if i.Name != "hassio" {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, _ := i.Addrs()
|
||||
for _, addr := range addrs {
|
||||
if addr, ok := addr.(*net.IPNet); ok {
|
||||
return addr.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
type addJSON struct {
|
||||
Name string `json:"name"`
|
||||
Channels struct {
|
||||
First struct {
|
||||
//Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
} `json:"0"`
|
||||
} `json:"channels"`
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package hass
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/roborock"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hass"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var conf struct {
|
||||
API struct {
|
||||
Listen string `yaml:"listen"`
|
||||
} `yaml:"api"`
|
||||
Mod struct {
|
||||
Config string `yaml:"config"`
|
||||
} `yaml:"hass"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&conf)
|
||||
|
||||
log = app.GetLogger("hass")
|
||||
|
||||
// support API for https://www.home-assistant.io/integrations/rtsp_to_webrtc/
|
||||
api.HandleFunc("/static", apiOK)
|
||||
api.HandleFunc("/streams", apiOK)
|
||||
api.HandleFunc("/stream/", apiStream)
|
||||
|
||||
streams.RedirectFunc("hass", func(rawURL string) (string, error) {
|
||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||
|
||||
if location := entities[rawURL[5:]]; location != "" {
|
||||
if rawQuery != "" {
|
||||
return location + "#" + rawQuery, nil
|
||||
}
|
||||
return location, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
})
|
||||
|
||||
streams.HandleFunc("hass", func(source string) (core.Producer, error) {
|
||||
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
||||
return hass.NewClient(source)
|
||||
})
|
||||
|
||||
// load static entries from Hass config
|
||||
if err := importConfig(conf.Mod.Config); err != nil {
|
||||
log.Trace().Msgf("[hass] can't import config: %s", err)
|
||||
|
||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.Error(w, "no hass config", http.StatusNotFound)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
||||
once.Do(func() {
|
||||
// load WebRTC entities from Hass API, works only for add-on version
|
||||
if token := hass.SupervisorToken(); token != "" {
|
||||
if err := importWebRTC(token); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var items []*api.Source
|
||||
for name, url := range entities {
|
||||
items = append(items, &api.Source{
|
||||
Name: name, URL: "hass:" + name, Location: url,
|
||||
})
|
||||
}
|
||||
api.ResponseSources(w, items)
|
||||
})
|
||||
|
||||
// for Addon listen on hassio interface, so WebUI feature will work
|
||||
if conf.API.Listen == "127.0.0.1:1984" {
|
||||
if addr := HassioAddr(); addr != "" {
|
||||
addr += ":1984"
|
||||
go func() {
|
||||
log.Info().Str("addr", addr).Msg("[hass] listen")
|
||||
if err := http.ListenAndServe(addr, api.Handler); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func importConfig(config string) error {
|
||||
// support load cameras from Hass config file
|
||||
filename := path.Join(config, ".storage/core.config_entries")
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var storage struct {
|
||||
Data struct {
|
||||
Entries []struct {
|
||||
Title string `json:"title"`
|
||||
Domain string `json:"domain"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Options json.RawMessage `json:"options"`
|
||||
} `json:"entries"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(b, &storage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entrie := range storage.Data.Entries {
|
||||
switch entrie.Domain {
|
||||
case "generic":
|
||||
var options struct {
|
||||
StreamSource string `json:"stream_source"`
|
||||
}
|
||||
if err = json.Unmarshal(entrie.Options, &options); err != nil {
|
||||
continue
|
||||
}
|
||||
entities[entrie.Title] = options.StreamSource
|
||||
|
||||
case "homekit_controller":
|
||||
if !bytes.Contains(entrie.Data, []byte("iOSPairingId")) {
|
||||
continue
|
||||
}
|
||||
|
||||
var data struct {
|
||||
ClientID string `json:"iOSPairingId"`
|
||||
ClientPrivate string `json:"iOSDeviceLTSK"`
|
||||
ClientPublic string `json:"iOSDeviceLTPK"`
|
||||
DeviceID string `json:"AccessoryPairingID"`
|
||||
DevicePublic string `json:"AccessoryLTPK"`
|
||||
DeviceHost string `json:"AccessoryIP"`
|
||||
DevicePort uint16 `json:"AccessoryPort"`
|
||||
}
|
||||
if err = json.Unmarshal(entrie.Data, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
entities[entrie.Title] = fmt.Sprintf(
|
||||
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
|
||||
data.DeviceHost, data.DevicePort,
|
||||
data.ClientID, data.ClientPrivate, data.ClientPublic,
|
||||
data.DeviceID, data.DevicePublic,
|
||||
)
|
||||
|
||||
case "roborock":
|
||||
_ = json.Unmarshal(entrie.Data, &roborock.Auth)
|
||||
|
||||
case "onvif":
|
||||
var data struct {
|
||||
Host string `json:"host" json:"host"`
|
||||
Port uint16 `json:"port" json:"port"`
|
||||
Username string `json:"username" json:"username"`
|
||||
Password string `json:"password" json:"password"`
|
||||
}
|
||||
if err = json.Unmarshal(entrie.Data, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if data.Username != "" && data.Password != "" {
|
||||
entities[entrie.Title] = fmt.Sprintf(
|
||||
"onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port,
|
||||
)
|
||||
} else {
|
||||
entities[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
|
||||
}
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debug().Str("url", "hass:"+entrie.Title).Msg("[hass] load config")
|
||||
//streams.Get("hass:" + entrie.Title)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importWebRTC(token string) error {
|
||||
hassAPI, err := hass.NewAPI("ws://supervisor/core/websocket", token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webrtcEntities, err := hassAPI.GetWebRTCEntities()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(webrtcEntities) == 0 {
|
||||
log.Debug().Msg("[hass] webrtc cameras not found")
|
||||
}
|
||||
|
||||
for name, entityID := range webrtcEntities {
|
||||
entities[name] = "hass://supervisor?entity_id=" + entityID
|
||||
|
||||
log.Debug().Msgf("[hass] load webrtc name=%s entity_id=%s", name, entityID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var entities = map[string]string{}
|
||||
var log zerolog.Logger
|
||||
var once sync.Once
|
||||
Reference in New Issue
Block a user