fix(backup): read the printer's verdict before counting a K-profile restored (#2656)

`18938a10` on `dev` changed `set_kprofiles_batch` from returning a `bool` to
returning the sequence_id it published the command under, and moved the
verdict to a separate `await client.await_cali_ack(seq)` returning
`(ok, detail)`. Every caller in `api/routes/kprofiles.py` was updated with it.
`_restore_kprofiles` was not — it still did `sent = client.set_kprofiles_batch(...)`
and branched on `if sent:`.

A sequence_id string is truthy, so that compiled, passed, and silently made
the restore the one path left in the codebase that reports a refused
K-profile write as saved — exactly the defect `18938a10` closed everywhere
else.

Keep the sequence_id, await the ack per batch, and route an explicit refusal
into `tally.failed` with a new `kprofilesRefused` note carrying the printer's
own `reason`. Reusing `kprofilesSendFailed` would have been wrong: the
command was sent, and the printer answered.

Silence still counts restored. That is `await_cali_ack`'s own contract and
the maintainer's rule — no answer is not evidence of refusal, and firmware
predating the ack never answers. An exception reading the ack degrades the
same way rather than inventing a failure out of a write that most likely
landed.

`kprofilesAckUnreliable` is reworded to match: a refusal is now believed, so
the caveat narrows to what is genuinely left uncertain. The ack is only worth
reading at all because `18938a10` also changed the payload's `tray_id` from
`-1` to `0` — single-nozzle firmware answered `result: "fail"` to `-1` on
writes that demonstrably applied. This restore builds no `tray_id` of its
own, so it inherits that fix for free.

