Imported from ChrisHaPunkt/stihl-imow-webapi (
AGENTS.md). Install upstream withnpx skills add ChrisHaPunkt/stihl-imow-webapi. Copyright stays with the author.
AGENTS.md — stihl-imow-webapi
Unofficial async Python wrapper for the STIHL iMOW cloud WebAPI (RMI / Gen ≤4 mowers only). Published to PyPI as imow-webapi. Consumed by the sibling ha-stihl-imow Home Assistant integration in this workspace.
Architecture
imow/api/__init__.py— theIMowApiclass is the entire client. It owns theaiohttp.ClientSession, auth/token lifecycle, and all HTTP calls. Everything funnels throughapi_request(url, method, payload, headers), which injects browser-like headers, aBearertoken, auto-reauths when the token has <1 day left, and callscheck_api_maintenance()on HTTP 500.- Auth is a scraped OAuth flow:
__fetch_new_csrf_token_and_request_id()uses BeautifulSoup to pullcsrf-token/requestIdfrom the login HTML, then__authenticate()posts form-encoded creds and extractsaccess_tokenfrom the redirect URL fragment viafurl. Tokens last 30 days. imow/common/mowerstate.py—MowerStatewraps an upstream dict.replace_state()doesself.__dict__.update(...)so every API field becomes a dynamic attribute (declared as type-hint stubs at the bottom of the class for IDEs, not real defaults). Instances hold a back-reference toIMowApi(self.imow) to lazily fetch statistics/startpoints/settings.imow/common/messages.py—Messagesresolves localized state/error text from STIHL's i18n JSON.MowerStatealways builds a language-neutralmachineState(UPPER_SNAKE) from the English messages plus a localizedstateMessagedict.imow/common/actions.py—IMowActionsenum is the only valid action vocabulary:EDGE_MOWING,TO_DOCKING,START_MOWING_FROM_POINT,START_MOWING.imow/common/consts.pyholds the two base URLs;exceptions.pydefinesLoginError,ApiMaintenanceError,LanguageNotFoundError.
Conventions
- Async-only. Every public method is a coroutine; callers must
await. Prefer passing in an externalaiohttpsession (IMowApi(aiohttp_session=session)); if none is given,api_requestlazily creates one withraise_for_status=True. - Actions are issued two ways —
api.intent(action, mower_name=...)ormower_state.intent(action)(the latter fillsmower_external_idfromself.externalId).intent()translates**kwargs(duration,startpoint,starttime,endtime) intofirst/second_action_value_param; datetimes go throughvalidate_and_fix_datetime("%Y-%m-%d %H:%M"). mower_external_idmust be exactly 16 chars — code asserts this. Useget_mower_action_id_from_name/idto resolve it.- Logging: use
logging.getLogger("imow"). Never log tokens/credentials (existing code redacts them). - Version lives in
imow/common/package_descriptions.py(__version__);pyproject.toml(hatchling) reads it dynamically via[tool.hatch.version]. Bump it there.
Workflows
- Install dev:
uv sync --group dev(runtime + dev deps declared inpyproject.toml; dev tools: pytest, pytest-asyncio, black, flake8, pdoc3). Run tools viauv run <tool>. - Unit tests (offline, use a fake token):
pytest -s tests/test_unit*. - Integration tests hit the live STIHL API and need a repo-root
secrets.pywithEMAIL,PASSWORD,MOWER_NAME:pytest -s tests/test_integration*. Do not commitsecrets.py. - Format with
blackbefore committing; docs are generated withpdoc3intodocs/.
Gotchas
- Because
MowerStateattributes come from the upstream payload, referencing a field that STIHL didn't return raisesAttributeErrorat access time — guard optional fields. - Only Gen ≤4 mowers (app.imow.stihl.com) work; myimow.stihl.com accounts are unsupported by design (see issue #13).