Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions bambulabs_api/mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@ def is_valid_gcode(line: str):
return True


def _deep_merge(target: dict[str, Any], updates: dict[str, Any]) -> None:
"""
Recursively merge updates into target, in place

Nested dictionaries are merged key by key, so an incremental report does
not drop sibling fields it did not mention. Lists and scalars are replaced.

Args:
target (dict[str, Any]): The cached data, modified in place
updates (dict[str, Any]): The newly received data
"""
for key, value in updates.items():
existing = target.get(key)
if isinstance(existing, dict) and isinstance(value, dict):
_deep_merge(existing, value)
else:
target[key] = value


class PrinterMQTTClient:
"""
Printer class for handling MQTT communication with the printer
Expand Down Expand Up @@ -195,10 +214,7 @@ def _on_message(
self.on_message_handler(self, client, userdata, msg)

def manual_update(self, doc: dict[str, Any]) -> None:
for k, v in doc.items():
if k not in self._data:
self._data[k] = {}
self._data[k] |= v
_deep_merge(self._data, doc)
logger.debug(self._data)

firmware_version = self.firmware_version()
Expand Down
42 changes: 42 additions & 0 deletions tests/test_mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,45 @@ def test_nozzle_diameter():
def test_get_firmware():
assert mqtt.firmware_version() == "01.07.00.00"
assert mqtt.printer_info.firmware_version == "01.07.00.00"


def test_incremental_update_preserves_sibling_fields():
"""A partial report must not wipe fields it does not mention."""
mqtt_ = bl.PrinterMQTTClient(hostname="", access="", printer_serial="")
mqtt_.manual_update(
{
"print": {
"gcode_state": "RUNNING",
"ams": {
"ams_exist_bits": "1",
"tray_exist_bits": "f",
"ams": [{"id": "0", "tray": [{"id": "0"}]}],
},
},
}
)
# An incremental report mentioning only part of the ams object
mqtt_.manual_update(
{
"print": {
"ams": {
"ams": [{"id": "0", "tray": [{"id": "0", "tray_color": "FF0000FF"}]}],
},
},
}
)

ams = mqtt_._data["print"]["ams"] # noqa: SLF001
assert ams["ams_exist_bits"] == "1", "sibling field dropped by partial update"
assert ams["tray_exist_bits"] == "f", "sibling field dropped by partial update"
assert ams["ams"][0]["tray"][0]["tray_color"] == "FF0000FF", "update not applied"
assert mqtt_._data["print"]["gcode_state"] == "RUNNING" # noqa: SLF001


def test_update_replaces_scalars_and_lists():
"""Lists and scalars are replaced, not merged."""
mqtt_ = bl.PrinterMQTTClient(hostname="", access="", printer_serial="")
mqtt_.manual_update({"print": {"gcode_state": "RUNNING", "s_obj": [1, 2]}})
mqtt_.manual_update({"print": {"gcode_state": "FINISH", "s_obj": [9]}})
assert mqtt_._data["print"]["gcode_state"] == "FINISH" # noqa: SLF001
assert mqtt_._data["print"]["s_obj"] == [9] # noqa: SLF001