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. One dependency: aiohttp for native async I/O.
For authentication internals, data endpoint reference, and debugging, see DEVELOPMENT.md.
Credentials
Credentials are resolved in order: CLI arguments > environment variables > config file.
| Source | 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:
-
Config file — store credentials in
~/.minol.jsonand restrict access:chmod 600 ~/.minol.json
The scraper warns at startup if the file is readable by group or other users.
-
Environment variables — set
MINOL_EMAIL,MINOL_PASSWORD, andMINOL_USER_NUMin your shell profile or via a secrets manager. -
--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 installto fail withEPERM. Install from a/tmpcopy 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--passwordand--password-stdinto 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 kwhor--unit m3.rooms— keyed by room name; each entry hastotal,device, andmonthly(nullfor 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.
Injecting an external aiohttp session
Integrations that manage their own aiohttp.ClientSession (e.g. Home Assistant) can
pass it in directly. The library uses it for all requests and never closes it:
import aiohttp
from minol import MinolScraper
async def main(client_session: aiohttp.ClientSession):
async with MinolScraper(
"user@example.com", "password", "000000000000",
session=client_session,
) as scraper:
await scraper.login(session_data=session_cache)
data = await scraper.fetch_all()
close()is a no-op when a session is injected (the caller owns the session).- Only Minol-related cookies are added to or removed from the injected session's jar.
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file minol-1.3.0.tar.gz.
File metadata
- Download URL: minol-1.3.0.tar.gz
- Upload date:
- Size: 38.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96571419514226d68eb7e2658a634c34369c03b9125c0732f2bfcab0a15a8969
|
|
| MD5 |
80c4312b6bd30e4b4af032fb036fbce0
|
|
| BLAKE2b-256 |
72c670182d12a86f3323b45ebf7a526146d23ffb8497bc0078e4be6e97f4b585
|
Provenance
The following attestation bundles were made for minol-1.3.0.tar.gz:
Publisher:
ci.yml on BastiOfBerlin/minol
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
minol-1.3.0.tar.gz -
Subject digest:
96571419514226d68eb7e2658a634c34369c03b9125c0732f2bfcab0a15a8969 - Sigstore transparency entry: 1124234784
- Sigstore integration time:
-
Permalink:
BastiOfBerlin/minol@e60cfdfbb590418bbe59164b49f39bb53bd93e0a -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/BastiOfBerlin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@e60cfdfbb590418bbe59164b49f39bb53bd93e0a -
Trigger Event:
push
-
Statement type:
File details
Details for the file minol-1.3.0-py3-none-any.whl.
File metadata
- Download URL: minol-1.3.0-py3-none-any.whl
- Upload date:
- Size: 24.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
786591759431c98b9744b70e1ee69732a27cca42459ebb62f8a51ca4a347645c
|
|
| MD5 |
13f113dda0ef01c75e081f96f38d3bdc
|
|
| BLAKE2b-256 |
913bf88521732f4d5f6baa82353809aa63b089a8eb22ccee0c17bea722f8e442
|
Provenance
The following attestation bundles were made for minol-1.3.0-py3-none-any.whl:
Publisher:
ci.yml on BastiOfBerlin/minol
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
minol-1.3.0-py3-none-any.whl -
Subject digest:
786591759431c98b9744b70e1ee69732a27cca42459ebb62f8a51ca4a347645c - Sigstore transparency entry: 1124234841
- Sigstore integration time:
-
Permalink:
BastiOfBerlin/minol@e60cfdfbb590418bbe59164b49f39bb53bd93e0a -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/BastiOfBerlin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@e60cfdfbb590418bbe59164b49f39bb53bd93e0a -
Trigger Event:
push
-
Statement type: