penguin/homekey-go

homekey-go

internal/hkserver/server.go

raw ยท 3897 bytes

// SPDX-License-Identifier: Apache-2.0
package hkserver

import (
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"log"
	"net/http"

	"github.com/brutella/hap"
	"github.com/brutella/hap/accessory"
	"github.com/brutella/hap/characteristic"
	"github.com/brutella/hap/service"
	"homekey.local/provisioner/internal/provision"
)

type Config struct {
	Name, Serial, Finish, PIN, Address string
	Interfaces                         []string
}
type Device struct {
	Server    *hap.Server
	Accessory *accessory.A
	Control   *characteristic.C
	Lock      *service.LockMechanism
}

func bytesCharacteristic(t string, raw []byte, permissions ...string) *characteristic.C {
	c := characteristic.NewBytes(t)
	c.Permissions = permissions
	c.SetValue(raw)
	return c.C
}

func New(store *provision.Store, cfg Config) (*Device, error) {
	colors := map[string]string{"black": "00000000", "tan": "ced5da00", "gold": "aad6ec00", "silver": "e3e3e300"}
	color, ok := colors[cfg.Finish]
	if !ok {
		return nil, fmt.Errorf("finish must be black, tan, gold, or silver")
	}
	finish, _ := hex.DecodeString(color)
	// HAP category 6 is a standalone door lock, not a bridge.
	a := accessory.New(accessory.Info{Name: cfg.Name, SerialNumber: cfg.Serial, Manufacturer: "DIY", Model: "Go Home Key", Firmware: "0.1.0"}, 6)
	a.IdentifyFunc = func(*http.Request) { log.Print("Identify: virtual Home Key lock") }
	a.Info.AddC(bytesCharacteristic("26C", provision.TLV(1, finish), characteristic.PermissionRead))
	lock := service.NewLockMechanism()
	lock.Primary = true
	lock.LockCurrentState.SetValue(1)
	lock.LockTargetState.SetValue(1)
	lock.LockTargetState.ValidVals = []int{0, 1}
	lock.LockTargetState.OnSetRemoteValue(func(v int) error {
		log.Printf("Virtual lock state: %d (simulation; no physical lock connected)", v)
		return lock.LockCurrentState.SetValue(v)
	})
	a.AddS(lock.S)
	management := service.NewLockManagement()
	management.Version.SetValue("1.0")
	management.LockControlPoint.SetValue([]byte{})
	management.LockControlPoint.Stateless = true
	management.LockControlPoint.SetValueRequestFunc = func(any, *http.Request) (any, int) { return nil, -70406 }
	a.AddS(management.S)
	nfc := service.New("266")
	state := characteristic.NewInt("263")
	state.Format = characteristic.FormatUInt16
	state.Permissions = []string{characteristic.PermissionRead, characteristic.PermissionEvents}
	state.SetValue(0) // Matches the reference implementation; semantics not established.
	nfc.AddC(state.C)
	control := bytesCharacteristic("264", []byte{}, characteristic.PermissionRead, characteristic.PermissionWrite, characteristic.PermissionWriteResponse)
	control.Stateless = true
	control.ValueRequestFunc = func(*http.Request) (any, int) { return "", 0 }
	control.SetValueRequestFunc = func(value any, _ *http.Request) (any, int) {
		encoded, ok := value.(string)
		if !ok || len(encoded) > base64.StdEncoding.EncodedLen(provision.MaxMessage) {
			return nil, -70410
		}
		raw, err := base64.StdEncoding.Strict().DecodeString(encoded)
		if err != nil {
			return nil, -70410
		}
		response, err := store.Handle(raw)
		if err != nil {
			// No raw TLVs, keys, public keys or credential payloads in logs.
			log.Printf("Provisioning request rejected: %v", err)
			return nil, -70402
		}
		v := store.Summary()
		log.Printf("Provisioning exchange complete: reader=%t issuers=%d endpoints=%d", v.ReaderProvisioned, v.Issuers, v.Endpoints)
		return base64.StdEncoding.EncodeToString(response), 0
	}
	nfc.AddC(control)
	supported := append(provision.TLV(1, []byte{provision.Capacity}), provision.TLV(2, []byte{provision.Capacity})...)
	nfc.AddC(bytesCharacteristic("265", supported, characteristic.PermissionRead))
	a.AddS(nfc)
	s, err := hap.NewServer(store, a)
	if err != nil {
		return nil, err
	}
	s.Pin = cfg.PIN
	s.Addr = cfg.Address
	s.Ifaces = cfg.Interfaces
	return &Device{Server: s, Accessory: a, Control: control, Lock: lock}, nil
}