penguin/homekey-go

homekey-go

internal/blegateway/discovery.go

raw ยท 3849 bytes

package blegateway

import (
	"context"
	"fmt"
	"github.com/godbus/dbus/v5"
	"sort"
	"strings"
	"time"
)

type AdapterInfo struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Address string `json:"address"`
	Powered bool   `json:"powered"`
}
type ReaderInfo struct {
	Name    string `json:"name"`
	Address string `json:"address"`
	Adapter string `json:"adapter"`
}
type Discovery struct {
	Adapters []AdapterInfo `json:"adapters"`
	Readers  []ReaderInfo  `json:"readers"`
	Warnings []string      `json:"warnings"`
}

func discoverySnapshot(o objects, known map[string]bool) Discovery {
	d := Discovery{Adapters: []AdapterInfo{}, Readers: []ReaderInfo{}, Warnings: []string{}}
	services := map[dbus.ObjectPath]bool{}
	for _, ifs := range o {
		p, ok := ifs["org.bluez.GattService1"]
		if ok && strings.EqualFold(text(p, "UUID"), ServiceUUID) {
			owner, _ := p["Device"].Value().(dbus.ObjectPath)
			services[owner] = true
		}
	}
	for path, ifs := range o {
		if p, ok := ifs["org.bluez.Adapter1"]; ok {
			powered, _ := p["Powered"].Value().(bool)
			name := text(p, "Alias")
			if name == "" {
				name = text(p, "Name")
			}
			id := string(path)
			id = id[strings.LastIndex(id, "/")+1:]
			d.Adapters = append(d.Adapters, AdapterInfo{ID: id, Name: name, Address: text(p, "Address"), Powered: powered})
		}
		if p, ok := ifs["org.bluez.Device1"]; ok {
			address := strings.ToUpper(text(p, "Address"))
			match := known[address] || services[path]
			uuids, _ := p["UUIDs"].Value().([]string)
			for _, u := range uuids {
				if strings.EqualFold(u, ServiceUUID) {
					match = true
				}
			}
			if !match {
				continue
			}
			adapter, _ := p["Adapter"].Value().(dbus.ObjectPath)
			id := string(adapter)
			id = id[strings.LastIndex(id, "/")+1:]
			name := text(p, "Alias")
			if name == "" {
				name = text(p, "Name")
			}
			if name == "" {
				name = "Home Key reader"
			}
			d.Readers = append(d.Readers, ReaderInfo{Name: name, Address: address, Adapter: id})
		}
	}
	sort.Slice(d.Adapters, func(i, j int) bool { return d.Adapters[i].ID < d.Adapters[j].ID })
	sort.Slice(d.Readers, func(i, j int) bool {
		a, b := d.Readers[i], d.Readers[j]
		return a.Adapter+a.Address < b.Adapter+b.Address
	})
	return d
}

// Discover uses a separate BlueZ connection and owns only its own discovery
// sessions. It never pairs, connects, disconnects, or changes adapter settings.
func Discover(ctx context.Context, known []string) (Discovery, error) {
	c, err := dbus.ConnectSystemBus()
	if err != nil {
		return Discovery{}, fmt.Errorf("Bluetooth unavailable: %w", err)
	}
	defer c.Close()
	o, err := getObjects(ctx, c)
	if err != nil {
		return Discovery{}, err
	}
	addresses := map[string]bool{}
	for _, a := range known {
		addresses[strings.ToUpper(strings.TrimSpace(a))] = true
	}
	initial := discoverySnapshot(o, addresses)
	var scans []dbus.BusObject
	warnings := []string{}
	defer func() {
		cleanup, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		for _, a := range scans {
			_ = a.CallWithContext(cleanup, "org.bluez.Adapter1.StopDiscovery", 0).Err
		}
	}()
	for _, a := range initial.Adapters {
		if !a.Powered {
			warnings = append(warnings, a.ID+" is switched off")
			continue
		}
		obj := c.Object("org.bluez", dbus.ObjectPath("/org/bluez/"+a.ID))
		if e := obj.CallWithContext(ctx, "org.bluez.Adapter1.StartDiscovery", 0).Err; e != nil {
			warnings = append(warnings, a.ID+": "+e.Error())
			continue
		}
		scans = append(scans, obj)
	}
	if len(scans) > 0 && !wait(ctx, 5*time.Second) {
		return Discovery{}, ctx.Err()
	}
	o, err = getObjects(ctx, c)
	if err != nil {
		return Discovery{}, err
	}
	d := discoverySnapshot(o, addresses)
	d.Warnings = warnings
	if len(d.Adapters) == 0 {
		d.Warnings = append(d.Warnings, "No local Bluetooth adapters found")
	}
	return d, nil
}