Metadata-Version: 2.5
Name: bayernwerk-client
Version: 0.0.5
Summary: Inoffizieller Python-Client fuer das Bayernwerk Netz Mein.Auftragsportal (MAP)
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: argcomplete>=3.7.2
Requires-Dist: httpx>=0.27
Requires-Dist: playwright>=1.45
Requires-Dist: python-dotenv>=1.2.2
Description-Content-Type: text/markdown

# bayernwerk-client

[![Release](https://github.com/the78mole/bayernwerk-client/actions/workflows/release.yml/badge.svg)](https://github.com/the78mole/bayernwerk-client/actions/workflows/release.yml)
[![PyPI](https://img.shields.io/pypi/v/bayernwerk-client?label=bayernwerk-client)](https://pypi.org/project/bayernwerk-client/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

Inoffizielle Python-Clients für Bayernwerk-Onlinedienste - reverse-engineered,
nicht von Bayernwerk/E.ON unterstützt oder autorisiert. Nutzung auf eigene
Verantwortung und nur mit einem eigenen, berechtigten Account.

Das Package ist nach Service aufgeteilt:

- `bayernwerk_client.map` - **Mein.Auftragsportal** (`meinauftragsportal.html`
  / REST-API `icon-api.eon.com`)
- `bayernwerk_client.efix` - **e-fix Installateur-Portal**
  (`bayernwerk.e-fix.info` / GraphQL-API `backend.e-fix.info`), gleiche
  Zugangsdaten wie MAP

Beide sitzen hinter demselben Salesforce-Aura-Login (nur unterschiedliche
Communities: `account.bayernwerk-netz.de` bzw. `login.e-fix.info`), daher
gilt der folgende Abschnitt für beide.

## Warum ein Browser fürs Login?

`bayernwerk-netz.de` sitzt hinter einer Cloudflare-JS-Challenge, und die
Login-Seite selbst ist keine einfache HTML-Form, sondern eine Salesforce
Experience-Cloud-App (Aura-Framework mit signierten, sessionbehafteten
Requests). Beides lässt sich nicht robust mit reinem `requests`/`httpx`
nachbauen. Deshalb übernimmt [Playwright](https://playwright.dev/python/)
einmalig den Login (Cloudflare-Challenge lösen + Formular ausfüllen) und
liefert die Tokens. **Alle** eigentlichen API-Calls danach laufen über
reines `httpx` - kein Browser nötig, solange der Token gültig ist.

Playwright ist deshalb eine normale (Kern-)Abhängigkeit, keine optionale
Extra - ohne funktionierenden Login ist der Client kaum zu gebrauchen,
also lohnt sich der Split nicht. Nötig ist trotzdem ein zusätzlicher
Schritt, um die Browser-Binary selbst zu installieren (siehe unten).

**Token-Lebensdauer:** Access-Tokens gelten ca. 1 Stunde. Bei MAP wird
zwar ein `refresh_token` mitgeliefert, das ist aber ein
Salesforce-Community-Refresh-Token - er erneuert zwar erfolgreich gegen
`.../services/oauth2/token`, liefert dabei aber ein natives
Salesforce-Session-Token statt des JWTs, das `icon-api.eon.com` erwartet
(das JWT entsteht in einem separaten, nicht identifizierten "Funke
IAM"-Austauschschritt, den nur das SPA-JS selbst ausführt). e-fix liefert
gar keinen Refresh-Token. Praktisch bedeutet das für beide: bei Ablauf
einfach erneut `login_interactive` aufrufen (dauert nur wenige Sekunden)
statt zu versuchen, den Token still zu erneuern - siehe `on_token_expired`
im Beispiel unten.

## Installation

Als CLI-Tool (empfohlen):

```bash
uv tool install bayernwerk-client
playwright install chromium
```

Als Abhängigkeit in einem eigenen Python-Projekt (Library-Nutzung, siehe
unten):

```bash
uv add bayernwerk-client
```

## Zugangsdaten

`map` und `efix` nutzen denselben Bayernwerk-Netz-Account (dieselbe
E-Mail/Passwort-Kombination aus dem Mein.Auftragsportal). Der `login`-Befehl
jedes Services (und der automatische Re-Login bei abgelaufenem Token) liest
die Zugangsdaten aus zwei Umgebungsvariablen:

```bash
export MAP_EMAIL="deine@email.de"
export MAP_PASSWORD="dein-passwort"
```

Sind sie nicht gesetzt, fragt `bayernwerk map login` / `bayernwerk efix
login` interaktiv danach (E-Mail per `input()`, Passwort per `getpass` -
landet nicht in der Shell-History). Der **automatische** Re-Login bei
abgelaufenem Token funktioniert dagegen nur, wenn die Variablen gesetzt
sind - sonst bricht der jeweilige Befehl mit einer Fehlermeldung ab, die
zum erneuten manuellen `login` auffordert (siehe `on_token_expired` in den
`cli.py`-Modulen).

`bayernwerk` sucht die Zugangsdaten in dieser Reihenfolge (die erste
gefundene gewinnt, nichts wird überschrieben, was schon gesetzt ist):

1. Bereits exportierte Umgebungsvariablen.
2. Eine `.env`-Datei (siehe unten).
3. `~/.config/bayernwerk-client/credentials.toml` (siehe unten) - als
   dauerhafter, verzeichnisunabhängiger Default fürs eigene System.

**Aus einer `.env`-Datei laden**, statt die Zugangsdaten in jeder Shell neu
zu exportieren: `bayernwerk` lädt beim Start automatisch eine `.env` aus
dem aktuellen (oder einem übergeordneten) Verzeichnis, via
[`python-dotenv`](https://github.com/theskumar/python-dotenv). Bereits
gesetzte Umgebungsvariablen haben immer Vorrang vor der `.env`-Datei.

```bash
# .env (liegt bereits in .gitignore - nie committen!)
MAP_EMAIL=deine@email.de
MAP_PASSWORD=dein-passwort
```

```bash
bayernwerk map login   # liest MAP_EMAIL/MAP_PASSWORD automatisch aus .env
```

Kein manuelles `source` nötig. Wer lieber ein allgemeineres Tool dafür
nutzt: [`direnv`](https://direnv.net/) lädt eine `.envrc` automatisch
beim Betreten des Verzeichnisses (dann auch für andere Tools verfügbar,
nicht nur `bayernwerk`).

**Dauerhaft in `~/.config/bayernwerk-client/credentials.toml` hinterlegen**,
falls du nicht in jedem Arbeitsverzeichnis eine eigene `.env` pflegen
willst - praktisch z.B. nach `uv tool install`, wenn du `bayernwerk` von
überall aus aufrufst:

```toml
# ~/.config/bayernwerk-client/credentials.toml
MAP_EMAIL = "deine@email.de"
MAP_PASSWORD = "dein-passwort"
```

Flache `KEY = "value"`-Paare, ein beliebiger String-Wert wird als
Umgebungsvariable gesetzt (aktuell nur `MAP_EMAIL`/`MAP_PASSWORD`
relevant). Wie bei `.env`: Datei enthält ein Klartext-Passwort, also
`chmod 600 ~/.config/bayernwerk-client/credentials.toml`.

Die gecachten *Tokens* (nicht die Zugangsdaten selbst) landen danach lokal
unter `~/.cache/bayernwerk-client/map-tokens.json` bzw.
`.../efix-tokens.json`, mit restriktiven Dateirechten (`chmod 600`).

## CLI

`bayernwerk` ist der Einstiegspunkt, `map`/`efix` sind die Subcommands je
Service - jeder cacht seinen Token getrennt (`map-tokens.json` /
`efix-tokens.json`), ein Login gilt nicht für den jeweils anderen Service.
`-j`/`--json` ist ein **globales** Flag *vor* dem Service
(`bayernwerk -j map orders`, nicht `bayernwerk map orders -j`) und schaltet
JSON- statt menschenlesbare Ausgabe für jeden beliebigen Befehl ein.
`bayernwerk --help`, `bayernwerk map --help` bzw. `bayernwerk efix --help`
zeigen die Befehle direkt im Terminal inkl. aller Argumente.

### `bayernwerk map` - Mein.Auftragsportal

| Befehl | Beschreibung |
|---|---|
| `login [--headless]` | Einmalig einloggen (sichtbarer Browser, außer mit `--headless`) und Token cachen. Nötig bevor irgendein anderer `map`-Befehl funktioniert. |
| `orders [-s] [-f\|-o]` | Alle Aufträge auflisten. Menschenlesbar als ein Absatz pro Auftrag mit Kopfzeile (Status, Datum, Auftragsnummer, Kunde, Produkt, Adresse - wie im Portal), Rest eingerückt darunter. `-s`/`--short` gibt nur die Kopfzeile aus, eine pro Zeile (wirkt nicht auf `-j`). `-f`/`--finished` bzw. `-o`/`--open` filtern auf abgeschlossene bzw. offene Aufträge (schließen sich gegenseitig aus, auch kombinierbar mit `-s`, z.B. `-fs`). |
| `order ORDER_ID` | Einen einzelnen Auftrag anzeigen (Rohdaten der `orders`-Liste, gefiltert auf eine Auftragsnummer). |
| `order-details ORDER_ID` | Zusätzliche Detaildaten zu einem Auftrag (separater Endpunkt als `order`). |
| `order-documents ORDER_ID` | Dokumente eines Auftrags auflisten (Dateiname, Typ, Scan-Status, ...) - zum Download siehe `sync-documents`. |
| `order-notes ORDER_ID` | Notizen zu einem Auftrag auflisten. |
| `order-instances ORDER_ID` | Der "Anschluss"-Baum eines Auftrags: Zähler → Verbraucher/Erzeuger → Wechselrichter → Speicher/PV, als eingerückter Baum (menschenlesbar) bzw. flache Liste (`-j`). |
| `sync-documents ORDER_ID FOLDER` | Dokumente eines Auftrags mit einem lokalen Ordner abgleichen: lädt nur die Dokumente herunter, die dort (per Dateiname) noch fehlen; vorhandene Dateien bleiben unangetastet. `FOLDER` wird bei Bedarf angelegt. |
| `installer` | Eigene Installateur-Stammdaten (Firma, Adresse, Kontakt) laut MAP-Backend. |
| `inverters --primary-energy-form FORM` | Wechselrichter-Stammdaten. `FORM` ist Pflicht (z.B. `PV` oder `AC_STORAGE`) und mehrfach angebbar. |
| `storages` | Speicher-Stammdaten (verfügbare Modelle/Kapazitäten). |
| `product-orders` | Produktaufträge auflisten (separat von `orders`). |

### `bayernwerk efix` - e-fix Installateur-Portal

| Befehl | Beschreibung |
|---|---|
| `login [--headless]` | Einmalig einloggen (eigener Token-Cache, unabhängig von `map`) und Token cachen. |
| `installer` | Eigene Installateur-Stammdaten laut e-fix-Backend (Firma, Adresse, Netzbetreiber-Zuordnung, Ausweis-Status, ...) - inhaltlich umfangreicher als `map installer`, da e-fix eigene Stammdaten pflegt. |
| `antraege` | Eigene Anträge ("Antraege") mit Status, Typ/Subtyp und Eingangsdatum. |
| `status` | Account-Status: Rolle, Benachrichtigungszähler, Abo-Status, Name/E-Mail. |
| `events` | Registrierte Veranstaltungen/Schulungen mit Terminen. |

### Shell-Completion

```bash
bayernwerk completion install
```

Erkennt die Shell automatisch (`$SHELL`, bash/zsh) und trägt eine
`eval`-Zeile in `~/.bashrc` bzw. `~/.zshrc` ein (idempotent - beim
zweiten Aufruf passiert nichts). Neue Shell starten oder die rc-Datei neu
sourcen, danach vervollständigt Tab sowohl Services (`map`/`efix`) als
auch alle Subcommands auf jeder Ebene, inklusive `--primary-energy-form`
& Co. Läuft über [`argcomplete`](https://github.com/kislyuk/argcomplete)
und wird direkt aus der tatsächlichen `argparse`-Struktur generiert -
kein händisch gepflegtes Completion-Skript, das veralten könnte.

Andere Shell oder manuelle Einrichtung: `bayernwerk completion bash`
(bzw. `zsh`/`fish`) gibt nur das Skript aus, zum selbst Einbinden. Für
Fish z.B. `bayernwerk completion fish > ~/.config/fish/completions/bayernwerk.fish`.

## Verwendung als Library

```python
from bayernwerk_client.map import MAP_TOKEN_PATH, MapClient, TokenStore
from bayernwerk_client.map.auth import login_interactive

store = TokenStore(MAP_TOKEN_PATH)
tokens = store.load()
if tokens is None or tokens.is_expired:
    tokens = login_interactive("email@example.com", "passwort", headless=False)
    store.save(tokens)

with MapClient(tokens, token_store=store) as client:
    for order in client.list_orders():
        print(order)
```

Siehe [`examples/list_orders.py`](examples/list_orders.py) für ein
vollständiges Beispiel inklusive automatischem Re-Login bei abgelaufenem
Token. `bayernwerk_client.efix` funktioniert analog (`EfixClient`,
`EFIX_TOKEN_PATH`, `bayernwerk_client.efix.auth.login_interactive`).

## API-Oberfläche (`bayernwerk_client.map`)

`MapClient` deckt die bekannten `icon-api.eon.com`-Endpunkte mit
Convenience-Methoden ab (`list_orders`, `get_order`, `get_order_details`,
`get_order_instances`, `get_order_documents`, `get_order_notes`,
`download_order_document`, `sync_order_documents`, `list_installers`,
`list_inverters`, `list_storages`, `list_product_orders`). Für alles
andere steht die generische `client.request(method, path, **httpx_kwargs)`
zur Verfügung.

### Anschluss-Baum (Zähler/Wallbox/Wärmepumpe/Wechselrichter/...)

`get_order_instances` liefert die Baumstruktur hinter dem "Anschluss"-Tab
im Portal (Zähler → Verbraucher/Erzeuger → Wechselrichter → Speicher/PV, ...).
Die Verschachtelung variiert je nach Ausstattung (`itemType`/`schemaType`)
und Bayernwerk kann jederzeit neue Gerätetypen ergänzen - ein starres
Datenmodell dafür würde ständig hinterherlaufen. `iter_instance_items`
läuft den Baum deshalb schemalos ab: alles mit einem `itemType`-Feld wird
als Knoten erkannt, egal unter welchem Schlüssel (`meters`, `actors`,
`inverterGroups`, `inverters`, ...) es hängt, und Duplikate (manche Akteure
tauchen im Rohformat zweimal auf - einmal direkt, einmal nochmal
verschachtelt unterm zugehörigen Wechselrichter) werden über `ivyId`
herausgefiltert.

```python
for item in client.iter_order_instance_items("2577481104"):
    print(item["itemType"], "/", item.get("schemaType"), "-", item.get("formData"))
```

`bayernwerk_client.map.iter_instance_items(tree)` ist die reine Funktion
dahinter, falls du bereits ein `get_order_instances`-Ergebnis vorliegen hast.

### Dokumente eines Auftrags mit einem lokalen Ordner abgleichen

```python
downloaded = client.sync_order_documents(
    "2577481104", "/pfad/zum/kundenordner"
)
print(f"{len(downloaded)} neue Dokument(e) heruntergeladen:", downloaded)
```

Vergleicht die Dokumentenliste des Auftrags mit den vorhandenen Dateien im
Ordner (per Dateiname) und lädt nur fehlende Dokumente herunter.
Bestehende Dateien werden nicht angerührt.

`tenant` (Default `BAG`) und `lang` (Default `de`) werden automatisch an
jeden Request angehängt - das Backend antwortet ohne diese beiden
Query-Parameter mit einem 500er. `list_inverters` braucht zusätzlich
`primary_energy_forms` (z.B. `["PV"]`).

## API-Oberfläche (`bayernwerk_client.efix`)

`EfixClient` ist ein GraphQL-Client (nicht REST wie MAP) für
`backend.e-fix.info`: `client.query(query, operation_name=..., variables=...)`
für beliebige Queries/Mutations, plus Convenience-Methoden für die bekannten
Queries: `get_installer()`, `list_installer_antraege()`, `get_user_status()`,
`list_my_registered_events()`.

## Package-Struktur

```
bayernwerk_client/
├── exceptions.py     # BayernwerkClientError, AuthenticationError, ApiError - service-uebergreifend
├── formatting.py      # generische Absatz-/Dict-Formatierung fuers CLI - service-uebergreifend
├── _jwt.py            # generische JWT-Payload-Dekodierung - service-uebergreifend
├── tokens.py           # generischer JWT-Token-Store (TokenSet/TokenStore) - service-uebergreifend
├── completion.py       # `bayernwerk completion` - argcomplete-Setup, kein eigener Service
├── cli.py             # `bayernwerk`-Einstiegspunkt, registriert Service-Subcommands
├── map/                # Mein.Auftragsportal (REST)
│   ├── client.py, auth.py, instances.py
│   ├── formatting.py   # `render_instance_tree`, `render_orders` (map-spezifisch)
│   └── cli.py           # `map`-Subcommand
└── efix/                # e-fix Installateur-Portal (GraphQL)
    ├── client.py, auth.py
    └── cli.py           # `efix`-Subcommand
```

## Entwicklung

```bash
uv sync
uv run pytest --cov=bayernwerk_client --cov-report=term-missing
pre-commit run --all-files
```
