python-mytnb 0.5.0__tar.gz → 0.5.2__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 (26) hide show
  1. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/PKG-INFO +1 -1
  2. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/pyproject.toml +1 -1
  3. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/models.py +26 -0
  4. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/tests/test_models.py +41 -4
  5. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/uv.lock +1 -1
  6. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/.github/workflows/ci.yml +0 -0
  7. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/.github/workflows/publish.yml +0 -0
  8. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/.gitignore +0 -0
  9. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/LICENSE +0 -0
  10. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/README.md +0 -0
  11. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/__init__.py +0 -0
  12. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/__main__.py +0 -0
  13. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/auth.py +0 -0
  14. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/cli.py +0 -0
  15. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/client/__init__.py +0 -0
  16. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/client/auth.py +0 -0
  17. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/client/client.py +0 -0
  18. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/client/config.py +0 -0
  19. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/client/legacy.py +0 -0
  20. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/client/rest.py +0 -0
  21. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/crypto.py +0 -0
  22. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/src/mytnb/exceptions.py +0 -0
  23. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/tests/test_auth.py +0 -0
  24. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/tests/test_cli.py +0 -0
  25. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/tests/test_client.py +0 -0
  26. {python_mytnb-0.5.0 → python_mytnb-0.5.2}/tests/test_crypto.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-mytnb
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Python library to interface with the myTNB API (Tenaga Nasional Berhad)
5
5
  Project-URL: Repository, https://github.com/danieyal/python-mytnb
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-mytnb"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "Python library to interface with the myTNB API (Tenaga Nasional Berhad)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -267,6 +267,32 @@ class AccountUsage(BaseModel):
267
267
  return m.numeric_value
268
268
  return None
269
269
 
270
+ @property
271
+ def average_usage_kwh(self) -> Optional[float]:
272
+ """Average daily consumption (kWh) across available daily readings.
273
+
274
+ Computed from ``by_day`` rather than a usage metric: the API's
275
+ ``AVERAGEUSAGE`` metric is a percentage comparison against the previous
276
+ bill period (e.g. "20% — More than the last bill period"), not a kWh
277
+ value. Days with no recorded consumption are excluded: both missing
278
+ readings and zero-usage days (such as the current, still-partial day).
279
+ Returns None when there is no usable daily data.
280
+ """
281
+ values: list[float] = []
282
+ for week in self.by_day:
283
+ for day in week.days:
284
+ if day.is_missing_reading:
285
+ continue
286
+ try:
287
+ consumption = day.consumption_kwh
288
+ except (ValueError, TypeError):
289
+ continue
290
+ if consumption > 0:
291
+ values.append(consumption)
292
+ if not values:
293
+ return None
294
+ return round(sum(values) / len(values), 2)
295
+
270
296
  @property
271
297
  def current_cost_rm(self) -> Optional[float]:
272
298
  for m in self.cost_metrics:
@@ -88,12 +88,15 @@ FULL_API_RESPONSE = {
88
88
  "OtherUsageMetrics": {
89
89
  "Usage": [
90
90
  USAGE_METRIC_DATA,
91
+ # Real API shape: AVERAGEUSAGE is a percentage comparison vs the last
92
+ # bill period, not a kWh average (that's why average_usage_kwh is
93
+ # computed from by_day instead of read from this metric).
91
94
  {
92
- "Key": "AVGUSAGE",
95
+ "Key": "AVERAGEUSAGE",
93
96
  "Title": "Avg Usage",
94
- "SubTitle": "daily",
95
- "Value": "15.6",
96
- "ValueUnit": "kWh",
97
+ "SubTitle": "More than the last bill period",
98
+ "Value": "20%",
99
+ "ValueUnit": "",
97
100
  },
98
101
  ],
99
102
  "Cost": [
@@ -244,6 +247,38 @@ class TestAccountUsage:
244
247
  usage = AccountUsage.from_api_response(FULL_API_RESPONSE)
245
248
  assert usage.current_usage_kwh == 234.5
246
249
 
250
+ def test_average_usage_kwh(self):
251
+ # Computed from by_day (mean of daily consumption), not a usage metric.
252
+ usage = AccountUsage.from_api_response(FULL_API_RESPONSE)
253
+ assert usage.average_usage_kwh == 8.5
254
+
255
+ def test_average_usage_kwh_multiple_days(self):
256
+ data = {
257
+ "ByDay": [
258
+ {
259
+ "Range": "Week 1",
260
+ "Days": [
261
+ # Two valid days that count toward the average.
262
+ {"Date": "2026-06-01", "Year": "2026", "Month": "06",
263
+ "Day": "01", "Consumption": "10", "Amount": "2.7"},
264
+ {"Date": "2026-06-02", "Year": "2026", "Month": "06",
265
+ "Day": "02", "Consumption": "20", "Amount": "5.4"},
266
+ # Excluded via the missing-reading flag (high value would
267
+ # skew the mean if the filter regressed).
268
+ {"Date": "2026-06-03", "Year": "2026", "Month": "06",
269
+ "Day": "03", "Consumption": "99", "Amount": "26.7",
270
+ "IsMissingReading": True},
271
+ # Excluded via zero consumption (the current partial day),
272
+ # independently of the missing-reading flag.
273
+ {"Date": "2026-06-04", "Year": "2026", "Month": "06",
274
+ "Day": "04", "Consumption": "0", "Amount": "0"},
275
+ ],
276
+ }
277
+ ],
278
+ }
279
+ usage = AccountUsage.from_api_response(data)
280
+ assert usage.average_usage_kwh == 15.0 # mean(10, 20)
281
+
247
282
  def test_current_cost_rm(self):
248
283
  usage = AccountUsage.from_api_response(FULL_API_RESPONSE)
249
284
  assert usage.current_cost_rm == 87.60
@@ -280,6 +315,7 @@ class TestAccountUsage:
280
315
  def test_empty_response(self):
281
316
  usage = AccountUsage.from_api_response({})
282
317
  assert usage.current_usage_kwh is None
318
+ assert usage.average_usage_kwh is None
283
319
  assert usage.current_cost_rm is None
284
320
  assert usage.projected_cost_rm is None
285
321
  assert usage.by_month is None
@@ -291,6 +327,7 @@ class TestAccountUsage:
291
327
  }
292
328
  usage = AccountUsage.from_api_response(data)
293
329
  assert usage.current_usage_kwh is None
330
+ assert usage.average_usage_kwh is None
294
331
  assert usage.current_cost_rm is None
295
332
 
296
333
 
@@ -600,7 +600,7 @@ wheels = [
600
600
 
601
601
  [[package]]
602
602
  name = "python-mytnb"
603
- version = "0.4.0"
603
+ version = "0.5.2"
604
604
  source = { editable = "." }
605
605
  dependencies = [
606
606
  { name = "cryptography" },
File without changes
File without changes
File without changes