Skip to content

feat: Add Sauter Smart Control realm - #2304

Merged
iMicknl merged 2 commits into
iMicknl:mainfrom
Mincka:sauter-smart-control
Oct 24, 2026
Merged

iMicknl merged 2 commits into
iMicknl:mainfrom
Mincka:sauter-smart-control

Conversation

@Mincka

@Mincka Mincka commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Add Sauter Smart Control realm (Server.SAUTER)

Why

Sauter Smart Control (the connected-appliance app for Sauter ovens, by Groupe
Brandt / Modulotech) runs on the same Overkiz backend and the same
smartcontrol-app.com cookie‑session middleware as Brandt Smart Control — but it
authenticates against a different partner/realm: sauter-electromenager
(Brandt uses brandt-electromenager).

Because of that, a Sauter account currently cannot be used: the Brandt flow returns
BrandtBadCredentialsError ("Password wrong password"), since the middleware session
login (POST /api/v1/sessions.json) is sent with the wrong partner. This is the
missing piece from #1202.

What

Adds first-class support so Sauter connected appliances (e.g. the SOP6615LB oven,
an Overkiz OGP gateway) can authenticate and be controlled:

  • enums.Server.SAUTER
  • const: SAUTER_PARTNER = "sauter-electromenager" and a Server.SAUTER
    ServerConfig (endpoint https://ha3-1.overkiz.com/enduser-mobile-web/enduserAPI/,
    same host as Brandt)
  • auth: BrandtAuthStrategy gains a partner class attribute (default
    BRANDT_PARTNER, so Brandt behaviour is unchanged); SauterAuthStrategy
    subclasses it with SAUTER_PARTNER; the factory wires Server.SAUTER
  • tests mirroring the Brandt ones (constants + server entry, factory wiring, and the
    middleware login asserting partner=sauter-electromenager)

The only functional difference from Brandt is the partner value; everything else
(JWT fetch via the session cookie, then the Overkiz login with the jwt) is reused.

Testing

  • ruff clean on the changed lines; full test suite green (513 passed, 2 skipped),
    including the 3 new Sauter tests.
  • Verified end-to-end against a live Sauter account and a real SOP6615LB oven:
    Server.SAUTER logs in, lists the gateway + oven, reads states, and executes
    commands (e.g. stop).

Notes

  • Server.SAUTER_COZYTOUCH (ha110-1) already exists but targets the Sauter heating /
    Cozytouch realm — this is the distinct Smart Control appliance realm (ha3-1).
  • Context: the Sauter Smart Control app has been removed from the stores; this lets
    existing owners keep using their appliances via this library / Home Assistant.

Refs: #1202

@github-actions github-actions Bot added the feature New feature or capability label Oct 24, 2026
@Mincka
Mincka force-pushed the sauter-smart-control branch from 093fad1 to 95f6102 Compare October 24, 2026 10:50

@iMicknl iMicknl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution! Have you verified that this works for your account?

And are you planning to use this via the python-overkiz-api, or via Home Assistant? We don't support ovens there yet afaik.

@Mincka

Mincka commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Hello,

Yes, tested on my own account. The only difference with Brandt is the partner field (sauter-electromenager), the rest of the flow is identical with Server.SAUTER login works, I get the gateway + the oven, and commands go through:

Server.SAUTER -> https://ha3-1.overkiz.com/enduser-mobile-web/enduserAPI/
LOGIN OK

GATEWAYS (1)
- id=XXXX-XXXX-XXXX  updateStatus=UP_TO_DATE  connectivity=OK  alive=True

DEVICES (2)
- 'Brandt_OGP_Gateway_HF005' [ogp:Gateway]  ogp://XXXX-XXXX-XXXX/16000210
- 'Four BNT8256'             [ogp:Oven]      ogp://XXXX-XXXX-XXXX/16000211

stop -> COMPLETED

On the HA side: that's the end goal, but I saw #177263 (climate for ogp:Oven) got closed after the author went inactive. You'd asked there for the full oven diagnostics. I have a real Sauter oven, so here's the structure from the live API:

ogp:Oven (uiClass: Oven)
Commands: activateOption(1), deactivateOption(1), identify(0), sendPrivate(1), setMode(1),
          setName(1), setRecipe(1), setTargetTemperature(1), setTargetTime(1),
          setTargetWeight(1), setTimezone(1), start(0), stop(0)
States:
- core:TargetTemperatureState = 180.0  (LowerBound 35.0 / UpperBound 250.0 / Recommended 180.0)
- core:TargetTemperatureReachedState = False
- core:ModeState = fanCooking
- core:AvailableModesState = [defrost, bottomHeat, bottomFan, conventional, conventionalFan,
    fannedGrill, keepingWarm, fanCooking, combinedHeat, bread, drying, pizza, eco, auto]
- core:RecipeState = custom
- core:AvailableRecipesState = [pizza, rareBeef, mediumRareBeef, wellDoneBeef, rareLamb,
    wellDoneLamb, pork, chicken, fish, vegetables, saltTart, sweetTart, cake, cookies, souffle]
- core:TargetWeightState = 0 ; core:TargetTimeState = 1800 ; core:RemainingTimeState = 1800
- core:RunningTimerState = False
- core:AvailableOptionsState = [remotedGuide, booster, demo] ; core:ActivatedOptionsState = []
- core:CycleStageSequenceState = [cooking, end] ; core:CurrentCycleStageState = end
- core:StartedStoppedState = stopped  (started/stopped)
- core:OpenClosedState = closed  (open/closed) ; core:LockedUnlockedState = unlocked  (locked/unlocked)
- core:IntensityState = 0 ; core:LightIntensityState = 0
- core:ErrorState = {"key":"internal","message":"NO_ERROR"}
- core:TimezoneState = Europe/Paris ; core:FirmwareRevisionState = 326
- ogp:UnavailableFeaturesState = [{"feature":"*","commands":["*"],
    "states":["recipe","intensity","targetWeight"],"cause":"localControl"}]

