anycubic-cloud-api 0.4.26__tar.gz → 0.4.27__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.
- {anycubic_cloud_api-0.4.26/src/anycubic_cloud_api.egg-info → anycubic_cloud_api-0.4.27}/PKG-INFO +1 -1
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/pyproject.toml +1 -1
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/printer.py +4 -1
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/printer_properties.py +63 -46
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/helpers/helpers.py +53 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27/src/anycubic_cloud_api.egg-info}/PKG-INFO +1 -1
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api.egg-info/SOURCES.txt +1 -0
- anycubic_cloud_api-0.4.27/tests/test_null_tolerant_parsing.py +143 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/COPYRIGHT +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/LICENSE +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/MANIFEST.in +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/README.md +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/setup.cfg +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/__init__.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/anycubic_api.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/api/base.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/api/functions.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/api/mqtt.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/api_endpoints.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/const.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/enums.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/error_codes.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/lan.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/mqtt.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/regions.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/consumable.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/files.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/gcode_file.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/orders.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/print_response.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/print_speed_mode.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/printing_settings.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/project.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/exceptions/error_strings.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/exceptions/exceptions.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/lan/__init__.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/lan/client.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/lan/commands.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/lan/handshake.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/models/auth.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/models/cloud_upload.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/models/http.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_ca.crt +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.crt +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.key +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api.egg-info/dependency_links.txt +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api.egg-info/requires.txt +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api.egg-info/top_level.txt +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_auth_mode_fallback.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_axis_position.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_endpoint_urls.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_lan_client.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_lan_commands.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_lan_handshake.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_lan_reports.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_partial_reports.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_printer_error_codes.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_project_null_status.py +0 -0
- {anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/tests/test_regions.py +0 -0
|
@@ -1127,7 +1127,10 @@ class AnycubicPrinter:
|
|
|
1127
1127
|
state: str,
|
|
1128
1128
|
payload: AnycubicConsumableData,
|
|
1129
1129
|
) -> None:
|
|
1130
|
-
|
|
1130
|
+
# `or {}` rather than a plain default: a present-but-null `data`
|
|
1131
|
+
# hands back None from .get and the chained .get then raises
|
|
1132
|
+
# AttributeError. Same trap as the null coercions, different shape.
|
|
1133
|
+
project_id = (payload.get('data') or {}).get('taskid', -1)
|
|
1131
1134
|
if action == 'start' and state == 'printing':
|
|
1132
1135
|
data = payload['data']
|
|
1133
1136
|
self._is_printing = 2
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ..exceptions.error_strings import ErrorsDataParsing
|
|
6
|
+
from ..helpers.helpers import as_int, as_int_list, as_str
|
|
6
7
|
from ..exceptions.exceptions import AnycubicDataParsingError
|
|
7
8
|
|
|
8
9
|
|
|
@@ -27,13 +28,13 @@ class AnycubicMaterialMapping:
|
|
|
27
28
|
color_blue: int,
|
|
28
29
|
paint_index: int = 0,
|
|
29
30
|
) -> None:
|
|
30
|
-
self._spool_index =
|
|
31
|
+
self._spool_index = as_int(spool_index, -1)
|
|
31
32
|
self._filament_used = float(filament_used)
|
|
32
|
-
self._material_type =
|
|
33
|
-
self._color_red =
|
|
34
|
-
self._color_green =
|
|
35
|
-
self._color_blue =
|
|
36
|
-
self._paint_index =
|
|
33
|
+
self._material_type = as_str(material_type)
|
|
34
|
+
self._color_red = as_int(color_red)
|
|
35
|
+
self._color_green = as_int(color_green)
|
|
36
|
+
self._color_blue = as_int(color_blue)
|
|
37
|
+
self._paint_index = as_int(paint_index, -1)
|
|
37
38
|
|
|
38
39
|
@property
|
|
39
40
|
def spool_index(self) -> int:
|
|
@@ -207,19 +208,22 @@ class AnycubicMachineToolInfo:
|
|
|
207
208
|
status: int,
|
|
208
209
|
show_place: int,
|
|
209
210
|
) -> None:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
self.
|
|
214
|
-
self.
|
|
215
|
-
self.
|
|
216
|
-
self.
|
|
217
|
-
self.
|
|
211
|
+
# Descriptive data about what the machine can do. Thirteen fields
|
|
212
|
+
# read straight out of the cloud's dict, any one of which arriving
|
|
213
|
+
# null used to raise and cost the caller the whole printer.
|
|
214
|
+
self._id = as_int(id, -1)
|
|
215
|
+
self._typd_id = as_int(typd_id, -1)
|
|
216
|
+
self._model_id = as_int(model_id, -1)
|
|
217
|
+
self._type_function_id = as_int(type_function_id, -1)
|
|
218
|
+
self._parent_id = as_int(parent_id, -1)
|
|
219
|
+
self._function_name = as_str(function_name)
|
|
220
|
+
self._function_des = as_str(function_des)
|
|
221
|
+
self._control = as_int(control)
|
|
218
222
|
self._param = param
|
|
219
|
-
self._icon_url =
|
|
220
|
-
self._function_type =
|
|
221
|
-
self._status =
|
|
222
|
-
self._show_place =
|
|
223
|
+
self._icon_url = as_str(icon_url)
|
|
224
|
+
self._function_type = as_int(function_type)
|
|
225
|
+
self._status = as_int(status)
|
|
226
|
+
self._show_place = as_int(show_place)
|
|
223
227
|
|
|
224
228
|
@classmethod
|
|
225
229
|
def from_json(cls, data: dict[str, Any] | None) -> AnycubicMachineToolInfo | None:
|
|
@@ -271,14 +275,18 @@ class AnycubicMachineExternalShelves:
|
|
|
271
275
|
status_type: int,
|
|
272
276
|
current_status: int,
|
|
273
277
|
) -> None:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
self.
|
|
281
|
-
self.
|
|
278
|
+
# Every one of these is coerced defensively. Firmware 2.0.1.9 began
|
|
279
|
+
# sending this object with `id` and `loaded` null, and the bare
|
|
280
|
+
# int() calls that used to be here raised TypeError -- which failed
|
|
281
|
+
# the whole printer's parse and left the entry in setup_retry with
|
|
282
|
+
# every entity unavailable (#28). The holder is an accessory; not
|
|
283
|
+
# knowing its id is not a reason to lose the printer.
|
|
284
|
+
self._id = as_int(id, -1)
|
|
285
|
+
self._type = as_str(type)
|
|
286
|
+
self._color = as_int_list(color)
|
|
287
|
+
self._loaded = as_int(loaded)
|
|
288
|
+
self._status_type = as_int(status_type)
|
|
289
|
+
self._current_status = as_int(current_status)
|
|
282
290
|
|
|
283
291
|
@classmethod
|
|
284
292
|
def from_json(cls, data: dict[str, Any] | None) -> AnycubicMachineExternalShelves | None:
|
|
@@ -298,14 +306,18 @@ class AnycubicMachineExternalShelves:
|
|
|
298
306
|
if data is None:
|
|
299
307
|
return None
|
|
300
308
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
self.
|
|
305
|
-
self.
|
|
309
|
+
# Coerced exactly as the constructor does. This path wrote whatever
|
|
310
|
+
# arrived straight onto the slots, so a null over MQTT produced an
|
|
311
|
+
# object whose own properties then raised on read instead.
|
|
312
|
+
self._type = as_str(data.get('type'))
|
|
313
|
+
self._color = as_int_list(data.get('color'))
|
|
314
|
+
self._loaded = as_int(data.get('loaded'))
|
|
315
|
+
self._status_type = as_int(data.get('status_type'))
|
|
316
|
+
self._current_status = as_int(data.get('current_status'))
|
|
306
317
|
|
|
307
318
|
@property
|
|
308
319
|
def id(self) -> int:
|
|
320
|
+
"""The holder's id, or -1 when the printer did not name one."""
|
|
309
321
|
return self._id
|
|
310
322
|
|
|
311
323
|
@property
|
|
@@ -400,10 +412,10 @@ class AnycubicFeedStatus:
|
|
|
400
412
|
current_status: int,
|
|
401
413
|
slot_index: int,
|
|
402
414
|
) -> None:
|
|
403
|
-
self._code =
|
|
404
|
-
self._type =
|
|
405
|
-
self._current_status =
|
|
406
|
-
self._slot_index =
|
|
415
|
+
self._code = as_int(code, -1)
|
|
416
|
+
self._type = as_int(type)
|
|
417
|
+
self._current_status = as_int(current_status)
|
|
418
|
+
self._slot_index = as_int(slot_index, -1)
|
|
407
419
|
|
|
408
420
|
@classmethod
|
|
409
421
|
def from_json(cls, data: dict[str, Any] | None) -> AnycubicFeedStatus | None:
|
|
@@ -449,10 +461,10 @@ class AnycubicDryingStatus:
|
|
|
449
461
|
duration: int,
|
|
450
462
|
remain_time: int,
|
|
451
463
|
) -> None:
|
|
452
|
-
self._status =
|
|
453
|
-
self._target_temp =
|
|
454
|
-
self._duration =
|
|
455
|
-
self._remain_time =
|
|
464
|
+
self._status = as_int(status)
|
|
465
|
+
self._target_temp = as_int(target_temp)
|
|
466
|
+
self._duration = as_int(duration)
|
|
467
|
+
self._remain_time = as_int(remain_time)
|
|
456
468
|
|
|
457
469
|
@classmethod
|
|
458
470
|
def from_json(cls, data: dict[str, Any] | None) -> AnycubicDryingStatus | None:
|
|
@@ -691,11 +703,14 @@ class AnycubicMultiColorBox:
|
|
|
691
703
|
slots: list[dict[str, Any]],
|
|
692
704
|
humidity: float | None = None,
|
|
693
705
|
) -> None:
|
|
706
|
+
# `id` keeps raising on purpose: it is the ACE's identity and the
|
|
707
|
+
# printer dict is keyed by it, so a box with no id cannot be placed.
|
|
708
|
+
# Everything after it is telemetry and must not be able to sink the box.
|
|
694
709
|
self._id: int = int(id)
|
|
695
|
-
self._status: int =
|
|
696
|
-
self._model_id: int =
|
|
697
|
-
self._auto_feed: int =
|
|
698
|
-
self._loaded_slot: int =
|
|
710
|
+
self._status: int = as_int(status)
|
|
711
|
+
self._model_id: int = as_int(model_id)
|
|
712
|
+
self._auto_feed: int = as_int(auto_feed)
|
|
713
|
+
self._loaded_slot: int = as_int(loaded_slot, -1)
|
|
699
714
|
self.set_feed_status(feed_status)
|
|
700
715
|
self.set_current_temperature(temp)
|
|
701
716
|
self._humidity: float | None = (
|
|
@@ -712,7 +727,7 @@ class AnycubicMultiColorBox:
|
|
|
712
727
|
raise AnycubicDataParsingError(ErrorsDataParsing.ace.format(slots))
|
|
713
728
|
|
|
714
729
|
def set_auto_feed(self, auto_feed: int) -> None:
|
|
715
|
-
self._auto_feed =
|
|
730
|
+
self._auto_feed = as_int(auto_feed)
|
|
716
731
|
|
|
717
732
|
def set_drying_status(self, drying_status: dict[str, Any] | None) -> None:
|
|
718
733
|
self._drying_status = AnycubicDryingStatus.from_json(drying_status)
|
|
@@ -721,10 +736,12 @@ class AnycubicMultiColorBox:
|
|
|
721
736
|
self._feed_status = AnycubicFeedStatus.from_json(feed_status)
|
|
722
737
|
|
|
723
738
|
def set_slot_loaded(self, slot_num: int) -> None:
|
|
724
|
-
self._loaded_slot = slot_num
|
|
739
|
+
self._loaded_slot = as_int(slot_num, -1)
|
|
725
740
|
|
|
726
741
|
def set_current_temperature(self, temp: int) -> None:
|
|
727
|
-
|
|
742
|
+
# Reached from the constructor as well as from MQTT, so a null
|
|
743
|
+
# temperature took the whole box down and not merely the reading.
|
|
744
|
+
self._temp = as_int(temp)
|
|
728
745
|
|
|
729
746
|
def update_slots_with_mqtt_data(self, slot_list: list[dict[str, Any]] | None) -> None:
|
|
730
747
|
if slot_list is None:
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/helpers/helpers.py
RENAMED
|
@@ -17,6 +17,59 @@ REX_GCODE_DATA_KEY_VALUE: re.Pattern[Any] = re.compile(r'; ([a-zA-Z0-9_\[\] ]+)
|
|
|
17
17
|
REX_PRINT_TOTAL_TIME: re.Pattern[Any] = re.compile(r'^([\d]+)hour([\d]+)min$')
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def as_int(value: Any, default: int = 0) -> int:
|
|
21
|
+
"""A whole number out of whatever the cloud sent, or `default`.
|
|
22
|
+
|
|
23
|
+
Anycubic adds fields and nulls them without warning: firmware 2.0.1.9
|
|
24
|
+
began sending `external_shelves` with `id` and `loaded` set to null, and
|
|
25
|
+
`int(None)` raised, which failed the parse of the whole printer and left
|
|
26
|
+
every entity unavailable behind a config entry stuck in setup_retry.
|
|
27
|
+
|
|
28
|
+
Note that `data.get("key", 0)` does NOT protect against this. The default
|
|
29
|
+
only applies when the key is ABSENT; a key that is present and null hands
|
|
30
|
+
back None and raises anyway. That is precisely how the above shipped.
|
|
31
|
+
|
|
32
|
+
A single unreadable telemetry field is never worth losing the printer
|
|
33
|
+
over, so this returns the stated fallback rather than raising.
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(value, bool):
|
|
36
|
+
# bool is an int subclass, so int(True) is 1 either way -- spelled out
|
|
37
|
+
# so the behaviour is a decision rather than an accident.
|
|
38
|
+
return int(value)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
return int(value)
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
return default
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def as_str(value: Any, default: str = "") -> str:
|
|
47
|
+
"""Text out of whatever the cloud sent. `None` becomes `default`, not "None"."""
|
|
48
|
+
if value is None:
|
|
49
|
+
return default
|
|
50
|
+
|
|
51
|
+
return str(value)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def as_int_list(value: Any) -> list[int]:
|
|
55
|
+
"""A list of whole numbers, dropping anything that is not one.
|
|
56
|
+
|
|
57
|
+
Colours arrive here. A null list, or a list with a null in it, used to
|
|
58
|
+
raise from inside the comprehension and take the printer down with it.
|
|
59
|
+
"""
|
|
60
|
+
if not isinstance(value, (list, tuple)):
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
numbers: list[int] = []
|
|
64
|
+
for item in value:
|
|
65
|
+
try:
|
|
66
|
+
numbers.append(int(item))
|
|
67
|
+
except (TypeError, ValueError):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
return numbers
|
|
71
|
+
|
|
72
|
+
|
|
20
73
|
def timedelta_to_total_minutes(
|
|
21
74
|
delta: timedelta,
|
|
22
75
|
) -> float:
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Nulls in the cloud payload must not cost the caller the printer.
|
|
2
|
+
|
|
3
|
+
Reported as hass-anycubic #28: Anycubic firmware 2.0.1.9 began sending
|
|
4
|
+
`external_shelves` with `id` and `loaded` set to null. `int(None)` raised,
|
|
5
|
+
the whole printer failed to parse, and the config entry sat in setup_retry
|
|
6
|
+
with every entity unavailable -- on a response the cloud had answered
|
|
7
|
+
successfully, carrying complete telemetry.
|
|
8
|
+
|
|
9
|
+
The trap is worth stating plainly, because the code looked defended:
|
|
10
|
+
|
|
11
|
+
data.get('loaded', False)
|
|
12
|
+
|
|
13
|
+
only supplies the default when the key is ABSENT. A key that is present and
|
|
14
|
+
null returns None, and the int() after it raises anyway. That is exactly how
|
|
15
|
+
this shipped.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from anycubic_cloud_api.data_models.printer_properties import (
|
|
21
|
+
AnycubicDryingStatus,
|
|
22
|
+
AnycubicFeedStatus,
|
|
23
|
+
AnycubicMachineExternalShelves,
|
|
24
|
+
AnycubicMachineToolInfo,
|
|
25
|
+
AnycubicMultiColorBox,
|
|
26
|
+
)
|
|
27
|
+
from anycubic_cloud_api.helpers.helpers import as_int, as_int_list, as_str
|
|
28
|
+
|
|
29
|
+
# Verbatim from the issue, nulls and all.
|
|
30
|
+
SHELVES_2019 = {
|
|
31
|
+
"brand_name": None,
|
|
32
|
+
"color": [255, 255, 255],
|
|
33
|
+
"current_status": 11,
|
|
34
|
+
"id": None,
|
|
35
|
+
"loaded": None,
|
|
36
|
+
"material_name": None,
|
|
37
|
+
"status_type": 2,
|
|
38
|
+
"type": "",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestTheReportedPayload:
|
|
43
|
+
def test_firmware_2019_external_shelves_parses(self):
|
|
44
|
+
"""The payload that took people's printers away."""
|
|
45
|
+
shelves = AnycubicMachineExternalShelves.from_json(SHELVES_2019)
|
|
46
|
+
|
|
47
|
+
assert shelves is not None
|
|
48
|
+
assert shelves.id == -1 # not named -> stated as unknown
|
|
49
|
+
assert shelves.loaded is False
|
|
50
|
+
assert shelves.color == [255, 255, 255]
|
|
51
|
+
assert shelves.material_type is None # '' means empty, not "empty"
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize("field", sorted(SHELVES_2019))
|
|
54
|
+
def test_any_single_field_may_be_null(self, field):
|
|
55
|
+
"""Not just the two that happened to be null this time.
|
|
56
|
+
|
|
57
|
+
Anycubic nulls fields without warning, so each is checked on its own
|
|
58
|
+
rather than trusting that the next firmware picks the same two.
|
|
59
|
+
"""
|
|
60
|
+
payload = dict(SHELVES_2019)
|
|
61
|
+
payload[field] = None
|
|
62
|
+
|
|
63
|
+
assert AnycubicMachineExternalShelves.from_json(payload) is not None
|
|
64
|
+
|
|
65
|
+
def test_a_null_inside_the_colour_list_is_survived(self):
|
|
66
|
+
payload = dict(SHELVES_2019, color=[255, None, 255])
|
|
67
|
+
|
|
68
|
+
assert AnycubicMachineExternalShelves.from_json(payload).color == [255, 255]
|
|
69
|
+
|
|
70
|
+
def test_the_mqtt_update_path_is_coerced_too(self):
|
|
71
|
+
"""It wrote raw values onto the slots, so a null landed intact and
|
|
72
|
+
the object's own properties raised on read instead of on parse."""
|
|
73
|
+
shelves = AnycubicMachineExternalShelves.from_json(SHELVES_2019)
|
|
74
|
+
|
|
75
|
+
shelves.update_with_mqtt_data(dict(SHELVES_2019, loaded=None, color=None))
|
|
76
|
+
|
|
77
|
+
assert shelves.loaded is False
|
|
78
|
+
assert shelves.color == []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestTheSiblingsWithTheSameShape:
|
|
82
|
+
"""Every constructor fed by `.get(key, <default>)` had the same hole."""
|
|
83
|
+
|
|
84
|
+
def test_feed_status_survives_nulls(self):
|
|
85
|
+
assert AnycubicFeedStatus.from_json(
|
|
86
|
+
{"code": None, "type": None, "current_status": None, "slot_index": None}
|
|
87
|
+
) is not None
|
|
88
|
+
|
|
89
|
+
def test_drying_status_survives_nulls(self):
|
|
90
|
+
drying = AnycubicDryingStatus.from_json(
|
|
91
|
+
{"status": None, "target_temp": None, "duration": None, "remain_time": None}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert drying is not None
|
|
95
|
+
assert drying.is_drying is False
|
|
96
|
+
|
|
97
|
+
def test_a_multi_colour_box_survives_null_telemetry(self):
|
|
98
|
+
box = AnycubicMultiColorBox.from_json({
|
|
99
|
+
"id": 0, "status": None, "model_id": None, "auto_feed": None,
|
|
100
|
+
"loaded_slot": None, "temp": None, "slots": [],
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
assert box is not None
|
|
104
|
+
assert box.loaded_slot == -1
|
|
105
|
+
|
|
106
|
+
def test_but_a_box_with_no_id_still_raises(self):
|
|
107
|
+
"""`id` is the ACE's identity and the printer dict is keyed by it.
|
|
108
|
+
A box that cannot be placed must not be silently invented."""
|
|
109
|
+
with pytest.raises((TypeError, ValueError, KeyError)):
|
|
110
|
+
AnycubicMultiColorBox.from_json({"id": None, "slots": []})
|
|
111
|
+
|
|
112
|
+
def test_machine_tool_info_survives_nulls(self):
|
|
113
|
+
assert AnycubicMachineToolInfo.from_json(
|
|
114
|
+
{k: None for k in (
|
|
115
|
+
"id", "typd_id", "model_id", "type_function_id", "parent_id",
|
|
116
|
+
"function_name", "function_des", "control", "param",
|
|
117
|
+
"icon_url", "function_type", "status", "show_place",
|
|
118
|
+
)}
|
|
119
|
+
) is not None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestTheCoercionHelpers:
|
|
123
|
+
@pytest.mark.parametrize(
|
|
124
|
+
("value", "expected"),
|
|
125
|
+
[(None, 0), ("", 0), ("nonsense", 0), ([], 0), (7, 7), ("7", 7), (7.9, 7), (True, 1)],
|
|
126
|
+
)
|
|
127
|
+
def test_as_int(self, value, expected):
|
|
128
|
+
assert as_int(value) == expected
|
|
129
|
+
|
|
130
|
+
def test_as_int_uses_the_stated_fallback(self):
|
|
131
|
+
assert as_int(None, -1) == -1
|
|
132
|
+
|
|
133
|
+
def test_as_str_does_not_stringify_none(self):
|
|
134
|
+
"""str(None) is "None", which would read as a filament called None."""
|
|
135
|
+
assert as_str(None) == ""
|
|
136
|
+
assert as_str("PLA") == "PLA"
|
|
137
|
+
|
|
138
|
+
@pytest.mark.parametrize(
|
|
139
|
+
("value", "expected"),
|
|
140
|
+
[(None, []), ("nope", []), ([1, None, 3], [1, 3]), ([1, 2, 3], [1, 2, 3])],
|
|
141
|
+
)
|
|
142
|
+
def test_as_int_list(self, value, expected):
|
|
143
|
+
assert as_int_list(value) == expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/anycubic_api.py
RENAMED
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/api/functions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/const.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/enums.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/error_codes.py
RENAMED
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/mqtt.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/const/regions.py
RENAMED
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/files.py
RENAMED
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/data_models/orders.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/lan/__init__.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/lan/client.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/lan/commands.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/lan/handshake.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/models/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api/models/http.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.26 → anycubic_cloud_api-0.4.27}/src/anycubic_cloud_api.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|