switch2 0.1.1__tar.gz → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: switch2
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Async client library for the Switch2 energy portal
5
5
  Author-email: Jelmer Vernooij <jelmer@jelmer.uk>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "switch2"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Async client library for the Switch2 energy portal"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,7 @@
1
1
  """Standalone client library for the Switch2 energy portal."""
2
2
 
3
3
  from .api import (
4
+ AccountBalance,
4
5
  Bill,
5
6
  BillCharge,
6
7
  BillDetail,
@@ -13,6 +14,7 @@ from .api import (
13
14
  )
14
15
 
15
16
  __all__ = [
17
+ "AccountBalance",
16
18
  "Bill",
17
19
  "BillCharge",
18
20
  "BillDetail",
@@ -81,6 +81,14 @@ class BillDetail:
81
81
  download_url: str
82
82
 
83
83
 
84
+ @dataclass
85
+ class AccountBalance:
86
+ """Current account balance from the Switch2 portal."""
87
+
88
+ balance: float
89
+ last_updated: datetime
90
+
91
+
84
92
  @dataclass
85
93
  class Switch2Data:
86
94
  """All data fetched from the Switch2 portal."""
@@ -89,6 +97,7 @@ class Switch2Data:
89
97
  readings: list[MeterReading]
90
98
  registers: dict[str, str] # register_id -> register_name
91
99
  bills: list[Bill]
100
+ account_balance: AccountBalance | None
92
101
 
93
102
 
94
103
  def _get_attr(tag: Tag, attr: str, default: str = "") -> str:
@@ -117,8 +126,8 @@ class Switch2ApiClient:
117
126
  if self._session and not self._session.closed:
118
127
  await self._session.close()
119
128
 
120
- async def authenticate(self) -> CustomerInfo:
121
- """Log in to the Switch2 portal and return customer info."""
129
+ async def authenticate(self) -> tuple[CustomerInfo, BeautifulSoup]:
130
+ """Log in to the Switch2 portal and return customer info and dashboard soup."""
122
131
  session = await self._ensure_session()
123
132
 
124
133
  try:
@@ -156,7 +165,7 @@ class Switch2ApiClient:
156
165
  if not customer.name:
157
166
  raise Switch2AuthError("Login failed: no customer info returned")
158
167
 
159
- return customer
168
+ return customer, soup
160
169
 
161
170
  except aiohttp.ClientError as err:
162
171
  raise Switch2ConnectionError(
@@ -186,7 +195,8 @@ class Switch2ApiClient:
186
195
 
187
196
  async def fetch_data(self) -> Switch2Data:
188
197
  """Authenticate and fetch all meter data."""
189
- customer = await self.authenticate()
198
+ customer, dashboard_soup = await self.authenticate()
199
+ account_balance = _parse_account_balance(dashboard_soup)
190
200
  session = await self._ensure_session()
191
201
 
192
202
  try:
@@ -246,6 +256,7 @@ class Switch2ApiClient:
246
256
  readings=readings,
247
257
  registers=registers,
248
258
  bills=bills,
259
+ account_balance=account_balance,
249
260
  )
250
261
 
251
262
  except aiohttp.ClientError as err:
@@ -347,6 +358,23 @@ def _parse_currency(text: str) -> float:
347
358
  return -value if negative else value
348
359
 
349
360
 
361
+ def _parse_account_balance(soup: BeautifulSoup) -> AccountBalance | None:
362
+ """Extract the current account balance from the dashboard page."""
363
+ amount_el = soup.select_one(".dashboard-credit-amount-desktop")
364
+ if not amount_el:
365
+ return None
366
+ balance = _parse_currency(amount_el.text)
367
+ updated_el = soup.select_one(".dashboard-credit-lastUpdated")
368
+ if not updated_el:
369
+ return None
370
+ updated_text = updated_el.text.strip()
371
+ # Text is like "Last updated 27/02/2026 10:13"
372
+ if updated_text.lower().startswith("last updated"):
373
+ updated_text = updated_text[len("last updated") :].strip()
374
+ last_updated = datetime.strptime(updated_text, "%d/%m/%Y %H:%M")
375
+ return AccountBalance(balance=balance, last_updated=last_updated)
376
+
377
+
350
378
  def _parse_bill_charges(
351
379
  soup: BeautifulSoup, container_selector: str
352
380
  ) -> list[BillCharge]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: switch2
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Async client library for the Switch2 energy portal
5
5
  Author-email: Jelmer Vernooij <jelmer@jelmer.uk>
6
6
  License-Expression: Apache-2.0
@@ -6,6 +6,7 @@ from datetime import datetime
6
6
  from bs4 import BeautifulSoup
7
7
 
8
8
  from switch2.api import (
9
+ _parse_account_balance,
9
10
  _parse_bill_detail,
10
11
  _parse_bills,
11
12
  _parse_currency,
@@ -350,3 +351,43 @@ class ParseBillDetailTests(unittest.TestCase):
350
351
  detail.download_url,
351
352
  "https://my.switch2.co.uk/Credit/Bill/Download/2289896",
352
353
  )
354
+
355
+
356
+ class ParseAccountBalanceTests(unittest.TestCase):
357
+ def test_balance_from_dashboard(self) -> None:
358
+ html = """
359
+ <div id="CreditPaymentBalanceDashTile">
360
+ <div class="dashboard-credit-amount-desktop font-largest">
361
+ &#163;0.00
362
+ </div>
363
+ <div class="dashboard-credit-lastUpdated font-small-light">
364
+ Last updated 27/02/2026 10:13
365
+ </div>
366
+ </div>
367
+ """
368
+ soup = BeautifulSoup(html, "html.parser")
369
+ balance = _parse_account_balance(soup)
370
+ assert balance is not None
371
+ self.assertEqual(balance.balance, 0.00)
372
+ self.assertEqual(balance.last_updated, datetime(2026, 2, 27, 10, 13))
373
+
374
+ def test_balance_with_amount_owed(self) -> None:
375
+ html = """
376
+ <div id="CreditPaymentBalanceDashTile">
377
+ <div class="dashboard-credit-amount-desktop font-largest">
378
+ &#163;172.26
379
+ </div>
380
+ <div class="dashboard-credit-lastUpdated font-small-light">
381
+ Last updated 15/03/2026 14:30
382
+ </div>
383
+ </div>
384
+ """
385
+ soup = BeautifulSoup(html, "html.parser")
386
+ balance = _parse_account_balance(soup)
387
+ assert balance is not None
388
+ self.assertEqual(balance.balance, 172.26)
389
+ self.assertEqual(balance.last_updated, datetime(2026, 3, 15, 14, 30))
390
+
391
+ def test_no_balance_element(self) -> None:
392
+ soup = BeautifulSoup("<div></div>", "html.parser")
393
+ self.assertIsNone(_parse_account_balance(soup))
File without changes
File without changes