bwt-api 1.0.2__tar.gz → 1.0.4__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.
Files changed (51) hide show
  1. {bwt_api-1.0.2 → bwt_api-1.0.4}/PKG-INFO +1 -1
  2. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/data.py +45 -5
  3. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/smart_dos_api.py +37 -10
  4. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api.egg-info/PKG-INFO +1 -1
  5. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api.egg-info/SOURCES.txt +14 -4
  6. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api.egg-info/scm_file_list.json +30 -21
  7. bwt_api-1.0.4/src/bwt_api.egg-info/scm_version.json +8 -0
  8. bwt_api-1.0.4/tests/data/perla/GetCurrentData.json +33 -0
  9. bwt_api-1.0.4/tests/data/perla/GetDailyData.json +50 -0
  10. bwt_api-1.0.4/tests/data/perla/GetMonthlyData.json +33 -0
  11. bwt_api-1.0.4/tests/data/perla/GetYearlyData.json +14 -0
  12. bwt_api-1.0.4/tests/data/silk/registers.json +3 -0
  13. bwt_api-1.0.4/tests/data/smartdos/gatt_0104.json +13 -0
  14. bwt_api-1.0.4/tests/data/smartdos/gatt_0201.json +17 -0
  15. bwt_api-1.0.4/tests/data/smartdos/gatt_0202.json +24 -0
  16. bwt_api-1.0.4/tests/data/smartdos/gatt_0208.json +4 -0
  17. bwt_api-1.0.4/tests/data/smartdos/gatt_0401.json +8 -0
  18. bwt_api-1.0.4/tests/data/smartdos/gatt_0402.json +8 -0
  19. bwt_api-1.0.4/tests/data/smartdos/gatt_0503.json +12 -0
  20. bwt_api-1.0.4/tests/data/smartdos/gatt_0505.json +3 -0
  21. {bwt_api-1.0.2 → bwt_api-1.0.4}/tests/test_api.py +101 -5
  22. bwt_api-1.0.2/AUTHORS.rst +0 -5
  23. bwt_api-1.0.2/CHANGELOG.rst +0 -8
  24. bwt_api-1.0.2/CONTRIBUTING.rst +0 -278
  25. bwt_api-1.0.2/src/bwt_api.egg-info/scm_version.json +0 -8
  26. {bwt_api-1.0.2 → bwt_api-1.0.4}/.coveragerc +0 -0
  27. {bwt_api-1.0.2 → bwt_api-1.0.4}/.devcontainer/devcontainer.json +0 -0
  28. {bwt_api-1.0.2 → bwt_api-1.0.4}/.github/workflows/python-package.yml +0 -0
  29. {bwt_api-1.0.2 → bwt_api-1.0.4}/.github/workflows/python-publish.yml +0 -0
  30. {bwt_api-1.0.2 → bwt_api-1.0.4}/.gitignore +0 -0
  31. {bwt_api-1.0.2 → bwt_api-1.0.4}/LICENSE.txt +0 -0
  32. {bwt_api-1.0.2 → bwt_api-1.0.4}/README.rst +0 -0
  33. {bwt_api-1.0.2 → bwt_api-1.0.4}/pyproject.toml +0 -0
  34. {bwt_api-1.0.2 → bwt_api-1.0.4}/setup.cfg +0 -0
  35. {bwt_api-1.0.2 → bwt_api-1.0.4}/setup.py +0 -0
  36. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/__init__.py +0 -0
  37. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/api.py +0 -0
  38. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/bwt.py +0 -0
  39. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/bwt_api.py +0 -0
  40. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/error.py +0 -0
  41. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/exception.py +0 -0
  42. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/silk_api.py +0 -0
  43. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api/skeleton.py +0 -0
  44. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api.egg-info/dependency_links.txt +0 -0
  45. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api.egg-info/entry_points.txt +0 -0
  46. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api.egg-info/not-zip-safe +0 -0
  47. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api.egg-info/requires.txt +0 -0
  48. {bwt_api-1.0.2 → bwt_api-1.0.4}/src/bwt_api.egg-info/top_level.txt +0 -0
  49. {bwt_api-1.0.2 → bwt_api-1.0.4}/tests/conftest.py +0 -0
  50. {bwt_api-1.0.2 → bwt_api-1.0.4}/tests/test_distinguish.py +0 -0
  51. {bwt_api-1.0.2 → bwt_api-1.0.4}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bwt_api
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Python API to access the local BWT Perla API.
5
5
  Home-page: https://github.com/dkarv/bwt_api
6
6
  Author: dkarv
@@ -33,6 +33,22 @@ class SmartDosStatus(enum.IntEnum):
33
33
  def _missing_(cls, value):
34
34
  return None
35
35
 
36
+ def is_fatal(self) -> bool:
37
+ return self not in WARNING_CODES
38
+
39
+ def is_normal(self) -> bool:
40
+ return self in NORMAL_CODES
41
+
42
+ NORMAL_CODES = [
43
+ SmartDosStatus.STANDBY,
44
+ SmartDosStatus.METERING_ACTIVE,
45
+ ]
46
+
47
+ WARNING_CODES = [
48
+ SmartDosStatus.MINERAL_CONTAINER_LEVEL_LOW,
49
+ SmartDosStatus.SMART_MINERAL_RUNNING_LOW,
50
+ ]
51
+
36
52
 
37
53
  class SubstanceType(enum.IntEnum):
38
54
  L1_LE = 1
@@ -68,7 +84,7 @@ class CurrentResponse:
68
84
  holiday_mode: int # -1 or 0: inactive, 1: active, unix timestamp: start in future
69
85
  regeneration_last_1: datetime
70
86
  regeneration_last_2: datetime
71
- service_customer: datetime
87
+ service_customer: datetime
72
88
  service_technician: datetime
73
89
  out_of_service: int
74
90
  regeneration_count_1: int
