pyyorkshirewater 0.4.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.
- pyyorkshirewater-0.4.0/.gitignore +44 -0
- pyyorkshirewater-0.4.0/CHANGELOG.md +146 -0
- pyyorkshirewater-0.4.0/LICENSE +21 -0
- pyyorkshirewater-0.4.0/PKG-INFO +171 -0
- pyyorkshirewater-0.4.0/README.md +136 -0
- pyyorkshirewater-0.4.0/pyproject.toml +94 -0
- pyyorkshirewater-0.4.0/pyyorkshirewater/__init__.py +69 -0
- pyyorkshirewater-0.4.0/pyyorkshirewater/auth.py +442 -0
- pyyorkshirewater-0.4.0/pyyorkshirewater/client.py +495 -0
- pyyorkshirewater-0.4.0/pyyorkshirewater/const.py +87 -0
- pyyorkshirewater-0.4.0/pyyorkshirewater/exceptions.py +84 -0
- pyyorkshirewater-0.4.0/pyyorkshirewater/models.py +461 -0
- pyyorkshirewater-0.4.0/pyyorkshirewater/py.typed +0 -0
- pyyorkshirewater-0.4.0/tests/__init__.py +0 -0
- pyyorkshirewater-0.4.0/tests/conftest.py +71 -0
- pyyorkshirewater-0.4.0/tests/test_auth.py +294 -0
- pyyorkshirewater-0.4.0/tests/test_client.py +339 -0
- pyyorkshirewater-0.4.0/tests/test_models.py +253 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Byte-compiled / optimised
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
wheels/
|
|
12
|
+
pip-wheel-metadata/
|
|
13
|
+
|
|
14
|
+
# Virtual environments
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
env/
|
|
18
|
+
|
|
19
|
+
# Editors
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
.DS_Store
|
|
23
|
+
|
|
24
|
+
# Testing and coverage
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.coverage
|
|
27
|
+
.coverage.*
|
|
28
|
+
htmlcov/
|
|
29
|
+
coverage.xml
|
|
30
|
+
.tox/
|
|
31
|
+
.nox/
|
|
32
|
+
|
|
33
|
+
# Type checkers
|
|
34
|
+
.mypy_cache/
|
|
35
|
+
.pyright/
|
|
36
|
+
.pyre/
|
|
37
|
+
|
|
38
|
+
# Ruff
|
|
39
|
+
.ruff_cache/
|
|
40
|
+
|
|
41
|
+
# Local secrets used during integration testing
|
|
42
|
+
.env
|
|
43
|
+
.env.local
|
|
44
|
+
secrets.toml
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `pyyorkshirewater` are recorded here. The project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) and the
|
|
5
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.4.1] - 2026-05-09
|
|
10
|
+
|
|
11
|
+
First PyPI release. Identical code to v0.4.0; this version exists
|
|
12
|
+
only because the v0.4.0 tag predated the GitHub Actions publish
|
|
13
|
+
workflow trigger that fires on tag push. The workflow now publishes
|
|
14
|
+
this and every subsequent tag automatically via PyPI Trusted
|
|
15
|
+
Publisher OIDC.
|
|
16
|
+
|
|
17
|
+
## [0.4.0] - 2026-05-06
|
|
18
|
+
|
|
19
|
+
Data-model expansion (multi-property), no behaviour change to single-
|
|
20
|
+
property accounts. See `git log v0.3.3..v0.4.0` for the diff. Not
|
|
21
|
+
published to PyPI.
|
|
22
|
+
|
|
23
|
+
## [0.3.0] - 2026-05-06
|
|
24
|
+
|
|
25
|
+
Data model rewrite based on SPA bundle analysis.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Breaking**: `DailyConsumptionPoint`, `UsagePeriod` and
|
|
30
|
+
`YearlyConsumptionPoint` rewritten with the field names the Yorkshire
|
|
31
|
+
Water SPA actually uses, per static analysis of the customer portal
|
|
32
|
+
bundle. The single `consumption` field is replaced with the typed pair
|
|
33
|
+
`total_consumption_litres` (litres) and `total_consumption_m3` (cubic
|
|
34
|
+
metres).
|
|
35
|
+
- New per-day fields: `total_cost`, `total_cost_including_sewerage`,
|
|
36
|
+
`clean_water_cost`, `is_estimated`, `continuous_flow_alarm`. The SPA
|
|
37
|
+
exposes three spellings of the sewerage field; all three are accepted.
|
|
38
|
+
- `UsagePeriod` now carries period totals (`period_total_litres`,
|
|
39
|
+
`period_total_consumption_m3`, costs and daily averages) plus the
|
|
40
|
+
per-day breakdown via `daily_points`.
|
|
41
|
+
|
|
42
|
+
Callers using the old `consumption` attribute will need to update to
|
|
43
|
+
`total_consumption_litres`.
|
|
44
|
+
|
|
45
|
+
## [0.2.3] - 2026-05-06
|
|
46
|
+
|
|
47
|
+
Smart meter rollout cohort fix.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- `get_meter_details` and `get_current_consumption` now treat HTTP 404 as
|
|
52
|
+
"the account has no smart meter yet" rather than raising. They return
|
|
53
|
+
empty `MeterDetails` / `CurrentConsumption` objects so callers' code
|
|
54
|
+
paths stay straightforward and `meter_status` correctly reflects
|
|
55
|
+
`NO_METER`. Other HTTP error codes (5xx, 429, etc.) still raise as
|
|
56
|
+
before.
|
|
57
|
+
|
|
58
|
+
## [0.2.2] - 2026-05-06
|
|
59
|
+
|
|
60
|
+
Second pre-publish security pass.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- Authorize-redirect target validation. The library now rejects any
|
|
65
|
+
authorize redirect whose `Location` host or path does not match the
|
|
66
|
+
registered redirect URI, even if the response carries a valid state
|
|
67
|
+
and code. Defence in depth against tampered IdP responses.
|
|
68
|
+
- RFC 9207 issuer parameter validation. If the redirect carries an
|
|
69
|
+
`iss` parameter, it must equal the configured IdP issuer (compared
|
|
70
|
+
with `hmac.compare_digest`). The Yorkshire Water IdP advertises
|
|
71
|
+
`authorization_response_iss_parameter_supported`, so this kicks in
|
|
72
|
+
whenever the IdP includes the parameter.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- Test suite expanded with cases covering the new redirect target and
|
|
77
|
+
iss parameter validations.
|
|
78
|
+
|
|
79
|
+
## [0.2.1] - 2026-05-06
|
|
80
|
+
|
|
81
|
+
Security hardening from a pre-publish review.
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- The OAuth `state` parameter on the silent renewal redirect is now
|
|
86
|
+
required (a missing `state` raises `YorkshireWaterAuthError`) and the
|
|
87
|
+
comparison uses `hmac.compare_digest` for constant-time equality.
|
|
88
|
+
- `YorkshireWaterAPIError` raised for non-JSON responses no longer inlines
|
|
89
|
+
the response body into the error message; the body remains available on
|
|
90
|
+
the exception's `body` attribute for debugging but cannot leak through
|
|
91
|
+
HA's user-visible repair issues.
|
|
92
|
+
|
|
93
|
+
## [0.2.0] - 2026-05-06
|
|
94
|
+
|
|
95
|
+
Auth model rewrite.
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
|
|
99
|
+
- **Breaking**: Authentication now uses cookie-based silent renewal against
|
|
100
|
+
the IdentityServer at `login.yorkshirewater.com`. The `YorkshireWaterClient`
|
|
101
|
+
constructor takes a `cookies` argument and no longer accepts `email`,
|
|
102
|
+
`password` or `refresh_token`. Empirical testing confirmed that the SPA
|
|
103
|
+
OAuth client is not permitted to use the password grant or the device flow,
|
|
104
|
+
the SPA scope set does not include `offline_access`, and the login form is
|
|
105
|
+
protected by invisible reCAPTCHA v3.
|
|
106
|
+
- The library now drives `/connect/authorize?prompt=none` with a fresh PKCE
|
|
107
|
+
verifier on every renewal, captures the authorization code from the
|
|
108
|
+
redirect Location header and exchanges it at `/connect/token`. There is no
|
|
109
|
+
refresh token flow because none is issued.
|
|
110
|
+
- `close()` no longer revokes anything. There is no server-side credential
|
|
111
|
+
the library can revoke without a refresh token. The IdP session cookie is
|
|
112
|
+
the long-lived credential, managed by the user's browser.
|
|
113
|
+
|
|
114
|
+
### Added
|
|
115
|
+
|
|
116
|
+
- `CookieSessionExpiredError` (subclass of `YorkshireWaterAuthError`) raised
|
|
117
|
+
when silent renewal returns `error=login_required`. Callers should catch
|
|
118
|
+
this and prompt the user to re-export cookies from a fresh browser session.
|
|
119
|
+
- `IDP_COOKIE_DOMAIN` constant exposed so callers can scope their cookie
|
|
120
|
+
capture correctly.
|
|
121
|
+
|
|
122
|
+
### Removed
|
|
123
|
+
|
|
124
|
+
- ROPC password grant support.
|
|
125
|
+
- `refresh_token` constructor parameter.
|
|
126
|
+
- Email and password storage in any client state.
|
|
127
|
+
|
|
128
|
+
## [0.1.0] - 2026-05-06
|
|
129
|
+
|
|
130
|
+
Initial release. ROPC-based authentication.
|
|
131
|
+
|
|
132
|
+
DEPRECATED. The auth model in 0.1.0 was based on a static reading of the
|
|
133
|
+
public OIDC discovery document, which lists the `password` grant as
|
|
134
|
+
supported. Empirical testing against the live IdP showed that
|
|
135
|
+
`password` is supported by the IdP but is rejected for the SPA client
|
|
136
|
+
(`unauthorized_client`). 0.1 therefore cannot authenticate against any real
|
|
137
|
+
account and was superseded by 0.2.0 within hours of release.
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
|
|
141
|
+
- Async client (`YorkshireWaterClient`) for the Yorkshire Water customer
|
|
142
|
+
self-service API at `my.yorkshirewater.com`.
|
|
143
|
+
- Authentication via OpenID Connect Resource Owner Password Credentials
|
|
144
|
+
against the IdentityServer. Did not work in practice.
|
|
145
|
+
- Smart meter endpoints, three-state `MeterStatus` enum, typed exceptions,
|
|
146
|
+
PEP 561 type information.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dan Simms
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyyorkshirewater
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Async Python client for Yorkshire Water's customer self-service API. Unofficial and not affiliated with Yorkshire Water.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dan-simms1/pyyorkshirewater
|
|
6
|
+
Project-URL: Repository, https://github.com/dan-simms1/pyyorkshirewater
|
|
7
|
+
Project-URL: Issues, https://github.com/dan-simms1/pyyorkshirewater/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/dan-simms1/pyyorkshirewater/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Dan Simms
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: home-assistant,smart-meter,utility,water,yorkshire-water
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Home Automation
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# pyyorkshirewater
|
|
37
|
+
|
|
38
|
+
Async Python client for the Yorkshire Water customer self-service API.
|
|
39
|
+
|
|
40
|
+
This library is unofficial and not affiliated with Yorkshire Water. It exists
|
|
41
|
+
so that customers can read their own smart meter consumption data from the
|
|
42
|
+
same backend that powers `my.yorkshirewater.com`.
|
|
43
|
+
|
|
44
|
+
## Status
|
|
45
|
+
|
|
46
|
+
Early alpha. The smart meter rollout is in progress across the Yorkshire
|
|
47
|
+
Water region between 2025 and 2030, so most accounts will not yet have a live
|
|
48
|
+
meter. Until a meter is live the client will report a `MeterStatus` of
|
|
49
|
+
`NO_METER` or `PENDING_ACTIVATION`, and the consumption endpoints
|
|
50
|
+
(`get_your_usage`, `get_daily_consumption`, `get_yearly_consumption`) will
|
|
51
|
+
raise `YorkshireWaterMeterNotReadyError`. The readiness probes
|
|
52
|
+
(`get_meter_details`, `get_current_consumption`) work in every state.
|
|
53
|
+
|
|
54
|
+
## Why cookies, not email and password
|
|
55
|
+
|
|
56
|
+
Yorkshire Water's portal SPA uses Duende IdentityServer with authorization
|
|
57
|
+
code plus PKCE only. The SPA OAuth client (`css-onlineaccount-fe`) is not
|
|
58
|
+
permitted to use the password grant, the device flow or `offline_access`,
|
|
59
|
+
and the login form is protected by invisible Google reCAPTCHA v3 which a
|
|
60
|
+
non-browser HTTP client cannot pass.
|
|
61
|
+
|
|
62
|
+
The workable architecture is therefore: the user logs in to
|
|
63
|
+
my.yorkshirewater.com once in their own browser, where reCAPTCHA succeeds
|
|
64
|
+
naturally, and exports the IdentityServer session cookie. This library uses
|
|
65
|
+
that cookie to drive `/connect/authorize?prompt=none` for silent renewal,
|
|
66
|
+
mints fresh access tokens on demand and never sees the user's password.
|
|
67
|
+
|
|
68
|
+
The companion Chrome extension (separate repository) makes cookie export a
|
|
69
|
+
single click.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install pyyorkshirewater
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Python 3.11 or newer.
|
|
78
|
+
|
|
79
|
+
## Quick start
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import asyncio
|
|
83
|
+
from pyyorkshirewater import YorkshireWaterClient, MeterStatus
|
|
84
|
+
|
|
85
|
+
cookies = {
|
|
86
|
+
"idsrv": "<value from a logged-in browser session>",
|
|
87
|
+
"idsrv.session": "<value from a logged-in browser session>",
|
|
88
|
+
# any other login.yorkshirewater.com cookies your browser holds
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async def main():
|
|
92
|
+
async with YorkshireWaterClient(cookies=cookies) as client:
|
|
93
|
+
await client.login()
|
|
94
|
+
|
|
95
|
+
if client.meter_status is MeterStatus.LIVE:
|
|
96
|
+
daily = await client.get_daily_consumption()
|
|
97
|
+
print(daily)
|
|
98
|
+
else:
|
|
99
|
+
print(f"Meter is {client.meter_status.value}, no data yet.")
|
|
100
|
+
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Cookie lifetime
|
|
105
|
+
|
|
106
|
+
The IdentityServer session cookie typically lives between several hours and
|
|
107
|
+
several weeks depending on the provider's configuration. Each silent renewal
|
|
108
|
+
call may extend it (sliding expiration) on servers configured for that. When
|
|
109
|
+
the cookie eventually expires, the library raises
|
|
110
|
+
`CookieSessionExpiredError` (a subclass of `YorkshireWaterAuthError`) with
|
|
111
|
+
`error="login_required"`. Callers should catch this and prompt the user for
|
|
112
|
+
fresh cookies. The Home Assistant integration handles this via a reauth
|
|
113
|
+
flow.
|
|
114
|
+
|
|
115
|
+
## Meter readiness states
|
|
116
|
+
|
|
117
|
+
Yorkshire Water's portal exposes a three-state model. The library mirrors it
|
|
118
|
+
through the `MeterStatus` enum:
|
|
119
|
+
|
|
120
|
+
- `NO_METER`: the account has no smart meter on it.
|
|
121
|
+
- `PENDING_ACTIVATION`: a meter is registered but is awaiting network setup
|
|
122
|
+
by Yorkshire Water.
|
|
123
|
+
- `LIVE`: the meter is reporting data and the consumption endpoints will
|
|
124
|
+
return real values.
|
|
125
|
+
|
|
126
|
+
Application code should check `client.meter_status` before requesting
|
|
127
|
+
consumption data.
|
|
128
|
+
|
|
129
|
+
## Logging
|
|
130
|
+
|
|
131
|
+
The library logs to a named logger called `pyyorkshirewater` at the `DEBUG`
|
|
132
|
+
level. Configure it in the usual way:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import logging
|
|
136
|
+
logging.getLogger("pyyorkshirewater").setLevel(logging.DEBUG)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Errors
|
|
140
|
+
|
|
141
|
+
All errors raised by the library inherit from `YorkshireWaterError`. The
|
|
142
|
+
specific subclasses are:
|
|
143
|
+
|
|
144
|
+
- `YorkshireWaterAuthError`: silent renewal failed for a reason that is not
|
|
145
|
+
cookie expiry. The original IdentityServer `error` and `error_description`
|
|
146
|
+
are exposed on the exception.
|
|
147
|
+
- `CookieSessionExpiredError` (subclass of `YorkshireWaterAuthError`): the
|
|
148
|
+
IdP session cookie is expired or invalid. Re-export from a fresh browser
|
|
149
|
+
session.
|
|
150
|
+
- `YorkshireWaterAPIError`: a non-auth HTTP error from the customer API.
|
|
151
|
+
- `YorkshireWaterMeterNotReadyError`: raised when a consumption endpoint is
|
|
152
|
+
called against a meter that is not yet `LIVE`.
|
|
153
|
+
- `YorkshireWaterRateLimitError`: 429 response with optional `retry_after`.
|
|
154
|
+
|
|
155
|
+
## Related projects
|
|
156
|
+
|
|
157
|
+
- [`dan-simms1/ha-yorkshire-water`](https://github.com/dan-simms1/ha-yorkshire-water):
|
|
158
|
+
Home Assistant integration that uses this library.
|
|
159
|
+
- (forthcoming) Chrome extension that extracts the cookies needed by this
|
|
160
|
+
library and the integration with one click.
|
|
161
|
+
|
|
162
|
+
## Disclaimer
|
|
163
|
+
|
|
164
|
+
This project is unofficial and not affiliated with, endorsed by or supported
|
|
165
|
+
by Yorkshire Water Services Limited. Trademarks are the property of their
|
|
166
|
+
respective owners. Use this software at your own risk and only against
|
|
167
|
+
accounts that you own.
|
|
168
|
+
|
|
169
|
+
## Licence
|
|
170
|
+
|
|
171
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# pyyorkshirewater
|
|
2
|
+
|
|
3
|
+
Async Python client for the Yorkshire Water customer self-service API.
|
|
4
|
+
|
|
5
|
+
This library is unofficial and not affiliated with Yorkshire Water. It exists
|
|
6
|
+
so that customers can read their own smart meter consumption data from the
|
|
7
|
+
same backend that powers `my.yorkshirewater.com`.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
Early alpha. The smart meter rollout is in progress across the Yorkshire
|
|
12
|
+
Water region between 2025 and 2030, so most accounts will not yet have a live
|
|
13
|
+
meter. Until a meter is live the client will report a `MeterStatus` of
|
|
14
|
+
`NO_METER` or `PENDING_ACTIVATION`, and the consumption endpoints
|
|
15
|
+
(`get_your_usage`, `get_daily_consumption`, `get_yearly_consumption`) will
|
|
16
|
+
raise `YorkshireWaterMeterNotReadyError`. The readiness probes
|
|
17
|
+
(`get_meter_details`, `get_current_consumption`) work in every state.
|
|
18
|
+
|
|
19
|
+
## Why cookies, not email and password
|
|
20
|
+
|
|
21
|
+
Yorkshire Water's portal SPA uses Duende IdentityServer with authorization
|
|
22
|
+
code plus PKCE only. The SPA OAuth client (`css-onlineaccount-fe`) is not
|
|
23
|
+
permitted to use the password grant, the device flow or `offline_access`,
|
|
24
|
+
and the login form is protected by invisible Google reCAPTCHA v3 which a
|
|
25
|
+
non-browser HTTP client cannot pass.
|
|
26
|
+
|
|
27
|
+
The workable architecture is therefore: the user logs in to
|
|
28
|
+
my.yorkshirewater.com once in their own browser, where reCAPTCHA succeeds
|
|
29
|
+
naturally, and exports the IdentityServer session cookie. This library uses
|
|
30
|
+
that cookie to drive `/connect/authorize?prompt=none` for silent renewal,
|
|
31
|
+
mints fresh access tokens on demand and never sees the user's password.
|
|
32
|
+
|
|
33
|
+
The companion Chrome extension (separate repository) makes cookie export a
|
|
34
|
+
single click.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install pyyorkshirewater
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Python 3.11 or newer.
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from pyyorkshirewater import YorkshireWaterClient, MeterStatus
|
|
49
|
+
|
|
50
|
+
cookies = {
|
|
51
|
+
"idsrv": "<value from a logged-in browser session>",
|
|
52
|
+
"idsrv.session": "<value from a logged-in browser session>",
|
|
53
|
+
# any other login.yorkshirewater.com cookies your browser holds
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
async with YorkshireWaterClient(cookies=cookies) as client:
|
|
58
|
+
await client.login()
|
|
59
|
+
|
|
60
|
+
if client.meter_status is MeterStatus.LIVE:
|
|
61
|
+
daily = await client.get_daily_consumption()
|
|
62
|
+
print(daily)
|
|
63
|
+
else:
|
|
64
|
+
print(f"Meter is {client.meter_status.value}, no data yet.")
|
|
65
|
+
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Cookie lifetime
|
|
70
|
+
|
|
71
|
+
The IdentityServer session cookie typically lives between several hours and
|
|
72
|
+
several weeks depending on the provider's configuration. Each silent renewal
|
|
73
|
+
call may extend it (sliding expiration) on servers configured for that. When
|
|
74
|
+
the cookie eventually expires, the library raises
|
|
75
|
+
`CookieSessionExpiredError` (a subclass of `YorkshireWaterAuthError`) with
|
|
76
|
+
`error="login_required"`. Callers should catch this and prompt the user for
|
|
77
|
+
fresh cookies. The Home Assistant integration handles this via a reauth
|
|
78
|
+
flow.
|
|
79
|
+
|
|
80
|
+
## Meter readiness states
|
|
81
|
+
|
|
82
|
+
Yorkshire Water's portal exposes a three-state model. The library mirrors it
|
|
83
|
+
through the `MeterStatus` enum:
|
|
84
|
+
|
|
85
|
+
- `NO_METER`: the account has no smart meter on it.
|
|
86
|
+
- `PENDING_ACTIVATION`: a meter is registered but is awaiting network setup
|
|
87
|
+
by Yorkshire Water.
|
|
88
|
+
- `LIVE`: the meter is reporting data and the consumption endpoints will
|
|
89
|
+
return real values.
|
|
90
|
+
|
|
91
|
+
Application code should check `client.meter_status` before requesting
|
|
92
|
+
consumption data.
|
|
93
|
+
|
|
94
|
+
## Logging
|
|
95
|
+
|
|
96
|
+
The library logs to a named logger called `pyyorkshirewater` at the `DEBUG`
|
|
97
|
+
level. Configure it in the usual way:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import logging
|
|
101
|
+
logging.getLogger("pyyorkshirewater").setLevel(logging.DEBUG)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Errors
|
|
105
|
+
|
|
106
|
+
All errors raised by the library inherit from `YorkshireWaterError`. The
|
|
107
|
+
specific subclasses are:
|
|
108
|
+
|
|
109
|
+
- `YorkshireWaterAuthError`: silent renewal failed for a reason that is not
|
|
110
|
+
cookie expiry. The original IdentityServer `error` and `error_description`
|
|
111
|
+
are exposed on the exception.
|
|
112
|
+
- `CookieSessionExpiredError` (subclass of `YorkshireWaterAuthError`): the
|
|
113
|
+
IdP session cookie is expired or invalid. Re-export from a fresh browser
|
|
114
|
+
session.
|
|
115
|
+
- `YorkshireWaterAPIError`: a non-auth HTTP error from the customer API.
|
|
116
|
+
- `YorkshireWaterMeterNotReadyError`: raised when a consumption endpoint is
|
|
117
|
+
called against a meter that is not yet `LIVE`.
|
|
118
|
+
- `YorkshireWaterRateLimitError`: 429 response with optional `retry_after`.
|
|
119
|
+
|
|
120
|
+
## Related projects
|
|
121
|
+
|
|
122
|
+
- [`dan-simms1/ha-yorkshire-water`](https://github.com/dan-simms1/ha-yorkshire-water):
|
|
123
|
+
Home Assistant integration that uses this library.
|
|
124
|
+
- (forthcoming) Chrome extension that extracts the cookies needed by this
|
|
125
|
+
library and the integration with one click.
|
|
126
|
+
|
|
127
|
+
## Disclaimer
|
|
128
|
+
|
|
129
|
+
This project is unofficial and not affiliated with, endorsed by or supported
|
|
130
|
+
by Yorkshire Water Services Limited. Trademarks are the property of their
|
|
131
|
+
respective owners. Use this software at your own risk and only against
|
|
132
|
+
accounts that you own.
|
|
133
|
+
|
|
134
|
+
## Licence
|
|
135
|
+
|
|
136
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyyorkshirewater"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Async Python client for Yorkshire Water's customer self-service API. Unofficial and not affiliated with Yorkshire Water."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Dan Simms" }]
|
|
13
|
+
keywords = ["yorkshire-water", "smart-meter", "home-assistant", "water", "utility"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Framework :: AsyncIO",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Home Automation",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.27,<1.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"pytest-asyncio>=0.23",
|
|
36
|
+
"pytest-cov>=5.0",
|
|
37
|
+
"respx>=0.21",
|
|
38
|
+
"ruff>=0.6",
|
|
39
|
+
"mypy>=1.10",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/dan-simms1/pyyorkshirewater"
|
|
44
|
+
Repository = "https://github.com/dan-simms1/pyyorkshirewater"
|
|
45
|
+
Issues = "https://github.com/dan-simms1/pyyorkshirewater/issues"
|
|
46
|
+
Changelog = "https://github.com/dan-simms1/pyyorkshirewater/blob/main/CHANGELOG.md"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["pyyorkshirewater"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = [
|
|
53
|
+
"pyyorkshirewater",
|
|
54
|
+
"tests",
|
|
55
|
+
"README.md",
|
|
56
|
+
"CHANGELOG.md",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"pyproject.toml",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
minversion = "8.0"
|
|
63
|
+
asyncio_mode = "auto"
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
addopts = [
|
|
66
|
+
"-ra",
|
|
67
|
+
"--strict-markers",
|
|
68
|
+
"--strict-config",
|
|
69
|
+
]
|
|
70
|
+
markers = [
|
|
71
|
+
"integration: live integration tests against the real Yorkshire Water API. Skipped unless YW_INTEGRATION_TESTS=1.",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.coverage.run]
|
|
75
|
+
source = ["pyyorkshirewater"]
|
|
76
|
+
branch = true
|
|
77
|
+
|
|
78
|
+
[tool.coverage.report]
|
|
79
|
+
show_missing = true
|
|
80
|
+
skip_covered = false
|
|
81
|
+
fail_under = 80
|
|
82
|
+
exclude_lines = [
|
|
83
|
+
"pragma: no cover",
|
|
84
|
+
"raise NotImplementedError",
|
|
85
|
+
"if TYPE_CHECKING:",
|
|
86
|
+
"if __name__ == .__main__.:",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[tool.mypy]
|
|
90
|
+
python_version = "3.11"
|
|
91
|
+
strict = true
|
|
92
|
+
warn_unused_ignores = true
|
|
93
|
+
warn_redundant_casts = true
|
|
94
|
+
disallow_untyped_defs = true
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Async Python client for Yorkshire Water's customer self-service API.
|
|
2
|
+
|
|
3
|
+
This library is unofficial and not affiliated with Yorkshire Water Services
|
|
4
|
+
Limited. It exists to let customers access their own smart meter data through
|
|
5
|
+
the same backend that powers `my.yorkshirewater.com`.
|
|
6
|
+
|
|
7
|
+
Authentication uses cookie-based silent renewal against the IdentityServer at
|
|
8
|
+
`login.yorkshirewater.com`. The user logs in once via their own browser
|
|
9
|
+
(reCAPTCHA passes naturally there) and exports the session cookie; the
|
|
10
|
+
library uses it to mint fresh access tokens via
|
|
11
|
+
`/connect/authorize?prompt=none` whenever needed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from .auth import (
|
|
17
|
+
Authenticator,
|
|
18
|
+
CookieSessionExpiredError,
|
|
19
|
+
)
|
|
20
|
+
from .client import YorkshireWaterClient
|
|
21
|
+
from .const import IDP_COOKIE_DOMAIN
|
|
22
|
+
from .exceptions import (
|
|
23
|
+
YorkshireWaterAPIError,
|
|
24
|
+
YorkshireWaterAuthError,
|
|
25
|
+
YorkshireWaterError,
|
|
26
|
+
YorkshireWaterMeterNotReadyError,
|
|
27
|
+
YorkshireWaterRateLimitError,
|
|
28
|
+
)
|
|
29
|
+
from .models import (
|
|
30
|
+
Address,
|
|
31
|
+
ContinuousFlowAlarm,
|
|
32
|
+
CurrentConsumption,
|
|
33
|
+
Customer,
|
|
34
|
+
DailyConsumptionPoint,
|
|
35
|
+
MeterDetails,
|
|
36
|
+
MeterStatus,
|
|
37
|
+
PropertiesPage,
|
|
38
|
+
Property,
|
|
39
|
+
TokenSet,
|
|
40
|
+
UsagePeriod,
|
|
41
|
+
YearlyConsumptionPoint,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__version__ = "0.4.0"
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"IDP_COOKIE_DOMAIN",
|
|
48
|
+
"Address",
|
|
49
|
+
"Authenticator",
|
|
50
|
+
"ContinuousFlowAlarm",
|
|
51
|
+
"CookieSessionExpiredError",
|
|
52
|
+
"CurrentConsumption",
|
|
53
|
+
"Customer",
|
|
54
|
+
"DailyConsumptionPoint",
|
|
55
|
+
"MeterDetails",
|
|
56
|
+
"MeterStatus",
|
|
57
|
+
"PropertiesPage",
|
|
58
|
+
"Property",
|
|
59
|
+
"TokenSet",
|
|
60
|
+
"UsagePeriod",
|
|
61
|
+
"YearlyConsumptionPoint",
|
|
62
|
+
"YorkshireWaterAPIError",
|
|
63
|
+
"YorkshireWaterAuthError",
|
|
64
|
+
"YorkshireWaterClient",
|
|
65
|
+
"YorkshireWaterError",
|
|
66
|
+
"YorkshireWaterMeterNotReadyError",
|
|
67
|
+
"YorkshireWaterRateLimitError",
|
|
68
|
+
"__version__",
|
|
69
|
+
]
|