feat: add official OAuth2 API support alongside legacy scraper#356
Open
KrOnAsK wants to merge 1 commit into
Open
feat: add official OAuth2 API support alongside legacy scraper#356KrOnAsK wants to merge 1 commit into
KrOnAsK wants to merge 1 commit into
Conversation
Adds support for the official WN_SMART_METER_API (OAuth2) as an alternative to the legacy username/password HTML-scraping method. Users can now choose their preferred authentication method in the config flow. New components: - official_client.py: Client for the public OAuth2-secured Wiener Netze API - adapter.py: Translates official API responses to legacy method shape - client_factory.py: Factory that returns appropriate client based on config Changes: - config_flow.py: Add OAuth2 credential fields and method selection - AsyncSmartmeter.py: Support both client implementations transparently - const.py: New config constants for OAuth2 parameters - api/constants.py: API endpoints, tokens, and type definitions - sensor.py: Support official API configuration - translations/en.json: New UI labels for OAuth2 fields Tests: - test_official_client.py: 47 comprehensive tests covering: - OAuth2 authentication and token refresh - API endpoint calls and response parsing - Data adaptation from official to legacy format - Edge cases and error handling Backward compatible: existing legacy method configs continue to work. All tests passing: 47/47 ✓
There was a problem hiding this comment.
Pull request overview
This PR introduces support for the official Wiener Netze WN_SMART_METER_API (OAuth2 client_credentials + x-Gateway-APIKey) alongside the existing legacy username/password scraper, and wires the integration to select the appropriate backend during config + runtime.
Changes:
- Added an
OfficialSmartmeterclient, plus anadapterand aclient_factoryshim to preserve the legacy client surface for sensors/importer. - Extended the config flow and translations to let users pick Official API vs Legacy authentication and enter OAuth2/API-key credentials.
- Added a large unit-test suite covering token handling, endpoint calls, adaptation, and the shim/factory behavior.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
custom_components/wnsm/api/official_client.py |
New synchronous OAuth2-based client for the official API (token caching, request plumbing, endpoints). |
custom_components/wnsm/api/adapter.py |
Translates official API payloads into the existing internal/legacy data shapes expected by sensors/importer. |
custom_components/wnsm/api/client_factory.py |
Factory + shim to return either legacy scraper or official client adapter based on config entry. |
custom_components/wnsm/config_flow.py |
Adds auth-method selection and OAuth2 credential collection/validation branch. |
custom_components/wnsm/wnsm_sensor.py |
Switches sensor to instantiate a client via make_client(entry_data) instead of raw username/password. |
custom_components/wnsm/sensor.py |
Passes the full entry payload to WNSMSensor; YAML path explicitly forces legacy auth. |
custom_components/wnsm/AsyncSmartmeter.py |
Broadens wrapper typing/documentation to accept both legacy client and shim. |
custom_components/wnsm/const.py |
Adds config keys/constants for auth-method selection and official API credentials. |
custom_components/wnsm/api/constants.py |
Adds official API URLs/constants and Wertetyp enum for official endpoints. |
custom_components/wnsm/api/__init__.py |
Exposes OfficialSmartmeter alongside legacy Smartmeter. |
custom_components/wnsm/translations/en.json |
New UI text for auth selection + official credential fields; improved auth error message. |
tests/test_official_client.py |
Comprehensive unit tests for the official client, adapter, and shim/factory. |
Export_WN_SMART_METER_API.json |
Adds the OpenAPI export for the official API (including embedded license metadata). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+20
to
+23
| import importlib.util | ||
| import os | ||
| import sys | ||
| import types |
Comment on lines
+689
to
+693
| from wnsm.api import client_factory # noqa: E402 | ||
| from wnsm.api.client_factory import ( # noqa: E402 | ||
| _OfficialSmartmeterShim, | ||
| make_client, | ||
| ) |
Comment on lines
+181
to
+187
| wertetyp = self._coerce_wertetyp(valuetype) | ||
| raw = self.client.messwerte( | ||
| datum_von=date_from, | ||
| datum_bis=date_until, | ||
| wertetyp=wertetyp, | ||
| zaehlpunkt=zaehlpunktnummer, | ||
| ) |
Comment on lines
+219
to
+225
| wertetyp = self._coerce_wertetyp(valuetype) | ||
| raw = self.client.messwerte( | ||
| datum_von=date_from, | ||
| datum_bis=date_until, | ||
| wertetyp=wertetyp, | ||
| zaehlpunkt=zaehlpunktnummer, | ||
| ) |
Comment on lines
+32
to
38
| def __init__(self, entry_data: Dict[str, Any], zaehlpunkt: str) -> None: | ||
| super().__init__() | ||
| self.username = username | ||
| self.password = password | ||
| # Store the full HA entry payload so ``make_client`` can pick the | ||
| # right backend (legacy scraper vs. OAuth2 official API) on every | ||
| # update without the sensor having to care which one is active. | ||
| self._entry_data = entry_data | ||
| self.zaehlpunkt = zaehlpunkt |
Comment on lines
+107
to
+113
| return self.async_show_menu( | ||
| step_id="user", | ||
| menu_options={ | ||
| AUTH_METHOD_OFFICIAL: "Official API (OAuth2)", | ||
| AUTH_METHOD_LEGACY: "Username & Password (legacy)", | ||
| }, | ||
| ) |
Comment on lines
+22
to
+24
| from datetime import date, datetime | ||
| from typing import Any, Dict, Iterable, List, Optional, Union | ||
| from urllib.parse import urljoin |
Comment on lines
+76
to
+78
| scope: | ||
| OAuth2 scope to request. Defaults to ``profile`` which is the scope | ||
| shown in the Wiener Netze portal. |
Comment on lines
+53
to
+56
| expires_at: float #: monotonic epoch seconds at which the token expires | ||
|
|
||
| def is_valid(self, leeway: int = const.OFFICIAL_TOKEN_REFRESH_LEEWAY_SECONDS) -> bool: | ||
| return bool(self.access_token) and (time.time() + leeway) < self.expires_at |
Comment on lines
+11
to
+14
| "license" : { | ||
| "name" : "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)", | ||
| "url" : "https://creativecommons.org/licenses/by-nc-nd/4.0/" | ||
| }, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for the official WN_SMART_METER_API (OAuth2) as an alternative to the legacy username/password HTML-scraping method. Users can now choose their preferred authentication method in the config flow.
Key Changes
New Components:
Modified Files:
Tests:
Test Results
✅ All tests passing: 47/47
Backward Compatibility
Existing legacy method configs continue to work without any changes. This is purely additive functionality.