Skip to content

Add Tomorrow.io weather integration for automatic weather reporting#42

Merged
simons-plugins merged 6 commits into
mainfrom
claude/add-tomorrow-weather-integration-IeqbO
Apr 10, 2026
Merged

Add Tomorrow.io weather integration for automatic weather reporting#42
simons-plugins merged 6 commits into
mainfrom
claude/add-tomorrow-weather-integration-IeqbO

Conversation

@simons-plugins

@simons-plugins simons-plugins commented Apr 9, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Tomorrow.io weather API integration to automatically fetch current weather conditions and report them to Netro sprinkler devices. This allows users to override Netro's built-in weather data with more accurate local forecasts for smarter watering schedules.

Key Changes

New Weather Client Module

  • tomorrow_client.py: New TomorrowClient class that fetches real-time weather from Tomorrow.io API and transforms responses into Netro-compatible format
    • Maps Tomorrow.io weather codes (1000-8000) to Netro conditions (0=Clear, 1=Cloudy, 2=Rain, 3=Snow, 4=Wind)
    • Returns weather data in metric units (Celsius, mm, m/s, hPa)
    • Handles API errors gracefully with detailed logging
    • Wind speed override: conditions 0-1 become condition 4 (Wind) when speed exceeds 15 m/s

Plugin Integration

  • plugin.py:
    • Added _create_tomorrow_client() to initialize client from plugin preferences
    • Added _update_weather_from_tomorrow() to periodically fetch weather and report to all enabled sprinkler devices
    • Integrated into main polling loop with configurable update interval
    • Automatically converts metric to US units for API v1 devices, keeps metric for v2
    • Logs weather updates and handles throttling/errors per device

Unit Conversion Utilities

  • utils.py: New conversion functions for metric-to-US unit transformation
    • celsius_to_fahrenheit(), mm_to_inches(), ms_to_mph(), hpa_to_inhg()
    • convert_weather_metric_to_us(): Converts complete weather dict from metric to US units

Configuration & Validation

  • PluginConfig.xml: New UI section for Tomorrow.io settings
    • Enable/disable toggle
    • API key input field
    • Location input (lat,lon format)
    • Weather update interval setting
  • validators.py: Added validation for Tomorrow.io fields
    • Requires API key and location when enabled
    • Validates weather update interval (10-1440 minutes, default 30)
    • Strips whitespace from API key and location
  • constants.py: Added MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES constant

Comprehensive Test Coverage

  • test_tomorrow_client.py (419 lines): Tests for TomorrowClient
    • Weather code mapping (clear, cloudy, rain, snow, wind conditions)
    • Response transformation with all field types
    • Wind override logic
    • Error handling (HTTP errors, connection failures, timeouts)
    • Optional field handling
    • Rounding/precision validation
  • test_weather_integration.py (232 lines): Tests for integration components
    • Metric-to-US unit conversions
    • Plugin preferences validation for Tomorrow.io fields
    • Edge cases (missing fields, None values, whitespace handling)

Implementation Details

  • Tomorrow.io client is optional and only initialized if enabled in preferences
  • Weather updates run on a separate schedule from device polling (configurable 10-1440 minute interval)
  • Each weather report uses 1 Tomorrow.io API call + 1 Netro API call per device
  • Metric units are preserved throughout the client to match Tomorrow.io's native format
  • Unit conversion happens at the plugin level based on device API version
  • Comprehensive error handling with per-device error logging and throttle awareness

https://claude.ai/code/session_01AdcdU17hU7qs5ecXCCUWJe

Summary by CodeRabbit

  • New Features

    • Tomorrow.io weather integration with enable toggle, API key/location inputs, and refresh interval (default 30 min, minimum 10 min). Settings apply live.
    • Automatic periodic weather updates and a new "Refresh Weather Now" menu action.
    • New weather states on sprinkler devices: condition, temperature, humidity, rain, rain probability, wind speed, pressure, last-updated.
  • Tests

    • Added unit tests for the weather client, data transformations, unit conversions, and preference validation.

… reporting

Fetches current weather from Tomorrow.io API on a configurable interval
(default 30 min) and reports it to all Netro sprinkler devices via the
report_weather endpoint, replacing Netro's built-in weather data for
more accurate watering schedules.

