Skip to content

Software Design Document

acdcnow edited this page Sep 21, 2026 · 3 revisions

Software Design Document (SDD)

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)

1. Module inventory

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).


2. Structural view

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
Loading

3. Interface contracts

3.1 SmartmeterClient (abstract base class)

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 [].

3.2 Contract: zaehlpunkte()

[
  {
    "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.

3.3 Contract: historical_data()

[
  {
    "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.

3.4 Contract: consumptions()

[
  {
    "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.


4. Component design

4.1 __init__.py — entry lifecycle

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 unknown for a trivial reason.
  • async_config_entry_first_refresh() converts ConfigEntryAuthFailed / ConfigEntryNotReady into 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.

4.2 config_flow.py

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) returns AustriaSmartMeterOptionsFlow() without arguments. OptionsFlow in Home Assistant 2026.9 has no __init__ that accepts a config entry; self.config_entry is 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.

4.3 coordinator.py

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).

4.4 sensor.py

async_setup_entry reads entry.runtime_data, iterates coordinator.data and builds one entity list per metering point:

  1. one AustriaSmartMeterSensor per entry in readings,
  2. AustriaSmartMeterDiagnostic for each recognised info key (zaehlpunktnummer, geschaeftspartner, isSmartMeterMarketReady, isActive, smartMeterType, showConsumption, anlage.typ, and every populated verbrauchsstelle address field including a composed full address),
  3. AustriaSmartMeterStatistic for 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_INCREASING

Value 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

4.5 api/ — adapters

Factory

def get_client(provider, username, password) -> SmartmeterClient:
    if provider == PROVIDER_NETZ_NOE:
        return NetzNoeClient(username, password)
    return WienerNetzeClient(username, password)   # default

WienerNetzeClient — 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.

4.6 diagnostics.py

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
}

5. Dynamic behaviour

5.1 Initial setup

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
Loading

5.2 Update cycle

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
Loading

5.3 Netz NÖ login and metering point resolution

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
Loading

5.4 Options change

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
Loading

6. Error handling matrix

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

7. Localisation

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.


8. Build, verification and release

8.1 Manual verification harness

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:

  1. injects minimal homeassistant.* modules into sys.modules,
  2. imports custom_components.asm from the working tree,
  3. drives the adapters through a fake HTTP session with a URL → response routing table,
  4. 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.

8.2 Brand assets

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.

8.3 Release checklist

  1. Bump manifest.json version and the tag name to the same value (HACS compares tag against manifest version). This repository uses tags without a v prefix.
  2. Keep hacs.json homeassistant in sync with the minimum supported Core version.
  3. Push the branch, then create an annotated tag and push it.
  4. Publish the GitHub release, marked as pre-release for a beta.
  5. Verify delivery by downloading …/archive/refs/tags/<tag>.tar.gz and confirming brand/ is present in the archive.

9. Coding conventions

  • from __future__ import annotations in every module; modern X | None unions.
  • PEP 695 type aliases 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.

10. Traceability

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

11. References

Austria Smartmeter

🏠 Documentation home


🟢 Current — 1.2.x (branch HA2026_09_dev, HA 2026.9+)


🗄️ Archived — 1.1.8 (branch main)


🔗 Links

Clone this wiki locally