install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,97 @@
# Apple HomeKit
This module supports both client and server for the [Apple HomeKit](https://www.apple.com/home-app/accessories/) protocol.
## HomeKit Client
**Important:**
- You can use HomeKit Cameras **without Apple devices** (iPhone, iPad, etc.), it's just a yet another protocol
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home), you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc, you can't pair it with an iPhone
- HomeKit device should be on the same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between the device and go2rtc
go2rtc supports importing paired HomeKit devices from [Home Assistant](../hass/README.md).
So you can use HomeKit camera with Home Assistant and go2rtc simultaneously.
If you are using Home Assistant, I recommend pairing devices with it; it will give you more options.
You can pair device with go2rtc on the HomeKit page. If you can't see your devices, reload the page.
Also, try rebooting your HomeKit device (power off). If you still can't see it, you have a problem with mDNS.
If you see a device but it does not have a pairing button, it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge, etc.). You need to delete the device from that ecosystem, and it will be available for pairing. If you cannot unpair the device, you will have to reset it.
**Important:**
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violations
- Audio can't be played in `VLC` and probably any other player
- Audio should be transcoded for use with MSE, WebRTC, etc.
### Client Configuration
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
```yaml
streams:
aqara_g3:
- hass:Camera-Hub-G3-AB12
- ffmpeg:aqara_g3#audio=aac#audio=opus
```
RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
## HomeKit Server
[`new in v1.7.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)
HomeKit module can work in two modes:
- export any H264 camera to Apple HomeKit
- transparent proxy any Apple HomeKit camera (Aqara, Eve, Eufy, etc.) back to Apple HomeKit, so you will have all camera features in Apple Home and also will have RTSP/WebRTC/MP4/etc. from your HomeKit camera
**Important**
- HomeKit cameras support only H264 video and OPUS audio
### Server Configuration
**Minimal config**
```yaml
streams:
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
homekit:
dahua1: # same stream ID from streams list, default PIN - 19550224
```
**Full config**
```yaml
streams:
dahua1:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
- ffmpeg:dahua1#video=h264#hardware # if your camera doesn't support H264, important for HomeKit
- ffmpeg:dahua1#audio=opus # only OPUS audio supported by HomeKit
homekit:
dahua1: # same stream ID from streams list
pin: 12345678 # custom PIN, default: 19550224
name: Dahua camera # custom camera name, default: generated from stream ID
device_id: dahua1 # custom ID, default: generated from stream ID
device_private: dahua1 # custom key, default: generated from stream ID
```
**Proxy HomeKit camera**
- Video stream from HomeKit camera to Apple device (iPhone, Apple TV) will be transmitted directly
- Video stream from HomeKit camera to RTSP/WebRTC/MP4/etc. will be transmitted via go2rtc
```yaml
streams:
aqara1:
- homekit://...
- ffmpeg:aqara1#audio=aac#audio=opus # optional audio transcoding
homekit:
aqara1: # same stream ID from streams list
```
@@ -0,0 +1,181 @@
package homekit
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
func apiDiscovery(w http.ResponseWriter, r *http.Request) {
sources, err := discovery()
if err != nil {
api.Error(w, err)
return
}
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)
}
func apiHomekit(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
switch r.Method {
case "GET":
if id := r.Form.Get("id"); id != "" {
if srv := servers[id]; srv != nil {
api.ResponsePrettyJSON(w, srv)
} else {
http.Error(w, "server not found", http.StatusNotFound)
}
} else {
api.ResponsePrettyJSON(w, servers)
}
case "POST":
id := r.Form.Get("id")
rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin")
if err := apiPair(id, rawURL); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
case "DELETE":
id := r.Form.Get("id")
if err := apiUnpair(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
stream := streams.Get(id)
if stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
rawURL := findHomeKitURL(stream.Sources())
if rawURL == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
client, err := hap.Dial(rawURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer client.Close()
res, err := client.Get(hap.PathAccessories)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", api.MimeJSON)
_, _ = io.Copy(w, res.Body)
}
func discovery() ([]*api.Source, error) {
var sources []*api.Source
// 1. Get streams from Discovery
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
log.Trace().Msgf("[homekit] mdns=%s", entry)
category := entry.Info[hap.TXTCategory]
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
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
}
return sources, nil
}
func apiPair(id, url string) error {
conn, err := hap.Pair(url)
if err != nil {
return err
}
streams.New(id, conn.URL())
return app.PatchConfig([]string{"streams", id}, conn.URL())
}
func apiUnpair(id string) error {
stream := streams.Get(id)
if stream == nil {
return errors.New(api.StreamNotFound)
}
rawURL := findHomeKitURL(stream.Sources())
if rawURL == "" {
return errors.New("not homekit source")
}
if err := hap.Unpair(rawURL); err != nil {
return err
}
streams.Delete(id)
return app.PatchConfig([]string{"streams", id}, nil)
}
func findHomeKitURLs() map[string]*url.URL {
urls := map[string]*url.URL{}
for name, sources := range streams.GetAllSources() {
if rawURL := findHomeKitURL(sources); rawURL != "" {
if u, err := url.Parse(rawURL); err == nil {
urls[name] = u
}
}
}
return urls
}
@@ -0,0 +1,211 @@
package homekit
import (
"errors"
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]struct {
Pin string `yaml:"pin"`
Name string `yaml:"name"`
DeviceID string `yaml:"device_id"`
DevicePrivate string `yaml:"device_private"`
CategoryID string `yaml:"category_id"`
Pairings []string `yaml:"pairings"`
} `yaml:"homekit"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("homekit")
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("api/homekit", apiHomekit)
api.HandleFunc("api/homekit/accessories", apiHomekitAccessories)
api.HandleFunc("api/discovery/homekit", apiDiscovery)
if cfg.Mod == nil {
return
}
hosts = map[string]*server{}
servers = map[string]*server{}
var entries []*mdns.ServiceEntry
for id, conf := range cfg.Mod {
stream := streams.Get(id)
if stream == nil {
log.Warn().Msgf("[homekit] missing stream: %s", id)
continue
}
if conf.Pin == "" {
conf.Pin = "19550224" // default PIN
}
pin, err := hap.SanitizePin(conf.Pin)
if err != nil {
log.Error().Err(err).Caller().Send()
continue
}
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
name := calcName(conf.Name, deviceID)
setupID := calcSetupID(id)
srv := &server{
stream: id,
pairings: conf.Pairings,
setupID: setupID,
}
srv.hap = &hap.Server{
Pin: pin,
DeviceID: deviceID,
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
GetClientPublic: srv.GetPair,
}
srv.mdns = &mdns.ServiceEntry{
Name: name,
Port: uint16(api.Port),
Info: map[string]string{
hap.TXTConfigNumber: "1",
hap.TXTFeatureFlags: "0",
hap.TXTDeviceID: deviceID,
hap.TXTModel: app.UserAgent,
hap.TXTProtoVersion: "1.1",
hap.TXTStateNumber: "1",
hap.TXTStatusFlags: hap.StatusNotPaired,
hap.TXTCategory: calcCategoryID(conf.CategoryID),
hap.TXTSetupHash: hap.SetupHash(setupID, deviceID),
},
}
entries = append(entries, srv.mdns)
srv.UpdateStatus()
if url := findHomeKitURL(stream.Sources()); url != "" {
// 1. Act as transparent proxy for HomeKit camera
srv.proxyURL = url
} else {
// 2. Act as basic HomeKit camera
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
}
host := srv.mdns.Host(mdns.ServiceHAP)
hosts[host] = srv
servers[id] = srv
log.Trace().Msgf("[homekit] new server: %s", srv.mdns)
}
api.HandleFunc(hap.PathPairSetup, hapHandler)
api.HandleFunc(hap.PathPairVerify, hapHandler)
go func() {
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
log.Error().Err(err).Caller().Send()
}
}()
}
var log zerolog.Logger
var hosts map[string]*server
var servers map[string]*server
func streamHandler(rawURL string) (core.Producer, error) {
if srtp.Server == nil {
return nil, errors.New("homekit: can't work without SRTP server")
}
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
client, err := homekit.Dial(rawURL, srtp.Server)
if client != nil && rawQuery != "" {
query := streams.ParseQuery(rawQuery)
client.MaxWidth = core.Atoi(query.Get("maxwidth"))
client.MaxHeight = core.Atoi(query.Get("maxheight"))
client.Bitrate = parseBitrate(query.Get("bitrate"))
}
return client, err
}
func resolve(host string) *server {
if len(hosts) == 1 {
for _, srv := range hosts {
return srv
}
}
if srv, ok := hosts[host]; ok {
return srv
}
return nil
}
func hapHandler(w http.ResponseWriter, r *http.Request) {
// Can support multiple HomeKit cameras on single port ONLY for Apple devices.
// Doesn't support Home Assistant and any other open source projects
// because they don't send the host header in requests.
srv := resolve(r.Host)
if srv == nil {
log.Error().Msg("[homekit] unknown host: " + r.Host)
return
}
srv.Handle(w, r)
}
func findHomeKitURL(sources []string) string {
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 location
}
}
return ""
}
func parseBitrate(s string) int {
n := len(s)
if n == 0 {
return 0
}
var k int
switch n--; s[n] {
case 'K':
k = 1024
s = s[:n]
case 'M':
k = 1024 * 1024
s = s[:n]
default:
k = 1
}
return k * core.Atoi(s)
}
@@ -0,0 +1,405 @@
package homekit
import (
"crypto/ed25519"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
srtp2 "github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/hap/hds"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
type server struct {
hap *hap.Server // server for HAP connection and encryption
mdns *mdns.ServiceEntry
pairings []string // pairings list
conns []any
mu sync.Mutex
accessory *hap.Accessory // HAP accessory
consumer *homekit.Consumer
proxyURL string
setupID string
stream string // stream name from YAML
}
func (s *server) MarshalJSON() ([]byte, error) {
v := struct {
Name string `json:"name"`
DeviceID string `json:"device_id"`
Paired int `json:"paired,omitempty"`
CategoryID string `json:"category_id,omitempty"`
SetupCode string `json:"setup_code,omitempty"`
SetupID string `json:"setup_id,omitempty"`
Conns []any `json:"connections,omitempty"`
}{
Name: s.mdns.Name,
DeviceID: s.mdns.Info[hap.TXTDeviceID],
CategoryID: s.mdns.Info[hap.TXTCategory],
Paired: len(s.pairings),
Conns: s.conns,
}
if v.Paired == 0 {
v.SetupCode = s.hap.Pin
v.SetupID = s.setupID
}
return json.Marshal(v)
}
func (s *server) Handle(w http.ResponseWriter, r *http.Request) {
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer conn.Close()
// Fix reading from Body after Hijack.
r.Body = io.NopCloser(rw)
switch r.RequestURI {
case hap.PathPairSetup:
id, key, err := s.hap.PairSetup(r, rw)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
s.AddPair(id, key, hap.PermissionAdmin)
case hap.PathPairVerify:
id, key, err := s.hap.PairVerify(r, rw)
if err != nil {
log.Debug().Err(err).Caller().Send()
return
}
log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[homekit] %s: new conn", conn.RemoteAddr())
controller, err := hap.NewConn(conn, rw, key, false)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
s.AddConn(controller)
defer s.DelConn(controller)
var handler homekit.HandlerFunc
switch {
case s.accessory != nil:
handler = homekit.ServerHandler(s)
case s.proxyURL != "":
client, err := hap.Dial(s.proxyURL)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
handler = homekit.ProxyHandler(s, client.Conn)
}
// If your iPhone goes to sleep, it will be an EOF error.
if err = handler(controller); err != nil && !errors.Is(err, io.EOF) {
log.Error().Err(err).Caller().Send()
return
}
}
}
type logger struct {
v any
}
func (l logger) String() string {
switch v := l.v.(type) {
case *hap.Conn:
return "hap " + v.RemoteAddr().String()
case *hds.Conn:
return "hds " + v.RemoteAddr().String()
case *homekit.Consumer:
return "rtp " + v.RemoteAddr
}
return "unknown"
}
func (s *server) AddConn(v any) {
log.Trace().Str("stream", s.stream).Msgf("[homekit] add conn %s", logger{v})
s.mu.Lock()
s.conns = append(s.conns, v)
s.mu.Unlock()
}
func (s *server) DelConn(v any) {
log.Trace().Str("stream", s.stream).Msgf("[homekit] del conn %s", logger{v})
s.mu.Lock()
if i := slices.Index(s.conns, v); i >= 0 {
s.conns = slices.Delete(s.conns, i, i+1)
}
s.mu.Unlock()
}
func (s *server) UpdateStatus() {
// true status is important, or device may be offline in Apple Home
if len(s.pairings) == 0 {
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusNotPaired
} else {
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusPaired
}
}
func (s *server) pairIndex(id string) int {
id = "client_id=" + id
for i, pairing := range s.pairings {
if strings.HasPrefix(pairing, id) {
return i
}
}
return -1
}
func (s *server) GetPair(id string) []byte {
s.mu.Lock()
defer s.mu.Unlock()
if i := s.pairIndex(id); i >= 0 {
query, _ := url.ParseQuery(s.pairings[i])
b, _ := hex.DecodeString(query.Get("client_public"))
return b
}
return nil
}
func (s *server) AddPair(id string, public []byte, permissions byte) {
log.Debug().Str("stream", s.stream).Msgf("[homekit] add pair id=%s public=%x perm=%d", id, public, permissions)
s.mu.Lock()
if s.pairIndex(id) < 0 {
s.pairings = append(s.pairings, fmt.Sprintf(
"client_id=%s&client_public=%x&permissions=%d", id, public, permissions,
))
s.UpdateStatus()
s.PatchConfig()
}
s.mu.Unlock()
}
func (s *server) DelPair(id string) {
log.Debug().Str("stream", s.stream).Msgf("[homekit] del pair id=%s", id)
s.mu.Lock()
if i := s.pairIndex(id); i >= 0 {
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
s.UpdateStatus()
s.PatchConfig()
}
s.mu.Unlock()
}
func (s *server) PatchConfig() {
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
log.Error().Err(err).Msgf(
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
)
}
}
func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory {
return []*hap.Accessory{s.accessory}
}
func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
log.Trace().Str("stream", s.stream).Msgf("[homekit] get char aid=%d iid=0x%x", aid, iid)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
log.Warn().Msgf("[homekit] get unknown characteristic: %d", iid)
return nil
}
switch char.Type {
case camera.TypeSetupEndpoints:
consumer := s.consumer
if consumer == nil {
return nil
}
answer := consumer.GetAnswer()
v, err := tlv8.MarshalBase64(answer)
if err != nil {
return nil
}
return v
}
return char.Value
}
func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {
log.Trace().Str("stream", s.stream).Msgf("[homekit] set char aid=%d iid=0x%x value=%v", aid, iid, value)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
log.Warn().Msgf("[homekit] set unknown characteristic: %d", iid)
return
}
switch char.Type {
case camera.TypeSetupEndpoints:
var offer camera.SetupEndpointsRequest
if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
return
}
consumer := homekit.NewConsumer(conn, srtp2.Server)
consumer.SetOffer(&offer)
s.consumer = consumer
case camera.TypeSelectedStreamConfiguration:
var conf camera.SelectedStreamConfiguration
if err := tlv8.UnmarshalBase64(value, &conf); err != nil {
return
}
log.Trace().Str("stream", s.stream).Msgf("[homekit] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command)
switch conf.Control.Command {
case camera.SessionCommandEnd:
for _, consumer := range s.conns {
if consumer, ok := consumer.(*homekit.Consumer); ok {
if consumer.SessionID() == conf.Control.SessionID {
_ = consumer.Stop()
return
}
}
}
case camera.SessionCommandStart:
consumer := s.consumer
if consumer == nil {
return
}
if !consumer.SetConfig(&conf) {
log.Warn().Msgf("[homekit] wrong config")
return
}
s.AddConn(consumer)
stream := streams.Get(s.stream)
if err := stream.AddConsumer(consumer); err != nil {
return
}
go func() {
_, _ = consumer.WriteTo(nil)
stream.RemoveConsumer(consumer)
s.DelConn(consumer)
}()
}
}
}
func (s *server) GetImage(conn net.Conn, width, height int) []byte {
log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", width, height)
stream := streams.Get(s.stream)
cons := magic.NewKeyframe()
if err := stream.AddConsumer(cons); err != nil {
return nil
}
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b := once.Buffer()
stream.RemoveConsumer(cons)
switch cons.CodecName() {
case core.CodecH264, core.CodecH265:
var err error
if b, err = ffmpeg.JPEGWithScale(b, width, height); err != nil {
return nil
}
}
return b
}
func calcName(name, seed string) string {
if name != "" {
return name
}
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("go2rtc-%02X%02X", b[0], b[2])
}
func calcDeviceID(deviceID, seed string) string {
if deviceID != "" {
if len(deviceID) >= 17 {
// 1. Returd device_id as is (ex. AA:BB:CC:DD:EE:FF)
return deviceID
}
// 2. Use device_id as seed if not zero
seed = deviceID
}
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("%02X:%02X:%02X:%02X:%02X:%02X", b[32], b[34], b[36], b[38], b[40], b[42])
}
func calcDevicePrivate(private, seed string) []byte {
if private != "" {
// 1. Decode private from HEX string
if b, _ := hex.DecodeString(private); len(b) == ed25519.PrivateKeySize {
// 2. Return if OK
return b
}
// 3. Use private as seed if not zero
seed = private
}
b := sha512.Sum512([]byte(seed))
return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])
}
func calcSetupID(seed string) string {
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("%02X%02X", b[44], b[46])
}
func calcCategoryID(categoryID string) string {
switch categoryID {
case "bridge":
return hap.CategoryBridge
case "doorbell":
return hap.CategoryDoorbell
}
if core.Atoi(categoryID) > 0 {
return categoryID
}
return hap.CategoryCamera
}