ogp:Gateway (uiClass: ProtocolGateway)
Commands: identify(0), reboot(0), sendPrivate(1), setName(1), update(0)
States: Availability, Status, SignalQuality, FirmwareRevision=4.13, ActivePartition, NewFirmwareAvailable

@iMicknl iMicknl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, and nice that you tested it on a real oven!

One thing outside the diff: add a Sauter Smart Control row to the supported hubs table in README.md and docs/index.md.

Comment thread pyoverkiz/enums/server.py Resolved
Comment thread tests/test_auth.py Resolved
Comment thread tests/test_auth.py Resolved
Comment thread pyoverkiz/auth/strategies.py Resolved
@iMicknl iMicknl changed the title feat: add Sauter Smart Control realm (Server.SAUTER) feat: Add Sauter Smart Control realm Oct 24, 2026
Sauter Smart Control runs on the same Overkiz host (ha3-1) and the same
smartcontrol-app.com cookie-session middleware as Brandt Smart Control, but
authenticates against a different partner/realm ("sauter-electromenager").
Without this, Sauter accounts fail the Brandt flow with BrandtBadCredentialsError.

- enums.Server.SAUTER_SMART_CONTROL
- const: SAUTER_PARTNER + Server.SAUTER_SMART_CONTROL ServerConfig (ha3-1)
- auth: BrandtAuthStrategy gains a `partner` attribute (default BRANDT_PARTNER,
  Brandt unchanged); SauterAuthStrategy subclasses it with SAUTER_PARTNER;
  factory wires Server.SAUTER_SMART_CONTROL; service-error messages use
  self.server.name so they read correctly for Sauter
- tests: Brandt login tests parametrized over Brandt + Sauter
- README.md + docs/index.md: Sauter Smart Control hubs row

Refs: iMicknl#1202

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Mincka
Mincka force-pushed the sauter-smart-control branch from 95f6102 to 4c0f0e7 Compare October 24, 2026 13:39
iMicknl added a commit that referenced this pull request Oct 24, 2026
## Why

The docs workflow has been failing since the pymdown-extensions 12.1 →
12.2 bump (#2291), on `main` (v2.2.3 release run) and on open PRs like
#2304:

```
mkdocstrings/_internal/handlers/rendering.py:85, in Highlighter.__init__
TypeError: Highlight.__init__() missing 1 required positional argument: 'md'
```

pymdown-extensions 12.2 made `md` a required argument of
`pymdownx.highlight.Highlight.__init__`. mkdocstrings 1.0.4 subclasses
it without passing `md`.

## What

- Lockfile-only bump of mkdocstrings 1.0.4 → 1.0.8, which supports the
new signature. `pyproject.toml` floors are unchanged.
- Add `uv.lock` to the docs workflow `pull_request` paths, so
lockfile-only bumps like #2291 run the docs build (that is how this
slipped through).

## Testing

- `uv run mkdocs build` succeeds (failed with the `TypeError` before).
- `prek run --all-files` and `pytest` pass.

@iMicknl iMicknl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@iMicknl
iMicknl merged commit 421d22d into iMicknl:main Oct 24, 2026
13 checks passed
iMicknl added a commit that referenced this pull request Oct 24, 2026
`BrandtAuthStrategy.login()` treated every status >= 400 from the
middleware `sessions.json` call as bad credentials, including 5xx.
During a Brandt middleware outage, Home Assistant would therefore ask
the user to reauthenticate instead of retrying. A non-JSON error body
(for example an HTML page from a proxy) also leaked a raw
`aiohttp.ContentTypeError`, because it is not a `ValueError`.

Changes:
- `sessions.json` and `profile/jwt.json` now call
`_raise_for_server_error` first, like the Cozytouch and Somfy
strategies, so 5xx raises `ServiceUnavailableError` /
`MaintenanceError`.
- The `sessions.json` error body is read with `_json_body`, so a
non-JSON 4xx still raises `BrandtBadCredentialsError`. The first `error`
entry is still used as the message.
- Added tests for a 503 from `sessions.json`, a non-JSON 401, and a 503
from `jwt.json`.

This also affects the upcoming Sauter Smart Control realm (#2304), which
uses the same middleware login.
@Mincka

Mincka commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks a lot for merging this, @iMicknl, and for getting it into 2.3.0 so fast.

While I was at it I looked at exposing the oven itself in the HA integration and found #177263 (climate support for ogp:Oven). It went stale, and I saw your steer that an oven most likely can't be a climate entity, with the number/select/button approach from home-assistant/core#173326 being the way to go.

I'd be glad to pick that up and see it through review. I also have full diagnostics for a Sauter ogp:Oven (the structure you asked the previous author for): 13 commands, ~37 states, AvailableModes/AvailableRecipes, etc. Happy to post a redacted dump.

How would you recommend approaching it?

Thanks again!

@iMicknl

iMicknl commented Oct 24, 2026

Copy link
Copy Markdown
Owner

@Mincka are you willing to create a PR on core with the required changes for your device? I won't be able to prioritize this and it is hard for me to build this without testing.

@Mincka

Mincka commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

I can give it a shot but I wanted your advice on the approach.
Should I propose something similar to home-assistant/core#173413 ?
I don't think the approach with the Climate entity from home-assistant/core#177263 makes sense IMHO.

@iMicknl

iMicknl commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Yes, take home-assistant/core#173326 (review) as guidance. Using the climate entity for ovens won't be accepted in core.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants