Skip to content

feat: add official OAuth2 API support alongside legacy scraper#356

Open
KrOnAsK wants to merge 1 commit into
DarwinsBuddy:mainfrom
KrOnAsK:main
Open

feat: add official OAuth2 API support alongside legacy scraper#356
KrOnAsK wants to merge 1 commit into
DarwinsBuddy:mainfrom
KrOnAsK:main

Conversation

@KrOnAsK

@KrOnAsK KrOnAsK commented Jun 20, 2026

Copy link
Copy Markdown

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:

  • 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 pattern to return appropriate client based on config

Modified Files:

  • config_flow.py - Add OAuth2 credential fields and method selection UI
  • AsyncSmartmeter.py - Support both client implementations transparently
  • const.py - New config constants for OAuth2 parameters
  • api/constants.py - API endpoints, token URLs, 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

Test Results

✅ All tests passing: 47/47

Backward Compatibility

Existing legacy method configs continue to work without any changes. This is purely additive functionality.

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 ✓
Copilot AI review requested due to automatic review settings June 20, 2026 17:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 OfficialSmartmeter client, plus an adapter and a client_factory shim 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/"
},
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants