Skip to content

Commit 406c377

Browse files
committed
Re-login and retry on PERMISSION_DENIED instead of dropping impulses
In `ha` mode with doorStatusSupported=false the gateway idle-expires the session token within minutes, but TokenExpirationTime is hardcoded to 6h so autoLoginBisecur won't re-login, and SetState swallowed the gateway's PERMISSION_DENIED (12) response (logged it, returned nil) - so presses failed silently with no error and no re-login. - sdk/Client.go: SetState surfaces the ErrorResponse instead of returning nil; isErrorResponse returns the typed *payload.ErrorResponse so callers can inspect the gateway error code. - cli/homeAssistant/actions.go: on PERMISSION_DENIED from SetState, forceReLogin and retry the impulse once, mirroring the official app (InfoCenter.onMCPError -> AppCache.relogin). - forceReLogin retries the login up to 3x (2s apart) and drops the pre-login logout: the gateway reliably kills the first login after a stale session (sends a LOGOUT and resets before replying, so attempt 1 times out; attempt 2 succeeds). This is the flakiness noted in the TokenExpirationTime TODO.
1 parent 8559608 commit 406c377

2 files changed

Lines changed: 55 additions & 19 deletions

File tree

‎cli/homeAssistant/actions.go‎

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package homeAssistant
22

33
import (
4+
"errors"
45
"fmt"
56
"halsecur/cli"
67
"halsecur/cli/bisecur"
@@ -9,6 +10,18 @@ import (
910
"time"
1011
)
1112

13+
// isPermissionDenied reports whether err (or any error it wraps) is a gateway
14+
// PERMISSION_DENIED response, which the gateway returns when the session token
15+
// has expired. The official Hörmann app reacts to this by logging out and
16+
// re-authenticating (HmProcessor.as); we do the same automatically.
17+
func isPermissionDenied(err error) bool {
18+
var errResp *payload.ErrorResponse
19+
if errors.As(err, &errResp) {
20+
return errResp.GetErrorCode() == payload.ERROR_PERMISSION_DENIED
21+
}
22+
return false
23+
}
24+
1225
func (ha *HomeAssistanceMqttClient) autoLoginBisecur() error {
1326
if ha.lastLoginTime.Add(bisecur.TokenExpirationTime).Before(time.Now()) {
1427
if !ha.autoTokenRefresh {
@@ -38,29 +51,34 @@ func (ha *HomeAssistanceMqttClient) LogoutBisecur() error {
3851
}
3952

4053
func (ha *HomeAssistanceMqttClient) forceReLogin() error {
41-
cli.Log.Infof("Logging in to the gateway...")
42-
43-
var err error
44-
45-
err = bisecur.Logout(ha.localMac, ha.deviceMac, ha.host, ha.port, ha.token)
46-
if err != nil {
47-
ha.log.Errorf("failed to logout. %v", err)
48-
}
49-
// clear token and the timestamp of the token after the successful logout
54+
// Drop any cached token so we never reuse a dead session.
5055
ha.token = 0
5156
ha.lastLoginTime = time.UnixMicro(0)
5257

53-
// Not sure, this is really needed, but since I know Hormann BS gateway can become crazy if it gets overloaded...
54-
time.Sleep(5 * time.Second)
58+
// The gateway reliably kills the FIRST login that follows a stale session: it emits a LOGOUT
59+
// frame for the previous session and resets the connection *before* replying to the login, so
60+
// that attempt times out (this is the flakiness noted in the TokenExpirationTime TODO in
61+
// cli/bisecur/consts.go). A fresh attempt then succeeds because the stale session is now gone.
62+
// The official app re-logs-in with no artificial delay (InfoCenter.onMCPError ->
63+
// AppCache.relogin), so we keep the inter-attempt wait short and just retry.
64+
const maxAttempts = 3
65+
const retryDelay = 2 * time.Second
5566

56-
ha.token, err = bisecur.Login(ha.localMac, ha.deviceMac, ha.host, ha.port, ha.deviceUsername, ha.devicePassword)
57-
if err != nil {
58-
return fmt.Errorf("login failed. %v", err)
67+
var err error
68+
for attempt := 1; attempt <= maxAttempts; attempt++ {
69+
cli.Log.Infof("Logging in to the gateway... (attempt %d/%d)", attempt, maxAttempts)
70+
ha.token, err = bisecur.Login(ha.localMac, ha.deviceMac, ha.host, ha.port, ha.deviceUsername, ha.devicePassword)
71+
if err == nil {
72+
ha.lastLoginTime = time.Now() // note when token was received
73+
return nil
74+
}
75+
ha.log.Warnf("login attempt %d/%d failed: %v", attempt, maxAttempts, err)
76+
if attempt < maxAttempts {
77+
time.Sleep(retryDelay)
78+
}
5979
}
6080

61-
ha.lastLoginTime = time.Now() // note when token was received
62-
63-
return nil
81+
return fmt.Errorf("login failed after %d attempts. %v", maxAttempts, err)
6482
}
6583

6684
func (ha *HomeAssistanceMqttClient) setStateMultiCall(count int, devicePort byte) error {
@@ -80,8 +98,18 @@ func (ha *HomeAssistanceMqttClient) setStateBisecurMultiCall(count int, devicePo
8098
}
8199

82100
err = bisecur.SetState(ha.localMac, ha.deviceMac, ha.host, ha.port, devicePort, ha.token)
101+
if err != nil && isPermissionDenied(err) {
102+
// Stale session: the gateway idle-expires the token well before our
103+
// proactive TokenExpirationTime elapses. Re-authenticate and retry once,
104+
// mirroring how the official app reacts to PERMISSION_DENIED.
105+
ha.log.Warnf("SetState rejected with PERMISSION_DENIED; session likely expired. Re-logging in and retrying.")
106+
if reErr := ha.forceReLogin(); reErr != nil {
107+
return fmt.Errorf("re-login after PERMISSION_DENIED failed. %v", reErr)
108+
}
109+
err = bisecur.SetState(ha.localMac, ha.deviceMac, ha.host, ha.port, devicePort, ha.token)
110+
}
83111
if err != nil {
84-
return fmt.Errorf("failed to get door status. %v", err)
112+
return fmt.Errorf("failed to set door state. %v", err)
85113
}
86114

87115
if i < count-1 {

‎sdk/Client.go‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,12 @@ func (c *Client) SetState(portID byte) error {
421421
return fmt.Errorf("received unexpected packet: %s", response)
422422
}
423423

424+
// The gateway acknowledges SetState with an ERROR frame (e.g. PERMISSION_DENIED
425+
// when the session token has expired). Surface it instead of silently succeeding.
426+
if err := isErrorResponse(response); err != nil {
427+
return err
428+
}
429+
424430
c.log.Debugf("Set State response: %s", response.String())
425431

426432
return nil
@@ -679,7 +685,9 @@ func castIfNotError[T payload.PayloadInterface](response *TransmissionContainer)
679685
func isErrorResponse(response *TransmissionContainer) error {
680686
errorResponse, isErrorResponseType := response.Packet.payload.(*payload.ErrorResponse)
681687
if isErrorResponseType {
682-
return fmt.Errorf("%s", errorResponse.Error())
688+
// Return the typed *payload.ErrorResponse (it implements error) so callers
689+
// can inspect the gateway error code, e.g. to react to PERMISSION_DENIED.
690+
return errorResponse
683691
}
684692
return nil
685693
}

0 commit comments

Comments
 (0)