Add Motor output type with raise/lower/stop-only semantics#129
Merged
Conversation
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>
cdheiser
commented
Apr 12, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds proper support for the
MOTORoutput type. Previously,_parse_outputrouted bothSYSTEM_SHADEandMOTORto theShadeclass, which exposesOutput.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
_MotorizedOutputbase class that owns thestart_raise/start_lower/stopactions (OUTPUT actions 2/3/4), soShadeandMotorshare one implementation.Shadeis now a trivial subclass of_MotorizedOutput-- no public API change,isinstance(x, Shade)still works for existing consumers.Motor(_MotorizedOutput)class.set_level(and thelevelproperty setter by inheritance) raisesAttributeErrorwith a message pointing callers atstart_raise/start_lower/stop.AttributeErrorwas chosen overNotImplementedErrorbecause it matches Python's semantics for "this operation doesn't exist on this type" and plays nicely withhasattr()feature-detection._parse_outputnow routesSYSTEM_SHADEtoShadeandMOTORtoMotor.MOTORadded to theis_dimmableexclusion list.Shade.stop("Starts raising the shade" -> "Stops any in-progress raising or lowering")."1954") matching realDbXmlInfo.xmlformat, replacing descriptive strings like"uuid-shade"or"OUT-MOTOR-1".What is deliberately NOT in this PR
set_level(e.g., translatinglevel = 0tostart_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 hardAttributeErrorsurfaces 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
tests/test_motor.pywith unit tests covering:start_raise/start_lower/stop(#OUTPUT,<id>,{2,3,4})set_levelraisesAttributeErrorvia direct call and via thelevelproperty setterset_levelis rejectedis_dimmableisFalsefor motorsstart_raise/start_lower/stopdo not touch the cachedlast_level()(consistent with existingShadebehavior -- the repeater is the source of truth)handle_updatestill tracks level reports as the motor travelstests/test_parser.pydrivingLutronXmlDbParser.parse()with a nested<Areas><Area>document containingMOTOR,SYSTEM_SHADE, andINCoutputs side-by-side, asserting each is instantiated as the correct concrete type and thatMotoris non-dimmable. Test XML uses realistic attribute values from a realDbXmlInfo.xmldeployment.DbXmlInfo.xmlformatmypy --strictis clean on all 16 source files with no new ignores🤖 Generated with Claude Code