The charger was never asked what it is set to

The trigger buys telemetry and only telemetry, so a charger that has been
read a hundred times and commanded none reports amps, volts and nothing
else: no schedule, no balancing, no Modbus server, not even its firmware.
The message that asks for that half is 0040, and the reference keeps it
commented out because the app sends its timestamp without a value type.
The app is what the charger answers, so the oddity is reproduced rather
than corrected — sent when the settings half is missing or older than ten
minutes, waited four seconds for, and after three unanswered requests
still sent but no longer waited on.

The three settings the panel has and the writer did not — swipe up, swipe
down, smart touch — are writable now, which is all eleven of the 0100
commands. Nothing else in the map was missing: every named field of every
message was already decoded, and the raw keys the card shows are fields
the reference does not name either.

Both cards drop a row with nothing in it, which turned a charger that
reports only its ceiling into a charger that reports no current range at
all. Half a range is still a bound.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5 committed 2026-09-02 23:05:40 +02:00
1 parent 3f9d5b943f
commit b2d333a63f
13 files changed
+171 -4

No files matched your search

@@ -90,6 +90,15 @@ const (
// having to reach it first.
statusWait = 12 * time.Second
// settingsMaxAge is how old the settings half may be before a status read
// asks for it again; settingsWait is how long that read then waits for the
// answer, and statusReqTries how many unanswered requests it takes before a
// charger is treated as one whose firmware ignores the message — after which
// the request still goes out, but no read pays the wait for it.
settingsMaxAge = 10 * time.Minute
settingsWait = 4 * time.Second
statusReqTries = 3
// commandWait is how long a command waits for the charger's confirmation
// message. A command is fire-and-forget on the wire, so this only decides
// whether we can say the charger answered — not whether it was sent.
@@ -262,6 +271,12 @@ type deviceState struct {
telemetryAt time.Time
settingsAt time.Time
triggeredUntil time.Time
// How many status requests this charger has been asked and not answered.
// The request is cheap to send and is sent regardless; what it buys is the
// right to wait a few seconds for the reply, and a charger whose firmware
// ignores the message should not cost every later read that wait.
statusReqMisses int
}
// mqttClient returns the account's broker connection, opening one if there is
@@ -671,6 +686,28 @@ func (c *mqttConn) snapshotOf(sn string) (map[string]any, time.Time, time.Time,
return out, st.telemetryAt, st.settingsAt, st.triggeredUntil
}
// noteStatusMiss records that a status request went unanswered, and reports how
// many have now in a row.
func (c *mqttConn) noteStatusMiss(sn string) int {
c.mu.Lock()
defer c.mu.Unlock()
st := c.devices[sn]
if st == nil {
return 0
}
st.statusReqMisses++
return st.statusReqMisses
}
// statusReqAnswered reports whether this charger has answered a status request
// recently enough to be worth waiting for again.
func (c *mqttConn) statusReqAnswered(sn string) bool {
c.mu.Lock()
defer c.mu.Unlock()
st := c.devices[sn]
return st == nil || st.statusReqMisses < statusReqTries
}
// noteTrigger records how long the charger has been asked to keep streaming.
func (c *mqttConn) noteTrigger(sn string, until time.Time) {
c.mu.Lock()
@@ -704,6 +741,20 @@ func (p *Plugin) mqttTrigger(ctx context.Context, c *mqttConn, model, sn string,
return nil
}
// mqttStatusRequest asks the charger to publish what it is set to. The trigger
// above buys telemetry and nothing else: the settings half arrives on its own
// message, and otherwise only after a command, which is why a charger that has
// been read but never commanded reports live amps and knows nothing about its
// own schedule, its Modbus server or its firmware. This is the message the app
// uses for that — a status request the charger answers with 0840.
func (p *Plugin) mqttStatusRequest(ctx context.Context, c *mqttConn, model, sn string) error {
frame, err := encodeFrame(msgEVStatusReq, []cmdField{bareTimestampField(time.Now())})
if err != nil {
return err
}
return c.publishFrame(ctx, model, sn, frame, 0)
}
// mqttSetMode sends the start / stop / skip-delay / boost command.
func (p *Plugin) mqttSetMode(ctx context.Context, c *mqttConn, model, sn, mode string) error {
v, ok := mqttModeValues[mode]