python-mobius 0.10.3__py3-none-any.whl → 0.11.0__py3-none-any.whl
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.
- mobius/__init__.py +11 -1
- mobius/device.py +57 -11
- mobius/device_status.py +3 -1
- mobius/firmware.py +242 -0
- {python_mobius-0.10.3.dist-info → python_mobius-0.11.0.dist-info}/METADATA +3 -1
- {python_mobius-0.10.3.dist-info → python_mobius-0.11.0.dist-info}/RECORD +9 -8
- {python_mobius-0.10.3.dist-info → python_mobius-0.11.0.dist-info}/WHEEL +1 -1
- {python_mobius-0.10.3.dist-info → python_mobius-0.11.0.dist-info}/entry_points.txt +0 -0
- {python_mobius-0.10.3.dist-info → python_mobius-0.11.0.dist-info}/licenses/LICENSE +0 -0
mobius/__init__.py
CHANGED
|
@@ -64,6 +64,12 @@ from .mesh_address import (
|
|
|
64
64
|
is_short_address_derived, format_mesh_address,
|
|
65
65
|
)
|
|
66
66
|
from .relay import RelayedMobiusDevice
|
|
67
|
+
from .device import firmware_label
|
|
68
|
+
from .firmware import (
|
|
69
|
+
FIRMWARE_CLOUD_HOST, ImageId, FirmwareImage, installable_images, firmware_images,
|
|
70
|
+
firmware_check_url, AvailableFirmware, FirmwareCheck, parse_firmware_check,
|
|
71
|
+
ChangelogEntry, Changelog, parse_changelog, version_tuple,
|
|
72
|
+
)
|
|
67
73
|
from .device import (
|
|
68
74
|
MobiusDevice, MobiusPump, LightIntensityResult, LightPollResult, FullPollResult,
|
|
69
75
|
schedule_capacity, min_schedule_capacity, tank_group,
|
|
@@ -87,7 +93,7 @@ from .mob import (
|
|
|
87
93
|
MAX_PUMP_PRIMITIVE_SIZE,
|
|
88
94
|
)
|
|
89
95
|
|
|
90
|
-
__version__ = "0.
|
|
96
|
+
__version__ = "0.11.0"
|
|
91
97
|
|
|
92
98
|
__all__ = [
|
|
93
99
|
"__version__",
|
|
@@ -113,6 +119,10 @@ __all__ = [
|
|
|
113
119
|
"supported_pump_modes",
|
|
114
120
|
"get_pump_reverse",
|
|
115
121
|
"FirmwareType", "FIRMWARE_TYPE_LABELS_ETM", "HardwareInfo",
|
|
122
|
+
# firmware
|
|
123
|
+
"firmware_label", "FIRMWARE_CLOUD_HOST", "ImageId", "FirmwareImage", "installable_images", "firmware_images",
|
|
124
|
+
"firmware_check_url", "AvailableFirmware", "FirmwareCheck", "parse_firmware_check",
|
|
125
|
+
"ChangelogEntry", "Changelog", "parse_changelog", "version_tuple",
|
|
116
126
|
"Color", "COLOR_LABELS", "RadioType", "RADIO_TYPE_LABELS",
|
|
117
127
|
"MotorType", "MOTOR_TYPE_LABELS", "ProductType", "PRODUCT_TYPE_LABELS",
|
|
118
128
|
# schedule
|
mobius/device.py
CHANGED
|
@@ -68,6 +68,7 @@ from .mesh_address import (
|
|
|
68
68
|
from .coap import decode_coap_response, CoapResponse, COAP_INDICATION_OPCODE
|
|
69
69
|
from .discovery import find_device_by_serial
|
|
70
70
|
from .timezone import encode_time_zone_field, posix_tz_string
|
|
71
|
+
from .firmware import firmware_images, installable_images
|
|
71
72
|
|
|
72
73
|
# --------------------------------------------------------------------------
|
|
73
74
|
# GATT UUIDs (service 01ff0100-...; four characteristics under it)
|
|
@@ -502,14 +503,10 @@ def _indexed_values(blocks: list[AttributeValue]):
|
|
|
502
503
|
yield block.index + i, value
|
|
503
504
|
|
|
504
505
|
|
|
505
|
-
def
|
|
506
|
-
"""
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
from FIRMWARE_TYPE_LABELS_ETM for EcoTech Marine models and
|
|
510
|
-
FIRMWARE_TYPE_LABELS_NON_ETM for other known manufacturers, falling back
|
|
511
|
-
to the FirmwareType name.
|
|
512
|
-
"""
|
|
506
|
+
def firmware_label(firmware_type: FirmwareType, model: Optional[Model] = None) -> str:
|
|
507
|
+
"""Display label of a firmware component: FIRMWARE_TYPE_LABELS_ETM for
|
|
508
|
+
EcoTech Marine models, FIRMWARE_TYPE_LABELS_NON_ETM for other known
|
|
509
|
+
manufacturers, else (or when unlabelled) the FirmwareType name."""
|
|
513
510
|
manufacturer = manufacturer_for_model(model) if model is not None else None
|
|
514
511
|
if manufacturer == "EcoTech Marine":
|
|
515
512
|
labels = FIRMWARE_TYPE_LABELS_ETM
|
|
@@ -517,16 +514,49 @@ def decode_firmware_version_blocks(blocks: list[AttributeValue], model: Optional
|
|
|
517
514
|
labels = FIRMWARE_TYPE_LABELS_NON_ETM
|
|
518
515
|
else:
|
|
519
516
|
labels = {}
|
|
517
|
+
return labels.get(firmware_type, firmware_type.name)
|
|
520
518
|
|
|
519
|
+
|
|
520
|
+
def decode_firmware_versions_by_type(blocks: list[AttributeValue]) -> dict:
|
|
521
|
+
"""{FirmwareType: "a.b.c"} from FirmwareVersion (1) blocks. Each
|
|
522
|
+
element's index is its FirmwareType; each version is its bytes joined
|
|
523
|
+
with dots."""
|
|
521
524
|
result = {}
|
|
522
525
|
for index, value_bytes in _indexed_values(blocks):
|
|
523
526
|
fw_type = enum_or_none(FirmwareType, index)
|
|
524
|
-
if
|
|
525
|
-
|
|
526
|
-
result[labels.get(fw_type, fw_type.name)] = ".".join(str(b) for b in value_bytes)
|
|
527
|
+
if value_bytes and fw_type is not None:
|
|
528
|
+
result[fw_type] = ".".join(str(b) for b in value_bytes)
|
|
527
529
|
return result
|
|
528
530
|
|
|
529
531
|
|
|
532
|
+
def decode_firmware_version_blocks(blocks: list[AttributeValue], model: Optional[Model] = None) -> dict:
|
|
533
|
+
"""
|
|
534
|
+
{label: "a.b.c"} from FirmwareVersion (1) blocks, labelled with
|
|
535
|
+
firmware_label(). Components sharing a label keep the last one; see
|
|
536
|
+
decode_firmware_versions_by_type() for every component.
|
|
537
|
+
"""
|
|
538
|
+
return {
|
|
539
|
+
firmware_label(fw_type, model): version
|
|
540
|
+
for fw_type, version in decode_firmware_versions_by_type(blocks).items()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def decode_radio_type(blocks: list[AttributeValue]) -> Optional[RadioType]:
|
|
545
|
+
"""HardwareInfo.RadioType from HardwareRevision (2) blocks, None if not
|
|
546
|
+
reported or unknown."""
|
|
547
|
+
for index, value_bytes in _indexed_values(blocks):
|
|
548
|
+
if index == HardwareInfo.RadioType and value_bytes:
|
|
549
|
+
return enum_or_none(RadioType, int.from_bytes(value_bytes, byteorder="little", signed=False))
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def decode_firmware_images(firmware_blocks: list[AttributeValue], hardware_blocks: list[AttributeValue],
|
|
554
|
+
model: Optional[Model]) -> list:
|
|
555
|
+
"""[FirmwareImage] the cloud can be asked about (see firmware.py)."""
|
|
556
|
+
return firmware_images(model, decode_firmware_versions_by_type(firmware_blocks),
|
|
557
|
+
decode_radio_type(hardware_blocks))
|
|
558
|
+
|
|
559
|
+
|
|
530
560
|
def decode_hardware_revision_blocks(blocks: list[AttributeValue]) -> dict:
|
|
531
561
|
"""
|
|
532
562
|
{HardwareInfo name: value} from HardwareRevision (2) blocks. Each
|
|
@@ -677,6 +707,9 @@ def decode_metadata_from_batch(by_attr_id: dict, model: Optional[Model]) -> Meta
|
|
|
677
707
|
),
|
|
678
708
|
hardware_info=decode_hardware_revision_blocks(by_attr_id.get(C2Attribute.HardwareRevision, [])),
|
|
679
709
|
firmware_versions=decode_firmware_version_blocks(by_attr_id.get(C2Attribute.FirmwareVersion, []), model),
|
|
710
|
+
firmware_images=decode_firmware_images(
|
|
711
|
+
by_attr_id.get(C2Attribute.FirmwareVersion, []), by_attr_id.get(C2Attribute.HardwareRevision, []), model,
|
|
712
|
+
),
|
|
680
713
|
supported_channels=decode_supported_channels(_values_for(by_attr_id, C2Attribute.SupportedColorChannels)),
|
|
681
714
|
error_state=decode_error_state(first(C2Attribute.ErrorState)),
|
|
682
715
|
epoch=_unpack(first(C2Attribute.Epoch), "<I"),
|
|
@@ -1758,6 +1791,7 @@ class MobiusDevice:
|
|
|
1758
1791
|
calibration=await soft(self.get_calibration_info(), None),
|
|
1759
1792
|
hardware_info=await soft(self.get_hardware_info(), {}),
|
|
1760
1793
|
firmware_versions=await soft(self.get_firmware_versions(model=model), {}),
|
|
1794
|
+
firmware_images=await soft(self.get_firmware_images(model=model), []),
|
|
1761
1795
|
supported_channels=await soft(self.get_supported_channels(), []),
|
|
1762
1796
|
error_state=decode_error_state(await self._try_first(C2Attribute.ErrorState)),
|
|
1763
1797
|
epoch=await self._try_unpack(C2Attribute.Epoch, "<I"),
|
|
@@ -2008,6 +2042,18 @@ class MobiusDevice:
|
|
|
2008
2042
|
return {}
|
|
2009
2043
|
return decode_firmware_version_blocks(blocks, model)
|
|
2010
2044
|
|
|
2045
|
+
async def get_firmware_images(self, model: Optional[Model] = None) -> list:
|
|
2046
|
+
"""[FirmwareImage] the cloud can be asked about for updates (see
|
|
2047
|
+
firmware.py). [] on failure or for models without updates."""
|
|
2048
|
+
if not installable_images(model):
|
|
2049
|
+
return []
|
|
2050
|
+
try:
|
|
2051
|
+
firmware_blocks = await self.get_attribute_raw_all(C2Attribute.FirmwareVersion, index=0, count=ALL_ELEMENTS)
|
|
2052
|
+
hardware_blocks = await self.get_attribute_raw_all(C2Attribute.HardwareRevision, index=0, count=ALL_ELEMENTS)
|
|
2053
|
+
except Exception:
|
|
2054
|
+
return []
|
|
2055
|
+
return decode_firmware_images(firmware_blocks, hardware_blocks, model)
|
|
2056
|
+
|
|
2011
2057
|
async def get_hardware_info(self) -> dict:
|
|
2012
2058
|
"""HardwareRevision sub-fields (all elements). See
|
|
2013
2059
|
decode_hardware_revision_blocks(). {} on failure."""
|
mobius/device_status.py
CHANGED
|
@@ -5,7 +5,7 @@ value has not been established.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from dataclasses import dataclass
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
11
|
from .constants import Model
|
|
@@ -420,6 +420,8 @@ time zone strings.
|
|
|
420
420
|
# PosixTimeZoneString / OlsonTimeZoneLocation (205 / 206).
|
|
421
421
|
posix_tz: Optional[str] = None
|
|
422
422
|
olson_tz: Optional[str] = None
|
|
423
|
+
# [FirmwareImage] the cloud can be asked about for updates (firmware.py).
|
|
424
|
+
firmware_images: list = field(default_factory=list)
|
|
423
425
|
|
|
424
426
|
|
|
425
427
|
@dataclass
|
mobius/firmware.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Firmware update checks against the Mobius cloud, as the app does them.
|
|
3
|
+
|
|
4
|
+
The app checks each firmware part a model can install (its "image") with
|
|
5
|
+
|
|
6
|
+
GET https://cloud.mobius.app/firmware/mobius/check?version=<installed>&image=<ImageId>&format=json
|
|
7
|
+
|
|
8
|
+
No login is needed. The reply always has "interval" (minutes before the
|
|
9
|
+
app checks the same image and version again); when a newer firmware exists
|
|
10
|
+
it also has "url", "version", "md5", "changelog" (a URL to a JSON
|
|
11
|
+
changelog), "install", "reset" and "id".
|
|
12
|
+
|
|
13
|
+
This module only builds the URLs and parses the replies: fetching them is
|
|
14
|
+
left to the caller. Installing firmware is not supported.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import IntEnum
|
|
21
|
+
from typing import Optional
|
|
22
|
+
from urllib.parse import urlencode
|
|
23
|
+
|
|
24
|
+
from .constants import FirmwareType, Model, RadioType
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
FIRMWARE_CLOUD_HOST = "cloud.mobius.app"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ImageId(IntEnum):
|
|
31
|
+
"""Firmware image the cloud is asked about (the app's M.ImageId)."""
|
|
32
|
+
Unknown = 0
|
|
33
|
+
RfModuleKw41 = 1
|
|
34
|
+
QuietDriveDriver = 2
|
|
35
|
+
RadionDriverKl26 = 3
|
|
36
|
+
AiPumpKw41 = 4
|
|
37
|
+
DosingPumpKw41 = 5
|
|
38
|
+
AiLightQCA4020 = 6
|
|
39
|
+
DosingPumpBaseStation = 7
|
|
40
|
+
RadionQCA4020 = 8
|
|
41
|
+
Chalupa = 9
|
|
42
|
+
RfModuleK32w = 10
|
|
43
|
+
AiLightQCA4024 = 11
|
|
44
|
+
RadionQCA4024 = 12
|
|
45
|
+
Saddle = 13
|
|
46
|
+
Cowboy = 14
|
|
47
|
+
Muffler = 15
|
|
48
|
+
Coffee = 16
|
|
49
|
+
Alfalfa = 17
|
|
50
|
+
CrunchyTaco = 18
|
|
51
|
+
SoftTaco = 19
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_KL26_RADIONS = frozenset({
|
|
55
|
+
Model.RadionXR30w, Model.RadionXR30wG2, Model.RadionXR30wPro, Model.RadionXR30wG3,
|
|
56
|
+
Model.RadionXR30wG3Pro, Model.RadionXR30wG4, Model.RadionXR30wG4Pro, Model.RadionXR30wFWPro,
|
|
57
|
+
Model.RadionXR30wPrototype, Model.RadionXR15wG3Pro, Model.RadionXR15wFW, Model.RadionXR15wG4Pro,
|
|
58
|
+
Model.RadionXR15wFWPro,
|
|
59
|
+
})
|
|
60
|
+
_QUIETDRIVE_PUMPS = frozenset({
|
|
61
|
+
Model.VectraS1, Model.VectraM1, Model.VectraL1, Model.VectraS2, Model.VectraM2, Model.VectraL2,
|
|
62
|
+
Model.VorTechMP10wQD, Model.VorTechMP40wQD, Model.VorTechMP60wQD, Model.VorTechMP40wG3QD,
|
|
63
|
+
Model.VorTechMP10wES,
|
|
64
|
+
})
|
|
65
|
+
_QCA_HYDRAS = frozenset({
|
|
66
|
+
Model.Hydra32HDFreshwater, Model.PrimeSixteenHD, Model.HydraThirtyTwoHD, Model.HydraSixtyFourHD,
|
|
67
|
+
Model.PrimeSixteenHdFuge, Model.PrimeSixteenHdFw, Model.PrimeSixteenHdSol,
|
|
68
|
+
})
|
|
69
|
+
_QCA_RADIONS = frozenset({
|
|
70
|
+
Model.RadionXR15wG5Blue, Model.RadionXR15wG5FW, Model.RadionXR30wG5Blue, Model.RadionXR30wG5FW,
|
|
71
|
+
Model.RadionXR30wG5Pro, Model.RadionXR15wPrototype, Model.RadionXR15G5Pro, Model.RadionXR30wG6Pro,
|
|
72
|
+
Model.RadionXR30wG6Blue, Model.RadionXR15wG6Pro, Model.RadionXR15wG6Blue,
|
|
73
|
+
})
|
|
74
|
+
_AI_PUMPS = frozenset({
|
|
75
|
+
Model.Nero5, Model.Nero3, Model.Nero7, Model.Axis20, Model.Axis40, Model.Axis90, Model.Axis200,
|
|
76
|
+
Model.Orbit2, Model.Orbit4,
|
|
77
|
+
})
|
|
78
|
+
_SINGLE_IMAGE_MODELS: dict = {
|
|
79
|
+
Model.SproutSL1: (FirmwareType.QCA4020Firmware, ImageId.Chalupa),
|
|
80
|
+
**{m: (FirmwareType.Esp32C3Firmware, ImageId.Saddle) for m in (
|
|
81
|
+
Model.Blade, Model.BladeD, Model.BladeCoralGlow, Model.BladeCoralGrow, Model.BladeFreshwater,
|
|
82
|
+
Model.BladeRefugium, Model.BladeGrowPlus, Model.BladeGloFish,
|
|
83
|
+
)},
|
|
84
|
+
Model.DoseQD: (FirmwareType.Esp32H2Firmware, ImageId.Muffler),
|
|
85
|
+
Model.Quantum160: (FirmwareType.Esp32H2Firmware, ImageId.Coffee),
|
|
86
|
+
Model.Quantum220300: (FirmwareType.Esp32H2Firmware, ImageId.Coffee),
|
|
87
|
+
**{m: (FirmwareType.Esp32C6Firmware, ImageId.Alfalfa) for m in (
|
|
88
|
+
Model.HydraEdge44HD, Model.HydraEdge68HD, Model.HydraEdge44HDFreshwater, Model.HydraEdge68HDFreshwater,
|
|
89
|
+
)},
|
|
90
|
+
Model.Hera: (FirmwareType.Esp32S3Firmware, ImageId.CrunchyTaco),
|
|
91
|
+
Model.HeraBackplane: (FirmwareType.Esp32C6Firmware, ImageId.SoftTaco),
|
|
92
|
+
**{m: (FirmwareType.OS, ImageId.AiPumpKw41) for m in _AI_PUMPS},
|
|
93
|
+
Model.MXM: (FirmwareType.OS, ImageId.Cowboy),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def installable_images(model: Optional[Model], radio_type: Optional[RadioType] = None) -> list:
|
|
98
|
+
"""[(FirmwareType, ImageId)] the app can update on a model, in the
|
|
99
|
+
app's order. Some models depend on their radio (HardwareInfo
|
|
100
|
+
RadioType). [] for models the app has no updates for."""
|
|
101
|
+
if model in _KL26_RADIONS:
|
|
102
|
+
return [(FirmwareType.OS, ImageId.RfModuleKw41), (FirmwareType.MainMicroOS, ImageId.RadionDriverKl26)]
|
|
103
|
+
if model in _QUIETDRIVE_PUMPS:
|
|
104
|
+
radio = ImageId.RfModuleK32w if radio_type == RadioType.K32W else ImageId.RfModuleKw41
|
|
105
|
+
return [(FirmwareType.OS, radio), (FirmwareType.MainMicroOS, ImageId.QuietDriveDriver)]
|
|
106
|
+
if model in _QCA_HYDRAS:
|
|
107
|
+
image = ImageId.AiLightQCA4024 if radio_type == RadioType.QCA4024 else ImageId.AiLightQCA4020
|
|
108
|
+
return [(FirmwareType.QCA4020Firmware, image)]
|
|
109
|
+
if model in _QCA_RADIONS:
|
|
110
|
+
image = ImageId.RadionQCA4024 if radio_type == RadioType.QCA4024 else ImageId.RadionQCA4020
|
|
111
|
+
return [(FirmwareType.QCA4020Firmware, image)]
|
|
112
|
+
if model == Model.VersaVX1:
|
|
113
|
+
return [(FirmwareType.OS, ImageId.DosingPumpKw41),
|
|
114
|
+
(FirmwareType.XMC1402Firmware, ImageId.DosingPumpBaseStation)]
|
|
115
|
+
single = _SINGLE_IMAGE_MODELS.get(model)
|
|
116
|
+
return [single] if single else []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class FirmwareImage:
|
|
121
|
+
"""One updatable firmware part of a device and its installed version."""
|
|
122
|
+
firmware_type: FirmwareType
|
|
123
|
+
image_id: ImageId
|
|
124
|
+
version: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def firmware_images(model: Optional[Model], versions_by_type: dict,
|
|
128
|
+
radio_type: Optional[RadioType] = None) -> list:
|
|
129
|
+
"""[FirmwareImage] for the parts of installable_images() the device
|
|
130
|
+
reports a version for ({FirmwareType: "a.b.c"}, see
|
|
131
|
+
decode_firmware_versions_by_type())."""
|
|
132
|
+
return [
|
|
133
|
+
FirmwareImage(firmware_type, image_id, versions_by_type[firmware_type])
|
|
134
|
+
for firmware_type, image_id in installable_images(model, radio_type)
|
|
135
|
+
if versions_by_type.get(firmware_type)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def firmware_check_url(image_id: ImageId, version: str, host: str = FIRMWARE_CLOUD_HOST) -> str:
|
|
140
|
+
"""URL the app checks for a newer firmware of `image_id`."""
|
|
141
|
+
query = urlencode({"version": version, "image": int(image_id), "format": "json"})
|
|
142
|
+
return f"https://{host}/firmware/mobius/check?{query}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class AvailableFirmware:
|
|
147
|
+
"""A newer firmware offered by the cloud."""
|
|
148
|
+
version: str
|
|
149
|
+
url: str
|
|
150
|
+
changelog_url: Optional[str] = None
|
|
151
|
+
md5: Optional[str] = None
|
|
152
|
+
install: bool = False
|
|
153
|
+
reset: bool = False
|
|
154
|
+
id: Optional[int] = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class FirmwareCheck:
|
|
159
|
+
"""A parsed check reply: `update` is None when the installed version is
|
|
160
|
+
the latest. `interval` is how long (seconds) the app waits before
|
|
161
|
+
checking the same image and version again, None if not given."""
|
|
162
|
+
update: Optional[AvailableFirmware]
|
|
163
|
+
interval: Optional[int]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def parse_firmware_check(data: dict) -> FirmwareCheck:
|
|
167
|
+
"""Parses a check reply (the JSON object). Raises ValueError if it isn't
|
|
168
|
+
a JSON object."""
|
|
169
|
+
if not isinstance(data, dict):
|
|
170
|
+
raise ValueError(f"firmware check reply is not an object: {data!r}")
|
|
171
|
+
interval = data.get("interval")
|
|
172
|
+
interval = int(interval) * 60 if isinstance(interval, (int, float)) else None
|
|
173
|
+
url, version = data.get("url"), data.get("version")
|
|
174
|
+
if not url or not version:
|
|
175
|
+
return FirmwareCheck(None, interval)
|
|
176
|
+
firmware_id = data.get("id")
|
|
177
|
+
return FirmwareCheck(AvailableFirmware(
|
|
178
|
+
version=str(version),
|
|
179
|
+
url=str(url),
|
|
180
|
+
changelog_url=data.get("changelog") or None,
|
|
181
|
+
md5=data.get("md5") or None,
|
|
182
|
+
install=bool(data.get("install")),
|
|
183
|
+
reset=bool(data.get("reset")),
|
|
184
|
+
id=firmware_id if isinstance(firmware_id, int) else None,
|
|
185
|
+
), interval)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True)
|
|
189
|
+
class ChangelogEntry:
|
|
190
|
+
"""One version of a firmware changelog."""
|
|
191
|
+
version: str
|
|
192
|
+
changes: list = field(default_factory=list) # list[str]
|
|
193
|
+
alerts: list = field(default_factory=list) # list[str]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass(frozen=True)
|
|
197
|
+
class Changelog:
|
|
198
|
+
"""A firmware changelog (the reply's "changelog" URL), newest first as
|
|
199
|
+
published."""
|
|
200
|
+
current_version: Optional[str]
|
|
201
|
+
entries: list # list[ChangelogEntry]
|
|
202
|
+
alerts: list = field(default_factory=list) # list[str]
|
|
203
|
+
|
|
204
|
+
def newer_than(self, version: str) -> list:
|
|
205
|
+
"""Entries for versions newer than `version`. All entries if either
|
|
206
|
+
version can't be compared."""
|
|
207
|
+
installed = version_tuple(version)
|
|
208
|
+
if installed is None:
|
|
209
|
+
return list(self.entries)
|
|
210
|
+
newer = []
|
|
211
|
+
for entry in self.entries:
|
|
212
|
+
entry_version = version_tuple(entry.version)
|
|
213
|
+
if entry_version is None or entry_version > installed:
|
|
214
|
+
newer.append(entry)
|
|
215
|
+
return newer
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _texts(items) -> list:
|
|
219
|
+
return [str(item["text"]) for item in items or [] if isinstance(item, dict) and item.get("text")]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def parse_changelog(data: dict) -> Changelog:
|
|
223
|
+
"""Parses a changelog JSON object ({"current_version", "change_list":
|
|
224
|
+
[{"version", "changes": [{"text"}], "alerts": [{"text"}]}], "alerts"}).
|
|
225
|
+
Raises ValueError if it isn't a JSON object."""
|
|
226
|
+
if not isinstance(data, dict):
|
|
227
|
+
raise ValueError(f"changelog is not an object: {data!r}")
|
|
228
|
+
entries = [
|
|
229
|
+
ChangelogEntry(str(item.get("version", "")), _texts(item.get("changes")), _texts(item.get("alerts")))
|
|
230
|
+
for item in data.get("change_list") or [] if isinstance(item, dict)
|
|
231
|
+
]
|
|
232
|
+
current = data.get("current_version")
|
|
233
|
+
return Changelog(str(current) if current else None, entries, _texts(data.get("alerts")))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def version_tuple(version: str) -> Optional[tuple]:
|
|
237
|
+
"""(3, 0, 35) from "3.0.35" (the app also accepts "-" as a separator).
|
|
238
|
+
None if a part isn't a number."""
|
|
239
|
+
try:
|
|
240
|
+
return tuple(int(part) for part in str(version).replace("-", ".").split("."))
|
|
241
|
+
except ValueError:
|
|
242
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)
|
|
5
5
|
Project-URL: Homepage, https://code.r3pek.org/r3pek/python-mobius
|
|
6
6
|
Project-URL: Documentation, https://code.r3pek.org/r3pek/python-mobius/src/branch/main/documentation
|
|
@@ -65,6 +65,8 @@ implement it in another language.
|
|
|
65
65
|
- **Batched polling**: everything a poll needs in one request.
|
|
66
66
|
- **Diagnostics**: attribute dumps, firmware and hardware information,
|
|
67
67
|
Thread and clock diagnostics.
|
|
68
|
+
- **Firmware update checks**: which firmware images to ask the Mobius cloud
|
|
69
|
+
about and parsing its replies and changelogs (installing is not supported).
|
|
68
70
|
- **`mobius-scan`** command line tool.
|
|
69
71
|
|
|
70
72
|
## Supported devices
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
mobius/__init__.py,sha256=
|
|
1
|
+
mobius/__init__.py,sha256=HxhUUJCjvLh_q8Sz6RwOBI8_DXY8F6EZXlbM1Rlmaco,9133
|
|
2
2
|
mobius/checksums.py,sha256=PB9RqvaZu9AqfT6tU6NI2qbbZXrqaHUnow3Qi_6UFHs,5191
|
|
3
3
|
mobius/cli.py,sha256=tyft0SzbuLgbmzps-Ym1hCzcr6A9D5MsALkg6o4ynBs,57876
|
|
4
4
|
mobius/coap.py,sha256=-XV3kI4q-JoL8r0oLFNnHAdZAc_yUP-aICagKh-qbf4,4862
|
|
5
5
|
mobius/constants.py,sha256=pcG9Su_b81pmMA_GxR41lZz9UM9BjtV0T6nuslMICko,41138
|
|
6
6
|
mobius/crc.py,sha256=LOCHZe3a1UnR5thdpZ4S4SFP8BdmoMEnhDNiNMd0iJI,2508
|
|
7
|
-
mobius/device.py,sha256=
|
|
8
|
-
mobius/device_status.py,sha256=
|
|
7
|
+
mobius/device.py,sha256=3UmGI4txfp_gOCM3nonahXcYNwBLzicOzqFBtksumis,136823
|
|
8
|
+
mobius/device_status.py,sha256=JhJQreluRAP6Ow2M187RDrPyATEsQAAETpL_d-dLhQQ,15799
|
|
9
9
|
mobius/discovery.py,sha256=poNeXGekB44cpHOFbGyps9d4mwW_hJptmJODwSMTUSg,7176
|
|
10
10
|
mobius/dump.py,sha256=eQBYoi8J6CxRd2ojgWK5QoB8z416oywLRTBnnh0pLwo,9253
|
|
11
|
+
mobius/firmware.py,sha256=TuUcJxv2A6Lrpz5qfZvXDVC_-06GRPNCjG0MG1_UH-M,9693
|
|
11
12
|
mobius/frame.py,sha256=hO7nc1Rn0HqsIIZP6KZXj0n91J7LA5SfBoQcu_PNKvQ,7852
|
|
12
13
|
mobius/manufacturer.py,sha256=WNpU1f69rrroJQdWhEw-ROw1dtPH7Rbn7IBEtGKLxqE,2382
|
|
13
14
|
mobius/mesh_address.py,sha256=hNpijfuzJFmfpmndjeSAqefr6mY4oZ4hQEXNvYjy4mY,3435
|
|
@@ -19,8 +20,8 @@ mobius/relay.py,sha256=trGkWQSarwURF05gXHeb5jTJ9UvWbQGu8NarEP6sRqE,10107
|
|
|
19
20
|
mobius/scenes.py,sha256=VLfDdTKSv3qTsBt2bJGg8iPtUkfNEmNgu0TTtYT6jJE,4274
|
|
20
21
|
mobius/schedule.py,sha256=si9uJdYOj5vh6owuxxMyMyZRY0-tAmCPt3s6ROAeRww,13243
|
|
21
22
|
mobius/timezone.py,sha256=Q7oPKtCdEYcEHXLx4lt-VPq6KH-MNgQwVnDtIXJV-kQ,2267
|
|
22
|
-
python_mobius-0.
|
|
23
|
-
python_mobius-0.
|
|
24
|
-
python_mobius-0.
|
|
25
|
-
python_mobius-0.
|
|
26
|
-
python_mobius-0.
|
|
23
|
+
python_mobius-0.11.0.dist-info/METADATA,sha256=pruuXYoAVOF91RncSpReYkt_zTGXwf0-q2h_00McBvM,5983
|
|
24
|
+
python_mobius-0.11.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
25
|
+
python_mobius-0.11.0.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
|
|
26
|
+
python_mobius-0.11.0.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
|
|
27
|
+
python_mobius-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|