Skip to content

v1.0 Milestone: Complete Plugin Refactoring#30

Closed
simons-plugins wants to merge 27 commits into
mainfrom
master
Closed

v1.0 Milestone: Complete Plugin Refactoring#30
simons-plugins wants to merge 27 commits into
mainfrom
master

Conversation

@simons-plugins

@simons-plugins simons-plugins commented Feb 3, 2026

Copy link
Copy Markdown
Owner

v1.0 Milestone: Complete Plugin Refactoring

Summary

Transformed the 1635-line monolithic Netro Sprinklers Indigo plugin into a maintainable, modular architecture with comprehensive test coverage and production-ready reliability features.

Timeline: February 1-3, 2026 (2 days)
Scope: 6 phases, 15 plans, 47 requirements (100% coverage)
Tag: v1.0

Key Accomplishments

  • Eliminated silent failures - Fixed all bare exception handlers (5 locations), added comprehensive logging with full tracebacks
  • Modular architecture - Extracted monolith into 7 focused modules (constants, exceptions, utils, api_client, validators, device_handlers, plugin)
  • Proactive throttle management - API rate limit prevention with state persistence across plugin restarts
  • Code quality improved - Pylint score from 8.75 → 9.90 average across all modules
  • Test coverage tripled - From 70% (64 tests) to 95% (247 tests), +186 tests covering edge cases and error paths
  • Production-ready - All E2E flows verified complete, zero critical gaps

Statistics

  • Files modified: 12 files (+5,290 insertions, -890 deletions)
  • Code:
    • Plugin: 2,973 lines across 7 modules
    • Tests: 3,062 lines (247 tests)
  • Quality:
    • Pylint: 9.90 average (7 modules with 9.0+)
    • Test coverage: 95% on testable modules
    • Zero critical issues

Technical Details

Module Structure

Module Lines Purpose Test Coverage
constants.py 117 API URLs, defaults, enums 100%
exceptions.py 151 Custom exception hierarchy 100%
utils.py 61 Timestamp parsing, helpers 100%
validators.py 510 Pure validation functions 91%
api_client.py 644 HTTP client, throttle mgmt 90%
device_handlers.py 452 Device update logic 98%
plugin.py 1,038 Slim coordinator N/A (Indigo runtime)

Phase Breakdown

  1. Phase 1: Foundation & Critical Fixes (3 plans) - Fixed silent failures, established workflow
  2. Phase 2: Base Modules (2 plans) - Extracted constants, exceptions, utils
  3. Phase 3: API Client (3 plans) - Isolated API layer with throttle management
  4. Phase 4: Validators (2 plans) - Extracted validation logic
  5. Phase 5: Device Handlers (2 plans) - Extracted device update logic
  6. Phase 6: Testing Expansion (3 plans) - Expanded coverage to 95%

Milestone Artifacts

All planning artifacts archived to .planning/milestones/:

  • v1-ROADMAP.md - Full phase details
  • v1-REQUIREMENTS.md - All 47 requirements (100% complete)
  • v1-MILESTONE-AUDIT.md - Integration verification report

Test Plan

  • ✅ All 247 tests passing
  • ✅ Pylint 9.0+ on all modules
  • ✅ E2E flows verified (3/3 complete)
  • ✅ Integration tests passed
  • ✅ Manual testing with real hardware ("Clark Castle Spark" controller)

Breaking Changes

None - All existing functionality preserved and enhanced.

Technical Debt

4 minor items documented (non-blocking):

  1. plugin.py line count (1038 vs 450 target) - architecturally justified
  2. API version detection implicit via schema validation
  3. 2 unused exception classes (cleanup in v2)
  4. PR workflow needs human verification

Related Issues

Closes #24, #25, #26


🤖 Generated with GSD milestone completion workflow

See .planning/MILESTONES.md for full milestone summary.

Summary by CodeRabbit

  • New Features

    • Added device handlers to transform API responses into safe device state updates with improved error handling and logging.
  • Refactor

    • Plugin update flow delegated to per-device update paths, simplifying orchestration and centralizing HTTP error handling.
  • Tests

    • Large expansion of unit and edge-case tests (timeouts, HTTP errors, Unicode, malformed data) and shared fixtures; coverage enforcement added.
  • Chores

    • Consolidated milestone/roadmap/requirements planning and audit docs; plugin version bumped.

simons-plugins and others added 24 commits February 2, 2026 22:24
Phase 05: Device Handlers
- Implementation decisions documented
- Phase boundary established

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Phase 5: Device Handlers
- Standard stack identified (handler pattern with state dict return)
- Architecture patterns documented (SprinklerHandler, WhispererHandler)
- Pitfalls catalogued (indigo.Dict handling, offline detection)
- Code examples provided from existing codebase patterns

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Phase 05: Device Handlers
- 2 plans in 2 waves
- Extract device update logic to device_handlers.py
- Reduce plugin.py to ~400 lines
- Ready for execution
…WhispererHandler

- SprinklerHandler transforms device info, schedules, moistures API responses
- WhispererHandler transforms sensor data API responses
- Pure Python handlers with no Indigo imports for testability
- Handlers return state dicts for updateStatesOnServer()
- Error handling returns error states when API data is malformed
- Pylint score: 9.85/10
Tasks completed: 3/3
- Create device_handlers.py with SprinklerHandler
- Add WhispererHandler to device_handlers.py
- Run Pylint and verify 9.85/10 score

SUMMARY: .planning/phases/05-device-handlers/05-01-SUMMARY.md
- Import SprinklerHandler and WhispererHandler
- Delegate state transformation to handlers in _update_from_netro
- Extract _update_sprinkler_device and _update_whisperer_device methods
- Remove callMoisturesAPI and callSensorAPI (now in handlers)
- Remove unused imports (itemgetter, get_key_from_dict)
- Clean up whitespace and pylint disables

