Skip to main content

Scraper for the Minol Kundenportal (utility metering data)

Project description

Minol Kundenportal Scraper

A Python scraper that authenticates to the Minol Kundenportal and fetches consumption data (heating, warm water, cold water) on a per-room basis. Pure Python — stdlib only, no third-party dependencies.

For authentication internals, data endpoint reference, and debugging, see DEVELOPMENT.md.


Credentials

Credentials are resolved in order: CLI arguments > environment variables > config file.

Source Email Password User Number
CLI --email --password --user-num
Env var MINOL_EMAIL MINOL_PASSWORD MINOL_USER_NUM
Config file email password user_num

The default config file location is ~/.minol.json (override with --config):

{
  "email": "user@example.com",
  "password": "password",
  "user_num": "000000000000"
}

Password security

Avoid --password on shared systems. Any value passed via --password is visible to other local users in the process listing (ps aux) and in /proc/PID/cmdline for the lifetime of the process.

Safer alternatives, in order of preference:

  1. Config file — store credentials in ~/.minol.json and restrict access:

    chmod 600 ~/.minol.json
    

    The scraper warns at startup if the file is readable by group or other users.

  2. Environment variables — set MINOL_EMAIL, MINOL_PASSWORD, and MINOL_USER_NUM in your shell profile or via a secrets manager.

  3. --password-stdin — pipe the password from a secrets store or a variable, avoiding it ever appearing in the argument list:

    echo "$MINOL_PASSWORD" | minol --email 'user@example.com' --user-num '000000000000' --password-stdin
    # Or from a file:
    minol --email 'user@example.com' --user-num '000000000000' --password-stdin < ~/.minol_password
    

The session cache (~/.minol_session.json) is created with permissions 0600 (owner-read-write only) and contains the session token rather than the plaintext password. See Session Caching.


Installation

Install from PyPI:

pip install minol

Or install from source:

git clone https://codeberg.org/BastiOfBerlin/minol
cd minol
pip install .

python -m minol also works without installation — just clone the repo and run from the project root.

