python-mobius 0.4.0__tar.gz → 0.4.1__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.
- {python_mobius-0.4.0 → python_mobius-0.4.1}/.forgejo/workflows/release.yml +25 -20
- {python_mobius-0.4.0 → python_mobius-0.4.1}/CHANGELOG.md +5 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/PKG-INFO +1 -1
- {python_mobius-0.4.0 → python_mobius-0.4.1}/pyproject.toml +1 -1
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/__init__.py +1 -1
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/device.py +26 -6
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/discovery.py +31 -7
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_mesh_discovery.py +72 -1
- {python_mobius-0.4.0 → python_mobius-0.4.1}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/.gitignore +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/LICENSE +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/README.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/00-overview.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/03-attributes-and-opcodes.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/05-scenes.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/06-light-schedule.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/07-pump-schedule.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/09-thread-coap-relay.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/10-known-gaps-and-open-questions.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/11-device-status-attributes.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/12-device-identity-and-address-stability.md +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/cli.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/coap.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/constants.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/crc.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/device_status.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/frame.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/manufacturer.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/mesh_address.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/modifiers.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/power.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/relay.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/src/mobius/schedule.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_coap.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_coap_indication_handling.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_connection_cleanup.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_device_status.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_device_summary.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_device_time.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_firmware_versions.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_frame.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_frame_sniffer.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_light_intensity_branching.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_light_schedule.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_mesh_discovery_direct_connect.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_modifiers.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_networked_thread_devices.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_power.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_pump_status.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_relay.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_schedule_intensity_scalar.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_serial_decoding.py +0 -0
- {python_mobius-0.4.0 → python_mobius-0.4.1}/tests/test_serial_identity.py +0 -0
|
@@ -88,25 +88,30 @@ jobs:
|
|
|
88
88
|
if: github.event_name == 'push'
|
|
89
89
|
run: python3 .forgejo/scripts/extract_changelog_section.py "${{ github.ref_name }}"
|
|
90
90
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
#
|
|
91
|
+
# Confirmed via direct comparison against ha-mobius's own release
|
|
92
|
+
# workflow (which has never hit this failure) exactly why an
|
|
93
|
+
# earlier version of this step kept failing on a real run ("tag
|
|
94
|
+
# vX.Y.Z exists" / "curl: (26) Failed to open/read local data from
|
|
95
|
+
# file/application") even after release-dir was set explicitly:
|
|
96
|
+
# the YAML here and in ha-mobius's version are structurally
|
|
97
|
+
# identical (same action, same inputs), but ha-mobius's
|
|
98
|
+
# release-dir always has at least one real file in it (a zip it
|
|
99
|
+
# builds beforehand for manual installers), while this repo's own
|
|
100
|
+
# release-dir was a directory that existed but was ALWAYS
|
|
101
|
+
# completely empty (deliberately, since PyPI is meant to be the
|
|
102
|
+
# source of truth here, not a Forgejo release asset). That's
|
|
103
|
+
# consistent with the action's own upload logic looping over
|
|
104
|
+
# release-dir's contents with an unquoted/unguarded shell glob:
|
|
105
|
+
# with zero matching files and no nullglob set, such a glob
|
|
106
|
+
# doesn't expand to nothing, it stays as the literal, unexpanded
|
|
107
|
+
# string -- and curl trying to read a file literally named
|
|
108
|
+
# "dist/release/*" produces exactly this error. Rather than an
|
|
109
|
+
# artificially empty directory, this points release-dir at dist/
|
|
110
|
+
# itself, which already has the actual built wheel/sdist in it
|
|
111
|
+
# from the Build package step above and is therefore never empty
|
|
112
|
+
# -- also means those get attached to the Forgejo release as a
|
|
113
|
+
# side effect, which is a reasonable thing to have anyway, not
|
|
114
|
+
# just a workaround.
|
|
110
115
|
- name: Create Forgejo release
|
|
111
116
|
if: github.event_name == 'push'
|
|
112
117
|
uses: https://code.forgejo.org/actions/forgejo-release@v2
|
|
@@ -116,5 +121,5 @@ jobs:
|
|
|
116
121
|
repo: r3pek/python-mobius
|
|
117
122
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
118
123
|
tag: ${{ github.ref_name }}
|
|
119
|
-
release-dir: dist
|
|
124
|
+
release-dir: dist
|
|
120
125
|
release-notes-file: release_notes.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-mobius"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "GPL-2.0-only" }
|
|
@@ -709,12 +709,22 @@ class MobiusDevice:
|
|
|
709
709
|
ShortAddressArray/SerialNumberArray/DeviceModelArray
|
|
710
710
|
(3700/3701/3702, the same "get all elements" pattern used
|
|
711
711
|
elsewhere in this library) from THIS connected device, acting as
|
|
712
|
-
a Thread mesh gateway, and returns
|
|
713
|
-
|
|
714
|
-
|
|
712
|
+
a Thread mesh gateway, and returns what it reports about devices
|
|
713
|
+
over the mesh -- not devices we necessarily have (or even can
|
|
714
|
+
have) a direct BLE connection to. See documentation/
|
|
715
715
|
09-thread-coap-relay.md for the full protocol trace this is
|
|
716
716
|
based on.
|
|
717
717
|
|
|
718
|
+
Whether this includes the connected device's own entry
|
|
719
|
+
alongside others isn't confirmed either way -- unlike
|
|
720
|
+
discover_networked_thread_devices() (whose sibling docstring
|
|
721
|
+
this once matched, until a real capture disproved it there),
|
|
722
|
+
real hardware evidence specific to THIS older mechanism was
|
|
723
|
+
never gathered. Callers that also separately add the connected
|
|
724
|
+
device's own entry (e.g. mobius.discovery.discover_tank()) treat
|
|
725
|
+
this as a real possibility and deduplicate by serial regardless,
|
|
726
|
+
rather than assuming either way.
|
|
727
|
+
|
|
718
728
|
**Confirmed via real hardware testing to be Cowboy-hub-specific.**
|
|
719
729
|
These three arrays were originally found being fetched as part of
|
|
720
730
|
the app's own network-setup logic for a dedicated Cowboy hub device,
|
|
@@ -789,9 +799,19 @@ class MobiusDevice:
|
|
|
789
799
|
"""
|
|
790
800
|
Fetches MLPrefix (2128) plus NetworkedThreadDevices (1001, the
|
|
791
801
|
same "get all elements" pattern used elsewhere in this library)
|
|
792
|
-
from THIS connected device, and returns every
|
|
793
|
-
|
|
794
|
-
|
|
802
|
+
from THIS connected device, and returns every device it knows
|
|
803
|
+
about over the Thread mesh -- not devices we necessarily have
|
|
804
|
+
(or even can have) a direct BLE connection to.
|
|
805
|
+
|
|
806
|
+
**Includes the connected device's own entry, not just others.**
|
|
807
|
+
An earlier version of this docstring claimed "every OTHER
|
|
808
|
+
device," an untested assumption rather than a confirmed one --
|
|
809
|
+
disproven by a real 4-device tank capture where a single
|
|
810
|
+
connected device's own NetworkedThreadDevices read returned all
|
|
811
|
+
4 real devices, necessarily including itself. Callers building
|
|
812
|
+
a peer list that also separately adds the connected device's own
|
|
813
|
+
entry (e.g. mobius.discovery.discover_tank()) need to
|
|
814
|
+
deduplicate by serial, or that device ends up listed twice.
|
|
795
815
|
|
|
796
816
|
A second, better mesh-peer-discovery mechanism than
|
|
797
817
|
discover_mesh_peers() -- confirmed via reverse engineering the
|
|
@@ -193,12 +193,28 @@ async def discover_tank(mdevice) -> "Tank":
|
|
|
193
193
|
(not just serial/model, the way advertisement-only grouping like
|
|
194
194
|
group_by_pan_id() can offer).
|
|
195
195
|
|
|
196
|
-
Calls mdevice.discover_mesh_peers_auto()
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
Calls mdevice.discover_mesh_peers_auto() plus
|
|
197
|
+
get_own_mesh_address()/get_device_info() to build the connected
|
|
198
|
+
device's own entry directly. These are deliberately DEDUPLICATED by
|
|
199
|
+
serial before returning, keeping the directly-built own entry over
|
|
200
|
+
whatever discover_mesh_peers_auto() reported for the same serial --
|
|
201
|
+
confirmed via a real 4-device tank capture that
|
|
202
|
+
discover_networked_thread_devices() (the mechanism
|
|
203
|
+
discover_mesh_peers_auto() tries first) is self-INCLUSIVE, not
|
|
204
|
+
self-exclusive as an earlier version of this docstring claimed
|
|
205
|
+
(based on an untested assumption, not a confirmed one): all 4 real
|
|
206
|
+
devices came back from a single connected device's own
|
|
207
|
+
NetworkedThreadDevices read, necessarily including whichever one was
|
|
208
|
+
actually connected to. Without deduplication, that connected
|
|
209
|
+
device's own peer would appear twice in the result -- once from this
|
|
210
|
+
function's own direct build, once from discover_mesh_peers_auto()'s
|
|
211
|
+
own report of it as "just another peer" on its own network. Not
|
|
212
|
+
confirmed whether the OLDER discover_mesh_peers() mechanism
|
|
213
|
+
(discover_mesh_peers_auto()'s own fallback) shares this same
|
|
214
|
+
self-inclusive behavior or not -- the deduplication here is
|
|
215
|
+
unconditional specifically so it's correct either way, rather than
|
|
216
|
+
depending on getting that confirmation for every code path that
|
|
217
|
+
could produce peers.
|
|
202
218
|
|
|
203
219
|
tank.prefix is None if this device didn't report a valid mesh
|
|
204
220
|
address at all (unsupported, or not currently part of an active
|
|
@@ -240,5 +256,13 @@ async def discover_tank(mdevice) -> "Tank":
|
|
|
240
256
|
other_peers = await mdevice.discover_mesh_peers_auto()
|
|
241
257
|
prefix = mesh_local_prefix_from_own_address(own_address)
|
|
242
258
|
|
|
243
|
-
|
|
259
|
+
# own_peer always wins over whatever discover_mesh_peers_auto()
|
|
260
|
+
# separately reported for the same serial -- see this function's
|
|
261
|
+
# own docstring for why that duplicate is a real, confirmed
|
|
262
|
+
# possibility, not just a defensive guard against a hypothetical.
|
|
263
|
+
deduplicated = [own_peer] + [
|
|
264
|
+
peer for peer in other_peers if peer.serial != own_peer.serial
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
return Tank(prefix=prefix, peers=deduplicated)
|
|
244
268
|
|
|
@@ -13,7 +13,7 @@ from bleak.backends.device import BLEDevice
|
|
|
13
13
|
|
|
14
14
|
from mobius import C2Attribute, Model, MeshPeer
|
|
15
15
|
from mobius.device import MobiusDevice
|
|
16
|
-
from mobius.mesh_address import build_rloc_address
|
|
16
|
+
from mobius.mesh_address import build_rloc_address, extract_short_address
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _fake_ble_device():
|
|
@@ -295,6 +295,77 @@ async def test_discover_tank_basic():
|
|
|
295
295
|
assert tank.peers[1] is other_peer
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
@pytest.mark.asyncio
|
|
299
|
+
async def test_discover_tank_deduplicates_self_from_discover_mesh_peers_auto():
|
|
300
|
+
"""
|
|
301
|
+
The actual real-hardware bug this guards against: a real 4-device
|
|
302
|
+
tank capture showed discover_networked_thread_devices() (the
|
|
303
|
+
mechanism discover_mesh_peers_auto() tries first) returning ALL 4
|
|
304
|
+
real devices from a single connected device's own read, including
|
|
305
|
+
itself -- disproving an earlier assumption that it only ever
|
|
306
|
+
reports OTHER devices (see device.py's own docstring for the full
|
|
307
|
+
correction). Without deduplication, discover_tank() would list the
|
|
308
|
+
connected device twice: once from its own direct
|
|
309
|
+
get_own_mesh_address()/get_device_info() build, once again from
|
|
310
|
+
whatever discover_mesh_peers_auto() separately reported for that
|
|
311
|
+
same serial. Confirms the fix: the connected device appears exactly
|
|
312
|
+
once, its own directly-built entry wins over the (here, identical)
|
|
313
|
+
one discover_mesh_peers_auto() reported for the same serial, and
|
|
314
|
+
genuinely different peers are unaffected.
|
|
315
|
+
"""
|
|
316
|
+
device = MobiusDevice(_fake_ble_device())
|
|
317
|
+
own_serial = "76495221059019"
|
|
318
|
+
own_address = PREFIX + bytes.fromhex("000000fffe005786")
|
|
319
|
+
|
|
320
|
+
# What discover_mesh_peers_auto() reports for the connected device's
|
|
321
|
+
# OWN serial -- same identity, but constructed independently (a
|
|
322
|
+
# DIFFERENT MeshPeer object, not literally the same instance the
|
|
323
|
+
# real own_peer build produces), matching how two separate discovery
|
|
324
|
+
# paths reporting the same physical device would actually look.
|
|
325
|
+
self_reported_by_auto = MeshPeer(
|
|
326
|
+
serial=own_serial, model_raw=Model.VorTechMP40wG3QD.value,
|
|
327
|
+
model=Model.VorTechMP40wG3QD, short_address=extract_short_address(own_address),
|
|
328
|
+
address=own_address, age=14519,
|
|
329
|
+
)
|
|
330
|
+
genuinely_other_peer = MeshPeer(
|
|
331
|
+
serial="76517731952041", model_raw=Model.VorTechMP40wG3QD.value,
|
|
332
|
+
model=Model.VorTechMP40wG3QD, short_address=0x9a0b,
|
|
333
|
+
address=PREFIX + bytes.fromhex("000000fffe009a0b"), age=373,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
async def fake_get_attribute(attr_id, index=0, count=1):
|
|
337
|
+
if attr_id == C2Attribute.MeshLocalAddresses:
|
|
338
|
+
return [own_address]
|
|
339
|
+
info_attrs = _device_info_attrs(Model.VorTechMP40wG3QD, own_serial)
|
|
340
|
+
if attr_id in info_attrs:
|
|
341
|
+
return info_attrs[attr_id]
|
|
342
|
+
raise Exception("FsciStatus.UnsupportedAttribute")
|
|
343
|
+
|
|
344
|
+
device.get_attribute = fake_get_attribute
|
|
345
|
+
device.discover_mesh_peers_auto = AsyncMock(
|
|
346
|
+
return_value=[self_reported_by_auto, genuinely_other_peer]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
tank = await discover_tank(device)
|
|
350
|
+
|
|
351
|
+
# The actual point: 2 real devices total, not 3 -- the connected
|
|
352
|
+
# device isn't double-counted.
|
|
353
|
+
assert len(tank.peers) == 2
|
|
354
|
+
serials = [p.serial for p in tank.peers]
|
|
355
|
+
assert serials.count(own_serial) == 1
|
|
356
|
+
assert own_serial in serials
|
|
357
|
+
assert "76517731952041" in serials
|
|
358
|
+
# The connected device's own directly-built entry wins -- confirmed
|
|
359
|
+
# by checking it's the exact instance discover_tank() constructs
|
|
360
|
+
# itself (has no .age set, unlike self_reported_by_auto which does),
|
|
361
|
+
# not the one discover_mesh_peers_auto() separately reported.
|
|
362
|
+
own_in_result = next(p for p in tank.peers if p.serial == own_serial)
|
|
363
|
+
assert own_in_result is not self_reported_by_auto
|
|
364
|
+
assert own_in_result.age is None
|
|
365
|
+
# The genuinely different peer is untouched.
|
|
366
|
+
assert genuinely_other_peer in tank.peers
|
|
367
|
+
|
|
368
|
+
|
|
298
369
|
@pytest.mark.asyncio
|
|
299
370
|
async def test_discover_tank_no_own_address_returns_empty_tank():
|
|
300
371
|
"""No own address means no valid connection to this Thread network --
|
|
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
|
|
File without changes
|
{python_mobius-0.4.0 → python_mobius-0.4.1}/documentation/10-known-gaps-and-open-questions.md
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|