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.
- {switch2-0.1.1 → switch2-0.1.2}/PKG-INFO +1 -1
- {switch2-0.1.1 → switch2-0.1.2}/pyproject.toml +1 -1
- {switch2-0.1.1 → switch2-0.1.2}/switch2/__init__.py +2 -0
- {switch2-0.1.1 → switch2-0.1.2}/switch2/api.py +32 -4
- {switch2-0.1.1 → switch2-0.1.2}/switch2.egg-info/PKG-INFO +1 -1
- {switch2-0.1.1 → switch2-0.1.2}/tests/test_api.py +41 -0
- {switch2-0.1.1 → switch2-0.1.2}/README.md +0 -0
- {switch2-0.1.1 → switch2-0.1.2}/setup.cfg +0 -0
- {switch2-0.1.1 → switch2-0.1.2}/switch2.egg-info/SOURCES.txt +0 -0
- {switch2-0.1.1 → switch2-0.1.2}/switch2.egg-info/dependency_links.txt +0 -0
- {switch2-0.1.1 → switch2-0.1.2}/switch2.egg-info/requires.txt +0 -0
- {switch2-0.1.1 → switch2-0.1.2}/switch2.egg-info/top_level.txt +0 -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]:
|
|
@@ -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
|
+
£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
|
+
£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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|