- New tomorrow_client.py module with weather code mapping (Tomorrow.io -> Netro)
- Plugin config UI for API key, location, and update interval
- Automatic unit conversion: metric for API v2, US for API v1
- Validation for Tomorrow.io settings when enabled
- 100% test coverage on new modules (49 new tests)

https://claude.ai/code/session_01AdcdU17hU7qs5ecXCCUWJe
@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Tomorrow.io weather integration: new plugin prefs and UI labels, a TomorrowClient to fetch/transform realtime weather, metric↔US conversion utilities, pref validation, runtime scheduling/reporting to Netro devices, new device weather states, menu action, and unit tests.

Changes

Cohort / File(s) Summary
Plugin configuration
Netro Sprinklers.indigoPlugin/Contents/Server Plugin/PluginConfig.xml
Add Tomorrow.io prefs: tomorrowEnabled, tomorrowApiKey, tomorrowLocation, weatherUpdateInterval, label/description fields, and separator; visibility gated by master toggle.
Constants & test config
.../Server Plugin/constants.py, pytest.ini
Default interval changed 10→30, added MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES = 10, and added pytest marker weather.
Weather client
.../Server Plugin/tomorrow_client.py
New TomorrowClient module: HTTP fetch to Tomorrow.io realtime endpoint, error handling, response parsing, mapping weather codes to Netro conditions, wind-override logic, and Netro-format output.
Plugin runtime integration
.../Server Plugin/plugin.py
Instantiate/manage TomorrowClient from prefs; schedule and run _update_weather_from_tomorrow() in main loop; fetch, optionally convert metric→US per-device, call api_client.report_weather for each sprinkler device; add refreshWeather() menu action; apply prefs live.
Utilities & validation
.../Server Plugin/utils.py, .../Server Plugin/validators.py
Add unit-conversion helpers and convert_weather_metric_to_us(); add weatherUpdateInterval pref spec with min/max validation; require/sanitize tomorrowApiKey and tomorrowLocation when enabled.
Device states
.../Server Plugin/Devices.xml
Add eight sprinkler states: weather_condition, weather_temperature, weather_humidity, weather_rain, weather_rain_prob, weather_wind_speed, weather_pressure, weather_updated plus trigger/control labels.
Menu item
.../Server Plugin/MenuItems.xml
Add refreshWeather menu item mapped to refreshWeather callback.
Tests
tests/test_tomorrow_client.py, tests/test_weather_integration.py, tests/test_base_modules.py
Add comprehensive tests for TomorrowClient transformation and error handling, conversion utilities, prefs validation, and update base test expecting default interval = 30.

Sequence Diagram

sequenceDiagram
    participant Plugin as Plugin (runner)
    participant TomorrowClient as TomorrowClient (client)
    participant TomorrowAPI as Tomorrow.io (external)
    participant NetroAPI as Netro API (api_client)

    Plugin->>Plugin: check _next_weather_update
    alt update due
        Plugin->>TomorrowClient: fetch_current_weather()
        TomorrowClient->>TomorrowAPI: GET /realtime (apikey, location, units=metric, timeout)
        TomorrowAPI-->>TomorrowClient: JSON response / error
        TomorrowClient->>TomorrowClient: _transform_response() -> Netro-format dict
        TomorrowClient-->>Plugin: weather_dict (metric)
        Plugin->>Plugin: convert_weather_metric_to_us() if device api_version == "1"
        loop per enabled sprinkler device
            Plugin->>NetroAPI: report_weather(device_id, weather_data)
            NetroAPI-->>Plugin: success / ThrottleDelayError / failure
        end
        Plugin->>Plugin: set _next_weather_update
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I nibble keys and hop through code,
Tomorrow's forecast finds its road.
Metrics turned and timers set,
Sprinklers sip what clouds beget —
Gardens dream in data sowed. 🌦️

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding Tomorrow.io weather integration for automatic weather reporting to Netro sprinkler devices.
Docstring Coverage ✅ Passed Docstring coverage is 97.33% 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/add-tomorrow-weather-integration-IeqbO

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: 3

🧹 Nitpick comments (3)
tests/test_weather_integration.py (1)

160-160: Rename the intentionally unused tuple slots.

