fix(devices): zero out unassigned bell outputs when saving Bell Mechanisms

relay.set_config is a partial update on the firmware side — any bell
index not present in the durations/outputs maps is left completely
untouched. EditBellOutputsModal and OnboardDevice's bell config step
both only sent entries for the currently-active bell count, so
deselecting a bell (lowering the active count) never actually
deactivated its output on the device — it stayed wired to whatever it
was last set to. The only way to disable a bell was to reselect it and
explicitly set it to "Disabled", which isn't obvious.

Both now send all maxOutputs slots on every save, forcing output to 0
for anything beyond the active count, so lowering the active bell
count reaches the firmware the way it visually appears to in the UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 20:35:03 +03:00
co-authored by Claude Sonnet 5
parent ae5da6046f
commit 80c0e955f0
2 changed files with 15 additions and 3 deletions
@@ -51,11 +51,19 @@ export default function EditBellOutputsModal({ open, onClose, onSaved, attr, sub
// Push to the device first — relay.set_config takes both maps in one call. // Push to the device first — relay.set_config takes both maps in one call.
// Only write to Firestore once the device confirms it actually applied // Only write to Firestore once the device confirms it actually applied
// and persisted the change (sendMqttCommand's toast reports the outcome). // and persisted the change (sendMqttCommand's toast reports the outcome).
//
// relay.set_config is a partial update — any bell index NOT present in
// these maps is left untouched by the firmware. So every slot beyond
// activeBells must be sent explicitly as output 0 (disabled), or a bell
// that was previously active stays wired to its old output forever,
// even though it's no longer shown as active here. The only way to
// deactivate a bell today is to select it and set it to Disabled — this
// makes "deselect it" do the same thing implicitly.
const durations = {} const durations = {}
const outputs = {} const outputs = {}
activeRows.forEach((r, i) => { rows.forEach((r, i) => {
durations[i] = r.timing durations[i] = r.timing
outputs[i] = r.output outputs[i] = i < activeBells ? r.output : 0
}) })
await sendMqttCommand('relay.set_config', { durations, outputs }) await sendMqttCommand('relay.set_config', { durations, outputs })
@@ -1275,9 +1275,13 @@ function ConfigureStep({ device, onDeviceUpdated, onNext, onBack }) {
// ── Build the command sequence + Firestore patch for each group ───────── // ── Build the command sequence + Firestore patch for each group ─────────
const buildGroups = useCallback(() => { const buildGroups = useCallback(() => {
const activeBellRows = bell.rows.slice(0, bell.activeBells) const activeBellRows = bell.rows.slice(0, bell.activeBells)
// relay.set_config is a partial update — any bell index NOT present in
// these maps is left untouched by the firmware. Send every row (not just
// the active ones) so anything beyond activeBells is explicitly zeroed —
// same fix as EditBellOutputsModal.
const durations = {} const durations = {}
const outputs = {} const outputs = {}
activeBellRows.forEach((r, i) => { durations[i] = r.timing; outputs[i] = r.output }) bell.rows.forEach((r, i) => { durations[i] = r.timing; outputs[i] = i < bell.activeBells ? r.output : 0 })
return [ return [
{ {