Documentation of Netro Public API (NPA) quirks, limitations, and undocumented behavior discovered during development and testing.
- Base URL:
http://api.netrohome.com/npa/v1/ - Authentication: Device serial number as URL parameter (
?key={serial}) - Rate Limit: 2000 calls per day per serial number
- Response Format: JSON
- HTTP Methods: GET, POST, PUT
Issue: API docs say timestamps are numbers, but API returns strings
Discovered: Live testing (commit 9e93000)
Example:
{
"start_time": "1740664800000" // String, not number!
}Workaround:
# Handle both string and number
start_time_raw = data.get("start_time", 0)
try:
start_time = float(start_time_raw) if isinstance(start_time_raw, str) else start_time_raw
except (ValueError, TypeError):
start_time = 0Affected Fields:
start_timein schedulestimein various responses- Any Unix timestamp field
Fixed in: plugin.py lines 309-314, 337-344
Issue: Inconsistent array/object response structure
API documentation says: Returns devices array
Reality: Returns single device object
Example:
{
"status": "OK",
"data": {
"device": { // Singular!
"serial": "...",
"name": "...",
"zones": [...]
}
}
}Not:
{
"data": {
"devices": [...] // This doesn't exist
}
}Workaround:
device = reply_dict["data"]["device"] // Not devices[0]Fixed in: plugin.py line 233, test_local_api.py line 98
Issue: Controller status changes are slow and inconsistent
Observations from live testing:
When controller turned OFF:
- Status changes from "ONLINE" to "STANDBY" (not "OFFLINE"!)
- Schedules reduce from 50 to 2
- Last active timestamp updates
- Can take 5-10 minutes to reflect in API
When controller turned ON:
- Status changes from "STANDBY" to "ONLINE"
- Schedules repopulate
- Can take even longer (10+ minutes)
Implications:
- Don't rely on immediate status changes
- "STANDBY" can mean either user-set standby OR offline
- Use
last_activetimestamp for better offline detection
Issue: Underdocumented schedule source types
Observed values:
"AUTOMATIC"- Auto-generated smart schedule"MANUAL"- User-created manual schedule"SMART"- Smart schedule variant"FIX"- Fixed schedule- Others unknown
Also seen:
smartfield can be booleantrue/false- OR string
"SMART"/"MANUAL"
Workaround:
smart = zone.get('smart', 'MANUAL')
# Handle both boolean and string
if isinstance(smart, bool):
smart_str = "SMART" if smart else "MANUAL"
else:
smart_str = smartIssue: Undocumented throttle response format
When rate limit exceeded:
- HTTP 429 response
- No specific error message in JSON
- Plugin must track throttle state locally
Token tracking:
{
"meta": {
"token_remaining": 1847,
"token_reset": 1609545600 // Unix timestamp
}
}Observations:
- Token count decreases by 1 per call
- Resets at midnight UTC
- Going over limit triggers 61-minute lockout
- Subsequent calls during lockout also return 429
Best practices:
- Check
token_remainingin every response - Warn user when <100 remaining
- Implement exponential backoff if needed
Issue: /moistures.json returns Netro's smart-model daily prediction
for each zone, not a sampled sensor value. The model assumes full
saturation immediately after irrigation, then decays based on local
weather inputs.
For a zone with no paired Whisperer, this is the best signal
available. For a zone with a paired Whisperer (pairing done in the
Netro mobile app), Netro's app overlays the Whisperer's reading on the
zone tile, but the /moistures.json response itself continues to
return the prediction — the public API does not expose the app-side
overlay.
This means the plugin's zone moisture state will diverge from a
paired Whisperer's actual reading whenever the model and reality
disagree. Observed example: same zone, same moment — /moistures.json
= 89%, paired Whisperer = 24%.
Response format:
{
"moistures": [
{
"id": 12345,
"zone": 1,
"moisture": 45,
"date": "2025-01-27"
}
]
}Plugin behavior:
- The zone
moistureForecaststate always holds the raw/moistures.jsonvalue. - The zone
moisturestate resolves to: the paired Whisperer's currentsoilMoistureif the pairing is configured on the zone device and the reading is ≤ 12 hours old (seeWHISPERER_STALENESS_HOURS), elsemoistureForecast. - Pairing is plugin-side (zone ConfigUI → "Paired Whisperer" dropdown), independent of any Netro-side pairing. Both can coexist; they don't interact.
Workarounds (when consuming the raw feed):
- Sort by ID to get most recent:
sort(key=lambda x: x['id'], reverse=True) - Filter by most recent date
- Show date with moisture value so users understand it is a daily prediction, not a live reading
Open empirical question: whether /moistures.json continues to
produce sane predictions when a Whisperer is paired on Netro's side but
has stopped reporting (dead battery, unplugged). The issue-#54
investigation showed /moistures.json returning a saturation-based
prediction even with a working paired Whisperer, suggesting the
prediction is independent of Whisperer reporting state. Recommend
dogfooding with a disconnected Whisperer for a week before relying on
the fallback in production.
Issue: Sensor update frequency is variable
Observations:
- Sensors report every 4-6 hours normally
- Can be longer if battery low
- No way to force immediate update
- Battery level crucial - <20% = unreliable
Response structure:
{
"sensor_data": [
{
"id": 123,
"moisture": 45,
"celsius": 22,
"fahrenheit": 72,
"sunlight": 1200,
"battery_level": 85,
"time": "...",
"local_date": "2025-01-27",
"local_time": "14:30:00"
}
]
}Field notes:
local_dateandlocal_timeare STRINGS (not swapped anymore - fixed in commit 617a630)battery_levelis percentage (0-100)sunlightis in lux- Most recent reading is first after sorting by ID
Issue: Weather parameters poorly documented
Required fields:
key: Serial numbert: Current temperature (Fahrenheit)date: YYYY-MM-DD format
Optional fields:
t_max,t_min: High/low temperaturehumidity: 0-100 percentagecondition: Weather code (0=clear, others undocumented)rain: Rainfall amount (units unclear)rain_prob: Rain probability 0-100wind_speed: Wind speed (units unclear - likely mph)pressure: Atmospheric pressure (units unclear - likely inHg)
Condition codes (observed/guessed):
0 = Clear
1 = Cloudy?
2 = Rain?
3+ = Unknown
Note: Weather reporting doesn't trigger immediate schedule changes
What works:
- ✅ Start individual zone with duration
- ✅ Stop all zones
- ✅ Set rain delay
- ✅ Set standby mode
What doesn't work (API limitations):
- ❌ Pause/resume individual zones
- ❌ Skip to next zone in schedule
- ❌ Go back to previous zone
- ❌ Modify zone duration while running
- ❌ Create/modify schedules via API
- ❌ Change zone settings (soil type, etc.)
Advanced features (available but less tested):
delay: Minutes before starting (0-60)start_time: Schedule for future (Unix timestamp)
Issue: Inconsistent error responses
Success:
{
"status": "OK",
"data": {...},
"meta": {...}
}Error examples:
// HTTP 401
{
"status": "ERROR",
"error": "Invalid key"
}
// HTTP 429
{
"status": "ERROR",
"error": "Rate limit exceeded"
}
// HTTP 500
{
"status": "ERROR",
"error": "Internal server error"
}Sometimes: Just HTTP status code, no JSON body
Workaround: Check HTTP status first, then parse JSON if available
Test controller: "Clark Castle Spark"
- Serial: 0cb8152f9f78
- 16 zones total
- 8 enabled zones
- API tokens: 1665/2000 remaining
When ONLINE:
- 50 schedules returned
- Status: "ONLINE"
- All zone data populated
When OFFLINE (controller powered off):
- 2 schedules returned (down from 50)
- Status: "STANDBY" (not "OFFLINE"!)
- Historical moisture data still available
last_activetimestamp updates when status changes
Polling behavior:
- 3-minute interval: ~480 calls/day (safe)
- 5-minute interval: ~288 calls/day (very safe)
- 1-minute interval: ~1440 calls/day (risky!)
- Always handle both string and number timestamps
- Don't assume response structure matches docs
- Test with controller both online and offline
- Implement generous error handling
- Log API responses in debug mode
- Check token_remaining in every response
- Use conservative polling intervals (5+ minutes)
- Increase polling interval if approaching rate limit
- Don't rely on immediate status updates
- Use Netro app for zone configuration
- Replace sensor batteries when <20%
- Allow 10-15 minutes for status changes
# Device info
curl "http://api.netrohome.com/npa/v1/info.json?key=YOUR_SERIAL"
# Schedules
curl "http://api.netrohome.com/npa/v1/schedules.json?key=YOUR_SERIAL"
# Moisture
curl "http://api.netrohome.com/npa/v1/moistures.json?key=YOUR_SERIAL"python3 test_local_api.py --serial YOUR_SERIALSee LOCAL_TESTING.md for details.
Official (limited) docs:
Better info sources:
- This file (API_NOTES.md)
- NETRO_API.md - Full endpoint documentation
- test_local_api.py source code
- plugin.py _make_api_call() implementation
Discoveries by version:
- v1.0 (initial): Basic API integration
- v2.0 (overhaul):
- Timestamp string/number handling (commit 9e93000)
- Device vs devices fix (commit 617a630)
- Offline status observations (Jan 2025 testing)
- Token warning thresholds added
- All quirks documented in this file
Behaviors to monitor:
- Schedule type values (may discover new types)
- Weather condition codes (need complete list)
- Error response formats (may vary)
- New API endpoints (if Netro adds features)
- Rate limit policy changes
Report issues:
- Unexpected API responses
- New schedule types
- Different error formats
- Undocumented fields