Tests: 4 regression (the ack is awaited for the returned sequence_id; a
refused batch counts failed and surfaces the printer's reason; one refused
nozzle does not condemn the other; the reworded caveat) + 3 controls (a
silent printer still counts restored; an unreadable ack does not fail the
batch; `None` keeps the existing send-failed path and awaits nothing). All
four confirmed failing against the pre-fix service.
This commit is contained in:
jmoore-skild
2026-08-04 08:57:34 -04:00
parent 27e97bc979
commit 4ee9c0eecb
15 changed files with 175 additions and 31 deletions
+45 -6
View File
@@ -1473,9 +1473,12 @@ class GitHubRestoreService:
# the profile occupying a slot, so writing is always an overwrite on the
# printer side.
tally.note("kprofilesAlwaysOverwrite", "K-profiles always overwrite the matching slot on the printer")
# A refusal is now believed and counted failed (#2718 made the ack worth
# reading), but silence still counts restored, so the caveat stands —
# narrowed to what is actually left uncertain.
tally.note(
"kprofilesAckUnreliable",
"The printer's acknowledgement is not reliable — verify the profiles on the printer",
"A printer that does not answer still counts as restored — verify the profiles on the printer",
)
for serial, entries in sorted(by_serial.items()):
@@ -1565,14 +1568,12 @@ class GitHubRestoreService:
)
try:
sent = client.set_kprofiles_batch(profile_dicts, nozzle)
seq = client.set_kprofiles_batch(profile_dicts, nozzle)
except Exception as e:
logger.warning("K-profile restore failed for %s nozzle %s: %s", serial, nozzle, e)
sent = False
seq = None
if sent:
tally.restored += len(profile_dicts)
else:
if not seq:
tally.failed += len(profile_dicts)
tally.note(
"kprofilesSendFailed",
@@ -1581,6 +1582,44 @@ class GitHubRestoreService:
printer=printer.name,
serial=serial,
)
continue
# What came back is the sequence_id the command was published
# under, not a verdict (#2718) — a truthy string only means the
# command left the building. The printer answers separately, and
# every other caller of this API now reads that answer; without
# this the restore would be the one path left that reports a
# refused write as saved.
ok, detail = await self._kprofile_ack(client, seq, serial, nozzle)
if ok:
tally.restored += len(profile_dicts)
else:
tally.failed += len(profile_dicts)
tally.note(
"kprofilesRefused",
f"{printer.name} ({serial}) refused the {nozzle} profiles: {detail}",
nozzle=nozzle,
printer=printer.name,
serial=serial,
reason=detail,
)
@staticmethod
async def _kprofile_ack(client, seq: str, serial: str, nozzle: str) -> tuple[bool, str]:
"""Read the printer's verdict on one batch write.
``await_cali_ack`` already treats silence as success — no answer is not
evidence of refusal, and firmware that predates the ack never answers at
all. An exception reading it is the same situation one layer up, so it
degrades the same way rather than turning a write that most likely
landed into a reported failure.
"""
try:
ok, detail = await client.await_cali_ack(seq)
return bool(ok), str(detail or "")
except Exception as e:
logger.warning("Could not read the K-profile ack for %s nozzle %s: %s", serial, nozzle, e)
return True, ""
@staticmethod
async def _current_kprofile_index(client, nozzle: str, serial: str) -> list:
+104 -12
View File
@@ -1236,10 +1236,17 @@ class TestRestoreKprofiles:
"""One profile as the printer currently reports it."""
return SimpleNamespace(slot_id=slot_id, filament_id=filament_id, name=name, setting_id=setting_id)
def _client(self, live=None, sent=True):
def _client(self, live=None, sent="7", ack=(True, "")):
"""A connected printer client.
``set_kprofiles_batch`` returns the sequence_id it published under, not
a success flag (#2718), and the verdict arrives separately from
``await_cali_ack`` as ``(ok, detail)``.
"""
client = MagicMock()
client.state.connected = True
client.set_kprofiles_batch = MagicMock(return_value=sent)
client.await_cali_ack = AsyncMock(return_value=ack)
client.get_kprofiles = AsyncMock(return_value=list(live or []))
return client
@@ -1266,9 +1273,7 @@ class TestRestoreKprofiles:
@pytest.mark.asyncio
async def test_sends_batch_to_connected_printer(self, db_session, printer_factory):
printer = await printer_factory(serial_number="00M09A123456789")
client = MagicMock()
client.state.connected = True
client.set_kprofiles_batch = MagicMock(return_value=True)
client = self._client()
tally = _CategoryTally()
with patch("backend.app.services.github_restore.printer_manager") as manager:
@@ -1293,9 +1298,10 @@ class TestRestoreKprofiles:
manager.get_client = MagicMock(return_value=client)
await _service()._restore_kprofiles(db_session, self._payload(), tally)
# The printer does answer extrusion_cali_set, but it reports "fail" on
# writes that land, so the note must not promise either way.
# A refusal is now read and counted failed, so the caveat is narrowed to
# what is genuinely left uncertain: a printer that never answers.
assert any("verify the profiles on the printer" in note for note in _messages(tally))
assert any("does not answer still counts as restored" in note for note in _messages(tally))
assert not any("without acknowledgement" in note for note in _messages(tally))
assert any("always overwrite" in note for note in _messages(tally))
@@ -1560,10 +1566,10 @@ class TestRestoreKprofiles:
@pytest.mark.asyncio
async def test_publish_failure_counts_as_failed(self, db_session, printer_factory):
# None is what set_kprofiles_batch returns when it could not publish —
# a disconnected client. There is no ack to wait for in that case.
await printer_factory(serial_number="00M09A123456789")
client = MagicMock()
client.state.connected = True
client.set_kprofiles_batch = MagicMock(return_value=False)
client = self._client(sent=None)
tally = _CategoryTally()
with patch("backend.app.services.github_restore.printer_manager") as manager:
@@ -1572,6 +1578,9 @@ class TestRestoreKprofiles:
assert tally.failed == 1
assert tally.restored == 0
assert "kprofilesSendFailed" in _codes(tally)
assert "kprofilesRefused" not in _codes(tally), "nothing was sent, so the printer refused nothing"
client.await_cali_ack.assert_not_awaited()
@pytest.mark.asyncio
async def test_publish_exception_is_contained(self, db_session, printer_factory):
@@ -1587,13 +1596,96 @@ class TestRestoreKprofiles:
assert tally.failed == 1
# --- the printer's verdict decides the tally, not the publish ------------
#
# #2718 changed set_kprofiles_batch from returning a bool to returning the
# sequence_id it published under. A sequence_id string is truthy, so a
# restore that branches on the return value alone reports every refused
# write as saved — the defect that fix closed in every other caller.
@pytest.mark.asyncio
async def test_awaits_the_ack_for_the_sequence_id_it_was_given(self, db_session, printer_factory):
await printer_factory(serial_number="00M09A123456789")
client = self._client(sent="4211")
tally = _CategoryTally()
with patch("backend.app.services.github_restore.printer_manager") as manager:
manager.get_client = MagicMock(return_value=client)
await _service()._restore_kprofiles(db_session, self._payload(), tally)
client.await_cali_ack.assert_awaited_once_with("4211")
assert tally.restored == 1
@pytest.mark.asyncio
async def test_a_refused_batch_counts_failed_not_restored(self, db_session, printer_factory):
await printer_factory(serial_number="00M09A123456789", name="Shelf Printer")
client = self._client(ack=(False, "invalid tray_id"))
tally = _CategoryTally()
with patch("backend.app.services.github_restore.printer_manager") as manager:
manager.get_client = MagicMock(return_value=client)
await _service()._restore_kprofiles(db_session, self._payload(), tally)
assert tally.restored == 0
assert tally.failed == 1
assert "kprofilesRefused" in _codes(tally)
assert "kprofilesSendFailed" not in _codes(tally), "it was sent — the printer answered no"
note = next(n for n in tally.notes if n["code"] == "kprofilesRefused")
assert note["params"]["reason"] == "invalid tray_id", "the printer's own reason has to survive"
assert "Shelf Printer" in note["message"] and "invalid tray_id" in note["message"]
@pytest.mark.asyncio
async def test_a_silent_printer_still_counts_restored(self, db_session, printer_factory):
# maziggy's rule, and await_cali_ack's own contract: no answer is not
# evidence of refusal. Firmware that predates the ack never answers.
await printer_factory(serial_number="00M09A123456789")
client = self._client(ack=(True, "no acknowledgement from printer"))
tally = _CategoryTally()
with patch("backend.app.services.github_restore.printer_manager") as manager:
manager.get_client = MagicMock(return_value=client)
await _service()._restore_kprofiles(db_session, self._payload(), tally)
assert tally.restored == 1
assert tally.failed == 0
assert "kprofilesRefused" not in _codes(tally)
@pytest.mark.asyncio
async def test_an_unreadable_ack_does_not_fail_the_batch(self, db_session, printer_factory):
# Same situation one layer up: the write most likely landed, so this
# degrades the way a timeout does rather than inventing a failure.
await printer_factory(serial_number="00M09A123456789")
client = self._client()
client.await_cali_ack = AsyncMock(side_effect=RuntimeError("mqtt down"))
tally = _CategoryTally()
with patch("backend.app.services.github_restore.printer_manager") as manager:
manager.get_client = MagicMock(return_value=client)
await _service()._restore_kprofiles(db_session, self._payload(), tally)
assert tally.restored == 1
assert tally.failed == 0
@pytest.mark.asyncio
async def test_one_refused_nozzle_does_not_condemn_the_other(self, db_session, printer_factory):
await printer_factory(serial_number="00M09A123456789")
payload = {**self._payload(nozzle="0.4"), **self._payload(nozzle="0.8")}
client = self._client()
client.await_cali_ack = AsyncMock(side_effect=[(False, "busy"), (True, "")])
tally = _CategoryTally()
with patch("backend.app.services.github_restore.printer_manager") as manager:
manager.get_client = MagicMock(return_value=client)
await _service()._restore_kprofiles(db_session, payload, tally)
assert tally.restored == 1
assert tally.failed == 1
@pytest.mark.asyncio
async def test_each_nozzle_is_sent_separately(self, db_session, printer_factory):
await printer_factory(serial_number="00M09A123456789")
payload = {**self._payload(nozzle="0.4"), **self._payload(nozzle="0.8")}
client = MagicMock()
client.state.connected = True
client.set_kprofiles_batch = MagicMock(return_value=True)
client = self._client()
tally = _CategoryTally()
with patch("backend.app.services.github_restore.printer_manager") as manager:
+2 -1
View File
@@ -4922,12 +4922,13 @@ export default {
settingsCompanionSkipped: '{{keys}} bleiben ausgeschaltet - die jeweils benötigten Zugangsdaten lassen sich nicht aus einem Backup wiederherstellen und sind auf dieser Instanz nicht hinterlegt, ein Einschalten würde die Integration also ohne Authentifizierung lassen',
settingsMqttRelayFailed: 'MQTT-Einstellungen wiederhergestellt, aber das Relay konnte nicht neu verbunden werden - Bambuddy neu starten',
kprofilesAlwaysOverwrite: 'K-Profile überschreiben immer den passenden Slot auf dem Drucker',
kprofilesAckUnreliable: 'Die Bestätigung des Druckers ist nicht zuverlässig - überprüfen Sie die Profile am Drucker',
kprofilesAckUnreliable: 'Ein Drucker, der nicht antwortet, zählt weiterhin als wiederhergestellt - überprüfen Sie die Profile am Drucker',
kprofilesPrinterMissing: 'Kein Drucker mit der Seriennummer {{serial}} - übersprungen',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) ist nicht verbunden - übersprungen',
kprofilesUnknownNozzle: 'Unerwarteter Düsendurchmesser {{nozzle}} für {{serial}} - unverändert gesendet',
kprofilesUnmatched: '{{count}} Profile für {{nozzle}} hatten kein Gegenstück auf {{printer}} - als neue Profile hinzugefügt',
kprofilesSendFailed: '{{nozzle}}-Profile konnten nicht an {{printer}} ({{serial}}) gesendet werden',
kprofilesRefused: '{{printer}} ({{serial}}) hat die {{nozzle}}-Profile abgelehnt: {{reason}}',
},
},
+2 -1
View File
@@ -4970,12 +4970,13 @@ export default {
settingsCompanionSkipped: '{{keys}} left switched off - the credential each one needs cannot be restored from a backup and this instance has none stored, so switching them on would leave the integration unauthenticated',
settingsMqttRelayFailed: 'MQTT settings restored, but the relay could not be reconnected - restart Bambuddy',
kprofilesAlwaysOverwrite: 'K-profiles always overwrite the matching slot on the printer',
kprofilesAckUnreliable: 'The printer\'s acknowledgement is not reliable - verify the profiles on the printer',
kprofilesAckUnreliable: 'A printer that does not answer still counts as restored - verify the profiles on the printer',
kprofilesPrinterMissing: 'No printer with serial {{serial}} - skipped',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) is not connected - skipped',
kprofilesUnknownNozzle: 'Unexpected nozzle diameter {{nozzle}} for {{serial}} - sent as-is',
kprofilesUnmatched: '{{count}} profile(s) for {{nozzle}} had no counterpart on {{printer}} - added as new profiles',
kprofilesSendFailed: 'Failed to send {{nozzle}} profiles to {{printer}} ({{serial}})',
kprofilesRefused: '{{printer}} ({{serial}}) refused the {{nozzle}} profiles: {{reason}}',
},
},
+2 -1
View File
@@ -4930,12 +4930,13 @@ export default {
settingsCompanionSkipped: '{{keys}} se han dejado desactivados - la credencial que cada uno necesita no puede restaurarse desde una copia y esta instancia no tiene ninguna guardada, así que activarlos dejaría la integración sin autenticación',
settingsMqttRelayFailed: 'Ajustes MQTT restaurados, pero no se pudo reconectar el relé - reinicia Bambuddy',
kprofilesAlwaysOverwrite: 'Los perfiles K siempre sobrescriben la ranura correspondiente en la impresora',
kprofilesAckUnreliable: 'La confirmación de la impresora no es fiable - verifica los perfiles en la impresora',
kprofilesAckUnreliable: 'Una impresora que no responde sigue contando como restaurada - verifica los perfiles en la impresora',
kprofilesPrinterMissing: 'No hay ninguna impresora con el número de serie {{serial}} - omitido',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) no está conectada - omitido',
kprofilesUnknownNozzle: 'Diámetro de boquilla inesperado {{nozzle}} para {{serial}} - enviado tal cual',
kprofilesUnmatched: '{{count}} perfiles para {{nozzle}} no tenían equivalente en {{printer}} - añadidos como perfiles nuevos',
kprofilesSendFailed: 'No se pudieron enviar los perfiles de {{nozzle}} a {{printer}} ({{serial}})',
kprofilesRefused: '{{printer}} ({{serial}}) rechazó los perfiles de {{nozzle}}: {{reason}}',
},
},
+2 -1
View File
@@ -4911,12 +4911,13 @@ export default {
settingsCompanionSkipped: '{{keys}} laissés désactivés - l\'identifiant dont chacun a besoin ne peut pas être restauré depuis une sauvegarde et cette instance n\'en a aucun enregistré ; les activer laisserait donc l\'intégration sans authentification',
settingsMqttRelayFailed: 'Réglages MQTT restaurés, mais le relais n\'a pas pu être reconnecté - redémarrez Bambuddy',
kprofilesAlwaysOverwrite: 'Les profils K écrasent toujours l\'emplacement correspondant sur l\'imprimante',
kprofilesAckUnreliable: 'L\'accusé de réception de l\'imprimante n\'est pas fiable - vérifiez les profils sur l\'imprimante',
kprofilesAckUnreliable: 'Une imprimante qui ne répond pas compte quand même comme restaurée - vérifiez les profils sur l\'imprimante',
kprofilesPrinterMissing: 'Aucune imprimante avec le numéro de série {{serial}} - ignoré',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) n\'est pas connectée - ignoré',
kprofilesUnknownNozzle: 'Diamètre de buse inattendu {{nozzle}} pour {{serial}} - envoyé tel quel',
kprofilesUnmatched: '{{count}} profils pour {{nozzle}} n\'avaient pas d\'équivalent sur {{printer}} - ajoutés comme nouveaux profils',
kprofilesSendFailed: 'Impossible d\'envoyer les profils {{nozzle}} à {{printer}} ({{serial}})',
kprofilesRefused: '{{printer}} ({{serial}}) a refusé les profils {{nozzle}} : {{reason}}',
},
},
+2 -1
View File
@@ -4910,12 +4910,13 @@ export default {
settingsCompanionSkipped: '{{keys}} lasciati disattivati - la credenziale richiesta da ciascuno non può essere ripristinata da un backup e questa istanza non ne ha nessuna salvata, quindi attivarli lascerebbe l\'integrazione senza autenticazione',
settingsMqttRelayFailed: 'Impostazioni MQTT ripristinate, ma il relay non è stato riconnesso - riavvia Bambuddy',
kprofilesAlwaysOverwrite: 'I profili K sovrascrivono sempre lo slot corrispondente sulla stampante',
kprofilesAckUnreliable: 'La conferma della stampante non è affidabile - verifica i profili sulla stampante',
kprofilesAckUnreliable: 'Una stampante che non risponde conta comunque come ripristinata - verifica i profili sulla stampante',
kprofilesPrinterMissing: 'Nessuna stampante con numero di serie {{serial}} - saltato',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) non è connessa - saltato',
kprofilesUnknownNozzle: 'Diametro ugello inatteso {{nozzle}} per {{serial}} - inviato così com\'è',
kprofilesUnmatched: '{{count}} profili per {{nozzle}} non avevano corrispondenza su {{printer}} - aggiunti come nuovi profili',
kprofilesSendFailed: 'Impossibile inviare i profili {{nozzle}} a {{printer}} ({{serial}})',
kprofilesRefused: '{{printer}} ({{serial}}) ha rifiutato i profili {{nozzle}}: {{reason}}',
},
},
+2 -1
View File
@@ -4922,12 +4922,13 @@ export default {
settingsCompanionSkipped: '{{keys}} はオフのままにしました - 各項目に必要な認証情報はバックアップから復元できず、このインスタンスにも保存されていないため、オンにすると連携が未認証のままになります',
settingsMqttRelayFailed: 'MQTT 設定を復元しましたが、リレーを再接続できませんでした - Bambuddy を再起動してください',
kprofilesAlwaysOverwrite: 'K プロファイルは常にプリンター側の該当スロットを上書きします',
kprofilesAckUnreliable: 'プリンターの応答は信頼できません - プロファイルはプリンター側で確認してください',
kprofilesAckUnreliable: '応答しないプリンターも復元済みとして数えます - プロファイルはプリンター側で確認してください',
kprofilesPrinterMissing: 'シリアル {{serial}} のプリンターがありません - スキップしました',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) は接続されていません - スキップしました',
kprofilesUnknownNozzle: '{{serial}} に想定外のノズル径 {{nozzle}} - そのまま送信しました',
kprofilesUnmatched: '{{nozzle}} 用のプロファイル {{count}} 件は {{printer}} に該当がありませんでした - 新規プロファイルとして追加しました',
kprofilesSendFailed: '{{nozzle}} のプロファイルを {{printer}} ({{serial}}) に送信できませんでした',
kprofilesRefused: '{{printer}} ({{serial}}) が {{nozzle}} のプロファイルを拒否しました: {{reason}}',
},
},
+2 -1
View File
@@ -4687,12 +4687,13 @@ export default {
settingsCompanionSkipped: '{{keys}}을(를) 꺼진 상태로 두었습니다 - 각 항목에 필요한 자격 증명은 백업에서 복원할 수 없고 이 인스턴스에도 저장되어 있지 않아, 켜면 연동이 인증 없이 열립니다',
settingsMqttRelayFailed: 'MQTT 설정을 복원했지만 릴레이를 다시 연결하지 못했습니다 - Bambuddy를 재시작하세요',
kprofilesAlwaysOverwrite: 'K 프로파일은 항상 프린터의 해당 슬롯을 덮어씁니다',
kprofilesAckUnreliable: '프린터의 응답은 신뢰할 수 없습니다 - 프린터에서 프로파일을 확인하세요',
kprofilesAckUnreliable: '응답하지 않는 프린터도 복원됨으로 집계됩니다 - 프린터에서 프로파일을 확인하세요',
kprofilesPrinterMissing: '시리얼 {{serial}}인 프린터가 없습니다 - 건너뛰었습니다',
kprofilesPrinterOffline: '{{printer}}({{serial}})이(가) 연결되어 있지 않습니다 - 건너뛰었습니다',
kprofilesUnknownNozzle: '{{serial}}의 예상치 못한 노즐 직경 {{nozzle}} - 그대로 전송했습니다',
kprofilesUnmatched: '{{nozzle}}용 프로파일 {{count}}개가 {{printer}}에 대응 항목이 없습니다 - 새 프로파일로 추가했습니다',
kprofilesSendFailed: '{{nozzle}} 프로파일을 {{printer}}({{serial}})에 보내지 못했습니다',
kprofilesRefused: '{{printer}}({{serial}})이(가) {{nozzle}} 프로파일을 거부했습니다: {{reason}}',
},
},
history: '기록',
+2 -1
View File
@@ -4910,12 +4910,13 @@ export default {
settingsCompanionSkipped: '{{keys}} ficaram desligados - a credencial que cada um precisa não pode ser restaurada de um backup e esta instância não tem nenhuma armazenada, então ligá-los deixaria a integração sem autenticação',
settingsMqttRelayFailed: 'Configurações MQTT restauradas, mas o relay não pôde ser reconectado - reinicie o Bambuddy',
kprofilesAlwaysOverwrite: 'Os perfis K sempre sobrescrevem o slot correspondente na impressora',
kprofilesAckUnreliable: 'A confirmação da impressora não é confiável - verifique os perfis na impressora',
kprofilesAckUnreliable: 'Uma impressora que não responde ainda conta como restaurada - verifique os perfis na impressora',
kprofilesPrinterMissing: 'Nenhuma impressora com o número de série {{serial}} - ignorado',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) não está conectada - ignorado',
kprofilesUnknownNozzle: 'Diâmetro de bico inesperado {{nozzle}} para {{serial}} - enviado como está',
kprofilesUnmatched: '{{count}} perfis para {{nozzle}} não tinham correspondente em {{printer}} - adicionados como novos perfis',
kprofilesSendFailed: 'Não foi possível enviar os perfis de {{nozzle}} para {{printer}} ({{serial}})',
kprofilesRefused: '{{printer}} ({{serial}}) recusou os perfis de {{nozzle}}: {{reason}}',
},
},
+2 -1
View File
@@ -4679,12 +4679,13 @@ export default {
settingsCompanionSkipped: '{{keys}} оставлены выключенными - нужные им учётные данные нельзя восстановить из резервной копии, и в этом экземпляре они не сохранены, поэтому включение оставило бы интеграцию без аутентификации',
settingsMqttRelayFailed: 'Настройки MQTT восстановлены, но переподключить реле не удалось - перезапустите Bambuddy',
kprofilesAlwaysOverwrite: 'K-профили всегда перезаписывают соответствующий слот на принтере',
kprofilesAckUnreliable: 'Подтверждение принтера ненадёжно - проверьте профили на принтере',
kprofilesAckUnreliable: 'Принтер, который не отвечает, всё равно считается восстановленным - проверьте профили на принтере',
kprofilesPrinterMissing: 'Нет принтера с серийным номером {{serial}} - пропущено',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) не подключён - пропущено',
kprofilesUnknownNozzle: 'Неожиданный диаметр сопла {{nozzle}} для {{serial}} - отправлено как есть',
kprofilesUnmatched: 'Профилей для {{nozzle}} без соответствия на {{printer}}: {{count}} - добавлены как новые профили',
kprofilesSendFailed: 'Не удалось отправить профили {{nozzle}} на {{printer}} ({{serial}})',
kprofilesRefused: '{{printer}} ({{serial}}) отклонил профили {{nozzle}}: {{reason}}',
},
},
history: "История",
+2 -1
View File
@@ -4900,12 +4900,13 @@ export default {
settingsCompanionSkipped: '{{keys}} kapalı bırakıldı - her birinin ihtiyaç duyduğu kimlik bilgisi bir yedekten geri yüklenemez ve bu örnekte kayıtlı değil, dolayısıyla açmak entegrasyonu kimlik doğrulamasız bırakırdı',
settingsMqttRelayFailed: 'MQTT ayarları geri yüklendi ancak röle yeniden bağlanamadı - Bambuddy\'yi yeniden başlatın',
kprofilesAlwaysOverwrite: 'K profilleri yazıcıdaki eşleşen yuvanın her zaman üzerine yazar',
kprofilesAckUnreliable: 'Yazıcının onayı güvenilir değil - profilleri yazıcıda doğrulayın',
kprofilesAckUnreliable: 'Yanıt vermeyen bir yazıcı yine de geri yüklendi sayılır - profilleri yazıcıda doğrulayın',
kprofilesPrinterMissing: '{{serial}} seri numaralı yazıcı yok - atlandı',
kprofilesPrinterOffline: '{{printer}} ({{serial}}) bağlı değil - atlandı',
kprofilesUnknownNozzle: '{{serial}} için beklenmeyen nozul çapı {{nozzle}} - olduğu gibi gönderildi',
kprofilesUnmatched: '{{nozzle}} için {{count}} profilin {{printer}} üzerinde karşılığı yoktu - yeni profil olarak eklendi',
kprofilesSendFailed: '{{nozzle}} profilleri {{printer}} ({{serial}}) yazıcısına gönderilemedi',
kprofilesRefused: '{{printer}} ({{serial}}) {{nozzle}} profillerini reddetti: {{reason}}',
},
},
+2 -1
View File
@@ -4965,12 +4965,13 @@ export default {
settingsCompanionSkipped: "{{keys}} залишено вимкненими - потрібні їм облікові дані не можна відновити з резервної копії, і в цьому екземплярі вони не збережені, тож увімкнення залишило б інтеграцію без автентифікації",
settingsMqttRelayFailed: "Налаштування MQTT відновлено, але реле не вдалося перепідключити - перезапустіть Bambuddy",
kprofilesAlwaysOverwrite: "K-профілі завжди перезаписують відповідний слот на принтері",
kprofilesAckUnreliable: "Підтвердження принтера ненадійне - перевірте профілі на принтері",
kprofilesAckUnreliable: "Принтер, який не відповідає, усе одно вважається відновленим - перевірте профілі на принтері",
kprofilesPrinterMissing: "Немає принтера із серійним номером {{serial}} - пропущено",
kprofilesPrinterOffline: "{{printer}} ({{serial}}) не підключено - пропущено",
kprofilesUnknownNozzle: "Неочікуваний діаметр сопла {{nozzle}} для {{serial}} - надіслано як є",
kprofilesUnmatched: "Профілів для {{nozzle}} без відповідника на {{printer}}: {{count}} - додано як нові профілі",
kprofilesSendFailed: "Не вдалося надіслати профілі {{nozzle}} на {{printer}} ({{serial}})",
kprofilesRefused: "{{printer}} ({{serial}}) відхилив профілі {{nozzle}}: {{reason}}",
},
},
+2 -1
View File
@@ -4910,12 +4910,13 @@ export default {
settingsCompanionSkipped: '{{keys}} 保持关闭 - 它们各自所需的凭据无法从备份恢复,本实例也没有存储,开启会让集成处于未认证状态',
settingsMqttRelayFailed: 'MQTT 设置已恢复,但中继无法重新连接 - 请重启 Bambuddy',
kprofilesAlwaysOverwrite: 'K 值配置总是覆盖打印机上对应的槽位',
kprofilesAckUnreliable: '打印机的确认信息不可靠 - 请在打印机上核对配置',
kprofilesAckUnreliable: '打印机不回应时仍计为已恢复 - 请在打印机上核对配置',
kprofilesPrinterMissing: '没有序列号为 {{serial}} 的打印机 - 已跳过',
kprofilesPrinterOffline: '{{printer}}({{serial}})未连接 - 已跳过',
kprofilesUnknownNozzle: '{{serial}} 的喷嘴直径 {{nozzle}} 不在预期范围内 - 已原样发送',
kprofilesUnmatched: '{{nozzle}} 的 {{count}} 个配置在 {{printer}} 上没有对应项 - 已作为新配置添加',
kprofilesSendFailed: '无法将 {{nozzle}} 的配置发送到 {{printer}}({{serial}})',
kprofilesRefused: '{{printer}}({{serial}})拒绝了 {{nozzle}} 的配置:{{reason}}',
},
},
+2 -1
View File
@@ -4910,12 +4910,13 @@ export default {
settingsCompanionSkipped: '{{keys}} 維持關閉 - 它們各自所需的憑證無法從備份還原,本執行個體也沒有儲存,開啟會讓整合處於未驗證狀態',
settingsMqttRelayFailed: 'MQTT 設定已還原,但中繼無法重新連線 - 請重新啟動 Bambuddy',
kprofilesAlwaysOverwrite: 'K 值設定檔一律覆寫印表機上對應的插槽',
kprofilesAckUnreliable: '印表機的確認訊息不可靠 - 請在印表機上核對設定檔',
kprofilesAckUnreliable: '印表機未回應時仍計為已還原 - 請在印表機上核對設定檔',
kprofilesPrinterMissing: '沒有序號為 {{serial}} 的印表機 - 已略過',
kprofilesPrinterOffline: '{{printer}}({{serial}})未連線 - 已略過',
kprofilesUnknownNozzle: '{{serial}} 的噴嘴直徑 {{nozzle}} 不在預期範圍內 - 已原樣傳送',
kprofilesUnmatched: '{{nozzle}} 的 {{count}} 個設定檔在 {{printer}} 上沒有對應項 - 已新增為新設定檔',
kprofilesSendFailed: '無法將 {{nozzle}} 的設定檔傳送到 {{printer}}({{serial}})',
kprofilesRefused: '{{printer}}({{serial}})拒絕了 {{nozzle}} 的設定檔:{{reason}}',
},
},