Repository navigation
Software Design Document
Austria Smartmeter integration for Home Assistant · domain
asm
| Field | Value |
|---|---|
| Document | Software Design Document (SDD) |
| Status | Draft / in review |
| Applies to | Integration 1.2.x |
| Branch | HA2026_09_dev |
| Commit | bd6c124 |
| Release |
1.2.0-beta.1 (pre-release) |
| Target platform | Home Assistant 2026.9+ |
| Companion document | Architecture Design Document |
| Supersedes | Design Documentation 1.1.8 (archived) · provider guide 1.1.8 (archived) |
| Module | Lines | Kind | Responsibility | Key symbols |
|---|---|---|---|---|
__init__.py |
30 | Entry point | Create/publish the coordinator, forward platforms, unload, reload on options change. |
PLATFORMS, async_setup_entry, async_unload_entry, _async_update_listener
|
const.py |
50 | Data | Domain, provider registry, option keys/defaults, OBIS name table. Re-exports CONF_USERNAME/CONF_PASSWORD from homeassistant.const. |
DOMAIN, PROVIDERS, DEFAULT_SCAN_INTERVAL, MIN_SCAN_INTERVAL, OBIS_NAMES
|
config_flow.py |
132 | UI flow | Provider selection, credential validation, entry creation, options (scan interval). |
AustriaSmartMeterConfigFlow, AustriaSmartMeterOptionsFlow
|
coordinator.py |
112 | Orchestration | Login lifecycle, data fetch, normalisation, exception mapping. |
AustriaSmartMeterCoordinator, _scan_interval, _match_stats
|
sensor.py |
307 | Presentation | Build entities/devices from the coordinator blob; derive unit, device class and state class. |
AustriaSmartMeterSensor, AustriaSmartMeterDiagnostic, AustriaSmartMeterStatistic, _state_class_for, _as_float
|
diagnostics.py |
23 | Operations | Redacted support dump. |
async_get_config_entry_diagnostics, TO_REDACT
|
api/base.py |
34 | Contract | Abstract client interface. | SmartmeterClient |
api/client.py |
16 | Factory | Provider id → implementation; re-exports the error types. | get_client |
api/errors.py |
13 | Contract | Error taxonomy. |
SmartmeterError, SmartmeterConnectionError, SmartmeterLoginError, SmartmeterQueryError
|
api/client_wn.py |
185 | Adapter | Wiener Netze: OIDC/PKCE login, B2B/B2C keys, METER_READ readings. |
WienerNetzeClient, _shift_years
|
api/client_noe.py |
269 | Adapter | Netz NÖ: cookie login, metering points (modern + legacy), daily consumption. |
NetzNoeClient, _decode_json, _normalise_metering_point, _sum_consumption
|
api/constants.py |
41 | Data | Wiener Netze URLs, valid OBIS codes, value type enums. |
API_URL, API_URL_B2B, AUTH_URL, VALID_OBIS_CODES, ValueType
|
manifest.json |
16 | Metadata | Domain, name, integration_type, iot_class, requirements, version. |
— |
strings.json, translations/*.json
|
— | i18n | Flow titles, field labels, error and abort messages (de/en/es/fr/it). | — |
brand/*.png |
— | Assets | Icon and logo, light and dark, 1× and 2×. | — |
Total integration code: ~1 170 lines of Python (excluding translations and assets).
classDiagram
class Entity {
<<Home Assistant>>
}
class SensorEntity {
<<Home Assistant>>
}
class CoordinatorEntity {
<<Home Assistant>>
}
class DataUpdateCoordinator {
<<Home Assistant>>
+data
+last_update_success
+async_config_entry_first_refresh()
#_async_update_data()
}
class AustriaSmartMeterCoordinator {
+str provider
+SmartmeterClient client
#_async_update_data() dict
#_async_consumption_stats() list
}
class SmartmeterClient {
<<abstract>>
+login()
+is_logged_in() bool
+is_login_expired() bool
+zaehlpunkte() list
+consumptions() list
+historical_data(zp, from, until) list
}
class WienerNetzeClient
class NetzNoeClient
class AustriaSmartMeterSensor
class AustriaSmartMeterStatistic
class AustriaSmartMeterDiagnostic
SensorEntity <|-- AustriaSmartMeterSensor
SensorEntity <|-- AustriaSmartMeterStatistic
SensorEntity <|-- AustriaSmartMeterDiagnostic
CoordinatorEntity <|-- AustriaSmartMeterSensor
CoordinatorEntity <|-- AustriaSmartMeterStatistic
CoordinatorEntity <|-- AustriaSmartMeterDiagnostic
DataUpdateCoordinator <|-- AustriaSmartMeterCoordinator
SmartmeterClient <|-- WienerNetzeClient
SmartmeterClient <|-- NetzNoeClient
AustriaSmartMeterCoordinator *-- SmartmeterClient
CoordinatorEntity --> AustriaSmartMeterCoordinator : entry.runtime_data
| Method | Signature | Returns / raises |
|---|---|---|
login |
login() |
self. Raises SmartmeterLoginError (credentials) or SmartmeterConnectionError (portal). |
is_logged_in |
is_logged_in() -> bool |
True while the session is usable. |
is_login_expired |
is_login_expired() -> bool |
True when a new login is required. |
zaehlpunkte |
zaehlpunkte() -> list[dict] |
List of contracts (see 3.2). Raises SmartmeterQueryError when the account has no usable metering point. |
consumptions |
consumptions() -> list[dict] |
Daily statistics (see 3.4). May return [] (provider has none). |
historical_data |
historical_data(zaehlpunktnummer, date_from=None, date_until=None) -> list[dict] |
OBIS readings (see 3.3). May return []. |
[
{
"geschaeftspartner": "123456", # optional, customer id
"zaehlpunkte": [
{
"zaehlpunktnummer": "AT002...", # REQUIRED, unique
"zaehlpunktName": "Home", # optional, device + entity name
"zaehlpunktAnlagentyp": "IME", # optional, becomes the device model
"geraetNumber": "12345", # optional, device serial number
"isSmartMeterMarketReady": True, # optional, diagnostic entity
"isActive": True, # optional, diagnostic entity
"smartMeterType": "IME", # optional, diagnostic entity (NOE)
"showConsumption": True, # optional, diagnostic entity (NOE)
"verbrauchsstelle": { # optional, address diagnostics
"strasse": "Main St", "hausnummer": "1",
"postleitzahl": "1010", "ort": "Vienna"
}
}
]
}
]Only zaehlpunktnummer is mandatory. Every additional key is rendered as a diagnostic
entity or as a device attribute when recognised; unknown keys are still surfaced through
extra_state_attributes.
Implementation note: the dict is stored as-is in the coordinator blob and also copied into
extra_state_attributes, so adapters must not put secrets in it.
[
{
"obisCode": "1-1:1.8.0", # REQUIRED
"wertetyp": "METER_READ", # optional: DAY | CONSUMPTION | QUARTER_HOUR => state_class total
"name": "Daily Consumption", # optional: overrides the OBIS_NAMES label for the entity name
"einheit": "Wh", # informational, echoed into attributes
"messwerte": [ # time series, MAY be empty
{"messwert": 123456.0, "zeitpunkt": "2026-09-17T00:00:00", "status": "VALID"}
]
}
]Value key tolerance. native_value reads the first present key of
messwert, value, amount.
Timestamp key tolerance. _get_latest_reading() accepts any of
zeitBis, zeitVon, zeitpunkt, date, timestamp, readAt and sorts the entries
lexicographically (all documented formats are ISO-8601 and therefore sort correctly).
Unit rule. The adapter must deliver Wh. sensor.py declares
UnitOfEnergy.WATT_HOUR for every energy reading and never converts.
[
{
"zaehlpunktnummer": "AT002...", # optional; omitted when the account has one meter
"consumptionYesterday": {"value": 1500.0, "date": "2026-09-18", "validated": True},
"consumptionDayBeforeYesterday": {"value": 1400.0, "date": "2026-09-17", "validated": True}
}
]A single dict instead of a list is accepted and wrapped by the coordinator. Statistic
values are assumed to already be in Wh.
async_setup_entry(hass, entry):
coordinator = AustriaSmartMeterCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh() # raises ConfigEntry* on failure
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
async_unload_entry(hass, entry):
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
_async_update_listener(hass, entry):
await hass.config_entries.async_reload(entry.entry_id)
- The first refresh happens before the platform is set up, so entities are created
with data already present and never start in
unknownfor a trivial reason. -
async_config_entry_first_refresh()convertsConfigEntryAuthFailed/ConfigEntryNotReadyinto the correct Home Assistant retry behaviour for free. - The update listener makes an options change take effect by reloading the entry; there is therefore no need for the coordinator to read options again at runtime.
Config flow (VERSION = 1)
| Step | Behaviour |
|---|---|
user |
Provider dropdown from PROVIDERS. Stores the choice in self._provider and in self.context[CONF_PROVIDER] so it survives a restart of an in-flight flow, then advances to credentials. |
credentials |
Sets unique_id = f"{provider}_{username.lower()}" and calls _abort_if_unique_id_configured() before any network I/O. Then logs in and fetches the contracts in the executor. |
Error mapping:
| Situation | errors["base"] |
Translation |
|---|---|---|
SmartmeterLoginError |
invalid_auth |
"Authentication failed…" |
SmartmeterError (connection/query) |
cannot_connect |
"Connection failed…" |
| Any other exception | unknown |
"Unexpected error." |
| Login succeeded, zero contracts | no_contracts |
"No metering points (contracts) found…" |
The created entry holds {username, password, provider}; the title is
"<Provider display name> (<username>)".
Options flow
class AustriaSmartMeterOptionsFlow(OptionsFlow):
async def async_step_init(self, user_input=None):
if user_input is not None:
return self.async_create_entry(data=user_input)
entry = self.config_entry # read-only property, available after init
current = entry.options.get(CONF_SCAN_INTERVAL) or entry.data.get(...) or DEFAULT
current = max(int(current), MIN_SCAN_INTERVAL)
return self.async_show_form("init", vol.Schema({vol.Optional(CONF_SCAN_INTERVAL, default=current): cv.positive_int}))-
async_get_options_flow(config_entry)returnsAustriaSmartMeterOptionsFlow()without arguments.OptionsFlowin Home Assistant 2026.9 has no__init__that accepts a config entry;self.config_entryis a read-only property that raises if accessed before the flow is initialised. - The interval is clamped to
MIN_SCAN_INTERVAL(60 min) on the way in.
class AustriaSmartMeterCoordinator(DataUpdateCoordinator[dict[str, Any]]):
def __init__(self, hass, entry):
self.provider = entry.data.get(CONF_PROVIDER, PROVIDER_WIENER_NETZE)
self.client = get_client(self.provider, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=timedelta(minutes=_scan_interval(entry)))self.provider is the single source of truth for provider-dependent presentation
(device manufacturer, configuration URL) and replaces the previous
"WienerNetzeClient" in type(client).__name__ string test.
Data blob produced by _async_update_data()
{
"<zaehlpunktnummer>": {
"info": { "...adapter payload as delivered..." },
"readings": [ { "obisCode": "...", "messwerte": [ ... ] } ],
"stats": { "consumptionYesterday": { "value": 1500, "date": "..." } }
}
}The keys are exactly the four contract shapes from §3 — the coordinator itself is provider-agnostic.
Helpers
| Helper | Purpose |
|---|---|
_scan_interval(entry) |
options → data → default; invalid values fall back; result clamped to MIN_SCAN_INTERVAL. |
_match_stats(stats, zp, contracts, meters) |
Assigns a statistics record to a meter by zaehlpunktnummer/zaehlpunkt; if the API omits the id and the entry has exactly one contract with exactly one meter, it is assigned implicitly (the API does this in practice). |
_async_consumption_stats() |
Fetches and type-sanitises consumptions(); a SmartmeterError degrades to [] instead of failing the whole cycle. |
Exception mapping
| Caught | Raised | Home Assistant behaviour |
|---|---|---|
SmartmeterLoginError |
ConfigEntryAuthFailed |
Triggers re-authentication, stops the retry loop. |
SmartmeterError |
UpdateFailed("Error communicating with the portal: …") |
Retried at the next interval, entities become unavailable. |
Exception |
UpdateFailed("Unexpected error: …") |
Same, with LOGGER.exception for the traceback. |
A failure while fetching one meter's readings is logged as a warning and the cycle continues for the remaining meters (graceful degradation, NFR-3).
async_setup_entry reads entry.runtime_data, iterates coordinator.data and builds
one entity list per metering point:
- one
AustriaSmartMeterSensorper entry inreadings, -
AustriaSmartMeterDiagnosticfor each recognisedinfokey (zaehlpunktnummer,geschaeftspartner,isSmartMeterMarketReady,isActive,smartMeterType,showConsumption,anlage.typ, and every populatedverbrauchsstelleaddress field including a composed full address), -
AustriaSmartMeterStatisticfor each present statistics field.
Device mapping. Every entity of a metering point shares one DeviceInfo:
identifiers={(DOMAIN, zaehlpunkt)}, name = portal friendly name (falling back to the
meter id), manufacturer and configuration_url from _PROVIDER_PORTALS, model from
zaehlpunktAnlagentyp, serial_number from geraetNumber, hw_version from
equipmentNumber.
State class rule (_state_class_for)
value_type = str(obis_data.get("wertetyp") or "").upper()
return SensorStateClass.TOTAL if value_type in {"DAY", "CONSUMPTION", "QUARTER_HOUR"} \
else SensorStateClass.TOTAL_INCREASINGValue safety. _as_float() returns None for None, bool and anything that is not
numerically convertible, so a malformed portal value produces unknown rather than a
ValueError inside the state machine. Note that 0 is returned as 0.0 — the lookup
uses explicit is not None checks rather than truthiness.
Entity inventory
| Class | Category | Device class | State class | Unit |
|---|---|---|---|---|
AustriaSmartMeterSensor |
— | energy |
derived (see above) | Wh |
AustriaSmartMeterDiagnostic |
diagnostic |
— | — | — |
AustriaSmartMeterStatistic |
— | energy |
total |
Wh |
Factory
def get_client(provider, username, password) -> SmartmeterClient:
if provider == PROVIDER_NETZ_NOE:
return NetzNoeClient(username, password)
return WienerNetzeClient(username, password) # defaultWienerNetzeClient — OpenID Connect / PKCE against log.wien, with three form posts
scraped through lxml XPath (//form/@action), an authorisation-code exchange, then a
config call that yields the B2C and B2B gateway keys. Readings come from the B2B gateway
(zaehlpunkte/{customerId}/{zp}/messwerte, wertetyp=METER_READ, default range: three
years back via _shift_years). _call_api converts requests failures and JSON decode
errors into SmartmeterConnectionError.
NetzNoeClient — cookie session, 30-minute modelled lifetime.
| Concern | Implementation |
|---|---|
| Login |
POST /orchestration/Authentication/Login with {"user", "pwd"}. |
| Login result |
401/403 → SmartmeterLoginError; other non-200 → SmartmeterConnectionError; an explicit {"success": false} → SmartmeterLoginError. |
| Non-JSON body |
_decode_json() raises SmartmeterConnectionError including the HTTP status and a response snippet — this replaced the opaque Expecting value: line 1 column 1 (char 0). |
| Metering points |
GET /User/GetMeteringPointsByBusinesspartnerId?context=5 first; on any SmartmeterError, empty result or unexpected shape, falls back to GetAccountIdByBussinespartnerId?context=1 + GetMeteringPointByAccountId?accountId=…&context=1. |
| Normalisation |
_normalise_metering_point() maps meteringPointId/meterId/countingPointId → zaehlpunktnummer, defaults the name to the meter id, maps smartMeterType → zaehlpunktAnlagentyp. |
| Readings |
GET /ConsumptionRecord/Day?meterId=…&day=YYYY-MM-DD (defaults to yesterday). _sum_consumption() sums meteredValues[] (current shape) and consumptionRecords[].value (legacy shape). |
| Unit | Portal reports kWh per interval → multiplied by 1000 and declared as Wh. |
| Semantics | Emitted with wertetyp="DAY" and obisCode="1-1:1.9.0" (energy within an interval), because the portal does not expose the cumulative reading 1.8.0. |
| Empty day | Returns [] so the entity reports unknown instead of a misleading 0. |
| Statistics |
consumptions() returns [] — the portal has no "yesterday" endpoint. |
Session and retry behaviour. _ensure_session() logs in whenever the session is
expired; zaehlpunkte() additionally retries once after a SmartmeterConnectionError
because a session can expire mid-update.
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async_get_config_entry_diagnostics(hass, entry) -> {
"entry": {"title", "data", "options"}, # username/password -> **REDACTED**
"provider", "last_update_success", "meters": coordinator.data
}sequenceDiagram
autonumber
actor U as User
participant CF as config_flow.py
participant CL as Provider client
participant P as Portal
participant INI as __init__.py
participant CO as coordinator.py
participant SE as sensor.py
U->>CF: Add integration, choose provider
CF->>U: Credentials form
U->>CF: username + password
CF->>CF: async_set_unique_id + _abort_if_unique_id_configured
CF->>CL: get_client(provider, user, pass)
CF->>CL: login() [executor]
CL->>P: POST login
P-->>CL: session / token
CF->>CL: zaehlpunkte() [executor]
CL->>P: GET metering points
P-->>CL: contracts
alt login failed
CF-->>U: invalid_auth
else unreachable / non-JSON / no account
CF-->>U: cannot_connect
else no contracts
CF-->>U: no_contracts
else success
CF->>INI: async_create_entry(provider, username, password)
INI->>CO: AustriaSmartMeterCoordinator(hass, entry)
INI->>CO: async_config_entry_first_refresh()
CO->>CL: login if needed, zaehlpunkte, consumptions, historical_data per meter
INI->>INI: entry.runtime_data = coordinator
INI->>SE: async_forward_entry_setups
SE->>SE: build entities + devices from coordinator.data
end
sequenceDiagram
autonumber
participant CO as coordinator.py
participant CL as Provider client
participant P as Portal
participant SE as sensor.py
CO->>CO: timer fires (scan interval, default 360 min)
alt not logged in or session expired
CO->>CL: login() [executor]
CL->>P: POST login
end
CO->>CL: zaehlpunkte() [executor]
CL->>P: GET contracts + metering points
P-->>CL: payload
CL-->>CO: normalised contracts
CO->>CL: consumptions() [executor]
alt provider has no statistics (Netz NOE)
CL-->>CO: []
else
CL->>P: GET statistics
CL-->>CO: normalised statistics
end
loop every metering point
CO->>CL: historical_data(zp) [executor]
CL->>P: GET readings
alt reading fetch fails
CO->>CO: LOGGER.warning, keep going
else
CL-->>CO: normalised readings
end
end
CO->>CO: _match_stats() to assign statistics per meter
CO-->>SE: data updated (CoordinatorEntity callback)
SE->>SE: native_value read from blob, entity state written
flowchart TD
start(["login()"]) --> post["POST /orchestration/Authentication/Login<br/>json {user, pwd}"]
post --> body{"body is JSON?"}
body -->|"no (HTML error page)"| cerr["SmartmeterConnectionError<br/>HTTP status + snippet"]
body -->|"yes"| st{"status code"}
st -->|"401 / 403"| lerr["SmartmeterLoginError<br/>invalid_auth"]
st -->|"other non-200"| cerr
st -->|"200"| succ{"success == false?"}
succ -->|"yes"| lerr
succ -->|"no"| ok["session established<br/>_login_time = now"]
ok --> zp(["zaehlpunkte()"])
zp --> modern["GET GetMeteringPointsByBusinesspartnerId?context=5"]
modern --> good{"list with items?"}
good -->|"yes"| norm["_normalise_metering_point()"]
good -->|"no / error / empty"| legacy["GET GetAccountIdByBussinespartnerId?context=1<br/>then GetMeteringPointByAccountId?accountId=..&context=1"]
legacy --> norm
norm --> empty{"any metering point?"}
empty -->|"no"| qerr["SmartmeterQueryError"]
empty -->|"yes"| out["[{'zaehlpunkte': [...]}]"]
classDef fail fill:#ffe4e6,stroke:#e11d48,color:#881337
classDef okc fill:#dcfce7,stroke:#16a34a,color:#14532d
class cerr,lerr,qerr fail
class ok,out okc
sequenceDiagram
autonumber
actor U as User
participant OF as OptionsFlow
participant EN as Config entry
participant INI as __init__.py
participant CO as Coordinator
U->>OF: Configure, change scan interval
OF->>OF: clamp to >= MIN_SCAN_INTERVAL
OF->>EN: async_create_entry(data={scan_interval})
EN->>INI: update listener fires
INI->>EN: async_reload(entry_id)
EN->>CO: unload + async_setup_entry
CO->>CO: new update_interval applied
| Symptom | Detected in | Mapped to | User-visible result |
|---|---|---|---|
| Wrong username/password |
login() → non-200 or success=false
|
SmartmeterLoginError → ConfigEntryAuthFailed / invalid_auth
|
"Authentication failed…" / re-auth dialog |
| DNS/TLS/timeout |
_request() → RequestException
|
SmartmeterConnectionError |
"Connection failed…" |
| Endpoint renamed (HTML 404 page) | _decode_json() |
SmartmeterConnectionError with status + snippet |
"Connection failed…" instead of a JSON decode message |
| Session expired mid-update | first SmartmeterConnectionError in zaehlpunkte()
|
one automatic retry after re-login | Transparent |
| Portal authenticated but account has no meter | zaehlpunkte() |
SmartmeterQueryError |
"No metering points (contracts) found…" |
| Statistics endpoint missing | consumptions() |
logged at debug, []
|
No statistic entities, main sensors unaffected |
| One meter's reading fails |
_async_update_data() per-meter block |
LOGGER.warning |
That sensor is unknown, other meters update |
| Value is not a number | _as_float() |
None |
Entity is unknown, no exception |
strings.json is the source of truth; translations/{de,en,es,fr,it}.json mirror it.
The flows define the steps user and credentials (with a {provider_name}
placeholder) and the options step init, plus the error keys cannot_connect,
invalid_auth, no_contracts, unknown and the abort key already_configured.
The verification harness asserts that all five translations have exactly the same
step and error key sets as strings.json.
Because Home Assistant 2026.9 needs Python 3.14.2 and is not installed in the development environment, the integration is verified with a stub harness that:
- injects minimal
homeassistant.*modules intosys.modules, - imports
custom_components.asmfrom the working tree, - drives the adapters through a fake HTTP session with a URL → response routing table,
- asserts on behaviour.
Covered: consumption summing (current + legacy shape), metering point normalisation,
login success/failure/HTML/401, legacy fallback, empty-account error, kWh→Wh conversion,
yesterday default, session expiry, scan interval clamping, statistics matching (direct and
implicit), config flow provider persistence, options flow construction and clamping,
_as_float edge cases, state class derivation, entity naming/unique ids/device info,
zero-value handling, manifest/hacs/strings/translation consistency, brand image
dimensions and documentation encoding.
Result at commit bd6c124: 127 assertions, all passing.
Run it with the local interpreter and the harness directory on sys.path; it exits
non-zero on the first failure category.
custom_components/asm/brand/ is generated by a Pillow script that draws at 4× and
downscales with LANCZOS, then verified programmatically for exact dimensions
(icon.png 256², icon@2x.png 512², logo.png 640×160, logo@2x.png 1280×320 and the
two dark_logo variants), alpha channel and non-empty bounding box.
- Bump
manifest.jsonversionand the tag name to the same value (HACS compares tag against manifest version). This repository uses tags without avprefix. - Keep
hacs.jsonhomeassistantin sync with the minimum supported Core version. - Push the branch, then create an annotated tag and push it.
- Publish the GitHub release, marked as pre-release for a beta.
- Verify delivery by downloading
…/archive/refs/tags/<tag>.tar.gzand confirmingbrand/is present in the archive.
-
from __future__ import annotationsin every module; modernX | Noneunions. - PEP 695
typealiases are avoided so the tree still parses on an older interpreter used for local compile checks. - Lazy
%-style logging (LOGGER.warning("…%s", value)), never f-strings in log calls. - No provider conditionals in the core — they belong in the adapter (AD-2).
- Adapters never import Home Assistant; they are plain Python and therefore testable without the framework.
- Public naming stays German where it mirrors the portal vocabulary
(
zaehlpunkt,zaehlpunktnummer,geschaeftspartner) to keep the mapping obvious.
| Requirement (ADD §2) | Implementation |
|---|---|
| FR-1 |
config_flow.py → async_step_user / async_step_credentials
|
| FR-2 |
coordinator.zaehlpunkte() loop; sensor.async_setup_entry per metering point |
| FR-3 |
_scan_interval() + AustriaSmartMeterOptionsFlow
|
| FR-4 |
AustriaSmartMeterSensor, AustriaSmartMeterStatistic
|
| FR-5 |
AustriaSmartMeterDiagnostic, _get_shared_device_info
|
| FR-6 |
SmartmeterLoginError → ConfigEntryAuthFailed
|
| FR-7 | diagnostics.py |
| NFR-1 | all portal calls via hass.async_add_executor_job
|
| NFR-2 | adapters convert to Wh; sensor.py declares UnitOfEnergy.WATT_HOUR
|
| NFR-3 | optional statistics, per-meter try/except, _as_float
|
| NFR-4 |
manifest.json requirements = ["lxml"]
|
| NFR-5 |
entry.runtime_data, ConfigFlowResult, read-only config_entry
|
🟢 Current — 1.2.x
(branch HA2026_09_dev, HA 2026.9+)
🗄️ Archived — 1.1.8
(branch main)
🔗 Links