python-qube-heatpump 1.6.3__tar.gz → 1.8.0__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_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/PKG-INFO +1 -1
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/pyproject.toml +1 -1
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/client.py +47 -14
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/models.py +51 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/tests/test_client.py +71 -101
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.github/workflows/ci.yml +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.github/workflows/python-publish.yml +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.gitignore +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/AGENTS.md +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/CLAUDE.md +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/LICENSE +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/README.md +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/docs/modbus-lijst-qube-totaal.pdf +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/pytest.ini +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/__init__.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/const.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/entities/__init__.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/entities/base.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/entities/binary_sensors.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/entities/sensors.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/entities/switches.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/network.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/py.typed +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/tests/conftest.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/tests/test_const.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/tests/test_entities.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/tests/test_models.py +0 -0
- {python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/tests/test_network.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-qube-heatpump
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: Async Modbus client for Qube Heat Pumps
|
|
5
5
|
Project-URL: Homepage, https://github.com/MattieGit/python-qube-heatpump
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/MattieGit/python-qube-heatpump/issues
|
{python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/client.py
RENAMED
|
@@ -33,7 +33,7 @@ class QubeClient:
|
|
|
33
33
|
self._backoff_max: float = 60.0
|
|
34
34
|
self._next_connect_at: float = 0.0
|
|
35
35
|
# Monotonic clamping for total_increasing counters
|
|
36
|
-
self.
|
|
36
|
+
self._monotonic_cache: dict[str, float] = {}
|
|
37
37
|
|
|
38
38
|
async def connect(self) -> bool:
|
|
39
39
|
"""Connect to the Modbus server."""
|
|
@@ -121,26 +121,59 @@ class QubeClient:
|
|
|
121
121
|
state.setpoint_dhw = await _read(const.USER_DHW_SETPOINT)
|
|
122
122
|
|
|
123
123
|
self._apply_monotonic_clamping(state)
|
|
124
|
+
|
|
125
|
+
# Fetch binary sensors
|
|
126
|
+
binary_data = await self.read_all_binary_sensors()
|
|
127
|
+
for key, value in binary_data.items():
|
|
128
|
+
if hasattr(state, key):
|
|
129
|
+
setattr(state, key, value)
|
|
130
|
+
|
|
124
131
|
return state
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
@property
|
|
134
|
+
def monotonic_cache(self) -> dict[str, float]:
|
|
135
|
+
"""Return the monotonic clamping cache.
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
"""
|
|
137
|
+
Can be used to persist/restore the cache across restarts.
|
|
138
|
+
"""
|
|
139
|
+
return self._monotonic_cache
|
|
140
|
+
|
|
141
|
+
@monotonic_cache.setter
|
|
142
|
+
def monotonic_cache(self, value: dict[str, float]) -> None:
|
|
143
|
+
"""Set the monotonic clamping cache (e.g. restored from disk)."""
|
|
144
|
+
self._monotonic_cache = dict(value)
|
|
145
|
+
|
|
146
|
+
def clamp_monotonic(self, key: str, value: float | None) -> float | None:
|
|
147
|
+
"""Clamp a value to prevent decreases for total_increasing counters.
|
|
148
|
+
|
|
149
|
+
Returns the clamped value. If the new value is lower than the
|
|
150
|
+
previously seen value for this key, the previous value is returned.
|
|
151
|
+
None and non-finite values pass through unchanged.
|
|
130
152
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
Args:
|
|
154
|
+
key: Identifier for this counter (e.g. entity unique_id).
|
|
155
|
+
value: The current reading.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The clamped value, or None if input was None/non-finite.
|
|
134
159
|
"""
|
|
160
|
+
if value is None or not math.isfinite(value):
|
|
161
|
+
return value
|
|
162
|
+
previous = self._monotonic_cache.get(key)
|
|
163
|
+
if previous is not None and value < previous:
|
|
164
|
+
return previous
|
|
165
|
+
self._monotonic_cache[key] = value
|
|
166
|
+
return value
|
|
167
|
+
|
|
168
|
+
_MONOTONIC_KEYS = frozenset({"energy_total_electric", "energy_total_thermic"})
|
|
169
|
+
|
|
170
|
+
def _apply_monotonic_clamping(self, state: QubeState) -> None:
|
|
171
|
+
"""Apply monotonic clamping to energy counters in a QubeState."""
|
|
135
172
|
for key in self._MONOTONIC_KEYS:
|
|
136
173
|
current = getattr(state, key, None)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if previous is not None and current < previous:
|
|
141
|
-
setattr(state, key, previous)
|
|
142
|
-
else:
|
|
143
|
-
self._previous_values[key] = current
|
|
174
|
+
clamped = self.clamp_monotonic(key, current)
|
|
175
|
+
if clamped is not current:
|
|
176
|
+
setattr(state, key, clamped)
|
|
144
177
|
|
|
145
178
|
async def async_get_software_version(self) -> str | None:
|
|
146
179
|
"""Read the software version from the device.
|
{python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/models.py
RENAMED
|
@@ -40,6 +40,57 @@ class QubeState:
|
|
|
40
40
|
setpoint_room_cool_night: float | None = None
|
|
41
41
|
setpoint_dhw: float | None = None
|
|
42
42
|
|
|
43
|
+
# Binary sensors - Outputs
|
|
44
|
+
dout_srcpmp_val: bool | None = None
|
|
45
|
+
dout_usrpmp_val: bool | None = None
|
|
46
|
+
dout_fourwayvlv_val: bool | None = None
|
|
47
|
+
dout_cooling_val: bool | None = None
|
|
48
|
+
dout_threewayvlv_val: bool | None = None
|
|
49
|
+
dout_bufferpmp_val: bool | None = None
|
|
50
|
+
dout_heaterstep1_val: bool | None = None
|
|
51
|
+
dout_heaterstep2_val: bool | None = None
|
|
52
|
+
dout_heaterstep3_val: bool | None = None
|
|
53
|
+
|
|
54
|
+
# Binary sensors - System status
|
|
55
|
+
keybonoff: bool | None = None
|
|
56
|
+
daynightmode: bool | None = None
|
|
57
|
+
|
|
58
|
+
# Binary sensors - Alarms
|
|
59
|
+
al_maxtime_antileg_active: bool | None = None
|
|
60
|
+
al_maxtime_dhw_active: bool | None = None
|
|
61
|
+
al_dewpoint_active: bool | None = None
|
|
62
|
+
al_underfloorsafety_active: bool | None = None
|
|
63
|
+
alrm_flw: bool | None = None
|
|
64
|
+
usralrms: bool | None = None
|
|
65
|
+
coolingalrms: bool | None = None
|
|
66
|
+
heatingalrms: bool | None = None
|
|
67
|
+
alarmmng_al_workinghour: bool | None = None
|
|
68
|
+
srsalrm: bool | None = None
|
|
69
|
+
glbal: bool | None = None
|
|
70
|
+
alarmmng_al_pwrplus: bool | None = None
|
|
71
|
+
|
|
72
|
+
# Binary sensors - Sensor/controller status
|
|
73
|
+
roomprb_en: bool | None = None
|
|
74
|
+
plantprb_en: bool | None = None
|
|
75
|
+
bufferprb_en: bool | None = None
|
|
76
|
+
en_dhwpid: bool | None = None
|
|
77
|
+
|
|
78
|
+
# Binary sensors - Demand signals
|
|
79
|
+
plantdemand: bool | None = None
|
|
80
|
+
id_demand: bool | None = None
|
|
81
|
+
thermostatdemand: bool | None = None
|
|
82
|
+
bms_demand: bool | None = None
|
|
83
|
+
|
|
84
|
+
# Binary sensors - Digital inputs
|
|
85
|
+
id_summerwinter: bool | None = None
|
|
86
|
+
dewpoint: bool | None = None
|
|
87
|
+
boostersecurity: bool | None = None
|
|
88
|
+
srcflw: bool | None = None
|
|
89
|
+
req_antileg_1: bool | None = None
|
|
90
|
+
|
|
91
|
+
# Binary sensors - Energy
|
|
92
|
+
surplus_pv: bool | None = None
|
|
93
|
+
|
|
43
94
|
# Extended dict for additional entities not covered by typed fields
|
|
44
95
|
_extended: dict[str, Any] = field(default_factory=dict)
|
|
45
96
|
|
|
@@ -345,123 +345,93 @@ async def test_get_all_data_auto_reconnects(mock_modbus_client):
|
|
|
345
345
|
|
|
346
346
|
|
|
347
347
|
@pytest.mark.asyncio
|
|
348
|
-
async def
|
|
349
|
-
"""Test
|
|
348
|
+
async def test_clamp_monotonic_basic(mock_modbus_client):
|
|
349
|
+
"""Test clamp_monotonic prevents decreases."""
|
|
350
350
|
client = QubeClient("1.2.3.4", 502)
|
|
351
|
-
mock_instance = mock_modbus_client.return_value
|
|
352
|
-
client._client = mock_instance
|
|
353
|
-
client._connected = True
|
|
354
|
-
|
|
355
|
-
# Helper to create mock responses returning specific float values
|
|
356
|
-
def make_resp(value_pairs):
|
|
357
|
-
"""Create mock responses for a sequence of register reads."""
|
|
358
|
-
import struct as s
|
|
359
|
-
responses = []
|
|
360
|
-
for val in value_pairs:
|
|
361
|
-
resp = MagicMock()
|
|
362
|
-
resp.isError.return_value = False
|
|
363
|
-
if val is not None:
|
|
364
|
-
packed = s.pack(">f", float(val))
|
|
365
|
-
int_val = s.unpack(">I", packed)[0]
|
|
366
|
-
resp.registers = [(int_val >> 16) & 0xFFFF, int_val & 0xFFFF]
|
|
367
|
-
else:
|
|
368
|
-
resp.registers = [0, 0]
|
|
369
|
-
responses.append(resp)
|
|
370
|
-
return responses
|
|
371
|
-
|
|
372
|
-
# First call: set initial energy values (100.0 electric, 200.0 thermic)
|
|
373
|
-
mock_resp = MagicMock()
|
|
374
|
-
mock_resp.isError.return_value = False
|
|
375
|
-
mock_resp.registers = [0, 0]
|
|
376
|
-
mock_instance.read_input_registers = AsyncMock(return_value=mock_resp)
|
|
377
|
-
mock_instance.read_holding_registers = AsyncMock(return_value=mock_resp)
|
|
378
351
|
|
|
379
|
-
|
|
380
|
-
assert
|
|
381
|
-
#
|
|
382
|
-
assert
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
assert client.
|
|
352
|
+
# First value establishes baseline
|
|
353
|
+
assert client.clamp_monotonic("energy", 100.0) == 100.0
|
|
354
|
+
# Increase passes through
|
|
355
|
+
assert client.clamp_monotonic("energy", 150.0) == 150.0
|
|
356
|
+
# Decrease gets clamped to previous
|
|
357
|
+
assert client.clamp_monotonic("energy", 50.0) == 150.0
|
|
358
|
+
# Recovery passes through
|
|
359
|
+
assert client.clamp_monotonic("energy", 160.0) == 160.0
|
|
387
360
|
|
|
388
361
|
|
|
389
362
|
@pytest.mark.asyncio
|
|
390
|
-
async def
|
|
391
|
-
"""Test
|
|
392
|
-
from python_qube_heatpump.models import QubeState
|
|
393
|
-
|
|
363
|
+
async def test_clamp_monotonic_none_and_nan(mock_modbus_client):
|
|
364
|
+
"""Test clamp_monotonic passes through None and NaN."""
|
|
394
365
|
client = QubeClient("1.2.3.4", 502)
|
|
395
366
|
|
|
396
|
-
#
|
|
397
|
-
|
|
398
|
-
state1.energy_total_electric = 100.0
|
|
399
|
-
state1.energy_total_thermic = 200.0
|
|
400
|
-
client._apply_monotonic_clamping(state1)
|
|
401
|
-
assert state1.energy_total_electric == 100.0
|
|
402
|
-
assert state1.energy_total_thermic == 200.0
|
|
403
|
-
|
|
404
|
-
# Second reading: values increase — should pass through
|
|
405
|
-
state2 = QubeState()
|
|
406
|
-
state2.energy_total_electric = 150.0
|
|
407
|
-
state2.energy_total_thermic = 250.0
|
|
408
|
-
client._apply_monotonic_clamping(state2)
|
|
409
|
-
assert state2.energy_total_electric == 150.0
|
|
410
|
-
assert state2.energy_total_thermic == 250.0
|
|
411
|
-
|
|
412
|
-
# Third reading: values decrease (glitch) — should clamp to previous
|
|
413
|
-
state3 = QubeState()
|
|
414
|
-
state3.energy_total_electric = 50.0 # Glitch: dropped below 150
|
|
415
|
-
state3.energy_total_thermic = 100.0 # Glitch: dropped below 250
|
|
416
|
-
client._apply_monotonic_clamping(state3)
|
|
417
|
-
assert state3.energy_total_electric == 150.0 # Clamped
|
|
418
|
-
assert state3.energy_total_thermic == 250.0 # Clamped
|
|
419
|
-
|
|
420
|
-
# Fourth reading: values recover — should pass through
|
|
421
|
-
state4 = QubeState()
|
|
422
|
-
state4.energy_total_electric = 160.0
|
|
423
|
-
state4.energy_total_thermic = 260.0
|
|
424
|
-
client._apply_monotonic_clamping(state4)
|
|
425
|
-
assert state4.energy_total_electric == 160.0
|
|
426
|
-
assert state4.energy_total_thermic == 260.0
|
|
367
|
+
# None passes through
|
|
368
|
+
assert client.clamp_monotonic("energy", None) is None
|
|
427
369
|
|
|
370
|
+
# Establish baseline
|
|
371
|
+
assert client.clamp_monotonic("energy", 100.0) == 100.0
|
|
372
|
+
|
|
373
|
+
# None doesn't update cache
|
|
374
|
+
assert client.clamp_monotonic("energy", None) is None
|
|
375
|
+
assert client.monotonic_cache["energy"] == 100.0
|
|
376
|
+
|
|
377
|
+
# NaN doesn't update cache
|
|
378
|
+
result = client.clamp_monotonic("energy", float("nan"))
|
|
379
|
+
assert result != result # NaN != NaN
|
|
380
|
+
assert client.monotonic_cache["energy"] == 100.0
|
|
428
381
|
|
|
429
|
-
@pytest.mark.asyncio
|
|
430
|
-
async def test_monotonic_clamping_skips_none(mock_modbus_client):
|
|
431
|
-
"""Test clamping skips None values."""
|
|
432
|
-
from python_qube_heatpump.models import QubeState
|
|
433
382
|
|
|
383
|
+
@pytest.mark.asyncio
|
|
384
|
+
async def test_clamp_monotonic_independent_keys(mock_modbus_client):
|
|
385
|
+
"""Test clamp_monotonic tracks keys independently."""
|
|
434
386
|
client = QubeClient("1.2.3.4", 502)
|
|
435
387
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
state1.energy_total_electric = 100.0
|
|
439
|
-
state1.energy_total_thermic = 200.0
|
|
440
|
-
client._apply_monotonic_clamping(state1)
|
|
388
|
+
client.clamp_monotonic("electric", 100.0)
|
|
389
|
+
client.clamp_monotonic("thermic", 200.0)
|
|
441
390
|
|
|
442
|
-
#
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
state2.energy_total_thermic = 200.0
|
|
446
|
-
client._apply_monotonic_clamping(state2)
|
|
447
|
-
assert state2.energy_total_electric is None # Unchanged
|
|
448
|
-
assert client._previous_values["energy_total_electric"] == 100.0 # Not updated
|
|
391
|
+
# Decrease in one doesn't affect the other
|
|
392
|
+
assert client.clamp_monotonic("electric", 50.0) == 100.0
|
|
393
|
+
assert client.clamp_monotonic("thermic", 250.0) == 250.0
|
|
449
394
|
|
|
450
395
|
|
|
451
396
|
@pytest.mark.asyncio
|
|
452
|
-
async def
|
|
453
|
-
"""Test
|
|
454
|
-
|
|
397
|
+
async def test_monotonic_cache_property(mock_modbus_client):
|
|
398
|
+
"""Test monotonic_cache can be get/set for persistence."""
|
|
399
|
+
client = QubeClient("1.2.3.4", 502)
|
|
400
|
+
|
|
401
|
+
# Cache starts empty
|
|
402
|
+
assert client.monotonic_cache == {}
|
|
455
403
|
|
|
404
|
+
# Set cache (e.g. restored from disk)
|
|
405
|
+
client.monotonic_cache = {"energy": 100.0, "thermic": 200.0}
|
|
406
|
+
assert client.monotonic_cache == {"energy": 100.0, "thermic": 200.0}
|
|
407
|
+
|
|
408
|
+
# Clamping uses the restored cache
|
|
409
|
+
assert client.clamp_monotonic("energy", 50.0) == 100.0
|
|
410
|
+
assert client.clamp_monotonic("thermic", 250.0) == 250.0
|
|
411
|
+
|
|
412
|
+
# Setting cache makes a copy
|
|
413
|
+
original = {"energy": 300.0}
|
|
414
|
+
client.monotonic_cache = original
|
|
415
|
+
original["energy"] = 999.0
|
|
416
|
+
assert client.monotonic_cache["energy"] == 300.0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@pytest.mark.asyncio
|
|
420
|
+
async def test_get_all_data_applies_clamping(mock_modbus_client):
|
|
421
|
+
"""Test get_all_data uses clamp_monotonic for energy counters."""
|
|
456
422
|
client = QubeClient("1.2.3.4", 502)
|
|
423
|
+
mock_instance = mock_modbus_client.return_value
|
|
424
|
+
client._client = mock_instance
|
|
425
|
+
client._connected = True
|
|
457
426
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
assert client.
|
|
427
|
+
mock_resp = MagicMock()
|
|
428
|
+
mock_resp.isError.return_value = False
|
|
429
|
+
mock_resp.registers = [0, 0]
|
|
430
|
+
mock_instance.read_input_registers = AsyncMock(return_value=mock_resp)
|
|
431
|
+
mock_instance.read_holding_registers = AsyncMock(return_value=mock_resp)
|
|
432
|
+
|
|
433
|
+
state = await client.get_all_data()
|
|
434
|
+
assert state is not None
|
|
435
|
+
# Energy values stored in cache
|
|
436
|
+
assert client.monotonic_cache.get("energy_total_electric") == 0.0
|
|
437
|
+
assert client.monotonic_cache.get("energy_total_thermic") == 0.0
|
{python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.yml
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/.github/workflows/python-publish.yml
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
|
{python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/entities/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.6.3 → python_qube_heatpump-1.8.0}/src/python_qube_heatpump/network.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|