anycubic-cloud-api 0.4.34__tar.gz → 0.4.35__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {anycubic_cloud_api-0.4.34/src/anycubic_cloud_api.egg-info → anycubic_cloud_api-0.4.35}/PKG-INFO +1 -1
  2. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/pyproject.toml +1 -1
  3. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/printer.py +52 -18
  4. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35/src/anycubic_cloud_api.egg-info}/PKG-INFO +1 -1
  5. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api.egg-info/SOURCES.txt +1 -0
  6. anycubic_cloud_api-0.4.35/tests/test_filament_change_0435.py +133 -0
  7. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_print_reports_0434.py +9 -4
  8. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/COPYRIGHT +0 -0
  9. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/LICENSE +0 -0
  10. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/MANIFEST.in +0 -0
  11. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/README.md +0 -0
  12. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/setup.cfg +0 -0
  13. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/__init__.py +0 -0
  14. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/anycubic_api.py +0 -0
  15. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/api/base.py +0 -0
  16. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/api/functions.py +0 -0
  17. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/api/mqtt.py +0 -0
  18. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/const/api_endpoints.py +0 -0
  19. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/const/const.py +0 -0
  20. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/const/enums.py +0 -0
  21. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/const/error_codes.py +0 -0
  22. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/const/lan.py +0 -0
  23. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/const/mqtt.py +0 -0
  24. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/const/regions.py +0 -0
  25. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/consumable.py +0 -0
  26. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/files.py +0 -0
  27. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/gcode_file.py +0 -0
  28. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/orders.py +0 -0
  29. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/print_response.py +0 -0
  30. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/print_speed_mode.py +0 -0
  31. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/printer_properties.py +0 -0
  32. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/printing_settings.py +0 -0
  33. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/data_models/project.py +0 -0
  34. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/exceptions/error_strings.py +0 -0
  35. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/exceptions/exceptions.py +0 -0
  36. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/helpers/helpers.py +0 -0
  37. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/lan/__init__.py +0 -0
  38. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/lan/client.py +0 -0
  39. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/lan/commands.py +0 -0
  40. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/lan/handshake.py +0 -0
  41. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/models/auth.py +0 -0
  42. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/models/cloud_upload.py +0 -0
  43. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/models/http.py +0 -0
  44. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_ca.crt +0 -0
  45. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.crt +0 -0
  46. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.key +0 -0
  47. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api.egg-info/dependency_links.txt +0 -0
  48. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api.egg-info/requires.txt +0 -0
  49. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/src/anycubic_cloud_api.egg-info/top_level.txt +0 -0
  50. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_auth_mode_fallback.py +0 -0
  51. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_axis_position.py +0 -0
  52. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_endpoint_urls.py +0 -0
  53. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_exchange_rate_limit.py +0 -0
  54. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_lan_client.py +0 -0
  55. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_lan_commands.py +0 -0
  56. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_lan_handshake.py +0 -0
  57. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_lan_reports.py +0 -0
  58. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_null_tolerant_parsing.py +0 -0
  59. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_partial_reports.py +0 -0
  60. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_printer_error_codes.py +0 -0
  61. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_project_null_status.py +0 -0
  62. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_regions.py +0 -0
  63. {anycubic_cloud_api-0.4.34 → anycubic_cloud_api-0.4.35}/tests/test_secondary_ace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anycubic-cloud-api
3
- Version: 0.4.34
3
+ Version: 0.4.35
4
4
  Summary: Async client for the Anycubic Cloud API and its MQTT stream
5
5
  Author: WaresWichall, Nino Bondonno
6
6
  Maintainer: Nino Bondonno
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "anycubic-cloud-api"
7
- version = "0.4.34"
7
+ version = "0.4.35"
8
8
  description = "Async client for the Anycubic Cloud API and its MQTT stream"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -64,6 +64,20 @@ _PRINT_PROGRESS_KEYS = (
64
64
  'supplies_usage',
65
65
  )
66
66
 
67
+ # The steps of a filament change, as a Kobra X with an ACE 2 Pro reports
68
+ # them (#33). The ACE unfeed is the mirror of the feed, not yet seen.
69
+ _FILAMENT_CHANGE_STATES = frozenset({
70
+ 'extfilbox_filament_feeding',
71
+ 'extfilbox_filament_unfeeding',
72
+ 'ace_filament_feeding',
73
+ 'ace_filament_unfeeding',
74
+ 'nozzle_purging',
75
+ })
76
+
77
+ # Printers with a feeder of their own, which shares the box list with any
78
+ # ACE fitted: the Kobra X.
79
+ _PRINTERS_WITH_OWN_FEEDER = frozenset({20030})
80
+
67
81
  # How a printer words a refused order when it gives no code.