Ruff is already flagging these unpacked sanitized/errors values as unused. Switching them to _sanitized / _errors keeps the test file clean without changing behavior.

Also applies to: 175-175, 190-190, 203-203, 214-214, 229-229

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_weather_integration.py` at line 160, Rename the intentionally
unused tuple slots returned from validate_prefs_config from sanitized and errors
to _sanitized and _errors at each call site (e.g., the statement currently
reading "is_valid, sanitized, errors = validate_prefs_config(values)") so
linters stop flagging unused variables; apply the same change for the other
occurrences referenced (around the other calls that currently unpack to
sanitized/errors). This only renames the local variables and does not change
behavior of validate_prefs_config or its return values.
tests/test_tomorrow_client.py (2)

12-20: Avoid unconditional module-level sys.path mutation.

Line 18 globally mutates import state at import time. Prefer an idempotent insert at minimum, or centralize this in shared test bootstrap (for example, conftest.py) to reduce cross-test coupling.

Minimal safe tweak
-sys.path.insert(0, str(SERVER_PLUGIN_DIR))
+if str(SERVER_PLUGIN_DIR) not in sys.path:
+    sys.path.insert(0, str(SERVER_PLUGIN_DIR))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_tomorrow_client.py` around lines 12 - 20, The test file mutates
sys.path unconditionally at import time; change the top-level import hack to an
idempotent operation or move it to test bootstrap: either (A) replace
sys.path.insert(0, str(SERVER_PLUGIN_DIR)) with a guard that checks if
str(SERVER_PLUGIN_DIR) is already in sys.path before inserting, or (B) remove
the module-level mutation and put the path adjustment into a shared conftest.py
fixture (or setup function) so tests that import
TomorrowClient/_TOMORROW_TO_NETRO_CONDITION get the path reliably without global
import-time side effects.

287-304: Add explicit None-value optional-field case.

This test covers missing keys, but not keys present with None values. Add one case to lock in the “omit optional fields when None” behavior described in scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_tomorrow_client.py` around lines 287 - 304, Update the
test_optional_fields_omitted_when_none case to also cover keys that are present
but set to None: inside the existing data payload add the optional keys
("humidity", "rain", "rain_prob", "wind_speed", "pressure") with value None
under data["data"]["values"], call client._transform_response(data) as before,
and assert the returned result does not include those keys; this locks in the
behavior that _transform_response omits optional fields when they are explicitly
None as well as when they are missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py:
- Around line 114-119: The weather update interval fallback currently uses the
wrong default; update the code so pluginPrefs.get("weatherUpdateInterval",
DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES) uses the single shared 30-minute
default constant used by PluginConfig.xml and validate_prefs_config() (i.e.,
ensure DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES is defined as 30 and used
everywhere), or replace the fallback with the same canonical constant/name that
validate_prefs_config() and PluginConfig.xml rely on so upgraded installs get a
30-minute default for _weather_update_interval.
- Around line 1034-1058: If Tomorrow.io stays enabled but its settings changed,
reset the next scheduled push so the new config takes effect immediately: after
creating new_client via _create_tomorrow_client and before assigning
self._tomorrow_client, detect a config change (compare the existing
self._tomorrow_client to new_client or compare a stable identifier on the
client) and if they differ and both are non-None, set self._next_weather_update
= datetime.now(); then assign self._tomorrow_client = new_client and keep the
existing log branches (_next_weather_update is already set when enabling, and
set here when updating settings).

In `@tests/test_tomorrow_client.py`:
- Around line 343-372: Add assertions to ensure the client calls
response.raise_for_status() in the happy-path tests: after invoking
client.fetch_current_weather() in both test_successful_fetch and
test_api_params_correct, assert that the mocked response's raise_for_status was
called (e.g., mock_response.raise_for_status.assert_called_once() or
mock_get.return_value.raise_for_status.assert_called_once()) so the tests fail
if the implementation omits calling raise_for_status().

---

Nitpick comments:
In `@tests/test_tomorrow_client.py`:
- Around line 12-20: The test file mutates sys.path unconditionally at import
time; change the top-level import hack to an idempotent operation or move it to
test bootstrap: either (A) replace sys.path.insert(0, str(SERVER_PLUGIN_DIR))
with a guard that checks if str(SERVER_PLUGIN_DIR) is already in sys.path before
inserting, or (B) remove the module-level mutation and put the path adjustment
into a shared conftest.py fixture (or setup function) so tests that import
TomorrowClient/_TOMORROW_TO_NETRO_CONDITION get the path reliably without global
import-time side effects.
- Around line 287-304: Update the test_optional_fields_omitted_when_none case to
also cover keys that are present but set to None: inside the existing data
payload add the optional keys ("humidity", "rain", "rain_prob", "wind_speed",
"pressure") with value None under data["data"]["values"], call
client._transform_response(data) as before, and assert the returned result does
not include those keys; this locks in the behavior that _transform_response
omits optional fields when they are explicitly None as well as when they are
missing.

In `@tests/test_weather_integration.py`:
- Line 160: Rename the intentionally unused tuple slots returned from
validate_prefs_config from sanitized and errors to _sanitized and _errors at
each call site (e.g., the statement currently reading "is_valid, sanitized,
errors = validate_prefs_config(values)") so linters stop flagging unused
variables; apply the same change for the other occurrences referenced (around
the other calls that currently unpack to sanitized/errors). This only renames
the local variables and does not change behavior of validate_prefs_config or its
return values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26d257ea-8ef3-4697-b007-2042cbfeccf0

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed2284 and aae3e6c.

📒 Files selected for processing (9)
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/PluginConfig.xml
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/constants.py
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/tomorrow_client.py
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/utils.py
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/validators.py
  • pytest.ini
  • tests/test_tomorrow_client.py
  • tests/test_weather_integration.py

Comment thread Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py
Comment thread Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py
Comment thread tests/test_tomorrow_client.py
simons-plugins and others added 2 commits April 10, 2026 15:44
- Align DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES to 30 (matching
  PluginConfig.xml) so upgraded installs get the documented default
- Reschedule next weather push when Tomorrow.io settings change
  (interval, API key, or location) so new config takes effect immediately
- Assert raise_for_status() called in happy-path weather client tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add weather_condition, weather_temperature, weather_humidity, weather_rain,
weather_rain_prob, weather_wind_speed, weather_pressure, and weather_updated
states to sprinkler devices. States update on each successful weather report
using metric values from Tomorrow.io.

Co-Authored-By: Claude Opus 4.6 (1M context) <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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py:
- Around line 116-119: Clamp the loaded and updated interval values to at least
MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES when reading from pluginPrefs and when
applying updates so _weather_update_interval can never be zero or negative;
specifically, replace the direct assignment to self._weather_update_interval
from pluginPrefs (where DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES is used) with a
min/max or max(...) clamp using MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES and do
the same wherever the prefs are reloaded/updated (see the other prefs-handling
block around 1036-1047) so that _next_weather_update (and subsequent
report_weather() polling) is not scheduled in the past.
- Around line 427-430: The per-device except block currently catches all
Exceptions; change it to only catch the documented per-request failures so
programming errors propagate to the outer polling-loop handler. Import
NetroAPIError and requests.exceptions (or the specific exceptions) at the top of
the file, then replace "except Exception as exc:" in the make_request/notify
block with an explicit tuple of expected errors, e.g. "except (NetroAPIError,
requests.exceptions.ConnectionError, requests.exceptions.Timeout,
requests.exceptions.HTTPError) as exc:" (keeping the existing ThrottleDelayError
except branch), so only Netro/requests-related errors are handled per-device and
other exceptions bubble up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 191ae8b5-470e-4df1-b9b0-1c9f56c97f7f

📥 Commits

Reviewing files that changed from the base of the PR and between aae3e6c and 8ee2f4d.

📒 Files selected for processing (4)
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/constants.py
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py
  • tests/test_base_modules.py
  • tests/test_tomorrow_client.py
✅ Files skipped from review due to trivial changes (2)
  • tests/test_base_modules.py
  • tests/test_tomorrow_client.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/constants.py

Comment on lines +116 to +119
self._weather_update_interval = int(
pluginPrefs.get("weatherUpdateInterval", DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES)
)
self._tomorrow_client = self._create_tomorrow_client(pluginPrefs)

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 | 🟠 Major

Clamp weatherUpdateInterval at runtime, not just in UI validation.

Both code paths accept any integer from prefs, including 0 or negatives. That makes _next_weather_update land on now/the past every time, so this feature will call Tomorrow.io and report_weather() once per polling cycle per device until the prefs are corrected. Please enforce MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES when loading and updating the interval, even for stale or hand-edited prefs.

Also applies to: 1036-1047

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py around lines
116 - 119, Clamp the loaded and updated interval values to at least
MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES when reading from pluginPrefs and when
applying updates so _weather_update_interval can never be zero or negative;
specifically, replace the direct assignment to self._weather_update_interval
from pluginPrefs (where DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES is used) with a
min/max or max(...) clamp using MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES and do
the same wherever the prefs are reloaded/updated (see the other prefs-handling
block around 1036-1047) so that _next_weather_update (and subsequent
report_weather() polling) is not scheduled in the past.

Comment on lines +427 to +430
except ThrottleDelayError:
self.logger.debug(f"Skipping weather report for '{dev.name}' - throttled")
except Exception as exc:
self.logger.error(f"Could not report weather to '{dev.name}': {exc}")

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i '^api_client\.py$' -x sh -c '
  echo "== {} ==";
  rg -n -C3 "def report_weather|def make_request|ThrottleDelayError|Netro(Connection|Timeout|API)Error|requests\\.exceptions" "{}"
'

fd -i '^plugin\.py$' -x sh -c '
  case "{}" in
    *"Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py")
      echo "== {}: weather handler ==";
      sed -n "423,432p" "{}"
      ;;
  esac
'

Repository: simons-plugins/netro-indigo

Length of output: 3934


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get full report_weather implementation
echo "=== report_weather() full implementation ==="
sed -n '724,800p' './Netro Sprinklers.indigoPlugin/Contents/Server Plugin/api_client.py'

echo ""
echo "=== Context around lines 427-430 in plugin.py (broader scope) ==="
sed -n '410,445p' './Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py'

echo ""
echo "=== Check for outer polling loop handler ==="
rg -n "except.*Error|except Exception" './Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py' -B2 -A1 | head -60

Repository: simons-plugins/netro-indigo

Length of output: 5734


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify available exception imports in plugin.py
echo "=== Exception imports in plugin.py ==="
head -100 './Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py' | grep -E "^from|^import" | grep -i "exception\|error"

echo ""
echo "=== Verify NetroAPIError is imported in api_client ==="
grep -n "^from exceptions import\|^import.*NetroAPIError" './Netro Sprinklers.indigoPlugin/Contents/Server Plugin/api_client.py'

echo ""
echo "=== Check if NetroAPIError is exported/available in exceptions module ==="
grep -n "class NetroAPIError\|NetroAPIError" './Netro Sprinklers.indigoPlugin/Contents/Server Plugin/exceptions.py' | head -5

echo ""
echo "=== Verify requests.exceptions are used elsewhere in the codebase ==="
rg "requests\.exceptions\.(ConnectionError|Timeout|HTTPError)" './Netro Sprinklers.indigoPlugin/Contents/Server Plugin/' -n

Repository: simons-plugins/netro-indigo

Length of output: 2072


Narrow the per-device exception handler to expected Netro/requests failures.

Catching bare Exception here swallows programming errors in the weather data preparation and hides bugs that should be logged centrally. Currently, any unexpected error (including typos or logic errors in lines 413–425) will silently skip that device with only the device-level error logged.

The make_request() method documents raising ThrottleDelayError, NetroAPIError, and requests.exceptions.{ConnectionError, Timeout, HTTPError}. Catch only those expected failures for per-device isolation; let unexpected exceptions propagate to the outer polling-loop handler (line 598), which already logs them centrally.

This requires importing NetroAPIError and requests.exceptions at the top of the file and updating the handler accordingly.

🧰 Tools
🪛 Ruff (0.15.9)

[warning] 429-429: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py around lines
427 - 430, The per-device except block currently catches all Exceptions; change
it to only catch the documented per-request failures so programming errors
propagate to the outer polling-loop handler. Import NetroAPIError and
requests.exceptions (or the specific exceptions) at the top of the file, then
replace "except Exception as exc:" in the make_request/notify block with an
explicit tuple of expected errors, e.g. "except (NetroAPIError,
requests.exceptions.ConnectionError, requests.exceptions.Timeout,
requests.exceptions.HTTPError) as exc:" (keeping the existing ThrottleDelayError
except branch), so only Netro/requests-related errors are handled per-device and
other exceptions bubble up.

@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.

♻️ Duplicate comments (2)
Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py (2)

452-454: ⚠️ Potential issue | 🟠 Major

Narrow the per-device exception handler in weather reporting.

Line 452 catches Exception, which hides programming errors in this block and makes regressions harder to detect. Keep per-device isolation, but catch only expected Netro/request failures here.

♻️ Proposed fix
-from exceptions import ThrottleDelayError
+from exceptions import ThrottleDelayError, NetroAPIError
@@
-            except Exception as exc:
+            except (
+                NetroAPIError,
+                requests.exceptions.ConnectionError,
+                requests.exceptions.Timeout,
+                requests.exceptions.HTTPError,
+            ) as exc:
                 self.logger.error(f"Could not report weather to '{dev.name}': {exc}")
                 self.logger.debug(f"Weather report error: \n{traceback.format_exc(10)}")

Use this read-only script to verify the exception contract before applying:

#!/bin/bash
set -euo pipefail

fd -i '^exceptions\.py$' -x rg -n 'class (ThrottleDelayError|NetroAPIError)'
fd -i '^api_client\.py$' -x rg -n -C2 'def make_request|raise .*NetroAPIError|raise .*ThrottleDelayError|requests\.exceptions\.(ConnectionError|Timeout|HTTPError)'
fd -i '^plugin\.py$' -x rg -n -C2 'except ThrottleDelayError|except Exception as exc'

Expected result: NetroAPIError exists and make_request raises known request/API exceptions; current weather block still uses a broad catch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py around lines
452 - 454, The per-device except block currently catches all Exceptions (the
lines logging "Could not report weather to '{dev.name}': {exc}" and the
traceback.debug call); narrow this to only expected request/API errors by
catching the specific exception types raised by the Netro client (e.g.
NetroAPIError and ThrottleDelayError) plus requests request-related exceptions
(e.g. requests.exceptions.RequestException or ConnectionError/Timeout/HTTPError)
and leave the logging the same; replace the broad "except Exception as exc"
around the weather reporting for each device with a multi-except for these known
exception classes to preserve per-device isolation while letting programming
errors propagate.

116-118: ⚠️ Potential issue | 🟠 Major

Clamp weatherUpdateInterval in runtime paths.

Line 116 and Line 1060 still trust raw prefs values. If stale/hand-edited prefs contain 0 or negative values, weather pushes can run every poll cycle instead of respecting the minimum.

♻️ Proposed fix
 from constants import (
     MAX_ZONE_DURATION_SECONDS,
     DEFAULT_API_TIMEOUT_SECONDS,
     DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES,
+    MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES,
     MINIMUM_POLLING_INTERVAL_MINUTES,
     ZONE_START_ENDPOINT,
     OPERATIONAL_ERROR_EVENTS,
     COMM_ERROR_EVENTS,
     DEVICE_EVENT_TYPES,
 )
@@
-        self._weather_update_interval = int(
-            pluginPrefs.get("weatherUpdateInterval", DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES)
-        )
+        self._weather_update_interval = max(
+            MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES,
+            int(pluginPrefs.get(
+                "weatherUpdateInterval", DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES
+            )),
+        )
@@
-                new_interval = int(valuesDict.get(
+                new_interval = max(
+                    MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES,
+                    int(valuesDict.get(
                     "weatherUpdateInterval", DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES
-                ))
+                    )),
+                )
                 if new_interval != self._weather_update_interval:
                     self._weather_update_interval = new_interval

Also applies to: 1060-1065

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py around lines
116 - 118, The plugin currently trusts raw prefs for weatherUpdateInterval and
can accept zero/negative values; change the assignment of
self._weather_update_interval (and the other runtime retrieval that uses
pluginPrefs.get around the same setting) to clamp to the minimum by converting
to int and taking max(..., DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES) so any 0 or
negative prefs fall back to the defined minimum, ensuring weather pushes respect
the minimum interval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py:
- Around line 452-454: The per-device except block currently catches all
Exceptions (the lines logging "Could not report weather to '{dev.name}': {exc}"
and the traceback.debug call); narrow this to only expected request/API errors
by catching the specific exception types raised by the Netro client (e.g.
NetroAPIError and ThrottleDelayError) plus requests request-related exceptions
(e.g. requests.exceptions.RequestException or ConnectionError/Timeout/HTTPError)
and leave the logging the same; replace the broad "except Exception as exc"
around the weather reporting for each device with a multi-except for these known
exception classes to preserve per-device isolation while letting programming
errors propagate.
- Around line 116-118: The plugin currently trusts raw prefs for
weatherUpdateInterval and can accept zero/negative values; change the assignment
of self._weather_update_interval (and the other runtime retrieval that uses
pluginPrefs.get around the same setting) to clamp to the minimum by converting
to int and taking max(..., DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES) so any 0 or
negative prefs fall back to the defined minimum, ensuring weather pushes respect
the minimum interval.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: acb72832-4d80-409b-856f-bad83dac69d9

📥 Commits

Reviewing files that changed from the base of the PR and between 8ee2f4d and 5e55c25.

📒 Files selected for processing (2)
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/Devices.xml
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py
✅ Files skipped from review due to trivial changes (1)
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/Devices.xml

- Hide Tomorrow.io config fields until enable checkbox is ticked
  (visibleBindingId on textfields, not just help labels)
- Move weather update outside Netro token pause block — Tomorrow.io
  uses its own API and shouldn't be blocked by low Netro tokens
- Add "Refresh Weather Now" plugin menu item for manual trigger
- Call stateListOrDisplayStateIdChanged() on startup so Indigo
  picks up new weather states without disabling/re-enabling devices
- Also trigger weather update from "Update All Status" menu item

Co-Authored-By: Claude Opus 4.6 (1M context) <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.

♻️ Duplicate comments (2)
Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py (2)

455-458: ⚠️ Potential issue | 🟠 Major

Narrow the per-device weather exception handler to expected request/API failures.

Line 457 catches Exception, which can hide coding bugs in weather preparation/state updates. Catch only expected Netro/request errors here and let unexpected exceptions bubble to the outer polling-loop handler.

#!/bin/bash
set -euo pipefail

# Verify exception types surfaced by report_weather path and available exception classes.
fd -i '^api_client\.py$' -x sh -c '
  echo "== {} ==";
  rg -n -C3 "def report_weather|raise .*Error|ThrottleDelayError|NetroAPIError|requests\\.exceptions" "{}"
'

fd -i '^exceptions\.py$' -x sh -c '
  echo "== {} ==";
  rg -n "class ThrottleDelayError|class NetroAPIError" "{}"
'

fd -i '^plugin\.py$' -x sh -c '
  echo "== {} ==";
  rg -n -C2 "except Exception as exc|_update_weather_from_tomorrow|report_weather\\(" "{}"
'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py around lines
455 - 458, The broad except Exception in the per-device report_weather block
should be narrowed to only the expected API/request failures so coding bugs can
bubble up; replace the generic except Exception as exc with a specific catch
(e.g., except NetroAPIError as exc) and include requests exceptions too if the
client uses them (e.g., except (NetroAPIError,
requests.exceptions.RequestException) as exc), ensuring you import NetroAPIError
and requests.exceptions.RequestException at the top and keep the existing
ThrottleDelayError handler and logging behavior in
report_weather/_update_weather_from_tomorrow so unexpected errors are not
swallowed.

116-118: ⚠️ Potential issue | 🟠 Major

Clamp and sanitize weather interval at runtime in both load/update paths.

Line 116 and Line 1072 still accept unchecked integers from prefs. 0/negative values can schedule weather updates continuously, and malformed persisted values can break initialization. Clamp to MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES with a safe parse fallback in both places.

♻️ Proposed fix
 from constants import (
     MAX_ZONE_DURATION_SECONDS,
     DEFAULT_API_TIMEOUT_SECONDS,
     DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES,
+    MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES,
     MINIMUM_POLLING_INTERVAL_MINUTES,
     ZONE_START_ENDPOINT,
     OPERATIONAL_ERROR_EVENTS,
     COMM_ERROR_EVENTS,
     DEVICE_EVENT_TYPES,
 )
@@
-        self._weather_update_interval = int(
-            pluginPrefs.get("weatherUpdateInterval", DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES)
-        )
+        try:
+            configured_interval = int(
+                pluginPrefs.get(
+                    "weatherUpdateInterval",
+                    DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES,
+                )
+            )
+        except (TypeError, ValueError):
+            configured_interval = DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES
+        self._weather_update_interval = max(
+            MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES,
+            configured_interval,
+        )
@@
             try:
-                new_interval = int(valuesDict.get(
-                    "weatherUpdateInterval", DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES
-                ))
+                new_interval = max(
+                    MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES,
+                    int(
+                        valuesDict.get(
+                            "weatherUpdateInterval",
+                            DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES,
+                        )
+                    ),
+                )
                 if new_interval != self._weather_update_interval:
                     self._weather_update_interval = new_interval
                     weather_settings_changed = True

Also applies to: 1072-1077

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py around lines
116 - 118, The weather update interval is taken directly from pluginPrefs and
may be zero, negative, or malformed; change both places that set
self._weather_update_interval (the initialization path using
pluginPrefs.get("weatherUpdateInterval",
DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES) and the prefs-update handler around
lines ~1072-1077) to parse the value safely (e.g., int conversion with
try/except or safe int coercion), fall back to
DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES on parse failure, and then clamp the
resulting integer to be at least MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES before
assigning to self._weather_update_interval so negative/zero/malformed values
cannot be used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Netro` Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py:
- Around line 455-458: The broad except Exception in the per-device
report_weather block should be narrowed to only the expected API/request
failures so coding bugs can bubble up; replace the generic except Exception as
exc with a specific catch (e.g., except NetroAPIError as exc) and include
requests exceptions too if the client uses them (e.g., except (NetroAPIError,
requests.exceptions.RequestException) as exc), ensuring you import NetroAPIError
and requests.exceptions.RequestException at the top and keep the existing
ThrottleDelayError handler and logging behavior in
report_weather/_update_weather_from_tomorrow so unexpected errors are not
swallowed.
- Around line 116-118: The weather update interval is taken directly from
pluginPrefs and may be zero, negative, or malformed; change both places that set
self._weather_update_interval (the initialization path using
pluginPrefs.get("weatherUpdateInterval",
DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES) and the prefs-update handler around
lines ~1072-1077) to parse the value safely (e.g., int conversion with
try/except or safe int coercion), fall back to
DEFAULT_WEATHER_UPDATE_INTERVAL_MINUTES on parse failure, and then clamp the
resulting integer to be at least MINIMUM_WEATHER_UPDATE_INTERVAL_MINUTES before
assigning to self._weather_update_interval so negative/zero/malformed values
cannot be used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f9299b6-41ce-459f-9959-f8d1b5f18a7a

