Skip to content

fix(cover): pass the yaml class: through to WHO 2 covers (#39) - #796

Merged
GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/cover-device-class
Oct 24, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/cover-device-class

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown

Requested by A

Fixes #39

What is the problem

class: (or device_class:) on a WHO 2 cover in myhome.yaml is accepted by the cover schema but never reaches the entity: async_setup_entry builds MyHOMECover without it, so every WHO 2 cover is a shutter, or a blind when slat_tilt: true is set. A curtain or awning configured as such is shown with the shutter icon and wording.

This is the yaml path of #39.

Rebased onto the current v2-phase1-architecture (ddc6d60): the one conflict with the merged #778 was the cover build() kwargs, resolved by keeping both min_position_delta= and device_class=.

What is the solution

  • cover.py async_setup_entry: the configured class is turned into a CoverDeviceClass and passed to the entity. The device schema defaults an absent class to "switch", which is not a cover class, so that value (or any unknown one) is treated as "not configured", the same way the impulse covers already handle it. The lookup reads cfg.get(CONF_DEVICE_CLASS) or cfg.get("device_class"): cover_schema folds a yaml device_class: into class, and the raw-key fallback stays for symmetry with build_impulse(), switch.py, binary_sensor.py and sensor.py should a caller ever seed hass.data without running the schema (review follow-up).
  • MyHOMECover.__init__: new device_class kwarg; a configured class sets _attr_device_class.
  • _enable_slat_tilt sets BLIND only when no class is configured, so slat_tilt: true with class: shade stays a shade.
  • Docs: a "Device class (class)" section of its own in docs/configuration/covers.md, next to the general cover YAML configuration (review follow-up: it was nested under the Venetian section). It says the class applies to point-to-point and to general / area / group covers, the Venetian bullet and the slat_tilt parameter row now read "sets device class to blind unless class is set", and UI-configured covers are pointed to Settings → Devices & Services → Entities → Show As, as switches.md and binary-sensors.md do. Release-notes delta line.

State Diagram

stateDiagram-v2
    direction LR

    state "myhome.yaml cover entry" as Yaml {
        NoClass : no yaml key - schema default switch
        WithClass : yaml key class curtain - a device_class key is folded into it
    }

    state "MyHOMECover device_class" as Entity {
        Shutter
        Blind
        Configured : curtain / awning / garage / shade ...
    }

    [*] --> NoClass
    [*] --> WithClass
    NoClass --> Shutter : before and after
    NoClass --> Blind : slat_tilt true, before and after
    WithClass --> Shutter : before , value dropped
    WithClass --> Configured : after , slat_tilt true keeps it
Loading

Hardware Verification

Live F461 2.0.11 plant (local Home Assistant, combined build of all our PRs, 2026-10-24) and, for status reads, the MyHomeServer1 3.87.13 plant; captures in MyHOME #799 (merged).

Live retest (2026-10-24)

Consensus label: confirmed for the tested case (combined build, F461 2.0.11, HA 2026.10.2). A yaml variant with class: curtain on cover 05 gave cover.shutter device_class: curtain; the plant yaml without the key gave shutter. No bus frame is involved, so none was exercised.

How is it tested

  • tests/test_cover_device_class.py::test_who2_cover_uses_the_configured_class (10 cases, from a cover_schema-validated yaml dict through async_setup_entry): default → shutter, class: curtain, device_class: awning, class: garage, slat_tilt → blind, slat_tilt + class: shade → shade, strategy: venetian → blind, strategy: venetian + class: shade → shade, area where: "1" + class: curtain → curtain on a MyHOMEScopeCover, general where: "0" → shutter on a MyHOMEScopeCover (the last four added in the review follow-up).
  • Fail-before: 6 of the 10 cases fail on v2-phase1-architecture (d8bac15) (curtain, awning, garage, the two shade cases, the area curtain); the shutter and blind cases pass before and after.
  • Full suite against a plain upstream OWNd master checkout (f04d8aa) on PYTHONPATH, HA 2026.10.2, Python 3.14, -n auto: 4489 passed, 1 skipped.
  • scripts/verify_ownd_coverage.py: 100 % statement coverage (16,052 / 16,052). ruff check ., scripts/typing_ratchet.py (mypy --strict, 0 errors), scripts/verify_ha_standards.py, scripts/sync_documentation.py --check, scripts/manage_translations.py check, tests/test_fixture_privacy.py pass.

Where are the traces

Subsystem Capture Plant Frames verified by this PR
WHO 2 cover configuration No physical trace applies: this change touches no bus frame (configuration / entity attribute only). The #799 fixture README lists this PR as "nothing on the bus; configuration only". BTicino F461 2.0.11, F411/2 at WHERE 05 (where the yaml check below was made) none: the frames a cover sends and receives are unchanged

What does the Firmware Oracle / Encyclopedia say

No oracle stimulus exists for this change: no frame is sent or parsed differently.

WHO 2 wire frames never carry the actuator's load SUBTYPE: a status is *2*<WHAT>*<WHERE>## and a level is *#2*<WHERE>*10*<LEVEL>*<STATUS>##, with no field that tells a shutter from a curtain, a gate or a garage door. The SUBTYPE values (2 = Shutter, 3 = Curtain, 4 = Gate, 5 = Garage door) exist only in the offline MyHOME_Suite catalogue metadata (Encyclopedia OWN-DEV-0003), which the gateway does not expose on the bus. The device class therefore has to come from the Home Assistant configuration (class: in myhome.yaml) or from the UI "Show As" setting; it cannot be discovered.

Checklist

  • PR Description: Follows the structured sections above (What is the problem, What is the solution, State Diagram, How is it tested, Where are the traces, What does the Firmware Oracle / Encyclopedia say).
  • Trace-Backed (if protocol/device change): not a protocol change; no bus frame is touched (stated above, not guessed).
  • V2 Architecture: MyHOMECover keeps its handle_event() path; no async_dispatcher_connect added.
  • Tests & Linters: pytest, ruff, and scripts/verify_ha_standards.py pass.

🤖 Generated with Claude Code

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@gdluck
gdluck marked this pull request as draft October 24, 2026 13:39
@gdluck
gdluck force-pushed the fix/cover-device-class branch from e7826ce to 7277805 Compare October 24, 2026 15:47
@gdluck
gdluck marked this pull request as ready for review October 24, 2026 16:27

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review

The change is correct and narrowly scoped to the WHO 2 path (build() used by PlatformDiscovery for who="2").
cover_schema / MyHomeDeviceSchema already folds a yaml device_class: into CONF_DEVICE_CLASS and defaults an absent class to "switch".
The new logic in build() turns a valid CoverDeviceClass into the value passed to the entity and treats the schema default (or any unknown string) as unset via the try/except ValueError.
MyHOMECover.init stores it in _configured_device_class and sets _attr_device_class only when it is present.

_enable_slat_tilt now sets CoverDeviceClass.BLIND only when no class was configured, so an explicit class: shade (or curtain, awning, etc.) is preserved.

WHO 1 impulse covers already had their own handling and are untouched.
Tests (tests/test_cover_device_class.py) exercise the important cases (default shutter, explicit classes, device_class: alias, slat_tilt alone, and slat_tilt + explicit class). Docs and the b16 release notes are updated.

No functional issues found. The implementation matches the description and the #39 request.

PR description

The body already contains the main required sections, but it does not fully match the current .github/pull_request_template.md and the checklist itself is stale:
The PR Description checklist item still lists old section names (Summary, Changes, …, Companion) instead of the template’s actual headings (What is the problem, What is the solution, State Diagram, Hardware Verification, How is it tested, Where are the traces, What does the Firmware Oracle / Encyclopedia say).

Companion is not part of the template (consider dropping it).

An explicit Fixes #39 (or Closes #39) near the top would let GitHub auto-link/close the issue.
Requested change: please update the description so the checklist matches the real sections and the template, and add the Fixes #39 line.

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Thanks. Description updated: Fixes #39 added under the header, the checklist's first item now quotes the template's section names, and the extra Companion section is gone (its content, where any, now sits as one line at the end of "What is the solution"). The same two description fixes were applied to the other open MyHOME PRs of this batch so they match the template as well. No code change.

…t-HA#39)

`class:` / `device_class:` in myhome.yaml was validated by the cover
schema but never handed to the entity, so every WHO 2 cover was a
`shutter` (or a `blind` with slat_tilt). The configured class now sets
the entity's device class and wins over the slat-tilt default.

Verified on a physical F461 2.0.11 with an F411/2 at WHERE 05:
`class: curtain` gives `device_class: curtain`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/cover-device-class branch from 7277805 to 823a2de Compare October 24, 2026 18:12
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

@gdluck the pr description is still not rendering ...

State Diagram

Unable to render rich display

Lexical error on line 6. Unrecognized text.
...itch) Class : class curtain (a d
----------------------^

For more information, see https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams#creating-mermaid-diagrams

stateDiagram-v2
direction LR

state "myhome.yaml cover entry" as Yaml {
    NoClass : no class key (schema default switch)
    Class : class curtain (a device_class key is folded into class)
}

state "MyHOMECover device_class" as Entity {
    Shutter
    Blind
    Configured : curtain / awning / garage / shade ...
}

[*] --> NoClass
[*] --> Class
NoClass --> Shutter : before and after
NoClass --> Blind : slat_tilt true, before and after
Class --> Shutter : before (value dropped)
Class --> Configured : after (slat_tilt true keeps it)

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Sorry about that. The diagram did not go through a mermaid parser before posting; I have now run every state diagram of this batch through the mermaid 11 parser locally (19 of the 24 failed on colons, semicolons or parentheses inside labels, and this one also on a state named Class), rewrote them so they parse, and updated the bodies of all open PRs in both repositories. This description now renders. The lesson is recorded on my side: every diagram is validated before it is posted.

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @gdluck for updating the PR template sections, adding Fixes #39, and validating the batch's Mermaid diagrams!

The code, typing, test coverage, standards checks, and rebase onto v2-phase1-architecture all look solid and verified cleanly. There is just one remaining bug in the PR description's Mermaid diagram to fix before we can approve and merge, plus a few polite, non-blocking polish suggestions if you'd like to include them here or in a small follow-up.


Requested change (blocking): Mermaid NoWithClass typo in ## State Diagram

When Class was renamed to WithClass to avoid Mermaid's reserved class keyword, a substring replacement accidentally turned NoClass into NoWithClass on the first transition line:

NoWithClass --> Shutter : before and after

Because stateDiagram-v2 implicitly creates undeclared state identifiers at the root level, the Mermaid 11 parser accepts this without throwing a syntax error, but renders a broken graph: NoWithClass appears as a disconnected orphan node outside "myhome.yaml cover entry", and NoClass loses its "before and after" transition arrow to Shutter.

Replacing NoWithClass with NoClass fixes the rendered diagram:

stateDiagram-v2
    direction LR

    state "myhome.yaml cover entry" as Yaml {
        NoClass : no yaml key - schema default switch
        WithClass : yaml key class curtain - a device_class key is folded into it
    }

    state "MyHOMECover device_class" as Entity {
        Shutter
        Blind
        Configured : curtain / awning / garage / shade ...
    }

    [*] --> NoClass
    [*] --> WithClass
    NoClass --> Shutter : before and after
    NoClass --> Blind : slat_tilt true, before and after
    WithClass --> Shutter : before , value dropped
    WithClass --> Configured : after , slat_tilt true keeps it
Loading

(Worth a quick grep across the other PR descriptions in the batch too, just in case a similar Class → WithClass replacement touched another NoClass node.)


Non-blocking follow-up suggestions (optional)

Left three small inline notes on docs/configuration/covers.md, custom_components/myhome/cover.py, and tests/test_cover_device_class.py (all non-blocking — happy to take them either in this PR or a follow-up):

  1. docs/configuration/covers.md structure & cross-references: Promoting Device class (class) out of the Venetian Blinds H2 section so general cover users find it easily, qualifying the slat_tilt "sets device class to blind" lines right above it ("unless class is set"), and mentioning Home Assistant's UI Settings → Entities → Show as option for v2 UI-configured covers (matching switches.md / binary-sensors.md).
  2. cover.py raw-dict parity: Keeping or cfg.get("device_class") in build() for symmetry with build_impulse() right below it when callers bypass cover_schema.
  3. Optional extra test case & Encyclopedia note: Optionally adding a MyHOMEScopeCover (area/group) or strategy: "venetian" + class: "shade" test case; and if helpful in the PR's Encyclopedia section, noting that WHO = 2 wire frames never carry actuator load SUBTYPE (2 = Shutter, 3 = Curtain, 4 = Gate, 5 = Garage door only exists in offline MyHOME_Suite catalogue metadata OWN-DEV-0003), which is why device class must come from HA configuration or UI "Show as".

Comment thread docs/configuration/covers.md Outdated

Higher tilt is assumed to be *open* (an opening pulse raises the angle, verified on an F401 roller shutter with tilting slats in [discussion #770](https://github.com/orgs/OpenWebNet-HA/discussions/770#discussioncomment-19144521) with `slat_time: 1.4`). Dedicated Venetian actuators (such as the F411U2 in Venetian mode) still await a keypad / app capture (#492).

### Device class (`class`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking doc polish: Would you mind considering three small tweaks here (either in this PR or a quick follow-up)?

  1. Section level / placement: ### Device class (\class`)is currently nested as anH3inside## 🪟 Venetian Blinds & Slat Tilt Positioning (Timed Slat Inching), whereas class:applies to all WHO 2 covers (curtains, awnings, shutters, garage doors, gates, windows, shades). Promoting it to its own##` section (or placing it with general cover YAML configuration) would make it much easier to spot.
  2. Consistency with Venetian bullets above: The bullet and slat_tilt parameter table row just above still say "Sets the entity device class to blind" without "unless class is set".
  3. v2 UI pointer: Like docs/configuration/switches.md and docs/configuration/binary-sensors.md, a short note that UI-configured covers (without myhome.yaml) can change their device class in Home Assistant via Settings → Devices & services → Entities → Show as would be helpful for v2 UI users.

Comment thread custom_components/myhome/cover.py Outdated
# with myhome.set_cover_travel_time wins until it is reset.
kwargs["calibration"] = {CONF_SLAT_TIME: stored[CONF_SLAT_TIME]}
# cover_schema already folds a yaml `device_class:` into CONF_DEVICE_CLASS.
raw_dev_class = cfg.get(CONF_DEVICE_CLASS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking nit: myhome.yaml is completely fine here because MyHomeDeviceSchema folds "device_class" into CONF_DEVICE_CLASS (and defaults omitted entries to "switch"), as the comment notes. Still, build_impulse() right below (line 266), as well as switch.py, binary_sensor.py, and sensor.py, writes cfg.get(CONF_DEVICE_CLASS) or cfg.get("device_class") — keeping that fallback here would stay symmetric if any caller ever pre-seeds hass.data without running cover_schema.

({CONF_DEVICE_CLASS: "garage"}, CoverDeviceClass.GARAGE),
({CONF_SLAT_TILT: True}, CoverDeviceClass.BLIND),
({CONF_SLAT_TILT: True, CONF_DEVICE_CLASS: "shade"}, CoverDeviceClass.SHADE),
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking test suggestion: Nice, concise parametrized coverage — this already hits 100% of the changed lines in cover.py (including the "switch" ValueError path). If you happen to touch this file, adding a quick case for an area/group MyHOMEScopeCover (e.g. where: "1") or strategy: "venetian" + class: "shade" could be nice extra defense-in-depth, though both paths work cleanly as written.

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Good catch, thank you: NoWithClass was my own substring slip when renaming Class; the diagram now reads NoClass --> Shutter and renders as intended. I checked the other descriptions of the batch: no other NoClass node was touched. The three polish points (covers.md section promotion, slat_tilt wording and the UI "Show as" note; the device_class raw-dict fallback for symmetry with build_impulse(); the scope-cover and venetian+shade test cases, plus the SUBTYPE note in the Encyclopedia section) are being added to this PR in one follow-up commit.

…back, add scope and venetian test cases (review follow-up)

Review follow-up on OpenWebNet-HA#796 (OpenWebNet-HA#39), the three non-blocking items:

- docs/configuration/covers.md: "Device class (`class`)" is a section of
  its own next to the general cover YAML configuration instead of a
  subsection of the Venetian section; the Venetian bullet and the
  `slat_tilt` parameter row say "blind unless `class` is set"; UI
  covers are pointed to Settings -> Devices & Services -> Entities ->
  Show As, as switches.md and binary-sensors.md do.
- cover.py build(): `cfg.get(CONF_DEVICE_CLASS) or cfg.get("device_class")`
  again, for symmetry with build_impulse(), switch.py, binary_sensor.py
  and sensor.py when a caller seeds hass.data without cover_schema.
- tests/test_cover_device_class.py: `strategy: venetian` with and
  without `class: shade`, an area `where: "1"` MyHOMEScopeCover with
  `class: curtain`, and a general `where: "0"` scope cover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Polish pushed as 7696194: Device class (class) is its own section after the cover types with the "unless class is set" qualifiers on the slat_tilt bullet and table row and the UI "Show As" pointer (same wording as switches.md / binary-sensors.md); build() reads cfg.get(CONF_DEVICE_CLASS) or cfg.get("device_class") again, symmetric with build_impulse() and the other platforms; four new parametrised cases (venetian → blind, venetian + class: shade → shade, area where: "1" + curtain and general where: "0" on MyHOMEScopeCover), 6 of 10 fail on base; Encyclopedia note on the SUBTYPE catalogue metadata added to the description. Suite 4489 passed, 100 % coverage, ruff / typing / standards / docs sync clean; branch merges cleanly on the current base.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

@Interstellar0verdrive raising your awaereness because of the sidepanel and maybe you also have some input for this

@Interstellar0verdrive Interstellar0verdrive left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving. My shutters have no class: set, so this is code review plus my own probes.

The schema only accepts the ten HA cover classes, so a typo fails validation instead of silently becoming a shutter, and treating the switch default as unset matches the impulse covers. A configured class survives slat tilt, promotion and demotion (I probed curtain through all three), scope covers get it through the same path, and covers without class are unchanged: still shutter, or blind with slat_tilt. My #702 probes give identical output.

On the side panel, which is @xtimmy86x's area: nothing here conflicts with it. Covers set up from the panel rely on "Show As" until the panel offers a class of its own, which the new docs section already explains; whether and when to add that is his call.

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What was requested vs. current state
Description updated to match the template + Fixes #39
Mermaid diagram fixed (NoClass restored) and validated
Polish items added in the latest commit:
Device class section promoted in docs/configuration/covers.md + “unless class is set” wording + UI “Show As” note
cfg.get(CONF_DEVICE_CLASS) or cfg.get("device_class") fallback in build()
Extra tests (venetian, venetian+shade, area/general scope covers)
Encyclopedia note about SUBTYPE only existing in offline catalogue metadata

All review threads are either outdated or non-blocking; the author addressed the blocking Mermaid issue and the optional polish.

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 31ea9fd into OpenWebNet-HA:v2-phase1-architecture Oct 24, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants