commit 7b3592d95b60d32ce3cfc71f9860ad34d7010b91
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-10-07T14:36:55Z |
| subject | Revert move to impl |
commit 7b3592d95b60d32ce3cfc71f9860ad34d7010b91
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-10-07T14:36:55Z
Revert move to impl
---
build.env | 2 +-
drivers/sesame/README.md | 8 -------
drivers/sesame/client.go | 34 +++++-----------------------
drivers/sesame/client_test.go | 52 -------------------------------------------
ha-app/CHANGELOG.md | 4 ++++
ha-app/config.yaml | 2 +-
internal/app/page.html | 8 +++----
internal/app/physical.go | 29 ++----------------------
internal/app/physical_test.go | 18 ---------------
internal/physical/manager.go | 10 ---------
10 files changed, 16 insertions(+), 151 deletions(-)
diff --git a/build.env b/build.env
index eafb2a5..8fadaae 100644
--- a/build.env
+++ b/build.env
@@ -1,4 +1,4 @@
IMAGE_NAME=ghcr.io/tgckpg/homekey-go
-IMAGE_TAG=0.0.11
+IMAGE_TAG=0.0.12
APT_PROXY=
diff --git a/drivers/sesame/README.md b/drivers/sesame/README.md
index a85d0de..566839e 100644
--- a/drivers/sesame/README.md
+++ b/drivers/sesame/README.md
@@ -33,14 +33,6 @@ other endpoint. Boundary capture is allowed only after firmware has published
its boundary capability. Positions use the SDK's signed 16-bit angle values;
negative values, including -1, are valid.
-`MoveTo(ctx, position)` experimentally sends OS3 item 84 with a signed
-16-bit little-endian absolute angle. The SDK defines this item but does not
-expose it for Sesame 5/6, so its payload and firmware behavior still require
-hardware validation. The calibration popup exposes it as **Set position**.
-It preserves saved endpoints, returns firmware rejection codes, and does not
-claim movement until live status reports it. -32768 is reserved for no target;
-accepted input is -32767 through 32767. Commands are not replayed on reconnect.
-
OS3 framing uses 19 payload bytes per GATT fragment. Local login uses AES-CMAC.
The fixed CCM profile is 13-byte nonce, 4-byte tag, AAD 00; the nonce contains an
8-byte LE counter, zero, and a 4-byte token. Unsupported token lengths fail
diff --git a/drivers/sesame/client.go b/drivers/sesame/client.go
index 7d69d91..59d3c86 100644
--- a/drivers/sesame/client.go
+++ b/drivers/sesame/client.go
@@ -238,10 +238,9 @@ func Probe(ctx context.Context, t Target, c Credential, dial Dialer) error {
}
type operation struct {
- ctx context.Context
- name string
- position *int16
- reply chan error
+ ctx context.Context
+ name string
+ reply chan error
}
type Client struct {
target Target
@@ -263,23 +262,10 @@ func (c *Client) Action(ctx context.Context, name string) error {
default:
return errors.New("unsupported Sesame action")
}
- return c.submit(ctx, name, nil)
-}
-
-// MoveTo tests the OS3 move-to command without changing calibrated endpoints.
-// Firmware support is not advertised; a rejected command is returned to callers.
-func (c *Client) MoveTo(ctx context.Context, position int16) error {
- if position == -32768 {
- return errors.New("-32768 is reserved for no target")
- }
- return c.submit(ctx, "move-to", &position)
-}
-
-func (c *Client) submit(ctx context.Context, name string, position *int16) error {
if !c.State().Online {
return errors.New("physical lock is offline")
}
- op := operation{ctx: ctx, name: name, position: position, reply: make(chan error, 1)}
+ op := operation{ctx, name, make(chan error, 1)}
select {
case c.ops <- op:
case <-ctx.Done():
@@ -357,7 +343,7 @@ func (c *Client) runSession(ctx context.Context) error {
command, cancel := context.WithTimeout(op.ctx, 8*time.Second)
// Service shutdown also cancels an in-flight web/HAP operation.
stop := context.AfterFunc(ctx, cancel)
- e = s.actionPosition(command, op.name, op.position)
+ e = s.action(command, op.name)
stop()
cancel()
op.reply <- e
@@ -369,19 +355,9 @@ func (c *Client) runSession(ctx context.Context) error {
}
}
func (s *session) action(ctx context.Context, name string) error {
- return s.actionPosition(ctx, name, nil)
-}
-
-func (s *session) actionPosition(ctx context.Context, name string, position *int16) error {
var item byte
var payload []byte
switch name {
- case "move-to":
- if position == nil || *position == -32768 {
- return &ResultError{84, 8}
- }
- item = 84
- payload = short(*position)
case "lock":
item = 82
payload = []byte{0, 14}
diff --git a/drivers/sesame/client_test.go b/drivers/sesame/client_test.go
index 029dbf6..1652a5e 100644
--- a/drivers/sesame/client_test.go
+++ b/drivers/sesame/client_test.go
@@ -358,55 +358,3 @@ func TestReconnectUsesNewTokenAndResetsCipher(t *testing.T) {
t.Fatal("unexpected reconnect attempts", attempts.Load())
}
}
-
-func TestMoveToEncodingRejectionAndLiveState(t *testing.T) {
- s, f, peer := readySession(t)
- s.state.publish(81, []byte{0, 0, 0, 128, 45, 0, 16})
- s.state.publish(80, []byte{90, 0, 0, 0, 0, 0})
- var want []byte
- code := byte(0)
- f.write = func(kind byte, b []byte) error {
- if kind != 2 {
- t.Fatal("plaintext move")
- }
- p, e := peer.decrypt(b)
- if e != nil {
- return e
- }
- if !bytes.Equal(p, want) {
- t.Fatalf("move-to = %x, want %x", p, want)
- }
- reply, _ := peer.encrypt([]byte{7, 84, code})
- f.emit(2, reply)
- return nil
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Second)
- defer cancel()
- for _, tc := range []struct {
- position int16
- payload []byte
- }{{0, []byte{84, 0, 0}}, {-1, []byte{84, 255, 255}}, {-32767, []byte{84, 1, 128}}, {32767, []byte{84, 255, 127}}} {
- want = tc.payload
- if e := s.actionPosition(ctx, "move-to", &tc.position); e != nil {
- t.Fatal(e)
- }
- if *s.state.Position != 45 || s.state.Target != nil || *s.state.LockPosition != 90 || *s.state.UnlockPosition != 0 {
- t.Fatal("move fabricated status or changed calibration")
- }
- }
- code = 2 // Unsupported firmware must reject without losing the session.
- position := int16(-1)
- want = []byte{84, 255, 255}
- var rejected *ResultError
- if e := s.actionPosition(ctx, "move-to", &position); !errors.As(e, &rejected) || rejected.Item != 84 || rejected.Code != 2 {
- t.Fatal("missing firmware rejection", e)
- }
- f.write = func(byte, []byte) error { t.Fatal("invalid move sent"); return nil }
- position = -32768
- if e := s.actionPosition(ctx, "move-to", &position); e == nil {
- t.Fatal("accepted reserved target")
- }
- if e := s.actionPosition(ctx, "move-to", nil); e == nil {
- t.Fatal("accepted missing target")
- }
-}
diff --git a/ha-app/CHANGELOG.md b/ha-app/CHANGELOG.md
index fe6d6e1..f23d0d2 100644
--- a/ha-app/CHANGELOG.md
+++ b/ha-app/CHANGELOG.md
@@ -1,5 +1,9 @@
## 0.0.10
+- Move to does not work:(
+
+## 0.0.10
+
- Testing undocumented Move To feature
## 0.0.10
diff --git a/ha-app/config.yaml b/ha-app/config.yaml
index 7b36cdf..53a368d 100644
--- a/ha-app/config.yaml
+++ b/ha-app/config.yaml
@@ -1,5 +1,5 @@
name: Home Key Go
-version: "0.0.11"
+version: "0.0.12"
slug: homekey_go
description: HomeKit virtual lock with Apple Home Key NFC authentication
url: https://sgit.k8s.astropenguin.net/penguin/homekey-go/
diff --git a/internal/app/page.html b/internal/app/page.html
index e628f77..0132aed 100644
--- a/internal/app/page.html
+++ b/internal/app/page.html
@@ -14,7 +14,7 @@
<dialog id="lock-dialog"><form id="lock-form"><h2 id="lock-title">Add lock</h2><small id="lock-id"></small><label>Name <input id="lock-name" required maxlength="128"></label><fieldset><legend>Assigned readers</legend><div id="assignments"></div></fieldset><fieldset><legend>Assigned physical locks</legend><div id="physical-assignments"></div><small>With none assigned, this remains a simulated lock.</small></fieldset><details><summary>HomeKit settings</summary><label>HomeKit port <input id="lock-port" type="number" min="1" max="65535" required></label><label>Wallet artwork <select id="lock-finish"><option>silver</option><option>black</option><option>gold</option><option>tan</option></select></label></details><p id="lock-error" class="error" role="alert"></p><div class="toolbar"><button class="primary" type="submit">Apply to configuration</button><button type="button" id="cancel-lock">Cancel</button></div></form></dialog>
<dialog id="reader-dialog"><form id="reader-form"><h2 id="reader-title">Add reader</h2><label>Available reader <select id="reader-device"></select></label><label id="manual-address-label" hidden>Bluetooth address <input id="reader-address" placeholder="70:AF:09:16:42:5A"></label><label>Bluetooth adapter <select id="reader-adapter"></select></label><label>Reader name <input id="reader-name" required maxlength="128"></label><small id="reader-id"></small><p id="reader-error" class="error" role="alert"></p><div class="toolbar"><button class="primary" type="submit">Apply to configuration</button><button type="button" id="cancel-reader">Cancel</button></div></form></dialog>
<dialog id="physical-dialog"><form id="physical-form"><h2>Add physical lock</h2><p>Select a nearby lock. For a lock already set up in Sesame, import its local key JSON.</p><label>Available lock <select id="physical-device" required></select></label><label>Name <input id="physical-name" required maxlength="128"></label><label>Setup method <select id="physical-mode"><option value="import">Import an existing key</option><option value="register">Register an unregistered lock</option></select></label><label id="physical-key-label">Sesame key JSON <textarea id="physical-key" rows="6" spellcheck="false" autocomplete="off" style="box-sizing:border-box;width:100%;display:block"></textarea></label><p id="physical-register-note" hidden>Registration claims this unregistered lock and saves its key here. This does not reset an existing lock or add it to the Sesame cloud account.</p><p id="physical-error" class="error" role="alert"></p><div class="toolbar"><button id="physical-submit" class="primary" type="submit">Add physical lock</button><button type="button" id="cancel-physical">Cancel</button></div></form></dialog>
-<dialog id="calibration-dialog"><h2 id="calibration-name">Lock positions</h2><p>Turn the thumb-turn manually to each desired position, wait for the displayed position to update, then save it. Test movement with the door open.</p><p id="calibration-status" role="status"></p><dl><dt>Current position</dt><dd id="calibration-position">Waiting…</dd><dt>Locked position</dt><dd id="calibration-locked">Waiting…</dd><dt>Unlocked position</dt><dd id="calibration-unlocked">Waiting…</dd><dt>Lock/unlock boundary</dt><dd id="calibration-boundary">Waiting…</dd></dl><div class="toolbar"><button id="capture-lock">Use current as locked</button><button id="capture-unlock">Use current as unlocked</button><button id="capture-boundary">Use current as boundary</button></div><small id="boundary-help"></small><div class="toolbar"><button id="test-lock">Test lock</button><button id="test-unlock">Test unlock</button></div><form id="position-form"><label>Custom position (°) <input id="custom-position" type="number" min="-32767" max="32767" step="1" required></label><small>Experimental: move to an absolute angle without changing saved positions. Firmware may reject this command.</small><div class="toolbar"><button id="set-position" type="submit">Set position</button></div></form><p id="position-result" role="status"></p><p id="calibration-error" class="error" role="alert"></p><button id="close-calibration">Close</button></dialog>
+<dialog id="calibration-dialog"><h2 id="calibration-name">Lock positions</h2><p>Turn the thumb-turn manually to each desired position, wait for the displayed position to update, then save it. Test movement with the door open.</p><p id="calibration-status" role="status"></p><dl><dt>Current position</dt><dd id="calibration-position">Waiting…</dd><dt>Locked position</dt><dd id="calibration-locked">Waiting…</dd><dt>Unlocked position</dt><dd id="calibration-unlocked">Waiting…</dd><dt>Lock/unlock boundary</dt><dd id="calibration-boundary">Waiting…</dd></dl><div class="toolbar"><button id="capture-lock">Use current as locked</button><button id="capture-unlock">Use current as unlocked</button><button id="capture-boundary">Use current as boundary</button></div><small id="boundary-help"></small><div class="toolbar"><button id="test-lock">Test lock</button><button id="test-unlock">Test unlock</button></div><p id="calibration-error" class="error" role="alert"></p><button id="close-calibration">Close</button></dialog>
<script>
const $=id=>document.getElementById(id);
const apiBase=location.pathname+(location.pathname.endsWith('/')?'':'/');
@@ -75,15 +75,13 @@ $('cancel-physical').onclick=()=>{if(!physicalAdding){$('physical-key').value=''
$('physical-dialog').addEventListener('cancel',e=>{if(physicalAdding)e.preventDefault();else $('physical-key').value=''});
$('physical-form').onsubmit=async e=>{e.preventDefault();if(physicalAdding)return;const target=selectedPhysical();if(!target){$('physical-error').textContent='Scan and select a supported lock first.';return}const mode=$('physical-mode').value,body={name:$('physical-name').value.trim(),target,mode};try{if(mode==='import'){const k=JSON.parse($('physical-key').value);body.key={deviceUUID:k.deviceUUID,deviceModel:k.deviceModel,secretKey:k.secretKey,keyIndex:k.keyIndex||'',sesame2PublicKey:k.sesame2PublicKey||''}}}catch{$('physical-error').textContent='Enter valid Sesame key JSON.';return}
physicalAdding=true;for(const n of $('physical-form').elements)n.disabled=true;$('physical-error').textContent=mode==='register'?'Registering and saving the key…':'Verifying the key…';try{const added=await request('api/physical-locks',{method:'POST',headers:{'Content-Type':'application/json','X-Homekey-Action':'physical-lock'},body:JSON.stringify(body)});$('physical-key').value='';$('physical-dialog').close();await refresh();openCalibration(added.id)}catch(err){$('physical-error').textContent=err.message}finally{physicalAdding=false;for(const n of $('physical-form').elements)n.disabled=false}}
-function openCalibration(id){calibrating=id;$('calibration-error').textContent='';$('position-result').textContent='';$('custom-position').value=physicalLocks.find(p=>p.id===id)?.state.position??'';updateCalibration();$('calibration-dialog').showModal()}
+function openCalibration(id){calibrating=id;$('calibration-error').textContent='';updateCalibration();$('calibration-dialog').showModal()}
function updateCalibration(){if(!calibrating)return;const p=physicalLocks.find(p=>p.id===calibrating);if(!p)return;const s=p.state,angle=v=>v===undefined?'Not reported':v+'°';$('calibration-name').textContent=p.name+' — positions';$('calibration-status').textContent=physicalState(s);$('calibration-position').textContent=angle(s.position);$('calibration-locked').textContent=angle(s.lock_position);$('calibration-unlocked').textContent=angle(s.unlock_position);$('calibration-boundary').textContent=angle(s.boundary);
const ready=s.online&&s.position!==undefined&&!calibrationBusy;
$('capture-lock').disabled=$('capture-unlock').disabled=!ready||s.lock_position===undefined||s.unlock_position===undefined;$('capture-boundary').disabled=!ready||s.boundary===undefined;$('test-lock').disabled=$('test-unlock').disabled=!s.online||calibrationBusy;
- $('custom-position').disabled=$('set-position').disabled=!s.online||calibrationBusy;
$('boundary-help').textContent=s.boundary===undefined?'Boundary adjustment becomes available when the lock firmware reports support.':'The boundary determines where the lock changes between locked and unlocked status. Settings are saved directly to the lock.';
}
-async function physicalAction(action,body){if(calibrationBusy||!calibrating)return;calibrationBusy=true;$('calibration-error').textContent='';$('position-result').textContent='';updateCalibration();try{await request('api/physical-locks/'+encodeURIComponent(calibrating)+'/'+action,{method:'POST',headers:{'X-Homekey-Action':'physical-lock',...(body?{'Content-Type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});if(action==='move-to')$('position-result').textContent='Command accepted for '+body.position+'°. Watch the current position to confirm movement.';await refresh()}catch(e){$('calibration-error').textContent=e.message}finally{calibrationBusy=false;updateCalibration()}}
-$('position-form').onsubmit=e=>{e.preventDefault();const input=$('custom-position');if(!input.reportValidity())return;physicalAction('move-to',{position:Number(input.value)})};
+async function physicalAction(action){if(calibrationBusy||!calibrating)return;calibrationBusy=true;$('calibration-error').textContent='';updateCalibration();try{await request('api/physical-locks/'+encodeURIComponent(calibrating)+'/'+action,{method:'POST',headers:{'X-Homekey-Action':'physical-lock'}});await refresh()}catch(e){$('calibration-error').textContent=e.message}finally{calibrationBusy=false;updateCalibration()}}
$('capture-lock').onclick=()=>physicalAction('set-lock');$('capture-unlock').onclick=()=>physicalAction('set-unlock');$('capture-boundary').onclick=()=>physicalAction('set-boundary');$('test-lock').onclick=()=>physicalAction('lock');$('test-unlock').onclick=()=>physicalAction('unlock');$('close-calibration').onclick=()=>{$('calibration-dialog').close();calibrating=null};
load().then(()=>scan()).catch(e=>message(e.message,true));setInterval(()=>{if(!restarting)refresh()},2000);
</script></body></html>
diff --git a/internal/app/physical.go b/internal/app/physical.go
index ef25ff0..f2b87fa 100644
--- a/internal/app/physical.go
+++ b/internal/app/physical.go
@@ -145,43 +145,18 @@ func physicalRoutes(mux *http.ServeMux, cfg *Settings) {
}
action := r.PathValue("action")
switch action {
- case "lock", "unlock", "set-lock", "set-unlock", "set-boundary", "move-to":
+ case "lock", "unlock", "set-lock", "set-unlock", "set-boundary":
default:
http.NotFound(w, r)
return
}
- var position *int16
- if action == "move-to" {
- var payload struct {
- Position *int16 `json:"position"`
- }
- r.Body = http.MaxBytesReader(w, r.Body, 1024)
- d := json.NewDecoder(r.Body)
- d.DisallowUnknownFields()
- if e := d.Decode(&payload); e != nil || payload.Position == nil || *payload.Position == -32768 {
- http.Error(w, "position must be an integer between -32767 and 32767", 400)
- return
- }
- var extra any
- if e := d.Decode(&extra); e != io.EOF {
- http.Error(w, "trailing data", 400)
- return
- }
- position = payload.Position
- }
if !cfg.Physical.Has(r.PathValue("id")) {
http.NotFound(w, r)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 8*time.Second)
defer cancel()
- var e error
- if position != nil {
- e = cfg.Physical.MoveTo(ctx, r.PathValue("id"), *position)
- } else {
- e = cfg.Physical.Action(ctx, r.PathValue("id"), action)
- }
- if e != nil {
+ if e := cfg.Physical.Action(ctx, r.PathValue("id"), action); e != nil {
http.Error(w, e.Error(), 409)
return
}
diff --git a/internal/app/physical_test.go b/internal/app/physical_test.go
index e46ef78..f79051d 100644
--- a/internal/app/physical_test.go
+++ b/internal/app/physical_test.go
@@ -42,24 +42,6 @@ func TestPhysicalHTTPGuardsAndEmptyRegistry(t *testing.T) {
t.Fatalf("%s: %d %s", tc.path, w.Code, w.Body.String())
}
}
- for _, body := range []string{"", "{}", "null", `{"position":null}`, `{"position":-32768}`, `{"position":32768}`, `{"position":-32769}`, `{"position":1.5}`, `{"position":"90"}`, `{"position":90,"extra":true}`, `{"position":90} {}`} {
- r := httptest.NewRequest("POST", "/api/physical-locks/unknown/move-to", strings.NewReader(body))
- r.Header.Set("X-Homekey-Action", "physical-lock")
- w := httptest.NewRecorder()
- h.ServeHTTP(w, r)
- if w.Code != 400 {
- t.Fatalf("move-to body %q: %d", body, w.Code)
- }
- }
- for _, body := range []string{`{"position":0}`, `{"position":-1}`, `{"position":-32767}`, `{"position":32767}`} {
- r := httptest.NewRequest("POST", "/api/physical-locks/unknown/move-to", strings.NewReader(body))
- r.Header.Set("X-Homekey-Action", "physical-lock")
- w := httptest.NewRecorder()
- h.ServeHTTP(w, r)
- if w.Code != 404 {
- t.Fatalf("valid move-to body %q rejected: %d", body, w.Code)
- }
- }
c, rev := s.Snapshot()
c.Locks = []LockConfig{{ID: "front", Name: "Front", Port: 51826, Finish: "silver", PhysicalLocks: []string{"sesame-missing"}}}
if e = s.Save(c, rev); e == nil {
diff --git a/internal/physical/manager.go b/internal/physical/manager.go
index 2884f0e..6ca034f 100644
--- a/internal/physical/manager.go
+++ b/internal/physical/manager.go
@@ -356,16 +356,6 @@ func (m *Manager) Action(ctx context.Context, id, name string) error {
return c.Action(ctx, name)
}
-func (m *Manager) MoveTo(ctx context.Context, id string, position int16) error {
- m.mu.RLock()
- c := m.clients[id]
- m.mu.RUnlock()
- if c == nil {
- return errors.New("unknown physical lock")
- }
- return c.MoveTo(ctx, position)
-}
-
// Dispatch accepts an authorized command without holding the provision store's
// mutex through BLE I/O. Cancellation and a short deadline apply to the work.
func (m *Manager) Dispatch(ctx context.Context, ids []string, locked bool) <-chan error {