📥 Commits

Reviewing files that changed from the base of the PR and between 5e55c25 and afa5480.

📒 Files selected for processing (3)
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/MenuItems.xml
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/PluginConfig.xml
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py
✅ Files skipped from review due to trivial changes (1)
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/MenuItems.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/PluginConfig.xml

simons-plugins and others added 2 commits April 10, 2026 17:21
Critical:
- Log warning on invalid weather interval instead of bare pass
- Log warning when Tomorrow.io enabled but API key/location missing

Important:
- Fix rain unit docstring: mm/hr (intensity) not total mm
- Avoid leaking API key in HTTP error logs
- Remove unused Tuple import from tomorrow_client
- Fix Devices.xml rain state label to say mm/hr

Suggestions:
- Add traceback logging to broad except Exception handler
- Log when weatherCode missing from API response (defaults to Cloudy)
- Add wind override boundary tests at exactly 15.0 and 15.1 m/s
- Update utils.py module docstring to list conversion functions
- Update validate_prefs_config docstring to mention Tomorrow.io
- Trigger weather update from RequestStatus action (consistency)
- Document Cloudy default rationale and Beaufort scale reference
- Update _update_weather_from_tomorrow docstring re device states

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@simons-plugins simons-plugins merged commit f618c5e into main Apr 10, 2026
2 checks passed
@simons-plugins simons-plugins deleted the claude/add-tomorrow-weather-integration-IeqbO branch April 10, 2026 16:27
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.

2 participants