Handlers process API responses and return state dicts; plugin
applies them to Indigo devices. Clean separation of concerns.
- Add 50 tests covering SprinklerHandler and WhispererHandler
- Test process_device_info with online/offline devices
- Test process_schedules with executing/valid/empty schedules
- Test process_moistures with zone data and edge cases
- Test extract_zone_info for property building
- Test process_sensor_data with readings and empty states
- Add 'handlers' marker to pytest.ini

93% coverage on device_handlers.py module
Tasks completed: 3/3
- Refactor plugin.py to use device handlers
- Create comprehensive tests for device handlers (50 tests)
- Verify full test suite and Pylint (197 tests, 9.69/10)

SUMMARY: .planning/phases/05-device-handlers/05-02-SUMMARY.md
Phase 6: Testing Expansion
- Standard stack: pytest, pytest-cov, pytest-mock (already configured)
- Coverage gap analysis: 59 new tests identified
- Testing patterns: AAA, parametrize, side_effect mocking
- Fixture strategy: shared conftest.py recommended
- Priority: network errors > Whisperer > malformed JSON > edge cases
Phase 06: Testing Expansion
- 3 plan(s) in 1 wave(s)
- 3 parallel, 0 sequential
- Ready for execution
- Add mock_logger fixture used across all test modules
- Add sample_api_response fixture for API response structure
- Add mock_prefs fixture for preference testing
- Fixtures auto-discovered by pytest for all test files
- Add parametrized test for 6 unicode zone names (emoji, CJK, RTL, accents)
- Add unicode device name test
- Add unicode timestamp test for WhispererHandler
- Add test for completely missing meta section

Covers unicode handling for zone names, device names, and timestamps.
8 unicode edge case tests added.
- test_make_request_timeout_on_post - POST timeout handling
- test_make_request_timeout_on_put - PUT timeout handling
- test_make_request_timeout_suppresses_repeated - Error suppression
- test_make_request_timeout_resets_after_success - State reset
- test_make_request_timeout_preserves_throttle_state - Throttle preservation
- test_make_request_timeout_with_custom_timeout_value - Timeout parameter
- test_make_request_read_timeout_vs_connect_timeout - Timeout subclasses
- test_get_device_info_timeout - Convenience method propagation
- Exception handling tests (KeyError, AttributeError, TypeError)
- Null value tests (moisture, celsius, battery_level)
- Boundary tests (battery 0/100, large reading IDs)
- Edge cases (unicode, missing fields, extra fields)

Covers WhispererHandler exception paths (lines 444-452).
27 Whisperer tests now passing (12 original + 15 new).
- test_handle_http_error_500_no_json_body - 500 with HTML response
- test_handle_http_error_500_with_json_error - 500 with JSON error
- test_handle_http_error_502_bad_gateway - 502 error handling
- test_handle_http_error_503_service_unavailable - 503 error handling
- test_handle_http_error_504_gateway_timeout - 504 error handling
- test_handle_http_error_response_none - HTTPError edge case
…T-06, TEST-07)

- Add 3 empty data tests (data key empty, missing zones key, completely empty response)
- Add 6 schedule parsing tests (float string timestamp, multiple executing, all invalid status, duration zero/negative, zone name fallback)
- Add moisture most recent date filtering test
- Add all zones disabled test

12 edge case tests added for empty data and schedule parsing.
…-08, TEST-09, TEST-10)

- Add 2 handler exception safety tests (KeyError handling in both handlers)
- Add 2 API client state isolation tests (multiple device requests, token tracking)
- Update pytest.ini with fail_under = 85 coverage threshold
- Verify existing thread safety tests (error suppression reset, throttle check)

Coverage achieved: 95% overall, 98% device_handlers, 90% api_client, 91% validators.
6 thread safety tests total (2 new + 2 existing verified + 2 new API client tests).
- SprinklerHandler: data_is_list, device_key_is_null
- SprinklerHandler schedules: schedules_is_dict
- SprinklerHandler moistures: moistures_is_string
- WhispererHandler: sensor_data_is_int, missing_status_key

Verifies handlers gracefully handle wrong-type API responses.
84 device_handlers tests now passing (50 original + 34 new).
Device handlers coverage: 98%.
Tasks completed: 3/3
- Create tests/conftest.py with shared fixtures
- Add 8 network timeout tests (TEST-02)
- Add 6 HTTP 5xx error tests (TEST-03)

SUMMARY: .planning/phases/06-testing-expansion/06-01-SUMMARY.md
- Add 06-02 and 06-03 SUMMARY.md (missed in executor commits)
- Add phase 5 VERIFICATION.md (missed in previous phase)
- Update STATE.md with phase 6 progress
Phase 6 complete - all success criteria met:
- Test coverage: 95% average on testable modules (exceeds 87% target)
- 247 total tests (up from 197, +25% growth)
- Whisperer coverage: 98% (exceeds 85% target)
- Network error tests: 14 tests covering timeouts and HTTP 5xx
- Edge case tests: 37 tests (unicode, empty data, schedule parsing)
- Coverage threshold: pytest.ini configured with fail_under = 85

All 6 phases complete. Milestone ready for audit.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Status: tech_debt (all requirements met, minor tech debt items)
- Requirements: 47/47 (100%)
- Phases: 6/6 (100%)
- Integration: passed
- E2E flows: 3/3 complete
- Test coverage: 95% (target: 87%)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Archived:
- milestones/v1-ROADMAP.md (full phase details, 6 phases)
- milestones/v1-REQUIREMENTS.md (all 47 requirements complete)
- milestones/v1-MILESTONE-AUDIT.md (integration verification)

Deleted (fresh for next milestone):
- ROADMAP.md
- REQUIREMENTS.md

Updated:
- MILESTONES.md (new v1.0 entry)
- PROJECT.md (requirements → Validated, context updated)
- STATE.md (reset for next milestone)