@@ -81,7 +97,7 @@ class CurrentResponse:
81
97
  treated_day: int # treated water current day
82
98
  treated_month: int # treated water current month
83
99
  treated_year: int # treated water current year
84
- columns: int # number of columns: 2 for BWT Duo and 1 for BWT Perla One
100
+ columns: int # number of columns: 2 for BWT Duo and 1 for BWT Perla One
85
101
 
86
102
 
87
103
  @dataclass
@@ -91,11 +107,13 @@ class DailyResponse:
91
107
  # [1] = 00:30 - 00:59
92
108
  values: list[int]
93
109
 
110
+
94
111
  @dataclass
95
112
  class MonthlyResponse:
96
113
  # treated water in 1 day intervals
97
114
  values: list[int]
98
115
 
116
+
99
117
  @dataclass
100
118
  class YearlyResponse:
101
119
  # treated water in 1 month intervals
@@ -109,6 +127,15 @@ class WifiResponse:
109
127
  """UUID 0104: Wi-Fi Information"""
110
128
  ssid: str # Wi-Fi network name
111
129
  rssi: int # Wi-Fi signal strength in dBm
130
+ rssiAvg: str # Average Wi-Fi signal strength in dBm
131
+ rssiSig: str # Signal strength standard deviation in dBm
132
+ dhcp: bool # DHCP enabled
133
+ ip: str | None # IP address
134
+ sn: str | None # Serial number
135
+ sg: str | None # Subnet mask
136
+ pDns: str | None # Primary DNS
137
+ sDns: str | None # Secondary DNS
138
+ mac: str | None # MAC address
112
139
 
113
140
 
114
141
  @dataclass
@@ -122,6 +149,11 @@ class DeviceInfoResponse:
122
149
  dev_state: SmartDosStatus | None # Current device status
123
150
  active_states: list[SmartDosStatus | None] # List of active device statuses
124
151
  comm_date: str # Start-up date (ISO format)
152
+ device_id: str # Device ID
153
+ device_type: str # Device type
154
+ device_variant: str # Device variant
155
+ total_flow: int # Total treated water in ml
156
+ total_dosed: int # Total dosed substance in ml
125
157
 
126
158
 
127
159
  @dataclass
@@ -133,6 +165,12 @@ class ConfigurationResponse:
133
165
  aqa_watch_en: bool # AQA Watch active
134
166
  aqa_max_flow_en: bool # AQA MaxFlow active
135
167
  aqa_volume_val: float # AQA Volume value (l)
168
+ volume_per_stroke: float # Volume per stroke (ml)
169
+ pouch_empty_timeout: int # Pouch empty timeout (s)
170
+ pouch_not_empty_timeout: int # Pouch not empty timeout (s)
171
+ aqa_watch_val: int # AQA Watch value (s)
172
+ aqa_max_flow_val: float # AQA MaxFlow value (l/h)
173
+ rest_server_en: bool # REST server active
136
174
 
137
175
 
138
176
  @dataclass
@@ -156,15 +194,17 @@ class PouchInfoResponse:
156
194
  @dataclass
157
195
  class RemainingCapacityResponse:
158
196
  """UUID 0402: Remaining Capacity"""
159
- rem_capacity: Optional[float] # Remaining volume in ml
160
- rem_capacity_pct: Optional[float] # Remaining volume in %
161
- rem_capacity_days: Optional[int] # Remaining volume in days
197
+ rem_capacity: float # Remaining volume in ml
198
+ rem_capacity_pct: float # Remaining volume in %
199
+ rem_capacity_days: int # Remaining volume in days
200
+ unit: int # Unit (0 = ml)
162
201
 
163
202
 
164
203
  @dataclass
165
204
  class TreatedWaterResponse:
166
205
  """UUID 0503: Treated Water"""
167
206
  total_flow: int # Total treated water in ml
207
+ total_ticks: int # Total treated water ticks??
168
208
 
169
209
 
170
210
  @dataclass
@@ -1,5 +1,7 @@
1
1
  """The BWT Smart Dos API class."""
2
2
 
3
+ from collections.abc import Mapping
4
+
3
5
  import aiohttp
4
6
  import logging
5
7
  from typing import Any
@@ -61,7 +63,19 @@ class BwtSmartDosApi:
61
63
  """UUID 0104: Get Wi-Fi name and signal strength."""
62
64
  self._logger.debug("Fetching Wi-Fi info from %s", self._host)
63
65
  raw = await self._get_gatt("0104")
64
- return WifiResponse(ssid=raw["ssid"], rssi=raw["rssi"])
66
+ return WifiResponse(
67
+ ssid=raw["ssid"],
68
+ rssi=raw["rssi"],
69
+ rssiAvg=raw["rssiAvg"],
70
+ rssiSig=raw["rssiSig"],
71
+ dhcp=raw["dhcp"],
72
+ ip=raw["ip"],
73
+ sn=raw["sn"],
74
+ sg=raw["sg"],
75
+ pDns=raw["pDns"],
76
+ sDns=raw["sDns"],
77
+ mac=raw["mac"]
78
+ )
65
79
 
66
80
  async def get_device_info(self) -> DeviceInfoResponse:
67
81
  """UUID 0201: Get device information."""
@@ -71,11 +85,16 @@ class BwtSmartDosApi:
71
85
  fw_rev=raw["fwRev"],
72
86
  hw_rev=raw["hwRev"],
73
87
  product_code=raw["productCode"],
88
+ device_id=raw["iotDevId"],
89
+ device_type=raw["iotDevType"],
90
+ device_variant=raw["iotDevVariant"],
74
91
  uptime=raw["uptime"],
75
92
  operating_time=raw["operatingTime"],
76
93
  dev_state=SmartDosStatus(raw["devState"]),
77
94
  active_states=[SmartDosStatus(state) for state in raw["activeStates"]],
78
95
  comm_date=raw["commDate"],
