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 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.10.3"
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 decode_firmware_version_blocks(blocks: list[AttributeValue], model: Optional[Model] = None) -> dict:
506
- """
507
- {label: "a.b.c"} from FirmwareVersion (1) blocks. Each element's index is
508
- its FirmwareType; each version is its bytes joined with dots. Labels come
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 not value_bytes or fw_type is None:
525
- continue
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.10.3
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=vXZoC15RSsPkTLgOhdRb6T0zjIuL4KX_PByrCGlUzuY,8555
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=fIoN0YvF0VE85Ln0hHMqqGW7SxRDMbyEqLDH3jNH_xY,134445
8
- mobius/device_status.py,sha256=XclYijAknrgT8SdSW4XvyBemWHUQqw6UPE4tmwq6Yug,15658
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.10.3.dist-info/METADATA,sha256=XZMR0-mC2Ph-T0tOxBn_Gl3hPEXzmRcwjim4Nem0QMs,5829
23
- python_mobius-0.10.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
24
- python_mobius-0.10.3.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
25
- python_mobius-0.10.3.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
26
- python_mobius-0.10.3.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any