Milestone v1.0 delivered:
- 6 phases, 15 plans, 47 requirements (100% coverage)
- Modular architecture (7 focused modules)
- 247 tests, 95% coverage (up from 64 tests, 70%)
- Pylint 9.90 average (up from 8.75)
- All E2E flows verified complete

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Feb 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a v1.0 refactor: new pure‑Python device handlers to convert Netro API responses, plugin.py refactored to delegate per‑device updates and centralize HTTP error handling, large test-suite and fixture additions, pytest config updates, and many planning/milestone documents added or archived to record the milestone.

Changes

Cohort / File(s) Summary
Device Handlers
Netro Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py
New pure‑Python module adding SprinklerHandler and WhispererHandler to parse API responses into Indigo state dicts (device info, schedules, moistures, sensor readings). Supports logger injection, robust defaults, and returns structured state tuples; no Indigo runtime imports.
Plugin Refactor
Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py
Refactors per‑device update flow: adds _update_sprinkler_device(), _update_whisperer_device(), and _handle_http_error(); instantiates handlers in Plugin.init; delegates state transformation to handlers and removes inlined parsing.
Tests & Fixtures
tests/conftest.py, tests/test_device_handlers.py, tests/test_api_client.py
Adds shared pytest fixtures and extensive unit tests: handler tests cover edge cases, unicode, malformed data; api_client tests cover timeouts, HTTP errors, throttle behavior, and state consistency.
Test Configuration
pytest.ini
Adds handlers pytest marker; sets coverage fail_under = 85, enables show_missing, and expands exclude_lines patterns.
Planning & Milestone Artifacts (added/updated)
.planning/MILESTONES.md, .planning/PROJECT.md, .planning/STATE.md, .planning/milestones/v1-MILESTONE-AUDIT.md, .planning/milestones/v1-REQUIREMENTS.md, .planning/milestones/v1-ROADMAP.md, .planning/phases/05-device-handlers/*, .planning/phases/06-testing-expansion/*
Adds archived and active milestone documents (audit, roadmap, requirements, plans, summaries, verification, research) reflecting the v1.0 refactor, phase planning, verification results, and next steps.
Planning Docs Removed
.planning/REQUIREMENTS.md, .planning/ROADMAP.md
Removed older planning documents replaced by archived/milestone-specific artifacts.
Plugin Metadata
Netro Sprinklers.indigoPlugin/Contents/Info.plist
Bumped PluginVersion from 2025.1.112025.1.13.

Sequence Diagram(s)

sequenceDiagram
    participant Plugin
    participant Handler as Device Handler
    participant Indigo as Indigo Device
    participant Logger

    rect rgba(100,150,255,0.5)
    Note over Plugin,Logger: Refactored v1.0 Flow
    end

    Plugin->>Handler: process_device_info(api_response, serial)
    Handler->>Handler: extract & validate fields
    Handler->>Logger: log malformed data / defaults
    Handler-->>Plugin: (state_list, is_online, device_data)

    Plugin->>Handler: process_schedules(api_response)
    Handler->>Handler: parse schedules & compute next
    Handler-->>Plugin: (schedule_states, next_schedule)

    Plugin->>Handler: process_moistures(api_response)
    Handler-->>Plugin: moisture_states

    Plugin->>Indigo: updateStatesOnServer(state_list)
    Indigo->>Indigo: apply batched updates

    rect rgba(200,100,100,0.5)
    Note over Plugin,Logger: Prior Inline Flow (pre-refactor)
    end

    Note over Plugin: plugin.py parsed device info, schedules, moistures inline with scattered error handling
    Plugin->>Logger: dispersed error logs
    Plugin->>Indigo: apply per-item updates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through logs and states tonight,

I parceled API crumbs till they looked right,
Handlers dress data, tests clap in a row,
Docs filed, milestone shipped — off I go! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'v1.0 Milestone: Complete Plugin Refactoring' clearly summarizes the main change: a comprehensive refactoring of the plugin to achieve v1.0 milestone with modular architecture.
Linked Issues check ✅ Passed Issue #24 requires fixing silent exception handlers (CRIT-01 to CRIT-04). The changeset shows extensive exception handling improvements: removal of bare exception patterns, addition of structured logging with tracebacks throughout plugin.py, and proper error handling in device_handlers.py and api_client.py, directly addressing all critical requirements.
Out of Scope Changes check ✅ Passed All changes align with the stated v1.0 refactoring objectives: modularization into seven modules, logging improvements, test coverage expansion (70%→95%), API throttle management, and the removal of silent exception handlers. Planning documentation updates and version bump are appropriate supporting changes. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 97.73% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch master

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.planning/phases/06-testing-expansion/06-UAT.md (1)

112-113: ⚠️ Potential issue | 🟡 Minor

Use consistent bold marker style for strong emphasis.
markdownlint flags strong-style mismatch; switch underscores to asterisks for bold.

🤖 Fix all issues with AI agents
In @.planning/milestones/v1-MILESTONE-AUDIT.md:
- Around line 58-60: The "Requirements Coverage" section has a heading level
jump: the first subheading under "Requirements Coverage" is currently an H3 but
should be H2 to avoid the H2 → H3 increment skip; update the offending heading
(the first subheading immediately beneath "Requirements Coverage", e.g., the
line showing "### Overall Score: 47/47 (100%)") to use H2 (## Overall Score:
47/47 (100%)) so markdownlint no longer flags the increment skip.

In @.planning/milestones/v1-REQUIREMENTS.md:
- Around line 16-18: The markdown has a heading level jump from H1 to H3: change
the "### Critical Fixes" heading to an H2 (replace "### Critical Fixes" with "##
Critical Fixes") or insert a missing H2 section before it so headings increment
correctly; update the "Critical Fixes" header text in the file where the exact
string "### Critical Fixes" appears.

In @.planning/milestones/v1-ROADMAP.md:
- Line 149: The paragraph starting with "Expanded test coverage from 70% (64
tests) to 95% (247 tests)." uses several sentences that begin with the same
construction "Added ..." which LanguageTool flags; pick one of the sentences
that starts with "Added" (for example the sentence "Added 15 Whisperer sensor
tests (achieved 98% coverage, exceeding 85% target).") and rephrase it to vary
the opening—e.g., merge it with the previous sentence, start with a gerund
("Including 15 Whisperer sensor tests..."), or lead with the result ("This added
98% coverage for Whisperer sensors...") so the paragraph reads less repetitive
while preserving all facts about test counts, types, conftest.py, pytest.ini,
and that all 247 tests are passing.

In @.planning/phases/05-device-handlers/05-02-SUMMARY.md:
- Around line 108-114: The file contains a heading-level jump flagged by
markdownlint: the "### Line Count Target Not Achieved" (and other `###`
headings) must be nested under a `##` heading rather than directly under a
top-level `#`; update the header hierarchy so each `###` is preceded by an
appropriate `##` (or reduce `#` to `##`/increase `##` to encompass the `###`) so
heading levels increment by one and the document structure is semantically
correct.

In @.planning/phases/05-device-handlers/05-CONTEXT.md:
- Line 16: The "State Update Patterns" heading uses three hashes (###) but
should be one level higher; locate the heading text "State Update Patterns" and
change the markdown heading from "### State Update Patterns" to "## State Update
Patterns" so heading levels increment correctly.

In @.planning/phases/05-device-handlers/05-VERIFICATION.md:
- Around line 112-113: Update the Markdown in
.planning/phases/05-device-handlers/05-VERIFICATION.md to use asterisks for
strong emphasis instead of underscores: replace any occurrences of
underscore-style bold (e.g., _WIRED_ or __WIRED__) with asterisk-style bold
(**WIRED**) in the table rows that reference plugin.py and the device handler
symbols (device_handlers.SprinklerHandler, device_handlers.WhispererHandler) and
ensure other references to __init__, _update_sprinkler_device, and
_update_whisperer_device retain their delimiters (do not convert single-lead
underscores used for identifiers).

In @.planning/phases/06-testing-expansion/06-01-SUMMARY.md:
- Around line 107-121: The document uses mixed strong-emphasis markers;
normalize all bold/strong emphasis to asterisk style (**) to match the rest of
the doc by replacing any underscore-based strong emphasis (e.g., _text_ or
__text__) with the asterisk equivalents around the affected phrases such as the
entries under "Rule 1 - Bug" (the lines mentioning "Fixed timeout parameter test
approach" and "Fixed HTTP error logging assertion") so every strong emphasis in
the file uses **double asterisks** consistently.

In @.planning/phases/06-testing-expansion/06-02-PLAN.md:
- Around line 112-113: The file uses underscore-based strong emphasis (e.g.,
__text__) which triggers MD050; replace all occurrences of underscore-based bold
with asterisk-based bold (e.g., **text**) in this document—look for instances
near the "Pattern for exception tests:" heading and inside the adjacent
markdown/```python``` blocks and update them to use **...** consistently.

In @.planning/phases/06-testing-expansion/06-03-PLAN.md:
- Around line 114-116: The markdown line containing response["data"]["device"]
is being interpreted as a reference link label; wrap that bracketed key path in
backticks (e.g., `response["data"]["device"]`) in the test description for
test_process_device_info_zones_key_missing (TestSprinklerHandlerDeviceInfo) so
the parser treats it as inline code and the MD052 warning is resolved.

In @.planning/phases/06-testing-expansion/06-RESEARCH.md:
- Around line 70-88: The fenced code blocks under "Current Test Structure" and
"Recommended Additions" are missing language identifiers; update each
triple-backtick block (the two ASCII tree samples) to include an appropriate
language tag like ```text or ```bash so markdownlint stops flagging them—locate
the blocks in .planning/phases/06-testing-expansion/06-RESEARCH.md around the
"Current Test Structure" and "Recommended Additions" headings and add the
language identifier to the opening ``` lines.
- Around line 624-627: Pick one spelling variant and normalize it throughout the
document; replace occurrences of "parametrization" (as seen in the list item "-
pytest-dev/pytest GitHub - fixture patterns and parametrization") with your
chosen variant ("parameterization" or "parametrization") everywhere (headings,
lists, link text, and surrounding prose), run a spellcheck/LanguageTool pass to
catch mixed uses, and commit the unified spelling.
- Around line 31-43: Add blank lines before and after the markdown tables shown
under the "### Core" and "### Supporting" headings so they comply with
markdownlint; specifically, ensure there's an empty line above the first table
(the line starting with "| Library | Version | Purpose | Why Standard |") and an
empty line after each table block so both the Core and Supporting tables are
separated from surrounding text.

In @.planning/phases/06-testing-expansion/06-VERIFICATION.md:
- Around line 170-205: In the verification doc (section headed "Coverage
Results:" and the "Testable Modules (plugin.py excluded):" table), fix Markdown
style by replacing the phrase "completely empty" with a shorter phrase such as
"empty", ensure there is a blank line before and after each markdown table so
they render correctly, and add a language tag to the fenced code block that
shows the pytest command (change the triple-backtick block currently labeled
```bash to include the tag `bash` if missing or ensure it is present) so the
code block is properly highlighted; update the same changes consistently for the
other table under "Untestable Module" referenced later in the file.

In `@tests/test_device_handlers.py`:
- Around line 367-372: The test should not expect an AttributeError from
process_device_info; instead assert the handler gracefully handles malformed
payloads by returning an empty/error state and logging an error. Replace the
pytest.raises(AttributeError) in test_process_device_info_device_key_is_null
with assertions that sprinkler_handler.process_device_info(response, "ABC123")
returns None or an empty device result (match your handler's non-throwing
contract) and that mock_logger.error was called; apply the same change to the
other malformed-payload tests in this file that currently expect exceptions (the
similar tests later in the file).
🧹 Nitpick comments (10)
tests/test_device_handlers.py (2)

6-36: Prefer shared fixtures and a single sys.path injection.

tests/conftest.py already adds the Server Plugin directory and provides mock_logger (including .exception). Keeping duplicates here risks divergence and extra maintenance. Consider relying on the shared fixtures instead.

♻️ Suggested cleanup
-import sys
-from pathlib import Path
-from unittest.mock import Mock
-
 import pytest
 
-# Add Server Plugin directory to path for imports
-SERVER_PLUGIN_DIR = (
-    Path(__file__).parent.parent
-    / "Netro Sprinklers.indigoPlugin"
-    / "Contents"
-    / "Server Plugin"
-)
-sys.path.insert(0, str(SERVER_PLUGIN_DIR))
-
 from device_handlers import SprinklerHandler, WhispererHandler
 
@@
-@pytest.fixture
-def mock_logger():
-    """Create a mock logger for testing."""
-    logger = Mock()
-    logger.debug = Mock()
-    logger.info = Mock()
-    logger.warning = Mock()
-    logger.error = Mock()
-    return logger

181-188: Silence Ruff “unused unpacked variable” warnings.

Ruff flags many unused unpacked values in this file; prefer _/_name for unused items to keep lint clean.

🔧 Example adjustment
-        states, is_online, device_data = sprinkler_handler.process_device_info(
+        states, is_online, _device_data = sprinkler_handler.process_device_info(
             sample_device_info_response, "ABC123456789"
         )
tests/test_api_client.py (1)

6-52: Reuse shared conftest fixtures and a single sys.path injection.

tests/conftest.py already provides mock_logger, mock_prefs, and the Server Plugin import path. Keeping duplicates here increases drift risk (e.g., missing .exception). Consider relying on the shared fixtures instead.

♻️ Suggested cleanup
-import sys
 from pathlib import Path
@@
-# Add Server Plugin directory to path for imports
-SERVER_PLUGIN_DIR = (
-    Path(__file__).parent.parent
-    / "Netro Sprinklers.indigoPlugin"
-    / "Contents"
-    / "Server Plugin"
-)
-sys.path.insert(0, str(SERVER_PLUGIN_DIR))
-
 from api_client import NetroAPIClient, TOKEN_PAUSE_THRESHOLD, TOKEN_WARNING_THRESHOLD
 from exceptions import ThrottleDelayError, NetroAPIError
@@
-@pytest.fixture
-def mock_logger():
-    """Create a mock logger for testing."""
-    logger = Mock()
-    logger.debug = Mock()
-    logger.info = Mock()
-    logger.warning = Mock()
-    logger.error = Mock()
-    return logger
-
-
-@pytest.fixture
-def mock_prefs():
-    """Create mock prefs getter/setter for testing."""
-    prefs_data = {}
-
-    def prefs_getter():
-        return prefs_data
-
-    def prefs_setter(key, value):
-        prefs_data[key] = value
-
-    return prefs_getter, prefs_setter, prefs_data
Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py (5)

191-193: Use logger.exception to automatically include traceback.

At Line 192, logger.error is used but the traceback is manually formatted on the next line. Using logger.exception would automatically include the traceback, making the code cleaner and ensuring the full exception chain is captured.

♻️ Proposed fix
         except Exception as exc:
-            self.logger.error(f"unexpected error updating netro devices: {exc.__class__.__name__}")
-            self.logger.debug(f"traceback:\n{traceback.format_exc(10)}")
+            self.logger.exception(f"unexpected error updating netro devices: {exc.__class__.__name__}")

231-235: Consider catching more specific exceptions for schedule API errors.

The bare except Exception here catches all errors including programming bugs. Since the handler already catches KeyError/TypeError for malformed data, this outer catch could be narrowed to network-related exceptions.

♻️ Suggested refinement
-            except Exception:
+            except (requests.exceptions.RequestException, ThrottleDelayError):
                 update_list.append(
                     {"key": "activeSchedule", "value": "Error getting current schedule"})
                 self.logger.debug(f"API error: \n{traceback.format_exc(10)}")
                 self._fireTrigger("getScheduleCall")

269-272: Use logger.exception for automatic traceback capture.

Similar to the earlier comment, using logger.exception would automatically include the full traceback without needing the separate debug call.

♻️ Proposed fix
         except Exception:
-            self.logger.error("Error getting user data from Netro via API.")
-            self.logger.debug(f"API error: \n{traceback.format_exc(10)}")
+            self.logger.exception("Error getting user data from Netro via API.")
             self._fireTrigger("personInfoCall")

317-319: Use logger.exception for consistent traceback logging.

Same pattern applies here - using logger.exception would automatically capture the traceback.

♻️ Proposed fix
         except Exception:
-            self.logger.error(f"error getting sensor data from netro api for device \"{dev.name}\"")
-            self.logger.debug(f"API error: \n{traceback.format_exc(10)}")
+            self.logger.exception(f"error getting sensor data from netro api for device \"{dev.name}\"")

321-348: Consider using logger.exception to reduce repetition.

The method has multiple paths that log the same error message with traceback. Using logger.exception would simplify this and ensure consistent traceback capture.

♻️ Suggested simplification
     def _handle_http_error(self, exc):
         """Handle HTTP errors with appropriate logging.

         Args:
             exc: HTTPError exception to handle
         """
         if hasattr(exc, 'response') and exc.response is not None:
             try:
                 error_data = exc.response.json()
                 if error_data.get("status") == "ERROR":
                     errors = error_data.get("errors", [])
                     recognized_codes = {1, 3}  # invalid key, rate limit
                     is_recognized = any(
                         error.get("code") in recognized_codes
                         for error in errors
                     )
                     if not is_recognized:
-                        self.logger.error("error getting user data from netro api")
-                        self.logger.debug(f"API error: \n{traceback.format_exc(10)}")
+                        self.logger.exception("error getting user data from netro api")
-                else:
-                    self.logger.error("error getting user data from netro api")
-                    self.logger.debug(f"API error: \n{traceback.format_exc(10)}")
+                    return
+                self.logger.exception("error getting user data from netro api")
             except (ValueError, AttributeError):
-                self.logger.error("error getting user data from netro api")
-                self.logger.debug(f"API error: \n{traceback.format_exc(10)}")
+                self.logger.exception("error getting user data from netro api")
         else:
-            self.logger.error("error getting user data from netro api")
-            self.logger.debug(f"API error: \n{traceback.format_exc(10)}")
+            self.logger.exception("error getting user data from netro api")
Netro Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py (2)

112-119: Consider using logger.exception for automatic traceback inclusion.

While logger.error with the exception message works, using logger.exception would automatically include the full traceback, which aids debugging of malformed API responses.

♻️ Suggested refinement
         except (KeyError, TypeError) as exc:
-            self.logger.error(f"Malformed device info for {serial}: {exc}")
+            self.logger.exception(f"Malformed device info for {serial}")
             # Return minimal update marking device in error state

276-281: Potential IndexError if jmoistures[0] has no 'date' key.

At Line 280, jmoistures[0]["date"] accesses the 'date' key directly. If a moisture entry is malformed and missing the 'date' key, this would raise a KeyError (which is caught), but the comprehension at line 281 uses m.get("date") which handles missing keys differently.

This inconsistency is minor since KeyError is caught, but using .get() consistently would be more defensive.

♻️ Suggested refinement for consistency
-            max_date = jmoistures[0]["date"]
+            max_date = jmoistures[0].get("date")
+            if max_date is None:
+                self.logger.debug("Most recent moisture entry missing date")
+                return []
             max_date_moistures = [m for m in jmoistures if m.get("date") == max_date]

Comment thread .planning/milestones/v1-MILESTONE-AUDIT.md Outdated
Comment thread .planning/milestones/v1-REQUIREMENTS.md Outdated
Comment thread .planning/milestones/v1-ROADMAP.md Outdated
Comment on lines +108 to +114
## Deviations from Plan

### Line Count Target Not Achieved

**1. [Deviation] plugin.py is 1038 lines, not under 450 as planned**

- **Issue:** Plan specified plugin.py should be under 450 lines (down from 1262)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

MD001: heading levels should increment by one.

markdownlint flagged a heading-level jump. Please ensure any ### heading is nested under a ## (not directly under #).

🤖 Prompt for AI Agents
In @.planning/phases/05-device-handlers/05-02-SUMMARY.md around lines 108 - 114,
The file contains a heading-level jump flagged by markdownlint: the "### Line
Count Target Not Achieved" (and other `###` headings) must be nested under a
`##` heading rather than directly under a top-level `#`; update the header
hierarchy so each `###` is preceded by an appropriate `##` (or reduce `#` to
`##`/increase `##` to encompass the `###`) so heading levels increment by one
and the document structure is semantically correct.

Comment thread .planning/phases/05-device-handlers/05-CONTEXT.md Outdated
Comment thread .planning/phases/06-testing-expansion/06-RESEARCH.md
Comment thread .planning/phases/06-testing-expansion/06-RESEARCH.md
Comment thread .planning/phases/06-testing-expansion/06-RESEARCH.md
Comment thread .planning/phases/06-testing-expansion/06-VERIFICATION.md Outdated
Comment thread tests/test_device_handlers.py Outdated
simons-plugins and others added 3 commits February 3, 2026 23:38
Version bump for v1.0 refactoring milestone completion.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fixed heading level jumps across all .planning documentation
- Normalized emphasis markers to asterisk style
- Added proper code block language tags
- Improved table formatting with blank lines
- Fixed test assertions for malformed payload handling
- Enhanced device handlers to catch AttributeError gracefully
- Bumped version to 2025.1.13

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@Netro` Sprinklers.indigoPlugin/Contents/Info.plist:
- Around line 5-6: Update the PluginVersion value in Info.plist to a new unused
version (e.g., change the <key>PluginVersion</key> <string>2025.1.13</string>
entry to 2025.1.14 or another unique semver-like tag) so the CI pipeline can
create a new Git tag; ensure the updated <string> for PluginVersion is committed
before merging.

In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py:
- Around line 210-235: The duration parsing is not defensive: update the block
that reads schedule_dict.get("duration") and computes duration_min to mirror the
start_time parsing — retrieve raw_duration = schedule_dict.get("duration", 0),
if raw_duration is a str attempt to parse to float/int, otherwise use it as-is,
wrap the conversion in the same except (ValueError, TypeError, OSError) handling
to fall back to 0 on error, then compute duration_min = int(parsed_duration /
60) and append the same updates entry to updates; reference schedule_dict,
raw_duration/duration_sec, duration_min, and the
updates.append({"key":"nextScheduleDuration",...}) so the change is localized to
that conversion block.
- Around line 323-338: The loop over zones can raise KeyError from
itemgetter("ith") and direct indexing like zone['name']; change the sort key to
use a safe getter (e.g., sort with key=lambda z: z.get("ith", 0)) and replace
direct zone['name'] access with zone.get("name", f"Zone {zone.get('ith',
'?')}"); also ensure other accesses in this block (building zone_names,
zones_data and id/name fields) use zone.get(...) with sensible defaults while
keeping existing logic that sets max_duration using zone.get("enabled", False)
and appends to max_durations and zones_data.
🧹 Nitpick comments (2)
.planning/milestones/v1-MILESTONE-AUDIT.md (1)

202-215: Add language identifier to fenced code block.

markdownlint flags the missing language specifier. Add text to the opening fence for the module dependency graph.

📝 Proposed fix
 ### Module Dependency Graph

-```
+```text
 Level 0 (Foundation - no dependencies):
.planning/phases/06-testing-expansion/06-VERIFICATION.md (1)

316-327: Add language identifier to fenced code block.

markdownlint flags the missing language specifier for the git commit log. Add text or git to the opening fence.

📝 Proposed fix
 Phase 6 commits from git log:
-```
+```text
 6b50ddb test(06-01): create shared pytest fixtures in conftest.py

Comment on lines 5 to +6
<key>PluginVersion</key>
<string>2025.1.11</string>
<string>2025.1.13</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Version conflict: tag 2025.1.13 already exists.

The CI pipeline reports that version 2025.1.13 already exists as a Git tag. Increment PluginVersion to an unused version (e.g., 2025.1.14) to allow the pipeline to pass and enable tagging on merge.

🔧 Proposed fix
 	<key>PluginVersion</key>
-	<string>2025.1.13</string>
+	<string>2025.1.14</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<key>PluginVersion</key>
<string>2025.1.11</string>
<string>2025.1.13</string>
<key>PluginVersion</key>
<string>2025.1.14</string>
🤖 Prompt for AI Agents
In `@Netro` Sprinklers.indigoPlugin/Contents/Info.plist around lines 5 - 6, Update
the PluginVersion value in Info.plist to a new unused version (e.g., change the
<key>PluginVersion</key> <string>2025.1.13</string> entry to 2025.1.14 or
another unique semver-like tag) so the CI pipeline can create a new Git tag;
ensure the updated <string> for PluginVersion is committed before merging.

Comment on lines +210 to +235
start_time_raw = schedule_dict.get("start_time", 0)
try:
start_time_ms = (
float(start_time_raw)
if isinstance(start_time_raw, str)
else start_time_raw
)
start_time_dt = datetime.fromtimestamp(start_time_ms / 1000.0)
start_time_str = start_time_dt.strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError, OSError):
start_time_str = "Invalid timestamp"

updates.append({"key": "nextScheduleTime", "value": start_time_str})
updates.append({
"key": "nextScheduleZone",
"value": schedule_dict.get("zone_name", f"Zone {schedule_dict.get('zone', '?')}")
})
updates.append({
"key": "nextScheduleSource",
"value": schedule_dict.get("source", "Unknown").title()
})

# Duration is in seconds, convert to minutes
duration_sec = schedule_dict.get("duration") or 0
duration_min = int(duration_sec / 60)
updates.append({"key": "nextScheduleDuration", "value": duration_min})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find the file
find . -name "device_handlers.py" -type f

Repository: simons-plugins/netro-indigo

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Read the file around lines 210-235 with broader context (200-250)
find . -name "device_handlers.py" -type f -exec cat -n {} \; | sed -n '180,260p'

Repository: simons-plugins/netro-indigo

Length of output: 3523


🏁 Script executed:

#!/bin/bash
# Search for duration handling patterns in the codebase
rg "duration" --type py -B 2 -A 2

Repository: simons-plugins/netro-indigo

Length of output: 27156


🏁 Script executed:

#!/bin/bash
# Check for API documentation or schema files
find . -name "*.py" -o -name "*.json" -o -name "*.md" | xargs grep -l "duration" 2>/dev/null | head -20

Repository: simons-plugins/netro-indigo

Length of output: 1042


Apply defensive parsing to duration field for consistency.

The duration field lacks the defensive type-checking applied to the adjacent start_time field (lines 212–215). If the API returns "duration" as a string, line 234 will raise TypeError and trigger the outer error handler (line 188), causing the entire schedule update to fail. While this error is caught, apply the same isinstance + type-conversion pattern used for start_time to handle mixed types gracefully:

Suggested fix
-        duration_sec = schedule_dict.get("duration") or 0
-        duration_min = int(duration_sec / 60)
+        duration_raw = schedule_dict.get("duration", 0)
+        try:
+            duration_sec = float(duration_raw)
+        except (TypeError, ValueError):
+            duration_sec = 0
+        duration_min = int(duration_sec / 60)
🤖 Prompt for AI Agents
In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py
around lines 210 - 235, The duration parsing is not defensive: update the block
that reads schedule_dict.get("duration") and computes duration_min to mirror the
start_time parsing — retrieve raw_duration = schedule_dict.get("duration", 0),
if raw_duration is a str attempt to parse to float/int, otherwise use it as-is,
wrap the conversion in the same except (ValueError, TypeError, OSError) handling
to fall back to 0 on error, then compute duration_min = int(parsed_duration /
60) and append the same updates entry to updates; reference schedule_dict,
raw_duration/duration_sec, duration_min, and the
updates.append({"key":"nextScheduleDuration",...}) so the change is localized to
that conversion block.

Comment on lines +323 to +338
try:
zones = device_data.get("zones", [])
for zone in sorted(zones, key=itemgetter("ith")):
# Build comma-separated zone names
zone_names += f", {zone['name']}" if zone_names else zone["name"]

# Set max duration to plugin max for enabled zones, 0 for disabled
max_duration = max_zone_runtime if zone.get("enabled", False) else 0
max_durations.append(str(max_duration))

# Store zone ID and name for dropdown lists
zones_data.append({
"id": zone.get("ith", 0),
"name": zone.get("name", f"Zone {zone.get('ith', '?')}"),
"enabled": zone.get("enabled", False)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
find . -name "device_handlers.py" -type f | head -5

Repository: simons-plugins/netro-indigo

Length of output: 144


🏁 Script executed:

# Read the specific lines mentioned in the review (323-338)
sed -n '320,340p' "./Netro Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py" | cat -n

Repository: simons-plugins/netro-indigo

Length of output: 1128


🏁 Script executed:

# Check if there's any input validation or error handling for zones
rg -B 10 -A 20 "zones = device_data.get\(\"zones\"" "./Netro Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py"

Repository: simons-plugins/netro-indigo

Length of output: 1353


🏁 Script executed:

# Look for where itemgetter is imported and used
rg "itemgetter" "./Netro Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py" -B 2 -A 2

Repository: simons-plugins/netro-indigo

Length of output: 460


🏁 Script executed:

# Check if the API documentation or tests show the structure of zone objects
find . -name "*.md" -o -name "*.txt" -o -name "*.json" | xargs grep -l "zones" 2>/dev/null | head -5

Repository: simons-plugins/netro-indigo

Length of output: 236


🏁 Script executed:

# Check the INTEGRATIONS file to understand the API contract
cat ./.planning/codebase/INTEGRATIONS.md | head -100

Repository: simons-plugins/netro-indigo

Length of output: 3790


🏁 Script executed:

# Check the PROJECT.md for any API documentation
cat ./.planning/PROJECT.md | grep -A 20 -B 5 "zone\|api" | head -80

Repository: simons-plugins/netro-indigo

Length of output: 3976


🏁 Script executed:

# Look at the full function to understand context better
sed -n '300,345p' "./Netro Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py" | cat -n

Repository: simons-plugins/netro-indigo

Length of output: 2163


🏁 Script executed:

# Search for tests or mock data that shows the zone structure
find . -name "*.py" -type f | xargs grep -l "zones.*ith\|test.*zone" 2>/dev/null | head -5

Repository: simons-plugins/netro-indigo

Length of output: 185


🏁 Script executed:

# Let's check if there are any tests for the zone extraction function
grep -A 30 "extract_zone_info\|test_zone" ./tests/test_device_handlers.py | head -50

Repository: simons-plugins/netro-indigo

Length of output: 2544


🏁 Script executed:

# Check if the API response validation shows the zone schema
grep -A 10 "zone\|ith\|name" ./Netro\ Sprinklers.indigoPlugin/Contents/Server\ Plugin/validators.py | head -40

Repository: simons-plugins/netro-indigo

Length of output: 1502


🏁 Script executed:

# Check if there are any tests for missing zone fields
grep -B 5 -A 15 "missing\|absent\|KeyError" ./tests/test_device_handlers.py | grep -A 15 "zone"

Repository: simons-plugins/netro-indigo

Length of output: 2005


🏁 Script executed:

# Look for how the sample_device_info_response fixture is defined
grep -A 50 "sample_device_info_response\|@pytest.fixture" ./tests/test_device_handlers.py | head -80

Repository: simons-plugins/netro-indigo

Length of output: 2530


🏁 Script executed:

# Check if there's error handling tests
grep -B 5 -A 10 "test_extract_zone_info.*error\|test_extract_zone_info.*empty\|test_extract_zone_info.*missing" ./tests/test_device_handlers.py

Repository: simons-plugins/netro-indigo

Length of output: 1080


🏁 Script executed:

# Check if there are any tests that specifically test for missing "ith" or "name" fields in a zone
grep -B 5 -A 15 "missing.*ith\|missing.*name\|ith.*missing\|name.*missing" ./tests/test_device_handlers.py

Repository: simons-plugins/netro-indigo

Length of output: 53


🏁 Script executed:

# Look for all tests related to extract_zone_info to see if edge cases are covered
grep "def test_extract_zone_info" ./tests/test_device_handlers.py

Repository: simons-plugins/netro-indigo

Length of output: 809


Use .get() for zone field access to prevent KeyError before error handling.

The code uses itemgetter("ith") and direct zone['name'] access, which will raise KeyError if those fields are missing. While the try-except block (line 41) does catch this, the exceptions are preventable with defensive .get() calls. This also makes the code more robust if the API schema evolves.

Update the sorting key and zone name extraction to use .get() with safe defaults, as shown in the suggested fix. This prevents exceptions from occurring rather than catching them after the fact.

🛠️ Suggested fix
-            zones = device_data.get("zones", [])
-            for zone in sorted(zones, key=itemgetter("ith")):
-                # Build comma-separated zone names
-                zone_names += f", {zone['name']}" if zone_names else zone["name"]
+            zones = device_data.get("zones", [])
+            for zone in sorted(zones, key=lambda z: z.get("ith", 0)):
+                # Build comma-separated zone names
+                zone_name = zone.get("name", f"Zone {zone.get('ith', '?')}")
+                zone_names += f", {zone_name}" if zone_names else zone_name
@@
-                zones_data.append({
-                    "id": zone.get("ith", 0),
-                    "name": zone.get("name", f"Zone {zone.get('ith', '?')}"),
-                    "enabled": zone.get("enabled", False)
-                })
+                zones_data.append({
+                    "id": zone.get("ith", 0),
+                    "name": zone_name,
+                    "enabled": zone.get("enabled", False)
+                })
🤖 Prompt for AI Agents
In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/device_handlers.py
around lines 323 - 338, The loop over zones can raise KeyError from
itemgetter("ith") and direct indexing like zone['name']; change the sort key to
use a safe getter (e.g., sort with key=lambda z: z.get("ith", 0)) and replace
direct zone['name'] access with zone.get("name", f"Zone {zone.get('ith',
'?')}"); also ensure other accesses in this block (building zone_names,
zones_data and id/name fields) use zone.get(...) with sensible defaults while
keeping existing logic that sets max_duration using zone.get("enabled", False)
and appends to max_durations and zones_data.

@simons-plugins simons-plugins deleted the master branch February 4, 2026 00:06
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.

Fix silent exception handlers in plugin.py

1 participant