Note for bind-mounted filesystems (e.g. container setup: some mounts do not support atomic file rename, which causes pip install to fail with EPERM. Install from a /tmp copy instead:

cp -r /workspace/minol /workspace/pyproject.toml /workspace/README.md /workspace/LICENSE /tmp/minol-build/
pip install /tmp/minol-build

Usage

All examples use the minol console script installed by pip install minol. If you are running from source without installing, substitute python -m minol for minol.

# Fetch all consumption types, last 12 months
minol \
  --email 'user@example.com' \
  --password 'password' \
  --user-num '000000000000'

# Heating only, specific date range, verbose, save to file
minol \
  --email 'user@example.com' \
  --password 'password' \
  --user-num '000000000000' \
  --type heating \
  --start 202501 \
  --end 202603 \
  --output consumption.json \
  -v

# Warm water in KWH instead of the default M3
minol \
  --email 'user@example.com' \
  --password 'password' \
  --user-num '000000000000' \
  --type warm_water \
  --unit kwh

# Raw API response (unprocessed JSON from the portal)
minol \
  --email 'user@example.com' \
  --password 'password' \
  --user-num '000000000000' \
  --raw

# Credentials from env vars or ~/.minol.json — no flags needed
minol

Shell escaping — Passwords containing $, !, backticks, or backslashes will be mangled by bash in double quotes. Always use single quotes for --password and --email on the command line, or use --password-stdin to avoid the issue entirely.


Output Format

By default the scraper returns structured data with only the relevant fields:

{
  "unit": "KWH",
  "rooms": {
    "Küche": {
      "total": 111.0,
      "device": "04B648FD82639440",
      "monthly": {
        "202503": 0,
        "202504": 5.107,
        "202505": null
      }
    }
  }
}
  • unit"KWH" (heating) or "M3" (warm water, cold water) by default. Override with --unit kwh or --unit m3.
  • rooms — keyed by room name; each entry has total, device, and monthly (null for months with no data yet).

Pass --raw to get the unprocessed API response instead.


Programmatic Usage

The library API is fully async. Use await inside an async context, or asyncio.run() for a quick script:

import asyncio
from minol import MinolScraper

async def main():
    scraper = MinolScraper("user@example.com", "password", "000000000000")
    await scraper.login()

    # Parsed structured data (default) — all three types fetched in parallel
    all_data = await scraper.fetch_all()

    # Individual types
    heating = await scraper.fetch_heating(timeline_start="202501", timeline_end="202603")
    warm = await scraper.fetch_warm_water()
    cold = await scraper.fetch_cold_water()

    # Override unit of measurement (warm water defaults to M3)
    warm_kwh = await scraper.fetch_warm_water(unit="kwh")

    # Raw API response
    all_raw = await scraper.fetch_all_raw()
    heating_raw = await scraper.fetch_heating(raw=True)

    # Force fresh login (skip session cache)
    await scraper.login(use_cache=False)

    # Use a custom session cache path
    from pathlib import Path
    await scraper.login(session_path=Path("/tmp/my_session.json"))

asyncio.run(main())

In-memory session caching (no file I/O)

API users (e.g. Home Assistant integrations) can manage the session cache themselves without touching the filesystem. Pass session_data to login():

import asyncio
from minol import MinolScraper

async def main():
    scraper = MinolScraper("user@example.com", "password", "000000000000")

    # First call: pass an empty dict to signal in-memory mode.
    # A fresh SAML login is performed and the new cache dict is returned.
    session_cache = await scraper.login(session_data={})
    # Persist session_cache however you like (database, HA storage, etc.)

    # Subsequent calls: pass the stored cache dict back.
    # If the token is still valid it is restored without any network requests.
    # If it has expired a fresh login runs and a new cache dict is returned.
    session_cache = await scraper.login(session_data=session_cache)

    data = await scraper.fetch_all()

asyncio.run(main())

When session_data is provided:

  • No session cache file is read or written.
  • login() always returns the cache dict: the existing dict on a cache hit, or a new dict after a fresh login.

Session Caching

After a successful login the scraper saves session cookies and the token expiry timestamp to ~/.minol_session.json. On the next run, expired tokens are rejected immediately without a network request; still-valid tokens are restored from the cache, skipping the full SAML login. Pass --no-cache to force a fresh login, or --session-path /path/to/session.json to use a custom cache file location.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

minol-1.2.0.tar.gz (33.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

minol-1.2.0-py3-none-any.whl (21.2 kB view details)

Uploaded Python 3

File details

Details for the file minol-1.2.0.tar.gz.

File metadata

  • Download URL: minol-1.2.0.tar.gz
  • Upload date:
  • Size: 33.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for minol-1.2.0.tar.gz
Algorithm Hash digest
SHA256 3d9d1222827347c95491f58d7cf12252624f086c60d1742e618947af2ac88822
MD5 5525a9089354cb1f97d998719231c923
BLAKE2b-256 526a03786e9ca733dd690be7bc94c165900c882d26117951b34cd299c5eedd22

See more details on using hashes here.

Provenance

The following attestation bundles were made for minol-1.2.0.tar.gz:

Publisher: ci.yml on BastiOfBerlin/minol

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file minol-1.2.0-py3-none-any.whl.

File metadata

  • Download URL: minol-1.2.0-py3-none-any.whl
  • Upload date:
  • Size: 21.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for minol-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 90aa207b07c22854c61f875b8c2c13b55b3f95e051a3a517a2ebc4abdf44217b
MD5 16c1cfdbbbcd59a0bcf61511998765e2
BLAKE2b-256 5504ac28d39a427e71e4435d183eb3ca547bb1c15ebc9e4c51f36f0c76c7ceb8

See more details on using hashes here.

Provenance

The following attestation bundles were made for minol-1.2.0-py3-none-any.whl:

Publisher: ci.yml on BastiOfBerlin/minol

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page