-
Notifications
You must be signed in to change notification settings - Fork 28
Developer Triage Rules
The diagnostics-triage engine turns a cover's config + last diagnostics snapshot into a list of findings. One declarative table β TRIAGE_RULES in custom_components/adaptive_cover_pro/diagnostics/triage.py β drives both the in-product Troubleshoot step and the offline scripts/triage_json.py run. Adding a check is intentionally small: four edits and it lights up on both surfaces.
-
One
TriageRulerow inTRIAGE_RULES(diagnostics/triage.py), plus itscheckfunction and a new member on theTriageCodeenum inconst.py. -
One English template β add the leaf to
_TRIAGE_TEMPLATES_ENintroubleshoot_i18n.pyand the byte-identical leaf introubleshoot_i18n/en.json. -
One JSON leaf per shipped language β add the translated leaf to
troubleshoot_i18n/de.jsonandtroubleshoot_i18n/fr.json(draft placeholder-exact, then run theacp-translateskill to polish). -
One test block in
tests/test_triage_rules.pyβ a fires case, a near-miss, and (for per-entity rules) an N-entity case. RUNTIME rules also get a real-DiagnosticsBuildercontract test intests/test_triage_contract.py.
Say you want to flag a max tracking elevation so low it truncates the sun-tracking window.
const.py β a stable code:
class TriageCode(StrEnum):
...
TRACKING_WINDOW_TRUNCATED = "triage.tracking_window_truncated"diagnostics/triage.py β a check that yields param dicts, and a row:
def _check_tracking_window_truncated(data: Mapping) -> Iterable[Mapping]:
options = _get(data, "options")
if not isinstance(options, Mapping):
return
max_elev = options.get(CONF_MAX_ELEVATION)
if isinstance(max_elev, (int, float)) and not isinstance(max_elev, bool):
if max_elev <= 25:
yield {"max_elevation": max_elev}
TriageRule(
code=TriageCode.TRACKING_WINDOW_TRUNCATED,
severity=Severity.WARNING,
inputs=RuleInput.CONFIG,
fix_step="sun_tracking",
wiki="Troubleshooting-Findings#tracking-window-truncated",
issues=(972,),
check=_check_tracking_window_truncated,
),troubleshoot_i18n.py + en.json β the {max_elevation} template, byte-identical in both.
de.json / fr.json β the same leaf translated, same {max_elevation} placeholder.
tests/test_triage_rules.py β a fires case (max_elevation: 25), a near-miss (26), and a key-absent case.
Every rule declares an inputs flag:
-
RuleInput.CONFIGβ reads onlyoptions(and, in the config flow,capabilities/axis_requirements). Deterministic, coordinator-free. -
RuleInput.RUNTIMEβ reads the diagnostics payload (decision trace, control status, cover commands, β¦). -
RuleInput.CONFIG | RuleInput.RUNTIMEβ a mixed row.
run_triage(data, only=RuleInput.CONFIG) keeps a rule iff every one of its flags is in only (subset semantics): only=CONFIG drops both RUNTIME and mixed rows. The config summary and setup wizard pass only=CONFIG; the troubleshooter passes None (everything). Pick CONFIG only when the check would give the same answer without a running coordinator.
Read every payload value through _get(data, "a.b.c") β a missing key or a non-mapping segment yields None, never an exception. run_triage also wraps each check in a try/except, so a single bad rule can never break triage:
run_triage({}) == []This is a hard invariant. A check that reaches into data["x"]["y"] directly (bypassing _get) and raises on a partial payload is a bug β use _get, or guard with isinstance(..., Mapping).
A check receives the view mapping and yields zero or more param dicts β one Finding per yielded dict. Single-shot rules yield 0 or 1; per-entity rules yield N (one per offending entity). Never return a bool or a single dict; yield (or return early to yield nothing).
Every rule's wiki field points at the single canonical findings page with a per-code anchor:
Troubleshooting-Findings#<code without the "triage." prefix, underscores β hyphens>
So triage.tracking_window_truncated β Troubleshooting-Findings#tracking-window-truncated. Add a matching ### Tracking window truncated section to Troubleshooting Findings β the heading must GitHub-slugify to exactly that anchor.
These run over the whole table in tests/test_triage_rules.py:
-
test_rule_table_covers_every_triage_codeβ the rule table andTriageCodeare a bijection. Add a code without a row (or vice versa) and this fails. -
test_rule_wiki_points_at_canonical_findings_pageβ thewikifield matches the anchor scheme. -
test_rule_wiki_anchor_resolves_on_findings_pageβ the anchor is a real###heading onTroubleshooting-Findings.md(skipped if no sibling wiki checkout). -
test_rule_wiki_anchor_formatβ thewikistring isPage#anchorshaped. -
test_rule_fix_step_is_reachable_from_cover_menuβfix_stepisNoneor a real cover-options step. -
test_rule_issues_non_empty_int_tupleβissuesis a non-empty tuple of ints. -
test_rule_template_exists_in_code_defaults_and_en_jsonβ the template leaf exists in both the code dict anden.json.
Plus i18n parity in tests/test_troubleshoot_i18n.py: DE/FR must carry the same leaves with the same placeholder set as English.
Some checks were considered and left out on purpose β do not re-add them:
-
delta_too_small/dry_runas skip faults. These are expected steady-state skips, not problems. The skip rules (SKIP_*) fire only on genuine faults (service_call_failed,no_capable_service,cover_unavailable). - Testimony rules β "manual override won but the user says they didn't touch it". The engine cannot know user intent; keep it to observable state.
-
Cover-type string branches.
diagnostics/triage.pymust never compare a cover-type string or read a hardcodedcaps.get("has_*")literal β that boundary is enforced bytests/test_cover_types/test_axes.py. When a rule needs cover-type-specific data (rule 13's capability requirements), fold a policy-derived field into the view at the HA boundary (the troubleshoot step / offline adapter) and have the check read it as plain data.
See also CODING_GUIDELINES.md and the For Developers hub.
π Home Β· β¨ Features Β· π° What's New
π Getting Started
- Installation
- Migrating from Custom Repository
- Migrating from Adaptive Cover
- First-Time Setup
- Building Profile β start here if you have multiple covers
- Cover Groups
- Copy Settings to Other Covers
- Cover Types
π§ Core Concepts
π Cover Types
- Vertical
- Horizontal
- Oscillating Awning
- Roof / Skylight Window
- Tilt
- Louvered Roof
- Sliding Curtain
- Day/Night Shade
- Dual Panel
- Venetian (Dual-Axis)
βοΈ Configuration
- Sun Tracking
- Position
- Position Matching
- Travel Time Calibration
- Glare Zones
- Automation
- Manual Override
- Command Queue
- Custom Position
- Force Override
- Weather Safety
- Climate
- Templated Thresholds
- Template Self-References
- Blindspot
- Summary Screen
- Debug & Diagnostics
π Entities & Services
- Entities
- Proxy Cover Entity
- Position Verification
- My Position Support (Somfy RTS)
- Runtime Configuration Services
π οΈ Operations
- Known Limitations
- Hardware Compatibility
- Troubleshooting
- Troubleshooting Findings
- Diagnostic Sensors
- Tips and Tricks
π§ Advanced Use Cases
- Overview
- Dynamic Temperature Thresholds
- Dynamic Tracking Window
- Bedroom Sleep Mode
- Handling Variable Cloud Cover
- Indoor Lux Sensor Behind the Cover
- Venetian Tilt-Only on Overcast Days
- Forecast-Based Shading
- Custom Position When Sun in FOV
- Suppress Closing While a Door Is Open
- Keep a Blind Clear of the Sill
π¨ Dashboard
- Dashboard Cards
- Panel Card
- Tile Card
- Sky Compass Card
- Decision Strip Card
- Solar Chart Card
- History Card
π§ͺ Testing & Simulation
π Reference
π©βπ» For Developers