68
82
  _REFUSED_STATES = frozenset({'failed', 'fail', 'error', 'refused'})
69
83
 
@@ -1317,13 +1331,23 @@ class AnycubicPrinter:
1317
1331
  data.get('settings')
1318
1332
 
1319
1333
  if 'channel_no' in data:
1320
- self._apply_active_channel(data['channel_no'])
1334
+ self._apply_active_channel(data['channel_no'], data.get('unit_no'))
1321
1335
 
1322
1336
  # A Kobra X reports progress this way between info reports, with
1323
1337
  # no task id. It was logged as unhandled and dropped (#34).
1324
1338
  if any(key in data for key in _PRINT_PROGRESS_KEYS):
1325
1339
  self._apply_print_progress(project_id, data)
1326
1340
  return
1341
+ elif action == 'start' and state in _FILAMENT_CHANGE_STATES:
1342
+ # Each step of a filament change, naming the channel (and, for an
1343
+ # ACE, the unit). Only a feed says what is now loaded (#33).
1344
+ data = payload.get('data') or {}
1345
+ # Read on every step, so the ones that change nothing are not
1346
+ # reported as unhandled.
1347
+ channel_no, unit_no = data.get('channel_no'), data.get('unit_no')
1348
+ if state.endswith('_filament_feeding'):
1349
+ self._apply_active_channel(channel_no, unit_no)
1350
+ return
1327
1351
  elif action in ['start', 'stop'] and state in ['failed']:
1328
1352
  err_msg = payload.get('msg')
1329
1353
  self._is_printing = 1
@@ -1336,31 +1360,37 @@ class AnycubicPrinter:
1336
1360
  else:
1337
1361
  raise AnycubicMQTTUnknownUpdate(ErrorsMQTTUpdate.job_status)
1338
1362
 
1339
- def _apply_active_channel(self, channel_no: Any) -> None:
1363
+ def _apply_active_channel(self, channel_no: Any, unit_no: Any = None) -> None:
1340
1364
  """Follow the filament channel the printer says it is printing from.
1341
1365
 
1342
1366
  A Kobra X reports each filament change as a print update carrying
1343
- only ``channel_no``, and stops sending the ACE's own ``loaded_slot``
1344
- after the first few changes of a print -- which is why the loaded
1345
- slot froze for the rest of the job (#34). The channel is numbered
1346
- from 1 across every box in order, so channel 5 on a printer whose
1347
- first box has four slots is the first slot of the second box.
1367
+ ``channel_no``, and stops sending the box's own ``loaded_slot`` after
1368
+ the first few changes of a print, which is why the loaded slot froze
1369
+ (#34). Each box numbers its channels from 1; an ACE adds ``unit_no``
1370
+ and the printer's own feeder sends none (#33, Kobra X + ACE 2 Pro).
1371
+
1372
+ With one box the channel can only mean that box. With several, an
1373
+ ACE's ``unit_no`` picks it on a printer whose boxes are all ACEs.
1374
+ On a Kobra X with an ACE, which list entry is the printer's own
1375
+ feeder is not yet confirmed, so nothing is guessed there: a wrong
1376
+ slot is worse than a stale one.
1348
1377
  """
1349
1378
  channel = as_int(channel_no, 0)
1379
+ boxes = self._multi_color_box
1350
1380
 
1351
- if channel < 1 or not self._multi_color_box:
1381
+ if channel < 1 or not boxes:
1352
1382
  return
1353
1383
 
1354
- index = channel - 1
1355
-
1356
- for box in self._multi_color_box:
1357
- slot_count = len(box.slots)
1358
-
1359
- if index < slot_count:
1360
- box.set_slot_loaded(index)
1384
+ if len(boxes) == 1:
1385
+ box = boxes[0]
1386
+ else:
1387
+ unit = as_int(unit_no, 0)
1388
+ if unit < 1 or unit > len(boxes) or self._machine_type in _PRINTERS_WITH_OWN_FEEDER:
1361
1389
  return
1390
+ box = boxes[unit - 1]
1362
1391
 
1363
- index -= slot_count
1392
+ if channel <= len(box.slots):
1393
+ box.set_slot_loaded(channel - 1)
1364
1394
 
1365
1395
  def _apply_print_progress(
1366
1396
  self,
@@ -1466,8 +1496,12 @@ class AnycubicPrinter:
1466
1496
  continue
1467
1497
 
1468
1498
  assert self._multi_color_box
1469
- self._multi_color_box[box_id].set_current_temperature(box['temp'])
1470
- self._multi_color_box[box_id].set_drying_status(box['drying_status'])
1499
+ # An ACE 2 Pro's periodic drying report carries no `temp`, and
1500
+ # reading it raised every six minutes, so drying never updated
1501
+ # (#33). Keep the last reading rather than inventing one.
1502
+ if 'temp' in box:
1503
+ self._multi_color_box[box_id].set_current_temperature(box['temp'])
1504
+ self._multi_color_box[box_id].set_drying_status(box.get('drying_status'))
1471
1505
  return
1472
1506
  elif action == 'setDry' and self._report_refused_drying(state, payload):
1473
1507
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anycubic-cloud-api
3
- Version: 0.4.34
3
+ Version: 0.4.35
4
4
  Summary: Async client for the Anycubic Cloud API and its MQTT stream
5
5
  Author: WaresWichall, Nino Bondonno
6
6
  Maintainer: Nino Bondonno
@@ -47,6 +47,7 @@ tests/test_auth_mode_fallback.py
47
47
  tests/test_axis_position.py
48
48
  tests/test_endpoint_urls.py
49
49
  tests/test_exchange_rate_limit.py
50
+ tests/test_filament_change_0435.py
50
51
  tests/test_lan_client.py
51
52
  tests/test_lan_commands.py
52
53
  tests/test_lan_handshake.py
@@ -0,0 +1,133 @@
1
+ """Filament-change reports and the ACE 2 Pro's drying report (0.4.35).
2
+
3
+ From a Kobra X with an ACE 2 Pro (#33). Each step of a filament change
4
+ arrives as a print ``start`` naming ``channel_no``, counted from 1 within
5
+ its box, plus ``unit_no`` when the feed comes from an ACE. These were
6
+ dropped as unhandled. The ACE 2 Pro's periodic drying report has no
7
+ ``temp``, and reading it raised every six minutes.
8
+ """
9
+
10
+ from unittest.mock import MagicMock
11
+
12
+ import pytest
13
+
14
+ from anycubic_cloud_api.data_models.consumable import AnycubicConsumableData
15
+ from anycubic_cloud_api.data_models.printer import AnycubicPrinter
16
+
17
+ KOBRA_X = 20030
18
+ KOBRA_S1 = 20025
19
+
20
+
21
+ def slot(index):
22
+ return {"index": index, "sku": "", "type": "PLA", "color": [1, 2, 3], "status": 5, "edit_status": 1}
23
+
24
+
25
+ def box(box_id, slots=4):
26
+ return {"id": box_id, "loaded_slot": -1, "temp": 21, "slots": [slot(i) for i in range(slots)]}
27
+
28
+
29
+ def make_printer(boxes, machine_type=KOBRA_X):
30
+ return AnycubicPrinter(
31
+ api_parent=MagicMock(),
32
+ machine_type=machine_type,
33
+ machine_name="Printer",
34
+ id=1,
35
+ multi_color_box=boxes,
36
+ ignore_init_errors=True,
37
+ )
38
+
39
+
40
+ def message(msg_type, action, state, data):
41
+ return AnycubicConsumableData({
42
+ "type": msg_type, "action": action, "state": state,
43
+ "code": 200, "msg": "done", "msgid": "m", "timestamp": 1, "data": data,
44
+ })
45
+
46
+
47
+ def topic(machine_type, kind):
48
+ return f"anycubic/anycubicCloud/v1/printer/public/{machine_type}/key/{kind}/report"
49
+
50
+
51
+ def feed(printer, state, channel_no, unit_no=None, machine_type=KOBRA_X):
52
+ data = {"channel_no": channel_no, "taskid": "t"}
53
+ if unit_no is not None:
54
+ data["unit_no"] = unit_no
55
+ printer.process_mqtt_update(topic(machine_type, "print"), message("print", "start", state, data))
56
+
57
+
58
+ def loaded(printer):
59
+ return [b.loaded_slot for b in printer.multi_color_box]
60
+
61
+
62
+ class TestFilamentChange:
63
+ @pytest.mark.parametrize(("state", "unit_no"), [
64
+ ("extfilbox_filament_feeding", None),
65
+ ("ace_filament_feeding", 1),
66
+ ])
67
+ def test_one_box_follows_the_feed(self, state, unit_no):
68
+ printer = make_printer([box(0)])
69
+
70
+ feed(printer, state, 3, unit_no)
71
+
72
+ assert loaded(printer) == [2]
73
+
74
+ @pytest.mark.parametrize("state", [
75
+ "extfilbox_filament_unfeeding", "ace_filament_unfeeding", "nozzle_purging",
76
+ ])
77
+ def test_other_steps_are_read_but_change_nothing(self, state):
78
+ """Consumed, so not logged as unhandled; only a feed says what is loaded."""
79
+ printer = make_printer([box(0)])
80
+ printer.multi_color_box[0].set_slot_loaded(1)
81
+
82
+ feed(printer, state, 3, 1)
83
+
84
+ assert loaded(printer) == [1]
85
+
86
+ @pytest.mark.parametrize(("unit_no", "expected"), [(1, [1, -1]), (2, [-1, 1])])
87
+ def test_an_ace_unit_picks_its_box_on_a_printer_of_aces(self, unit_no, expected):
88
+ """A Kobra S1 has no feeder of its own: unit 1 is the first ACE."""
89
+ printer = make_printer([box(0), box(1)], machine_type=KOBRA_S1)
90
+
91
+ feed(printer, "ace_filament_feeding", 2, unit_no, machine_type=KOBRA_S1)
92
+
93
+ assert loaded(printer) == expected
94
+
95
+ @pytest.mark.parametrize(("state", "unit_no"), [
96
+ ("extfilbox_filament_feeding", None),
97
+ ("ace_filament_feeding", 1),
98
+ ])
99
+ def test_a_kobra_x_with_an_ace_guesses_nothing(self, state, unit_no):
100
+ """Which list entry is the Kobra X's own feeder is unconfirmed (#33)."""
101
+ printer = make_printer([box(0), box(1)])
102
+
103
+ feed(printer, state, 1, unit_no)
104
+
105
+ assert loaded(printer) == [-1, -1]
106
+
107
+ @pytest.mark.parametrize(("channel_no", "unit_no"), [(5, 1), (0, 1), (2, 3), ("x", 1), (2, None)])
108
+ def test_a_channel_or_unit_that_fits_no_box_changes_nothing(self, channel_no, unit_no):
109
+ printer = make_printer([box(0), box(1)], machine_type=KOBRA_S1)
110
+
111
+ feed(printer, "ace_filament_feeding", channel_no, unit_no, machine_type=KOBRA_S1)
112
+
113
+ assert loaded(printer) == [-1, -1]
114
+
115
+
116
+ class TestAce2ProDryingReport:
117
+ REPORT = {"multi_color_box": [{
118
+ "drying_status": {"duration": 14400, "humidity": 28, "remain_time": 162, "status": 1, "target_temp": 50},
119
+ "id": 0,
120
+ }]}
121
+
122
+ def test_a_report_without_temp_still_updates_drying(self):
123
+ printer = make_printer([box(0)])
124
+
125
+ printer.process_mqtt_update(
126
+ topic(KOBRA_X, "multiColorBox"),
127
+ message("multiColorBox", "autoUpdateDryStatus", "success", self.REPORT),
128
+ )
129
+
130
+ ace = printer.multi_color_box[0]
131
+ assert ace.drying_status.is_drying
132
+ assert ace.drying_status.target_temperature == 50
133
+ assert ace.current_temperature == 21 # the last reading is kept
@@ -90,13 +90,18 @@ class TestActiveChannel:
90
90
 
91
91
  assert printer.primary_multi_color_box_loaded_slot == slot_index
92
92
 
93
- def test_channels_beyond_the_first_box_land_in_the_next(self):
94
- """Channel 5 with four slots in the first box is the second box's first."""
93
+ @pytest.mark.parametrize("channel_no", [1, 5])
94
+ def test_with_two_boxes_and_no_unit_nothing_is_guessed(self, channel_no):
95
+ """Channels are numbered per box (#33), so without a unit the box is unknown.
96
+
97
+ 0.4.34 counted across boxes and put channel 5 in the second box.
98
+ """
95
99
  printer = make_printer([box(0), box(1)])
100
+ before = [b.loaded_slot for b in printer.multi_color_box]
96
101
 
97
- self._channel(printer, 5)
102
+ self._channel(printer, channel_no)
98
103
 
99
- assert printer.secondary_multi_color_box_loaded_slot == 0
104
+ assert [b.loaded_slot for b in printer.multi_color_box] == before
100
105
 
101
106
  @pytest.mark.parametrize("channel_no", [0, -1, 9, None, "x"])
102
107
  def test_a_channel_with_no_slot_changes_nothing(self, channel_no):