diff --git a/.gitignore b/.gitignore
index ea877b9..9363903 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,7 @@ test_results/
docs/
requirements-dev.txt
+
+# TestingBase — gitignored test creds and dedicated venv
+tests/.env
+tests/venv/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..06c05a5
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "tests/shared"]
+ path = tests/shared
+ url = https://github.com/IndigoDomotics/TestingBase.git
diff --git a/Netro Sprinklers.indigoPlugin/Contents/Info.plist b/Netro Sprinklers.indigoPlugin/Contents/Info.plist
index de6c0a8..d90bf15 100644
--- a/Netro Sprinklers.indigoPlugin/Contents/Info.plist
+++ b/Netro Sprinklers.indigoPlugin/Contents/Info.plist
@@ -2,18 +2,10 @@
- PluginVersion
- 2026.5.5
- ServerApiVersion
- 3.6
- IwsApiVersion
- 1.0.0
CFBundleDisplayName
Netro Smart Sprinklers
CFBundleIdentifier
com.simons-plugins.netro
- CFBundleVersion
- 2.0.0
CFBundleShortVersionString
2.0
CFBundleURLTypes
@@ -23,6 +15,8 @@
https://github.com/simons-plugins/netro-indigo
+ CFBundleVersion
+ 2.0.0
GithubInfo
GithubRepo
@@ -30,5 +24,11 @@
GithubUser
simons-plugins
+ IwsApiVersion
+ 1.0.0
+ PluginVersion
+ 2026.5.6
+ ServerApiVersion
+ 3.6
diff --git a/docs/plans/2026-05-04-netro-testingbase-pilot-design.md b/docs/plans/2026-05-04-netro-testingbase-pilot-design.md
new file mode 100644
index 0000000..72f7d6d
--- /dev/null
+++ b/docs/plans/2026-05-04-netro-testingbase-pilot-design.md
@@ -0,0 +1,152 @@
+# Netro TestingBase pilot — design
+
+**Date:** 2026-05-04
+**Status:** Approved (awaiting implementation plan)
+
+## Context
+
+Indigo 2025.2 ships [`TestingBase`](https://github.com/IndigoDomotics/TestingBase) — a shared submodule for live HTTP integration tests against a running Indigo server. The framework provides `APIBase` (a `unittest.TestCase` subclass that opens an `httpx` session to Indigo's HTTP API) and `ValidateXmlFile` (offline schema validation for plugin XML files).
+
+The workspace's existing test pattern is `unittest.mock`-based pytest tests against in-process plugin code (Pattern A — see `netro/tests/conftest.py` for the canonical example). TestingBase introduces a complementary pattern (Pattern B) that exercises the live runtime.
+
+This pilot adopts Pattern B in netro as a learning vehicle for workspace-wide adoption. **Intent: full adoption signal.** Once the pilot lands successfully, the pattern rolls out to other production-critical plugins (`heatmiser`, `home-intelligence`, `UK-Trains`) and the workspace `CLAUDE.md` is updated to make TestingBase the standard for live integration tests.
+
+## Goal
+
+Stand up TestingBase in netro using the upstream-strict layout, exercise both halves of the framework end-to-end against local Indigo 2025.2, and capture the friction so we know what we're signing up for at scale.
+
+## Approach
+
+**Two-venv layout, upstream-strict.** Pattern A continues to use netro's main pyproject.toml env. TestingBase tests live in `tests/integration/` and run from a separate `tests/venv/` populated from `tests/testing-requirements.txt`. This matches the convention used in IndigoDomotics' own repos and is the only layout that works uniformly across all workspace plugins (most don't have pyproject.toml).
+
+**Live test = plugin-load smoke.** One `APIBase` test that fetches Indigo's plugin list and asserts netro is loaded and enabled. Exercises auth, connectivity, and plugin discovery in a single call — no netro hardware or device fixtures required.
+
+**XML validation = full coverage.** All five netro XML files (`Actions.xml`, `Devices.xml`, `Events.xml`, `MenuItems.xml`, `PluginConfig.xml`) get a `ValidateXmlFile` subclass. These are pure offline schema checks — they don't need Indigo running.
+
+## Architecture
+
+```
+netro/
+├── tests/ # existing — Pattern A (13 tests)
+│ ├── conftest.py
+│ ├── test_*.py × 13
+│ ├── shared/ # NEW — TestingBase submodule
+│ ├── .env # NEW — gitignored, real creds
+│ ├── .env.example # NEW — committed template
+│ ├── testing-requirements.txt # NEW — chains to shared/module-requirements.txt
+│ ├── venv/ # NEW — gitignored, separate venv
+│ └── integration/ # NEW — TestingBase test files
+│ ├── __init__.py
+│ ├── test_xml_validation.py # 5× ValidateXmlFile classes
+│ └── test_indigo_smoke.py # 1× APIBase smoke test
+├── pyproject.toml # unchanged — main env stays for Pattern A
+└── .gitignore # add tests/.env, tests/venv/
+```
+
+## Components
+
+### `tests/shared/` (submodule)
+
+Pinned to `IndigoDomotics/TestingBase@main`. Read-only — never edited locally. Per upstream's explicit warning, contributors needing changes go through upstream.
+
+### `tests/testing-requirements.txt`
+
+```
+-r shared/module-requirements.txt
+```
+
+Nothing else for the pilot. Future tests can add deps below this line.
+
+### `tests/.env.example`
+
+Committed template, mirrors upstream `ENV_TEMPLATE`. Real values go in `tests/.env` (gitignored). Key variables:
+
+```
+shared.GOOD_API_KEY=
+shared.URL_PREFIX=https://localhost:8176
+shared.API_PREFIX=/v2/api
+shared.PLUGIN_ID=com.simons-plugins.netro
+shared.LOGGING_LEVEL=logging.INFO
+shared.PLUGIN_RESTART_WAIT_TIME=5
+shared.PAUSE_AFTER_UPDATE=0.0
+shared.RESTART_IN_DEBUGGER=false
+```
+
+### `tests/integration/test_xml_validation.py`
+
+Five classes, one per XML file. Each subclasses `ValidateXmlFile, APIBase` (in that MRO order — upstream is explicit). `server_plugin_dir_path` resolved relatively:
+
+```python
+import os
+from shared import APIBase, ValidateXmlFile
+
+PLUGIN_ROOT = os.path.abspath(os.path.join(
+ os.path.dirname(__file__),
+ "../../Netro Sprinklers.indigoPlugin/Contents/Server Plugin",
+))
+
+class TestActionsXml(ValidateXmlFile, APIBase):
+ server_plugin_dir_path = PLUGIN_ROOT
+ file_name = "Actions.xml"
+```
+
+(Repeat for Devices.xml, Events.xml, MenuItems.xml, PluginConfig.xml.)
+
+### `tests/integration/test_indigo_smoke.py`
+
+One class, one test:
+
+```python
+from shared import APIBase
+
+class TestNetroPluginLoaded(APIBase):
+ def test_plugin_is_loaded_and_enabled(self):
+ # Fetch Indigo's plugin list and assert netro is present + enabled.
+ # Uses APIBase's send_simple_command / get_indigo_object helpers
+ # against the URL_PREFIX from .env.
+ ...
+```
+
+Exact API call resolved during implementation — `APIBase` wraps Indigo's HTTP API and the upstream README hints at `send_simple_command` for plugin-level interactions.
+
+## Data flow
+
+1. Developer activates the pilot venv: `source tests/venv/bin/activate`.
+2. Runs `pytest tests/integration/`.
+3. Pytest auto-discovers `unittest.TestCase` subclasses (TestingBase classes).
+4. Each test class's `__init__` (inherited from `APIBase`) loads `tests/.env` via `python-dotenv`.
+5. `ValidateXmlFile` tests read the XML file from disk and validate the schema. No network.
+6. `APIBase` smoke test opens an `httpx` session to `https://localhost:8176/v2/api`, fetches the plugin list, asserts.
+
+## Error handling
+
+- **Submodule not initialized**: `from shared import APIBase` fails at import. Fix: `git submodule update --init`. Documented in `tests/integration/__init__.py` and a future `tests/README.md`.
+- **Missing `tests/.env`**: TestingBase fails fast at setUp with a clear error (per upstream).
+- **Wrong API key / Indigo not running**: smoke test fails with HTTP error in setUp; XML validation tests still pass (they don't touch Indigo).
+- **netro plugin not loaded**: smoke test asserts `loaded=true` and fails informatively.
+
+## Testing the pilot
+
+- All 13 existing Pattern A tests must continue to pass via main env: `pytest tests/` (with `tests/integration/` excluded by pyproject.toml's pytest config).
+- New integration tests pass against local Indigo 2025.2: `source tests/venv/bin/activate && pytest tests/integration/` shows 6/6 green (5 XML + 1 smoke).
+- `git submodule status` shows `tests/shared` initialised and pinned.
+- `tests/.env.example` is committed; `tests/.env` is not.
+
+## Out of scope
+
+- **CI integration** — deferred. Pilot establishes the local pattern. CI access to Indigo (or a running Indigo in CI) is a follow-up.
+- **More than one APIBase test** — the framework smoke is enough to validate the integration. Substantive netro-specific live tests (device states, action invocation) come after rollout.
+- **Migrating Pattern A tests** — Pattern A and B do different things. They coexist; nothing migrates.
+- **Workspace rollout** — separate work after this pilot lands and we know it works.
+
+## Open verification questions for implementation
+
+- Does TestingBase's `APIBase` work on a fresh Indigo 2025.2 with a new API key issued via Indigo's preferences? (Probably yes — verifying this is part of the pilot's value.)
+- What's the exact `send_*` call to fetch the plugin list and assert load state? Resolved by reading `shared/classes.py` once the submodule is in.
+
+## Success criteria
+
+- 6/6 integration tests green against local Indigo 2025.2.
+- 13/13 Pattern A tests still green.
+- Design and resulting friction documented well enough to inform workspace rollout.
+- PR opened, reviewed, merged.
diff --git a/docs/plans/2026-05-04-netro-testingbase-pilot-plan.md b/docs/plans/2026-05-04-netro-testingbase-pilot-plan.md
new file mode 100644
index 0000000..e97a5ba
--- /dev/null
+++ b/docs/plans/2026-05-04-netro-testingbase-pilot-plan.md
@@ -0,0 +1,633 @@
+# Netro TestingBase pilot — implementation plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Set up TestingBase as the live-integration test framework in netro, with all 5 XML files validated and 1 plugin-loaded smoke test green against local Indigo 2025.2.
+
+**Architecture:** Upstream-strict two-venv layout. Submodule at `tests/shared/`, separate `tests/venv/`, deps from `tests/testing-requirements.txt`. New tests live in `tests/integration/`. Pattern A pytest tests are unaffected.
+
+**Tech Stack:** Python 3.13, `unittest.TestCase` (via TestingBase's `APIBase`), `httpx`, `python-dotenv`, pytest as the runner.
+
+**Companion design doc:** `docs/plans/2026-05-04-netro-testingbase-pilot-design.md` — read first if you need context.
+
+**Branch:** `feat/testingbase-pilot` (already created; design doc already committed).
+
+---
+
+### Task 1: Update .gitignore
+
+**Files:**
+- Modify: `.gitignore`
+
+**Step 1:** Read current `.gitignore` to find a good insertion point (look for the existing `docs/` line at L50 for style reference).
+
+**Step 2:** Append at the end of `.gitignore`:
+
+```
+# TestingBase — gitignored test creds and dedicated venv
+tests/.env
+tests/venv/
+```
+
+**Step 3:** Verify nothing currently tracked matches:
+
+```bash
+git ls-files tests/.env tests/venv 2>&1
+```
+
+Expected: empty output (nothing tracked at those paths).
+
+**Step 4:** Commit.
+
+```bash
+git add .gitignore
+git commit -m "chore: gitignore tests/.env and tests/venv for TestingBase"
+```
+
+---
+
+### Task 2: Add TestingBase as a submodule
+
+**Files:**
+- Create: `.gitmodules` (auto-generated by `git submodule add`)
+- Create: `tests/shared/` (submodule)
+
+**Step 1:** Add the submodule.
+
+```bash
+git submodule add https://github.com/IndigoDomotics/TestingBase.git tests/shared
+```
+
+Expected: clones the repo into `tests/shared/`, creates `.gitmodules`.
+
+**Step 2:** Confirm submodule status.
+
+```bash
+git submodule status
+```
+
+Expected: shows `tests/shared` initialised at the upstream HEAD commit.
+
+**Step 3:** Verify the upstream files are present.
+
+```bash
+ls tests/shared/
+```
+
+Expected: `__init__.py`, `classes.py`, `constants.py`, `utils.py`, `module-requirements.txt`, `ENV_TEMPLATE`, `README.md`.
+
+**Step 4:** Commit.
+
+```bash
+git add .gitmodules tests/shared
+git commit -m "feat: add TestingBase submodule at tests/shared/"
+```
+
+---
+
+### Task 3: Create `tests/testing-requirements.txt`
+
+**Files:**
+- Create: `tests/testing-requirements.txt`
+
+**Step 1:** Create the file with this exact content:
+
+```
+-r shared/module-requirements.txt
+```
+
+Nothing else. The pilot's only deps are TestingBase's own — no extras.
+
+**Step 2:** Confirm `tests/shared/module-requirements.txt` exists and lists at least `python-dotenv` and `httpx`.
+
+```bash
+cat tests/shared/module-requirements.txt
+```
+
+Expected: contains `python-dotenv` and `httpx`. (If upstream changes, the requirements still chain through `-r`.)
+
+**Step 3:** Commit.
+
+```bash
+git add tests/testing-requirements.txt
+git commit -m "feat: add tests/testing-requirements.txt chaining to TestingBase shared deps"
+```
+
+---
+
+### Task 4: Create `tests/.env.example`
+
+**Files:**
+- Create: `tests/.env.example`
+
+**Step 1:** Create with this content (mirrors upstream `ENV_TEMPLATE`, populated with netro-specific defaults):
+
+```
+# Copy to tests/.env (gitignored) and fill in your local values.
+# Format: shared.=. Booleans are "true" or "false".
+
+# An Indigo HTTP API key. Get one from Indigo's Server menu →
+# "API Keys…", or from your indigodomo.com account.
+shared.GOOD_API_KEY=YOUR-API-KEY-HERE
+
+# Where Indigo is running. Default is the local server.
+shared.URL_PREFIX=https://localhost:8176
+
+# API version path — Indigo 2025.2 uses /v2/api.
+shared.API_PREFIX=/v2/api
+
+# Bundle identifier of the plugin under test (used by APIBase.restart_plugin).
+shared.PLUGIN_ID=com.simons-plugins.netro
+
+# Logging level for the test run.
+shared.LOGGING_LEVEL=logging.INFO
+
+# Wait this many seconds after restarting the plugin under test.
+shared.PLUGIN_RESTART_WAIT_TIME=5
+
+# Pause after a state-changing call so the change can propagate before the
+# next assertion. 0.0 = no pause.
+shared.PAUSE_AFTER_UPDATE=0.0
+
+# Whether to restart the plugin in the debugger.
+shared.RESTART_IN_DEBUGGER=false
+```
+
+**Step 2:** Commit.
+
+```bash
+git add tests/.env.example
+git commit -m "feat: add tests/.env.example template for TestingBase"
+```
+
+---
+
+### Task 5: Create `tests/venv/` and install dependencies (local — not committed)
+
+**Files:**
+- Create (untracked): `tests/venv/`
+
+**Step 1:** Create the venv with Python 3.13.
+
+```bash
+/Library/Frameworks/Python.framework/Versions/3.13/bin/python3 -m venv tests/venv
+```
+
+If the path is different on this Mac (we previously found 3.13 wasn't installed system-wide), use whichever 3.13 the local Indigo 2025.2 install brought in. As a fallback, Homebrew's 3.13: `/opt/homebrew/opt/python@3.13/bin/python3.13 -m venv tests/venv`.
+
+**Step 2:** Activate and install.
+
+```bash
+source tests/venv/bin/activate
+pip install --upgrade pip
+pip install -r tests/testing-requirements.txt
+```
+
+Expected: `httpx`, `python-dotenv`, and any transitive deps install successfully.
+
+**Step 3:** Verify imports resolve from the new venv.
+
+```bash
+python -c "from shared import APIBase, ValidateXmlFile; print('OK')"
+```
+
+Expected: prints `OK`.
+
+(This step has no commit — `tests/venv/` is gitignored.)
+
+---
+
+### Task 6: Create `tests/.env` (local — not committed)
+
+**Files:**
+- Create (untracked): `tests/.env`
+
+**Step 1:** In Indigo 2025.2's UI, generate a fresh API key (Server menu → "API Keys…" → "New API Key"). Note the value — it's only shown once.
+
+**Step 2:** Copy the example.
+
+```bash
+cp tests/.env.example tests/.env
+```
+
+**Step 3:** Edit `tests/.env` and replace `YOUR-API-KEY-HERE` with the real key. Leave the rest at defaults if the local Indigo is on `https://localhost:8176`.
+
+**Step 4:** Verify `python-dotenv` loads it.
+
+```bash
+source tests/venv/bin/activate
+python -c "import os; from dotenv import load_dotenv; load_dotenv('tests/.env'); print(os.getenv('shared.GOOD_API_KEY')[:8] + '…')"
+```
+
+Expected: prints first 8 characters of your API key followed by `…`. Confirms the file is read and the variable is exposed.
+
+(No commit — `tests/.env` is gitignored.)
+
+---
+
+### Task 7: Create `tests/integration/__init__.py`
+
+**Files:**
+- Create: `tests/integration/__init__.py`
+
+**Step 1:** Create the file with this content:
+
+```python
+"""TestingBase live-integration tests for netro.
+
+Run from the dedicated venv (not netro's main env):
+
+ source tests/venv/bin/activate
+ pytest tests/integration/
+
+Requires `tests/.env` to exist with a valid Indigo API key. See
+`tests/.env.example` for the template, and the `docs/plans/`
+design doc for the full pilot context.
+
+If you see `ModuleNotFoundError: No module named 'shared'`, the
+TestingBase submodule isn't initialised. Run:
+
+ git submodule update --init
+"""
+```
+
+**Step 2:** Commit.
+
+```bash
+git add tests/integration/__init__.py
+git commit -m "feat: add tests/integration package with run instructions"
+```
+
+---
+
+### Task 8: Write the XML validation test file
+
+**Files:**
+- Create: `tests/integration/test_xml_validation.py`
+
+**Step 1:** Create the file with this exact content:
+
+```python
+"""ValidateXmlFile coverage for all 5 netro plugin XML files.
+
+These tests don't need Indigo running — they validate the XML
+files on disk against TestingBase's bundled schema. Catch broken
+XML before it reaches end users.
+"""
+import os
+
+from shared import APIBase, ValidateXmlFile
+
+# Resolve the Server Plugin directory relative to this test file so the
+# tests run on any machine.
+PLUGIN_ROOT = os.path.abspath(
+ os.path.join(
+ os.path.dirname(__file__),
+ "../../Netro Sprinklers.indigoPlugin/Contents/Server Plugin",
+ )
+)
+
+
+class TestActionsXml(ValidateXmlFile, APIBase):
+ server_plugin_dir_path = PLUGIN_ROOT
+ file_name = "Actions.xml"
+
+
+class TestDevicesXml(ValidateXmlFile, APIBase):
+ server_plugin_dir_path = PLUGIN_ROOT
+ file_name = "Devices.xml"
+
+
+class TestEventsXml(ValidateXmlFile, APIBase):
+ server_plugin_dir_path = PLUGIN_ROOT
+ file_name = "Events.xml"
+
+
+class TestMenuItemsXml(ValidateXmlFile, APIBase):
+ server_plugin_dir_path = PLUGIN_ROOT
+ file_name = "MenuItems.xml"
+
+
+class TestPluginConfigXml(ValidateXmlFile, APIBase):
+ server_plugin_dir_path = PLUGIN_ROOT
+ file_name = "PluginConfig.xml"
+```
+
+**Step 2:** Run the tests.
+
+```bash
+source tests/venv/bin/activate
+cd tests && pytest integration/test_xml_validation.py -v
+cd ..
+```
+
+Expected: all 5 test classes' validation tests pass. The exact test method names come from `ValidateXmlFile`'s implementation (run with `-v` to see them).
+
+**Step 3:** If any fail, the failing XML file has a real schema problem — fix the XML, not the test. Then re-run.
+
+**Step 4:** Commit.
+
+```bash
+git add tests/integration/test_xml_validation.py
+git commit -m "test: add ValidateXmlFile coverage for all 5 netro XML files"
+```
+
+---
+
+### Task 9: Write the plugin-loaded smoke test
+
+**Files:**
+- Create: `tests/integration/test_indigo_smoke.py`
+
+**Step 1:** Confirm the right Indigo HTTP API endpoint for plugins. From upstream `tests/shared/classes.py`, `APIBase.get_indigo_object(endpoint, obj_id)` builds `{url_prefix}/v2/api/indigo.{endpoint}` (and appends `/{obj_id}` if given). So `get_indigo_object("plugins")` fetches all plugins and returns a list of dicts. We'll filter for our `PLUGIN_ID`.
+
+**Step 2:** Create the test file with this content:
+
+```python
+"""Plugin-loaded smoke test using TestingBase's APIBase.
+
+Asserts the netro plugin is loaded and enabled in the running
+Indigo server. Validates auth, connectivity, and plugin
+discovery in one call. No netro hardware required.
+"""
+from shared import APIBase
+
+
+class TestNetroPluginLoaded(APIBase):
+ def test_netro_is_loaded_and_enabled(self):
+ plugins = self.get_indigo_object("plugins")
+ self.assertIsInstance(plugins, list, "plugins endpoint must return a list")
+ netro = next(
+ (p for p in plugins if p.get("pluginId") == self.plugin_id),
+ None,
+ )
+ self.assertIsNotNone(
+ netro,
+ f"netro plugin {self.plugin_id} not found in Indigo's plugin list. "
+ f"Loaded plugin IDs: {sorted(p.get('pluginId', '?') for p in plugins)}",
+ )
+ # Indigo's plugin object exposes 'enabled' and 'isRunning' (or similar).
+ # If the exact key differs in Indigo 2025.2, the assertion message
+ # will tell us what keys are actually there.
+ self.assertTrue(
+ netro.get("enabled", False),
+ f"netro plugin found but not enabled. Plugin object: {netro}",
+ )
+```
+
+**Step 3:** Run only this test file, with verbose output so we can see what Indigo actually returns if the assertions fail:
+
+```bash
+source tests/venv/bin/activate
+cd tests && pytest integration/test_indigo_smoke.py -v -s
+cd ..
+```
+
+Expected: passes. If it fails on `enabled`, the assertion message will show the actual plugin dict keys — adjust the key name (e.g., to `isEnabled` or `isRunning`) and re-run. **Do not silently weaken the assertion** — find the right key.
+
+**Step 4:** Once green, commit.
+
+```bash
+git add tests/integration/test_indigo_smoke.py
+git commit -m "test: add APIBase smoke test asserting netro is loaded"
+```
+
+---
+
+### Task 10: Configure pytest to exclude `tests/integration/` by default
+
+**Files:**
+- Modify: `pyproject.toml` (find the `[tool.pytest.ini_options]` section, or `pytest.ini` if that's where it lives)
+
+**Step 1:** Read current pytest config.
+
+```bash
+grep -A20 "pytest" pyproject.toml
+```
+
+Note the existing config — particularly any `testpaths` or `addopts` keys.
+
+**Step 2:** Add `tests/integration` to pytest's `norecursedirs` (so the main env's pytest doesn't try to discover and import TestingBase tests, which would fail in that env without `from shared import …`):
+
+If `[tool.pytest.ini_options]` exists, add:
+```toml
+norecursedirs = ["tests/integration", "tests/shared", "tests/venv"]
+```
+
+If it doesn't, add the whole block.
+
+**Step 3:** Verify Pattern A still runs cleanly from netro's main env.
+
+```bash
+deactivate 2>/dev/null # leave the integration venv if active
+pytest tests/ -v
+```
+
+Expected: 13 Pattern A tests collected, 0 from `tests/integration/`. All pass (or whatever the existing baseline is — should not regress).
+
+**Step 4:** Verify integration tests still run from the integration venv.
+
+```bash
+source tests/venv/bin/activate
+cd tests && pytest integration/ -v
+cd ..
+```
+
+Expected: all 6 integration tests (5 XML + 1 smoke) pass.
+
+**Step 5:** Commit.
+
+```bash
+git add pyproject.toml
+git commit -m "chore: exclude tests/integration tests/shared tests/venv from main pytest discovery"
+```
+
+---
+
+### Task 11: Add a tests/README.md
+
+**Files:**
+- Create: `tests/README.md`
+
+**Step 1:** Create with this content:
+
+```markdown
+# Netro tests
+
+Two test patterns coexist here:
+
+## Pattern A — pytest with mocked Indigo (existing)
+
+`unittest.mock`-based tests in `tests/test_*.py`. Run from netro's
+main env:
+
+```bash
+pytest tests/
+```
+
+Fast, no Indigo runtime, runs in CI.
+
+## Pattern B — TestingBase live integration
+
+Lives in `tests/integration/` and uses the [TestingBase submodule](https://github.com/IndigoDomotics/TestingBase)
+at `tests/shared/`. Runs against a live Indigo server.
+
+### One-time setup
+
+```bash
+git submodule update --init
+python3.13 -m venv tests/venv
+source tests/venv/bin/activate
+pip install -r tests/testing-requirements.txt
+cp tests/.env.example tests/.env
+# edit tests/.env, fill in your Indigo API key
+```
+
+### Running
+
+```bash
+source tests/venv/bin/activate
+pytest tests/integration/
+```
+
+### What's tested
+
+- `tests/integration/test_xml_validation.py` — ValidateXmlFile for
+ Actions.xml, Devices.xml, Events.xml, MenuItems.xml,
+ PluginConfig.xml. Schema validation, no Indigo runtime needed.
+- `tests/integration/test_indigo_smoke.py` — APIBase smoke test
+ asserting netro is loaded and enabled in the running Indigo
+ server.
+
+### Updating TestingBase
+
+The submodule tracks upstream `main`. Pull changes with:
+
+```bash
+git submodule update --recursive --remote tests/shared
+```
+
+Per upstream's README, never edit `tests/shared/` locally. Open an
+issue at https://github.com/IndigoDomotics/TestingBase if you need
+changes.
+```
+
+**Step 2:** Commit.
+
+```bash
+git add tests/README.md
+git commit -m "docs: add tests/README explaining Pattern A + Pattern B"
+```
+
+---
+
+### Task 12: Bump PluginVersion + open PR
+
+**Files:**
+- Modify: `Netro Sprinklers.indigoPlugin/Contents/Info.plist`
+
+**Step 1:** Read current version.
+
+```bash
+plutil -p "Netro Sprinklers.indigoPlugin/Contents/Info.plist" | grep PluginVersion
+```
+
+Expected: shows current version (`2026.5.3` per the audit memo).
+
+**Step 2:** Bump patch version (e.g., `2026.5.3 → 2026.5.4`). This is a test-infra change with zero user-facing behaviour, so patch is right per the workspace's version-bump-scope rule.
+
+```bash
+plutil -replace PluginVersion -string "2026.5.4" "Netro Sprinklers.indigoPlugin/Contents/Info.plist"
+```
+
+(Adjust the new version if `2026.5.3` is no longer current — just bump the rightmost number.)
+
+**Step 3:** Verify.
+
+```bash
+plutil -p "Netro Sprinklers.indigoPlugin/Contents/Info.plist" | grep PluginVersion
+```
+
+**Step 4:** Commit.
+
+```bash
+git add "Netro Sprinklers.indigoPlugin/Contents/Info.plist"
+git commit -m "chore: bump PluginVersion to 2026.5.4 for TestingBase pilot PR"
+```
+
+**Step 5:** Push the branch.
+
+```bash
+git push -u origin feat/testingbase-pilot
+```
+
+**Step 6:** Open PR. Use a HEREDOC so formatting is preserved:
+
+```bash
+gh pr create --title "feat: TestingBase pilot — submodule + XML validation + plugin smoke (2026.5.4)" --body "$(cat <<'EOF'
+## Summary
+
+First adoption of [TestingBase](https://github.com/IndigoDomotics/TestingBase) in the workspace. Establishes the **upstream-strict two-venv layout** as the workspace standard for live integration tests, validated end-to-end on local Indigo 2025.2.
+
+## What's added
+
+- \`tests/shared/\` — TestingBase submodule pinned to upstream main
+- \`tests/testing-requirements.txt\` — chains to \`shared/module-requirements.txt\`
+- \`tests/.env.example\` — committed template; real \`.env\` is gitignored
+- \`tests/integration/test_xml_validation.py\` — ValidateXmlFile coverage for all 5 netro XML files (Actions/Devices/Events/MenuItems/PluginConfig)
+- \`tests/integration/test_indigo_smoke.py\` — single APIBase test asserting netro is loaded and enabled
+- \`tests/README.md\` — Pattern A vs Pattern B run instructions
+- \`pyproject.toml\` — excludes \`tests/integration\`, \`tests/shared\`, \`tests/venv\` from main-env pytest discovery
+- \`.gitignore\` — adds \`tests/.env\` and \`tests/venv/\`
+
+Design and rationale: \`docs/plans/2026-05-04-netro-testingbase-pilot-design.md\`.
+
+## Test plan
+
+- [x] \`pytest tests/\` (main env) — all 13 Pattern A tests still green; integration tests are not collected
+- [x] \`source tests/venv/bin/activate && pytest tests/integration/\` — 6/6 green (5 XML + 1 smoke)
+- [x] Smoke test fetches Indigo's plugin list and confirms \`com.simons-plugins.netro\` is loaded and enabled
+- [x] \`git submodule status\` shows \`tests/shared\` initialised at the upstream commit pinned in \`.gitmodules\`
+
+## Out of scope
+
+- CI integration for Pattern B — deferred (CI doesn't currently have an Indigo to talk to)
+- Rolling out the same setup to other workspace plugins — separate work after this lands
+- Workspace \`CLAUDE.md\` standard update — separate PR after this lands
+EOF
+)"
+```
+
+**Step 7:** Watch CI.
+
+```bash
+gh pr checks --watch
+```
+
+Wait for green. Do NOT merge — wait for explicit user go-ahead per workspace standards.
+
+---
+
+## Verification (run before reporting "done")
+
+```bash
+# Pattern A still green from main env
+deactivate 2>/dev/null
+pytest tests/ -v
+# Expected: 13 tests pass, 0 from tests/integration/
+
+# Pattern B green from integration venv
+source tests/venv/bin/activate
+cd tests && pytest integration/ -v && cd ..
+# Expected: 6 tests pass (5 XML + 1 smoke)
+
+# Submodule pinned correctly
+git submodule status
+# Expected: shows tests/shared initialised at upstream HEAD
+
+# Branch is pushed and CI is green
+gh pr checks
+# Expected: all checks pass
+```
+
+If any of these fail, fix the failure — do not mark the plan complete.
diff --git a/pyproject.toml b/pyproject.toml
index 35daebd..e8a0072 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,3 +44,7 @@ testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
+# TestingBase lives in tests/venv/ and tests/shared/ (submodule). Don't
+# recurse there from the main env — they need their own venv to import
+# `shared`. tests/integration/ runs only via that venv too.
+norecursedirs = ["tests/integration", "tests/shared", "tests/venv"]
diff --git a/pytest.ini b/pytest.ini
index 27865af..938c7e7 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -9,6 +9,12 @@ python_functions = test_*
# Test paths
testpaths = tests
+# TestingBase lives in its own venv (tests/venv/) and the submodule
+# (tests/shared/). Don't recurse there from the main env — `from shared
+# import APIBase` will fail without the dedicated deps. Likewise
+# tests/integration/ runs only via that venv.
+norecursedirs = tests/integration tests/shared tests/venv
+
# Output options
addopts =
-v
diff --git a/tests/.env.example b/tests/.env.example
new file mode 100644
index 0000000..82b742a
--- /dev/null
+++ b/tests/.env.example
@@ -0,0 +1,28 @@
+# Copy to tests/.env (gitignored) and fill in your local values.
+# Format: shared.=. Booleans are "true" or "false".
+
+# An Indigo HTTP API key. Get one from Indigo's Server menu →
+# "API Keys…", or from your indigodomo.com account.
+shared.GOOD_API_KEY=YOUR-API-KEY-HERE
+
+# Where Indigo is running. Default is the local server.
+shared.URL_PREFIX=https://localhost:8176
+
+# API version path — Indigo 2025.2 uses /v2/api.
+shared.API_PREFIX=/v2/api
+
+# Bundle identifier of the plugin under test (used by APIBase.restart_plugin).
+shared.PLUGIN_ID=com.simons-plugins.netro
+
+# Logging level for the test run.
+shared.LOGGING_LEVEL=logging.INFO
+
+# Wait this many seconds after restarting the plugin under test.
+shared.PLUGIN_RESTART_WAIT_TIME=5
+
+# Pause after a state-changing call so the change can propagate before the
+# next assertion. 0.0 = no pause.
+shared.PAUSE_AFTER_UPDATE=0.0
+
+# Whether to restart the plugin in the debugger.
+shared.RESTART_IN_DEBUGGER=false
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..d615b0a
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,77 @@
+# Netro tests
+
+Two test patterns coexist here.
+
+## Pattern A — pytest with mocked Indigo (default, runs in CI)
+
+`unittest.mock`-based tests in `tests/test_*.py`. Run from netro's main env:
+
+```bash
+pytest tests/
+```
+
+Fast, no Indigo runtime. The repo's main `pytest.ini` covers this layer
+(coverage flags, markers, etc.).
+
+`tests/integration/`, `tests/shared/`, and `tests/venv/` are excluded from
+discovery via `norecursedirs` in both `pytest.ini` and `pyproject.toml`.
+
+## Pattern B — TestingBase live integration
+
+Lives in `tests/integration/` and uses the [TestingBase submodule](https://github.com/IndigoDomotics/TestingBase)
+at `tests/shared/`. Tests subclass `APIBase` (`unittest.TestCase` + `httpx`)
+and exercise a running Indigo server. **Requires Indigo installed locally**
+— `APIBase.setUpClass` spawns `/usr/local/indigo/indigo-host` to read the
+install path, so tests can only run on a developer machine with Indigo
+present (not in CI without an Indigo install).
+
+### One-time setup
+
+```bash
+git submodule update --init # if you cloned without --recurse-submodules
+/Library/Frameworks/Python.framework/Versions/3.13/bin/python3 -m venv tests/venv
+source tests/venv/bin/activate
+pip install -r tests/testing-requirements.txt # installs httpx + python-dotenv + pytest
+cp tests/.env.example tests/.env
+# edit tests/.env, set shared.GOOD_API_KEY to a real Indigo API key
+# (Indigo Server menu → "API Keys…" → New API Key)
+```
+
+### Running
+
+```bash
+source tests/venv/bin/activate
+cd tests
+pytest integration/ # picks up tests/integration/pytest.ini
+```
+
+`tests/integration/pytest.ini` makes the integration directory its own
+pytest rootdir. That isolates this suite from the main `pytest.ini`'s
+coverage flags (which require `pytest-cov`, which the integration venv
+deliberately doesn't install — coverage of TestingBase tests against live
+Indigo isn't meaningful).
+
+### What's tested
+
+- `tests/integration/test_xml_validation.py` — 5 `ValidateXmlFile` classes
+ covering Actions.xml, Devices.xml, Events.xml, MenuItems.xml,
+ PluginConfig.xml. Schema validation only; no Indigo runtime needed for
+ the validation itself, but `APIBase.setUpClass` still runs (it reads
+ the Indigo install folder).
+- `tests/integration/test_indigo_smoke.py` — one `APIBase` test asserting
+ `com.simons-plugins.netro` appears in Indigo's plugin list and is
+ enabled. If your Indigo 2025.2 plugin object uses a different field
+ name than `enabled`, the assertion message will dump the full plugin
+ dict so you can adjust the test.
+
+### Updating TestingBase
+
+The submodule tracks upstream `main`. Pull changes with:
+
+```bash
+git submodule update --recursive --remote tests/shared
+```
+
+Per upstream's README, never edit `tests/shared/` locally. File issues
+upstream at https://github.com/IndigoDomotics/TestingBase if you need
+changes.
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644
index 0000000..ff2f548
--- /dev/null
+++ b/tests/integration/__init__.py
@@ -0,0 +1,16 @@
+"""TestingBase live-integration tests for netro.
+
+Run from the dedicated venv (not netro's main env):
+
+ source tests/venv/bin/activate
+ pytest tests/integration/
+
+Requires `tests/.env` to exist with a valid Indigo API key. See
+`tests/.env.example` for the template, and the `docs/plans/`
+design doc for the full pilot context.
+
+If you see `ModuleNotFoundError: No module named 'shared'`, the
+TestingBase submodule isn't initialised. Run:
+
+ git submodule update --init
+"""
diff --git a/tests/integration/pytest.ini b/tests/integration/pytest.ini
new file mode 100644
index 0000000..a9b1233
--- /dev/null
+++ b/tests/integration/pytest.ini
@@ -0,0 +1,20 @@
+[pytest]
+# Pytest config for the TestingBase integration suite (Pattern B).
+#
+# The netro repo's main pytest.ini at the repo root sets `addopts` with
+# coverage flags for Pattern A. Those flags require pytest-cov, which the
+# integration venv doesn't install (coverage of TestingBase tests against
+# live Indigo isn't meaningful — they don't import netro source).
+#
+# A pytest.ini here makes this directory its own rootdir, isolating the
+# integration suite from the main env's addopts. Run with:
+#
+# source ../venv/bin/activate
+# pytest # picks up THIS config
+#
+# Or from netro repo root:
+#
+# pytest tests/integration/ # pytest finds this config via rootdir search
+
+# Empty addopts — no coverage, no strict markers from the main config.
+addopts =
diff --git a/tests/integration/test_indigo_smoke.py b/tests/integration/test_indigo_smoke.py
new file mode 100644
index 0000000..1e68391
--- /dev/null
+++ b/tests/integration/test_indigo_smoke.py
@@ -0,0 +1,55 @@
+"""Plugin-loaded smoke test using TestingBase's APIBase + run_host_script.
+
+Asserts the netro plugin is loaded and enabled in the running Indigo
+server. Indigo's HTTP API does not expose plugin state — only devices,
+variables, actionGroups, controlPages, logs, triggers, schedules — so
+plugin queries go through the IOM via `indigo-host -e