Skip to content

Add Motor output type with raise/lower/stop-only semantics#129

Merged
cdheiser merged 3 commits into
thecynic:masterfrom
cdheiser:motors
Apr 12, 2026
Merged

Add Motor output type with raise/lower/stop-only semantics#129
cdheiser merged 3 commits into
thecynic:masterfrom
cdheiser:motors

Conversation

@cdheiser

@cdheiser cdheiser commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds proper support for the MOTOR output type. Previously, _parse_output routed both SYSTEM_SHADE and MOTOR to the Shade class, which exposes Output.set_level. In practice, the Lutron repeater silently ignores zone-level commands on motors -- most notably at 0% and 100% -- even though the integration protocol documentation (page 8, action 1) claims motors accept levels from 0-100. Field reports from users confirm this divergence.

Changes

  • Extracted a private _MotorizedOutput base class that owns the start_raise / start_lower / stop actions (OUTPUT actions 2/3/4), so Shade and Motor share one implementation.
  • Shade is now a trivial subclass of _MotorizedOutput -- no public API change, isinstance(x, Shade) still works for existing consumers.
  • New Motor(_MotorizedOutput) class. set_level (and the level property setter by inheritance) raises AttributeError with a message pointing callers at start_raise / start_lower / stop. AttributeError was chosen over NotImplementedError because it matches Python's semantics for "this operation doesn't exist on this type" and plays nicely with hasattr() feature-detection.
  • _parse_output now routes SYSTEM_SHADE to Shade and MOTOR to Motor.
  • MOTOR added to the is_dimmable exclusion list.
  • Fixed a small docstring typo on Shade.stop ("Starts raising the shade" -> "Stops any in-progress raising or lowering").
  • Normalized all test UUIDs across the suite to use numeric strings (e.g. "1954") matching real DbXmlInfo.xml format, replacing descriptive strings like "uuid-shade" or "OUT-MOTOR-1".

What is deliberately NOT in this PR

  • Jog raise / jog lower (actions 18/19). Per the integration protocol doc, these are Quantum 2.5+ only -- note 7 excludes RadioRA 2 and the HomeWorks QS OUTPUT table (page 61) omits them entirely. Adding silently-no-op methods for the two systems that cover essentially all pylutron users seemed like a footgun. Happy to add them in a follow-up PR if a Quantum user ever needs them.
  • 4-stage jog (actions 20/21). Same reasoning, and would not map cleanly onto Home Assistant's cover entity anyway.
  • Boundary-aware set_level (e.g., translating level = 0 to start_lower()). Considered during design but rejected: the protocol doc doesn't restrict set-level by value, the behavior is a field bug, and quietly re-routing API calls would mask other bugs for callers. A hard AttributeError surfaces the incompatibility immediately.

Credits

Parser test XML adapted from @sergiobaiao's test case in #128. That PR took a different approach to the implementation, but the test fixture shape was useful for exercising the end-to-end parser pipeline, and credit is due.

Test plan

  • New tests/test_motor.py with unit tests covering:
    • Wire format for start_raise / start_lower / stop (#OUTPUT,<id>,{2,3,4})
    • set_level raises AttributeError via direct call and via the level property setter
    • No command is emitted when set_level is rejected
    • is_dimmable is False for motors
    • start_raise / start_lower / stop do not touch the cached last_level() (consistent with existing Shade behavior -- the repeater is the source of truth)
    • Inbound handle_update still tracks level reports as the motor travels
  • New end-to-end parser tests in tests/test_parser.py driving LutronXmlDbParser.parse() with a nested <Areas><Area> document containing MOTOR, SYSTEM_SHADE, and INC outputs side-by-side, asserting each is instantiated as the correct concrete type and that Motor is non-dimmable. Test XML uses realistic attribute values from a real DbXmlInfo.xml deployment.
  • All test UUIDs across the suite normalized to numeric strings matching real DbXmlInfo.xml format
  • Full test suite passes (69 tests total)
  • mypy --strict is clean on all 16 source files with no new ignores
  • Manual verification on real hardware (I don't have a Lutron motor to test against -- would appreciate a reviewer confirming wire behavior if they do)

🤖 Generated with Claude Code

cdheiser and others added 2 commits April 12, 2026 02:24
Motors (OutputType=MOTOR) previously reused the Shade class, which exposed
Output.set_level even though field reports show the Lutron repeater
silently ignores zone-level commands on motors -- most notably at the 0%
and 100% extremes -- despite what the integration protocol documentation
claims.

Introduce a private _MotorizedOutput base holding the shared raise/lower/
stop actions, and split Shade and Motor as sibling subclasses. Motor
overrides set_level to raise AttributeError so callers get an immediate
signal to use start_raise / start_lower / stop instead, and MOTOR is
excluded from is_dimmable. The parser now routes SYSTEM_SHADE to Shade
and MOTOR to Motor.

Includes unit tests covering the wire protocol for raise/lower/stop,
set_level rejection (direct, via the level setter, and at boundaries),
is_dimmable, level cache invariants, inbound handle_update, and parser
routing for all three output types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exercises LutronXmlDbParser.parse() with a nested <Areas><Area> document
containing MOTOR, SYSTEM_SHADE, and AUTO_DETECT outputs, and asserts that
each one is instantiated as the correct concrete type. This covers the
full parser pipeline, not just the internal _parse_output helper.

The XML pattern is adapted from @sergiobaiao's test case in thecynic#128.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread tests/test_parser.py Outdated
Comment thread tests/test_motor.py Outdated
Comment thread tests/test_motor.py Outdated
Real Lutron deployments use numeric string UUIDs (e.g. "1954", "714").
Update all test files to use this format instead of descriptive strings
like "uuid-shade" or "OUT-MOTOR-1". Also remove redundant motor tests
(boundary set_level, parser routing) per PR review, and add XML comment
clarifying the non-motorized output fixture entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cdheiser cdheiser merged commit 4b87cf9 into thecynic:master Apr 12, 2026
7 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.

1 participant