96
+ total_flow=raw["lifeTimeFlow_ml"],
97
+ total_dosed=raw["lifeTimeDosed_ml"],
79
98
  )
80
99
 
81
100
  async def get_configuration(self) -> ConfigurationResponse:
@@ -85,10 +104,16 @@ class BwtSmartDosApi:
85
104
  return ConfigurationResponse(
86
105
  buzzer_en=raw["buzzerEn"],
87
106
  dosing_rate=raw["dosingRate"],
107
+ volume_per_stroke=raw["volumePerStroke"],
108
+ pouch_empty_timeout=raw["pouchEmptyTimeout"],
109
+ pouch_not_empty_timeout=raw["pouchNotEmptyTimeout"],
88
110
  aqa_volume_en=raw["aqaVolumeEn"],
89
111
  aqa_watch_en=raw["aqaWatchEn"],
90
112
  aqa_max_flow_en=raw["aqaMaxFlowEn"],
91
113
  aqa_volume_val=raw["aqaVolumeVal"],
114
+ aqa_watch_val=raw["aqaWatchVal"],
115
+ aqa_max_flow_val=raw["aqaMaxFlowVal"],
116
+ rest_server_en=raw["restServerEn"],
92
117
  )
93
118
 
94
119
  async def get_time_info(self) -> TimeResponse:
@@ -110,22 +135,24 @@ class BwtSmartDosApi:
110
135
  unit=raw["unit"],
111
136
  )
112
137
 
113
- async def get_remaining_capacity(self) -> RemainingCapacityResponse:
138
+ async def get_remaining_capacity(self) -> Mapping[int, RemainingCapacityResponse]:
114
139
  """UUID 0402: Get remaining capacity information."""
115
140
  self._logger.debug("Fetching remaining capacity from %s", self._host)
116
141
  raw = await self._get_gatt("0402")
117
- return RemainingCapacityResponse(
118
- rem_capacity=raw.get("remCapacity"),
119
- rem_capacity_pct=raw.get("remCapacityPct"),
120
- rem_capacity_days=raw.get("remCapacityDays"),
121
- )
142
+ return {int(k): RemainingCapacityResponse(
143
+ rem_capacity=v["remCapacity"],
144
+ rem_capacity_pct=v["remCapacityPct"],
145
+ rem_capacity_days=v["remCapacityDays"],
146
+ unit=v["unit"]) for k, v in raw.items()}
122
147
 
123
- async def get_treated_water(self) -> TreatedWaterResponse:
148
+ async def get_treated_water(self) -> Mapping[int, TreatedWaterResponse]:
124
149
  """UUID 0503: Get treated water information."""
125
150
  self._logger.debug("Fetching treated water from %s", self._host)
126
151
  raw = await self._get_gatt("0503")
127
- total_flow = raw["flow"]["1"]["totFlow"]
128
- return TreatedWaterResponse(total_flow=total_flow)
152
+ return {int(k): TreatedWaterResponse(
153
+ total_flow=v["totFlow"],
154
+ total_ticks=v["totTicks"],
155
+ ) for k, v in raw["flow"].items()}
129
156
 
130
157
  async def get_substance_dosage(self) -> SubstanceDosageResponse:
131
158
  """UUID 0505: Get substance dosage information."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bwt_api
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Python API to access the local BWT Perla API.
5
5
  Home-page: https://github.com/dkarv/bwt_api
6
6
  Author: dkarv
@@ -1,8 +1,5 @@
1
1
  .coveragerc
2
2
  .gitignore
3
- AUTHORS.rst
4
- CHANGELOG.rst
5
- CONTRIBUTING.rst
6
3
  LICENSE.txt
7
4
  README.rst
8
5
  pyproject.toml
@@ -33,4 +30,17 @@ src/bwt_api.egg-info/scm_version.json
33
30
  src/bwt_api.egg-info/top_level.txt
34
31
  tests/conftest.py
35
32
  tests/test_api.py
36
- tests/test_distinguish.py
33
+ tests/test_distinguish.py
34
+ tests/data/perla/GetCurrentData.json
35
+ tests/data/perla/GetDailyData.json
36
+ tests/data/perla/GetMonthlyData.json
37
+ tests/data/perla/GetYearlyData.json
38
+ tests/data/silk/registers.json
39
+ tests/data/smartdos/gatt_0104.json
40
+ tests/data/smartdos/gatt_0201.json
41
+ tests/data/smartdos/gatt_0202.json
42
+ tests/data/smartdos/gatt_0208.json
43
+ tests/data/smartdos/gatt_0401.json
44
+ tests/data/smartdos/gatt_0402.json
45
+ tests/data/smartdos/gatt_0503.json
46
+ tests/data/smartdos/gatt_0505.json
@@ -1,32 +1,41 @@
1
1
  {
2
2
  "files": [
3
- ".gitignore",
4
- "AUTHORS.rst",
5
- "LICENSE.txt",
6
- "CHANGELOG.rst",
7
3
  "tox.ini",
8
- "pyproject.toml",
9
- ".coveragerc",
4
+ "LICENSE.txt",
5
+ "README.rst",
10
6
  "setup.py",
11
- "CONTRIBUTING.rst",
7
+ "pyproject.toml",
12
8
  "setup.cfg",
13
- "README.rst",
14
- "src/bwt_api/bwt_api",
15
- "src/bwt_api/api.py",
16
- "src/bwt_api/exception.py",
17
- "src/bwt_api/bwt.py",
18
- "src/bwt_api/data.py",
19
- "src/bwt_api/smart_dos_api.py",
20
- "src/bwt_api/bwt_api.py",
21
- "src/bwt_api/silk_api.py",
22
- "src/bwt_api/error.py",
23
- "src/bwt_api/skeleton.py",
24
- "src/bwt_api/__init__.py",
25
- ".devcontainer/devcontainer.json",
9
+ ".gitignore",
10
+ ".coveragerc",
26
11
  ".github/workflows/python-publish.yml",
27
12
  ".github/workflows/python-package.yml",
13
+ ".devcontainer/devcontainer.json",
14
+ "tests/test_api.py",
28
15
  "tests/test_distinguish.py",
29
16
  "tests/conftest.py",
30
- "tests/test_api.py"
17
+ "tests/data/silk/registers.json",
18
+ "tests/data/smartdos/gatt_0202.json",
19
+ "tests/data/smartdos/gatt_0208.json",
20
+ "tests/data/smartdos/gatt_0401.json",
21
+ "tests/data/smartdos/gatt_0104.json",
22
+ "tests/data/smartdos/gatt_0503.json",
23
+ "tests/data/smartdos/gatt_0505.json",
24
+ "tests/data/smartdos/gatt_0402.json",
25
+ "tests/data/smartdos/gatt_0201.json",
26
+ "tests/data/perla/GetMonthlyData.json",
27
+ "tests/data/perla/GetDailyData.json",
28
+ "tests/data/perla/GetCurrentData.json",
29
+ "tests/data/perla/GetYearlyData.json",
30
+ "src/bwt_api/data.py",
31
+ "src/bwt_api/smart_dos_api.py",
32
+ "src/bwt_api/exception.py",
33
+ "src/bwt_api/__init__.py",
34
+ "src/bwt_api/bwt.py",
35
+ "src/bwt_api/api.py",
36
+ "src/bwt_api/error.py",
37
+ "src/bwt_api/bwt_api.py",
38
+ "src/bwt_api/skeleton.py",
39
+ "src/bwt_api/silk_api.py"
31
40
  ]
32
41
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.0.4",
3
+ "distance": 0,
4
+ "node": "g104722c353ad914ee1e375179ceb8f46d4f1766f",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-05"
8
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "ActiveErrorIDs": "",
3
+ "BlendedWaterSinceSetup_l": 123456,
4
+ "CapacityColumn1_ml_dH": 500000,
5
+ "CapacityColumn2_ml_dH": -1,
6
+ "CurrentFlowrate_l_h": 100,
7
+ "DosingSinceSetup_ml": 2500,
8
+ "FirmwareVersion": "1.2.3",
9
+ "HardnessIN_CaCO3": 200,
10
+ "HardnessIN_dH": 11,
11
+ "HardnessIN_fH": 19,
12
+ "HardnessIN_mmol_l": 2.0,
13
+ "HardnessOUT_CaCO3": 20,
14
+ "HardnessOUT_dH": 1,
15
+ "HardnessOUT_fH": 2,
16
+ "HardnessOUT_mmol_l": 0.2,
17
+ "HolidayModeStartTime": "",
18
+ "LastRegenerationColumn1": "2023-07-01 12:00:00",
19
+ "LastRegenerationColumn2": "2023-07-02 12:00:00",
20
+ "LastServiceCustomer": "2024-01-01 09:00:00",
21
+ "LastServiceTechnican": "2024-02-01 10:00:00",
22
+ "OutOfService": 0,
23
+ "RegenerationCounterColumn1": 5,
24
+ "RegenerationCounterColumn2": 0,
25
+ "RegenerationCountSinceSetup": 10,
26
+ "RegenerativLevel": 80,
27
+ "RegenerativRemainingDays": 100,
28
+ "RegenerativSinceSetup_g": 12345,
29
+ "ShowError": 0,
30
+ "WaterTreatedCurrentDay_l": 1234,
31
+ "WaterTreatedCurrentMonth_l": 5678,
32
+ "WaterTreatedCurrentYear_l": 23456
33
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "0000_0029_l": 0,
3
+ "0030_0059_l": 1,
4
+ "0100_0129_l": 2,
5
+ "0130_0159_l": 3,
6
+ "0200_0229_l": 4,
7
+ "0230_0259_l": 5,
8
+ "0300_0329_l": 6,
9
+ "0330_0359_l": 7,
10
+ "0400_0429_l": 8,
11
+ "0430_0459_l": 9,
12
+ "0500_0529_l": 10,
13
+ "0530_0559_l": 11,
14
+ "0600_0629_l": 12,
15
+ "0630_0659_l": 13,
16
+ "0700_0729_l": 14,
17
+ "0730_0759_l": 15,
18
+ "0800_0829_l": 16,
19
+ "0830_0859_l": 17,
20
+ "0900_0929_l": 18,
21
+ "0930_0959_l": 19,
22
+ "1000_1029_l": 20,
23
+ "1030_1059_l": 21,
24
+ "1100_1129_l": 22,
25
+ "1130_1159_l": 23,
26
+ "1200_1229_l": 24,
27
+ "1230_1259_l": 25,
28
+ "1300_1329_l": 26,
29
+ "1330_1359_l": 27,
30
+ "1400_1429_l": 28,
31
+ "1430_1459_l": 29,
32
+ "1500_1529_l": 30,
33
+ "1530_1559_l": 31,
34
+ "1600_1629_l": 32,
35
+ "1630_1659_l": 33,
36
+ "1700_1729_l": 34,
37
+ "1730_1759_l": 35,
38
+ "1800_1829_l": 36,
39
+ "1830_1859_l": 37,
40
+ "1900_1929_l": 38,
41
+ "1930_1959_l": 39,
42
+ "2000_2029_l": 40,
43
+ "2030_2059_l": 41,
44
+ "2100_2129_l": 42,
45
+ "2130_2159_l": 43,
46
+ "2200_2229_l": 44,
47
+ "2230_2259_l": 45,
48
+ "2300_2329_l": 46,
49
+ "2330_2359_l": 47
50
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "Day01_l": 1,
3
+ "Day02_l": 2,
4
+ "Day03_l": 3,
5
+ "Day04_l": 4,
6
+ "Day05_l": 5,
7
+ "Day06_l": 6,
8
+ "Day07_l": 7,
9
+ "Day08_l": 8,
10
+ "Day09_l": 9,
11
+ "Day10_l": 10,
12
+ "Day11_l": 11,
13
+ "Day12_l": 12,
14
+ "Day13_l": 13,
15
+ "Day14_l": 14,
16
+ "Day15_l": 15,
17
+ "Day16_l": 16,
18
+ "Day17_l": 17,
19
+ "Day18_l": 18,
20
+ "Day19_l": 19,
21
+ "Day20_l": 20,
22
+ "Day21_l": 21,
23
+ "Day22_l": 22,
24
+ "Day23_l": 23,
25
+ "Day24_l": 24,
26
+ "Day25_l": 25,
27
+ "Day26_l": 26,
28
+ "Day27_l": 27,
29
+ "Day28_l": 28,
30
+ "Day29_l": 29,
31
+ "Day30_l": 30,
32
+ "Day31_l": 31
33
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "Month01_l": 100,
3
+ "Month02_l": 110,
4
+ "Month03_l": 120,
5
+ "Month04_l": 130,
6
+ "Month05_l": 140,
7
+ "Month06_l": 150,
8
+ "Month07_l": 160,
9
+ "Month08_l": 170,
10
+ "Month09_l": 180,
11
+ "Month10_l": 190,
12
+ "Month11_l": 200,
13
+ "Month12_l": 210
14
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "params": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
3
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "ssid": "MyWiFi",
3
+ "rssi": null,
4
+ "rssiAvg": "-54.00",
5
+ "rssiSig": "0.00",
6
+ "dhcp": true,
7
+ "ip": null,
8
+ "sn": null,
9
+ "sg": null,
10
+ "pDns": null,
11
+ "sDns": null,
12
+ "mac": "AA:BB:CC:DD:EE:FF"
13
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "fwRev": "1.2.0",
3
+ "hwRev": "2.4.0(A)",
4
+ "productCode": "1AAA-2BBB",
5
+ "iotDevId": "abcdefgh-1234-5678-abcd-1234567890ab",
6
+ "iotDevType": "bwt_bewados",
7
+ "iotDevVariant": "dev",
8
+ "uptime": 6510811,
9
+ "operatingTime": 40773211,
10
+ "devState": 2001,
11
+ "activeStates": [
12
+ 2001
13
+ ],
14
+ "commDate": "2026-01-04T10:01:05.891Z",
15
+ "lifeTimeFlow_ml": 112507432,
16
+ "lifeTimeDosed_ml": 2746.1572265625
17
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "buzzerEn": false,
3
+ "dosingRate": 10,
4
+ "volumePerStroke": 0.053784001618623734,
5
+ "pouchEmptyTimeout": 7200,
6
+ "pouchNotEmptyTimeout": 20,
7
+ "aqaVolumeEn": false,
8
+ "aqaWatchEn": false,
9
+ "aqaMaxFlowEn": false,
10
+ "aqaVolumeVal": 9999,
11
+ "aqaWatchVal": 0,
12
+ "aqaMaxFlowVal": 7,
13
+ "restServerEn": true,
14
+ "defaults": {
15
+ "consumable": {
16
+ "totCap": 1500
17
+ },
18
+ "aqaAlert": {
19
+ "aqaVolumeVal": 9999,
20
+ "aqaWatchVal": 60,
21
+ "aqaMaxFlowVal": 7
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "time": "2026-07-14 12:00:00",
3
+ "timezone": "UTC"
4
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "totCap": 1000,
3
+ "expDate": "2027-01-01",
4
+ "orderNr": "ORD123",
5
+ "batchNr": "BATCH1",
6
+ "id": 1,
7
+ "unit": "ml"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "1": {
3
+ "remCapacity": 1451.1103515625,
4
+ "remCapacityPct": 97,
5
+ "remCapacityDays": 354,
6
+ "unit": 0
7
+ }
8
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "flow": {
3
+ "1": {
4
+ "totFlow": 112507432,
5
+ "totTicks": 2587671
6
+ },
7
+ "2": {
8
+ "totFlow": 20244130,
9
+ "totTicks": 465615
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "dosedMineral": 2746.1572265625
3
+ }
@@ -1,6 +1,7 @@
1
1
  from datetime import datetime
2
2
  from zoneinfo import ZoneInfo
3
3
  from unittest.mock import Mock
4
+ from pathlib import Path
4
5
  import pytest
5
6
  from aiohttp.client_reqrep import ClientResponse
6
7
 
@@ -101,6 +102,11 @@ current_json_empty_errors = """
101
102
  }
102
103
  """
103
104
 
105
+
106
+ def load_json_str(name: str) -> str:
107
+ """Load a JSON file from tests/data/smartdos and return its text."""
108
+ return (Path(__file__).parent / "data" / "smartdos" / name).read_text()
109
+
104
110
  current_json_perla_one = """
105
111
  {
106
112
  "ActiveErrorIDs" : "",
@@ -300,17 +306,33 @@ def test_unknown_error_no_mutation():
300
306
  # Different unknown codes produce different instances
301
307
  assert err1 is not err2
302
308
 
309
+ async def test_smartdos_get_wifi_info():
310
+ with aioresponses() as mocked:
311
+ mocked.get(
312
+ "http://host:80/api/v1/gatt/0104",
313
+ status=200,
314
+ body=load_json_str("gatt_0104.json"),
315
+ headers={"Content-Type": "application/json"},
316
+ )
317
+ async with BwtSmartDosApi("host") as api:
318
+ result = await api.get_wifi_info()
319
+ assert result.ssid == "Graf WLAN"
320
+ assert result.rssiAvg == "-54.00"
321
+ assert result.mac == "AA:BB:CC:DD:EE:FF"
322
+
323
+
303
324
  async def test_smartdos_get_gatt_0201():
304
325
  with aioresponses() as mocked:
305
326
  mocked.get(
306
327
  "http://host:80/api/v1/gatt/0201",
307
328
  status=200,
308
- body='{"characteristic":"0201","value":[1,2,3]}',
329
+ body=load_json_str("gatt_0201.json"),
309
330
  headers={"Content-Type": "application/json"},
310
331
  )
311
332
  async with BwtSmartDosApi("host") as api:
312
333
  result = await api.get_gatt_0201()
313
- assert result == {"characteristic": "0201", "value": [1, 2, 3]}
334
+ assert result["fwRev"] == "1.2.0"
335
+ assert result["productCode"] == "3HZR-1R37"
314
336
 
315
337
 
316
338
  async def test_smartdos_unknown_response():
@@ -321,12 +343,41 @@ async def test_smartdos_unknown_response():
321
343
  await api.get_gatt_0201()
322
344
 
323
345
 
346
+ async def test_smartdos_get_configuration():
347
+ with aioresponses() as mocked:
348
+ mocked.get(
349
+ "http://host:80/api/v1/gatt/0202",
350
+ status=200,
351
+ body=load_json_str("gatt_0202.json"),
352
+ headers={"Content-Type": "application/json"},
353
+ )
354
+ async with BwtSmartDosApi("host") as api:
355
+ result = await api.get_configuration()
356
+ assert result.buzzer_en is False
357
+ assert result.dosing_rate == 10
358
+ assert result.rest_server_en is True
359
+
360
+
361
+ async def test_smartdos_get_time_info():
362
+ with aioresponses() as mocked:
363
+ mocked.get(
364
+ "http://host:80/api/v1/gatt/0208",
365
+ status=200,
366
+ body=load_json_str("gatt_0208.json"),
367
+ headers={"Content-Type": "application/json"},
368
+ )
369
+ async with BwtSmartDosApi("host") as api:
370
+ result = await api.get_time_info()
371
+ assert result.time == "2026-07-14 12:00:00"
372
+ assert result.timezone == "UTC"
373
+
374
+
324
375
  async def test_smartdos_device_info_parses_status_values():
325
376
  with aioresponses() as mocked:
326
377
  mocked.get(
327
378
  "http://host:80/api/v1/gatt/0201",
328
379
  status=200,
329
- body='{"fwRev":"1.1.0+4","hwRev":"2.4.0(B)","productCode":"8R19-CX2A","uptime":4889,"operatingTime":706889,"devState":2001,"activeStates":[2001],"commDate":"2024-12-04T13:45:38.833Z"}',
380
+ body='{"fwRev":"1.2.0","hwRev":"2.4.0(A)","productCode":"3HZR-1R37","iotDevId":"5b29b9e9-24c5-4eea-a599-927922a89f5a","iotDevType":"bwt_bewados","iotDevVariant":"dev","uptime":6510811,"operatingTime":40773211,"devState":2001,"activeStates":[2001],"commDate":"2026-01-04T10:01:05.891Z","lifeTimeFlow_ml":112507432,"lifeTimeDosed_ml":2746.1572265625}',
330
381
  headers={"Content-Type": "application/json"},
331
382
  )
332
383
  async with BwtSmartDosApi("host") as api:
@@ -335,17 +386,62 @@ async def test_smartdos_device_info_parses_status_values():
335
386
  assert result.active_states == [SmartDosStatus.STANDBY]
336
387
 
337
388
 
389
+ async def test_smartdos_get_remaining_capacity():
390
+ with aioresponses() as mocked:
391
+ mocked.get(
392
+ "http://host:80/api/v1/gatt/0402",
393
+ status=200,
394
+ body=load_json_str("gatt_0402.json"),
395
+ headers={"Content-Type": "application/json"},
396
+ )
397
+ async with BwtSmartDosApi("host") as api:
398
+ result = await api.get_remaining_capacity()
399
+ assert 1 in result
400
+ assert result[1].rem_capacity == 1451.1103515625
401
+ assert result[1].rem_capacity_pct == 97
402
+
403
+
404
+ async def test_smartdos_get_treated_water():
405
+ with aioresponses() as mocked:
406
+ mocked.get(
407
+ "http://host:80/api/v1/gatt/0503",
408
+ status=200,
409
+ body=load_json_str("gatt_0503.json"),
410
+ headers={"Content-Type": "application/json"},
411
+ )
412
+ async with BwtSmartDosApi("host") as api:
413
+ result = await api.get_treated_water()
414
+ assert 1 in result
415
+ assert result[1].total_flow == 112507432
416
+ assert 2 in result
417
+ assert result[2].total_flow == 20244130
418
+
419
+
420
+ async def test_smartdos_get_substance_dosage():
421
+ with aioresponses() as mocked:
422
+ mocked.get(
423
+ "http://host:80/api/v1/gatt/0505",
424
+ status=200,
425
+ body=load_json_str("gatt_0505.json"),
426
+ headers={"Content-Type": "application/json"},
427
+ )
428
+ async with BwtSmartDosApi("host") as api:
429
+ result = await api.get_substance_dosage()
430
+ assert result.dosed_mineral == 2746.1572265625
431
+
432
+
338
433
  async def test_smartdos_pouch_info_parses_substance_type():
339
434
  with aioresponses() as mocked:
340
435
  mocked.get(
341
436
  "http://host:80/api/v1/gatt/0401",
342
437
  status=200,
343
- body='{"totCap":10000,"expDate":"05.12.2025","orderNr":125123456,"batchNr":12345,"id":2,"unit":0}',
438
+ body=load_json_str("gatt_0401.json"),
344
439
  headers={"Content-Type": "application/json"},
345
440
  )
346
441
  async with BwtSmartDosApi("host") as api:
347
442
  result = await api.get_pouch_info()
348
- assert result.substance_type == SubstanceType.L2_L3
443
+ assert result.substance_type == SubstanceType.L1_LE
444
+ assert result.tot_cap == 1000
349
445
 
350
446
  def test_treated_to_blended():
351
447
  assert treated_to_blended(0, 21, 4) == 0
bwt_api-1.0.2/AUTHORS.rst DELETED
@@ -1,5 +0,0 @@
1
- ============
2
- Contributors
3
- ============
4
-
5
- * dkarv <punzet.david@gmail.com>
@@ -1,8 +0,0 @@
1
- =========
2
- Changelog
3
- =========
4
-
5
- Version 0.2.1
6
- =============
7
-
8
- - Initial release
@@ -1,278 +0,0 @@
1
- ============
2
- Contributing
3
- ============
4
-
5
- Welcome to ``bwt_api`` contributor's guide.
6
-
7
- This document focuses on getting any potential contributor familiarized
8
- with the development processes, but `other kinds of contributions`_ are also
9
- appreciated.
10
-
11
- If you are new to using git_ or have never collaborated in a project previously,
12
- please have a look at `contribution-guide.org`_. Other resources are also
13
- listed in the excellent `guide created by FreeCodeCamp`_ [#contrib1]_.
14
-
15
- Please notice, all users and contributors are expected to be **open,
16
- considerate, reasonable, and respectful**. When in doubt, `Python Software
17
- Foundation's Code of Conduct`_ is a good reference in terms of behavior
18
- guidelines.
19
-
20
-
21
- Issue Reports
22
- =============
23
-
24
- If you experience bugs or general issues with ``bwt_api``, please have a look
25
- on the `issue tracker`_. If you don't see anything useful there, please feel
26
- free to fire an issue report.
27
-
28
- .. tip::
29
- Please don't forget to include the closed issues in your search.
30
- Sometimes a solution was already reported, and the problem is considered
31
- **solved**.
32
-
33
- New issue reports should include information about your programming environment
34
- (e.g., operating system, Python version) and steps to reproduce the problem.
35
- Please try also to simplify the reproduction steps to a very minimal example
36
- that still illustrates the problem you are facing. By removing other factors,
37
- you help us to identify the root cause of the issue.
38
-
39
-
40
- Documentation Improvements
41
- ==========================
42
-
43
- You can help improve ``bwt_api`` docs by making them more readable and coherent, or
44
- by adding missing information and correcting mistakes.
45
-
46
- .. tip::
47
- Please notice that the `GitHub web interface`_ provides a quick way of
48
- propose changes in ``bwt_api``'s files. While this mechanism can
49
- be tricky for normal code contributions, it works perfectly fine for
50
- contributing to the docs, and can be quite handy.
51
-
52
- If you are interested in trying this method out, please navigate to
53
- the ``docs`` folder in the source repository_, find which file you
54
- would like to propose changes and click in the little pencil icon at the
55
- top, to open `GitHub's code editor`_. Once you finish editing the file,
56
- please write a message in the form at the bottom of the page describing
57
- which changes have you made and what are the motivations behind them and
58
- submit your proposal.
59
-
60
-
61
- Code Contributions
62
- ==================
63
-
64
- Submit an issue
65
- ---------------
66
-
67
- Before you work on any non-trivial code contribution it's best to first create
68
- a report in the `issue tracker`_ to start a discussion on the subject.
69
- This often provides additional considerations and avoids unnecessary work.
70
-
71
- Create an environment
72
- ---------------------
73
-
74
- Before you start coding, we recommend creating an isolated `virtual
75
- environment`_ to avoid any problems with your installed Python packages.
76
- This can easily be done via either |virtualenv|_::
77
-
78
- virtualenv <PATH TO VENV>
79
- source <PATH TO VENV>/bin/activate
80
-
81
- or Miniconda_::
82
-
83
- conda create -n bwt_api python=3 six virtualenv pytest pytest-cov
84
- conda activate bwt_api
85
-
86
- Clone the repository
87
- --------------------
88
-
89
- #. Create an user account on |the repository service| if you do not already have one.
90
- #. Fork the project repository_: click on the *Fork* button near the top of the
91
- page. This creates a copy of the code under your account on |the repository service|.
92
- #. Clone this copy to your local disk::
93
-
94
- git clone git@github.com:YourLogin/bwt_api.git
95
- cd bwt_api
96
-
97
- #. You should run::
98
-
99
- pip install -U pip setuptools -e .
100
-
101
- to be able to import the package under development in the Python REPL.
102
-
103
-
104
- Implement your changes
105
- ----------------------
106
-
107
- #. Create a branch to hold your changes::
108
-
109
- git checkout -b my-feature
110
-
111
- and start making changes. Never work on the main branch!
112
-
113
- #. Start your work on this branch. Don't forget to add docstrings_ to new
114
- functions, modules and classes, especially if they are part of public APIs.
115
-
116
- #. Add yourself to the list of contributors in ``AUTHORS.rst``.
117
-
118
- #. When you’re done editing, do::
119
-
120
- git add <MODIFIED FILES>
121
- git commit
122
-
123
- to record your changes in git_.
124
-
125
- .. important:: Don't forget to add unit tests and documentation in case your
126
- contribution adds an additional feature and is not just a bugfix.
127
-
128
- Moreover, writing a `descriptive commit message`_ is highly recommended.
129
- In case of doubt, you can check the commit history with::
130
-
131
- git log --graph --decorate --pretty=oneline --abbrev-commit --all
132
-
133
- to look for recurring communication patterns.
134
-
135
- #. Please check that your changes don't break any unit tests with::
136
-
137
- tox
138
-
139
- (after having installed |tox|_ with ``pip install tox`` or ``pipx``).
140
-
141
- You can also use |tox|_ to run several other pre-configured tasks in the
142
- repository. Try ``tox -av`` to see a list of the available checks.
143
-
144
- Submit your contribution
145
- ------------------------
146
-
147
- #. If everything works fine, push your local branch to |the repository service| with::
148
-
149
- git push -u origin my-feature
150
-
151
- #. Go to the web page of your fork and click |contribute button|
152
- to send your changes for review.
153
-
154
- Find more detailed information in `creating a PR`_. You might also want to open
155
- the PR as a draft first and mark it as ready for review after the feedbacks
156
- from the continuous integration (CI) system or any required fixes.
157
-
158
-
159
- Troubleshooting
160
- ---------------
161
-
162
- The following tips can be used when facing problems to build or test the
163
- package:
164
-
165
- #. Make sure to fetch all the tags from the upstream repository_.
166
- The command ``git describe --abbrev=0 --tags`` should return the version you
167
- are expecting. If you are trying to run CI scripts in a fork repository,
168
- make sure to push all the tags.
169
- You can also try to remove all the egg files or the complete egg folder, i.e.,
170
- ``.eggs``, as well as the ``*.egg-info`` folders in the ``src`` folder or
171
- potentially in the root of your project.
172
-
173
- #. Sometimes |tox|_ misses out when new dependencies are added, especially to
174
- ``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with
175
- missing dependencies when running a command with |tox|_, try to recreate the
176
- ``tox`` environment using the ``-r`` flag. For example, instead of::
177
-
178
- tox -e docs
179
-
180
- Try running::
181
-
182
- tox -r -e docs
183
-
184
- #. Make sure to have a reliable |tox|_ installation that uses the correct
185
- Python version (e.g., 3.7+). When in doubt you can run::
186
-
187
- tox --version
188
- # OR
189
- which tox
190
-
191
- If you have trouble and are seeing weird errors upon running |tox|_, you can
192
- also try to create a dedicated `virtual environment`_ with a |tox|_ binary
193
- freshly installed. For example::
194
-
195
- virtualenv .venv
196
- source .venv/bin/activate
197
- .venv/bin/pip install tox
198
- .venv/bin/tox -e all
199
-
200
- #. `Pytest can drop you`_ in an interactive session in the case an error occurs.
201
- In order to do that you need to pass a ``--pdb`` option (for example by
202
- running ``tox -- -k <NAME OF THE FALLING TEST> --pdb``).
203
- You can also setup breakpoints manually instead of using the ``--pdb`` option.
204
-
205
-
206
- Maintainer tasks
207
- ================
208
-
209
- Releases
210
- --------
211
-
212
- If you are part of the group of maintainers and have correct user permissions
213
- on PyPI_, the following steps can be used to release a new version for
214
- ``bwt_api``:
215
-
216
- #. Make sure all unit tests are successful.
217
- #. Tag the current commit on the main branch with a release tag, e.g., ``v1.2.3``.
218
- #. Push the new tag to the upstream repository_, e.g., ``git push upstream v1.2.3``
219
- #. Clean up the ``dist`` and ``build`` folders with ``tox -e clean``
220
- (or ``rm -rf dist build``)
221
- to avoid confusion with old builds and Sphinx docs.
222
- #. Run ``tox -e build`` and check that the files in ``dist`` have
223
- the correct version (no ``.dirty`` or git_ hash) according to the git_ tag.
224
- Also check the sizes of the distributions, if they are too big (e.g., >
225
- 500KB), unwanted clutter may have been accidentally included.
226
- #. Run ``tox -e publish -- --repository pypi`` and check that everything was
227
- uploaded to PyPI_ correctly.
228
-
229
-
230
-
231
- .. [#contrib1] Even though, these resources focus on open source projects and
232
- communities, the general ideas behind collaborating with other developers
233
- to collectively create software are general and can be applied to all sorts
234
- of environments, including private companies and proprietary code bases.
235
-
236
-
237
- .. <-- start -->
238
-
239
- .. |the repository service| replace:: GitHub
240
- .. |contribute button| replace:: "Create pull request"
241
-
242
- .. _repository: https://github.com/dkarv/bwt_api
243
- .. _issue tracker: https://github.com/dkarv/bwt_api/issues
244
- .. <-- end -->
245
-
246
-
247
- .. |virtualenv| replace:: ``virtualenv``
248
- .. |pre-commit| replace:: ``pre-commit``
249
- .. |tox| replace:: ``tox``
250
-
251
-
252
- .. _black: https://pypi.org/project/black/
253
- .. _CommonMark: https://commonmark.org/
254
- .. _contribution-guide.org: https://www.contribution-guide.org/
255
- .. _creating a PR: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
256
- .. _descriptive commit message: https://chris.beams.io/posts/git-commit
257
- .. _docstrings: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
258
- .. _first-contributions tutorial: https://github.com/firstcontributions/first-contributions
259
- .. _flake8: https://flake8.pycqa.org/en/stable/
260
- .. _git: https://git-scm.com
261
- .. _GitHub's fork and pull request workflow: https://guides.github.com/activities/forking/
262
- .. _guide created by FreeCodeCamp: https://github.com/FreeCodeCamp/how-to-contribute-to-open-source
263
- .. _Miniconda: https://docs.conda.io/en/latest/miniconda.html
264
- .. _MyST: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html
265
- .. _other kinds of contributions: https://opensource.guide/how-to-contribute
266
- .. _pre-commit: https://pre-commit.com/
267
- .. _PyPI: https://pypi.org/
268
- .. _PyScaffold's contributor's guide: https://pyscaffold.org/en/stable/contributing.html
269
- .. _Pytest can drop you: https://docs.pytest.org/en/stable/how-to/failures.html#using-python-library-pdb-with-pytest
270
- .. _Python Software Foundation's Code of Conduct: https://www.python.org/psf/conduct/
271
- .. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/
272
- .. _Sphinx: https://www.sphinx-doc.org/en/master/
273
- .. _tox: https://tox.wiki/en/stable/
274
- .. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/
275
- .. _virtualenv: https://virtualenv.pypa.io/en/stable/
276
-
277
- .. _GitHub web interface: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
278
- .. _GitHub's code editor: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.0.2",
3
- "distance": 0,
4
- "node": "g5bd1108162a356c6ba843801c305aa396bb2b6f4",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-07-18"
8
- }
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