FranklinWH Hardware Test Guide¶
Purpose: Safe, recorded testing of battery control commands against live aGate hardware
Location:tests/hardware/test_live_battery_control.py
Runner:run_hardware_tests.py
Last Updated: 2026-02-28
β οΈ CRITICAL: Always Release Control After Testing¶
The aGate MUST be returned to Cloud API control after testing.
If tests are interrupted or fail, the aGate may be left in Modbus control mode (WSetEna=1), which: - Prevents the FranklinWH app from working - Blocks Cloud API/TOU schedules - May cause unexpected behavior
Quick Release Commands¶
# Method 1: Using CLI --stop flag (RECOMMENDED)
python franklinwh_cli.py -i YOUR_AGATE_IP --stop
# Method 2: Using release test
python run_hardware_tests.py --release
# Method 3: Direct reset via Python
python -c "
import sys; sys.path.insert(0, 'src')
from franklinwh_modbus import FranklinWHController
ctrl = FranklinWHController('YOUR_AGATE_IP')
ctrl.connect()
ctrl.reset_control_state()
print('β Control released - WSetEna=0')
ctrl.disconnect()
"
Post-Test Verification Checklist¶
After ANY testing, verify the aGate is in a safe state:
# Check 1: Verify released state
python franklinwh_cli.py -i YOUR_AGATE_IP --status | grep "Control Source"
# Expected: "Control Source: Cloud API (aGate native mode)" OR "Idle (no active control)"
# Check 2: Verify no zombie state
python franklinwh_cli.py -i YOUR_AGATE_IP --healthcheck | grep "zombie_state"
# Expected: "β zombie_state: OK (not in zombie state)"
# Check 3: Verify alarms clear
python franklinwh_cli.py -i YOUR_AGATE_IP --status | grep -A5 "ALARMS"
# Expected: "β No alarms active"
If Control Source shows "Modbus Control: ACTIVE" - run --stop immediately.
π Test Results Location¶
All test results are automatically saved to:
Result File Structure¶
{
"test_session": {
"start_time": "2026-02-28T23:30:00",
"end_time": "2026-02-28T23:35:00",
"total_tests": 5,
"passed": 4,
"failed": 0,
"skipped": 1
},
"results": [
{
"test_name": "test_charge_low_power",
"timestamp": "2026-02-28T23:30:15",
"status": "passed",
"duration_seconds": 12.5,
"pre_soc": 70.0,
"pre_battery_power": 500,
"pre_grid_power": -36,
"pre_control_mode": "Self-Consumption",
"pre_wset_ena": 0,
"command_power": -500,
"command_mode": "manual",
"post_soc": 70.1,
"post_battery_power": -450,
"post_grid_power": 520,
"post_wset_ena": 1,
"expected_behavior": "Battery charging at ~500W (negative power)",
"actual_behavior": "Battery power: -450W, WSetEna: 1",
"validation_passed": true,
"error_message": null,
"traceback": null
}
]
}
Viewing Recent Results¶
# List all result files
ls -lt data/test_results_*.json | head -5
# View latest results
latest=$(ls -t data/test_results_*.json | head -1)
cat "$latest" | python -m json.tool
# Get summary only
cat "$latest" | python -c "import json,sys; d=json.load(sys.stdin); print(f\"Passed: {d['test_session']['passed']}/{d['test_session']['total_tests']}\")"
π Running Tests¶
Option 1: Using the Test Runner (Recommended)¶
The test runner provides a guided interface with safety checks:
# 1. List available tests
python run_hardware_tests.py --list
# 2. Run read-only tests (safest - no writes to battery)
python run_hardware_tests.py --read-only
# 3. Run low-power write tests (500W for ~10 seconds each)
python run_hardware_tests.py --low-power
# 4. Run with auto-confirmation (for automation)
python run_hardware_tests.py --low-power --yes
# 5. Custom results file
python run_hardware_tests.py --low-power --results-file my_test.json
Option 2: Using Pytest Directly¶
For more control or CI integration:
# Source the virtual environment
source venv/bin/activate
# Read-only tests
pytest tests/hardware/test_live_battery_control.py -v -m hardware --collect-only
pytest tests/hardware/test_live_battery_control.py -v -m "hardware and not destructive"
# Destructive tests (writes to battery)
pytest tests/hardware/test_live_battery_control.py -v -m "hardware and destructive" --destructive-enabled
# Specific test
pytest tests/hardware/test_live_battery_control.py::test_charge_low_power -v --destructive-enabled
π§ͺ Available Tests¶
Read-Only Tests (Always Safe)¶
| Test | Description | Duration |
|---|---|---|
test_read_all_models |
Reads all SunSpec models (1, 501, 502, 701, 702, 703, 704, 713, 714, 715) | ~2s |
test_battery_status_consistency |
Reads battery 5 times over 5 seconds, validates stability | ~5s |
test_alarms_clear |
Verifies no critical alarms are active | ~1s |
Low-Power Write Tests (500W)¶
| Test | Description | Duration | Safety |
|---|---|---|---|
test_charge_low_power |
Commands 500W charge, validates negative power | ~15s | Auto-rollback |
test_discharge_low_power |
Commands 500W discharge, validates positive power | ~15s | Auto-rollback |
test_standby |
Commands 0W standby | ~10s | Auto-rollback |
test_release_control |
Takes control then releases (WSetEnaβ0) | ~10s | Validates release |
Advanced Tests (Conditional)¶
| Test | Description | Condition |
|---|---|---|
test_soc_ramping_near_limit |
Validates power ramping near SoC limits | Only runs if SoC > 85% |
π‘οΈ Safety Mechanisms¶
1. Pre-Test Safety Checks¶
Every destructive test validates: - β SoC between 10-95% (prevents overcharge/deep discharge) - β Grid connected (off-grid operation blocked by default) - β Voltage 200-270V (safe operating range) - β No critical alarms (safety faults block operation)
If any check fails, test is skipped with explanation.
2. Automatic Rollback¶
Every test automatically:
1. Captures pre-test state
2. Executes test
3. Waits for power stabilization (up to 30s)
4. Validates results
5. Calls reset_control_state() to release control
6. Records results to JSON
3. Power Stabilization Detection¶
After each command, the test waits for: - 5 consecutive readings within 100W variance - Or 30-second timeout (whichever comes first)
This ensures validation occurs after the battery has responded.
4. Test Result Recording¶
Every test records: - Pre/post SoC, power, grid state - Command parameters - Expected vs actual behavior - Pass/fail validation - Error messages on failure
π Post-Test Procedure (MANDATORY)¶
After completing tests, ALWAYS execute this sequence:
Step 1: Release Control¶
Expected output:
INFO - Resetting control state to idle...
INFO - Before reset: WSetEna=1, WSetPct=XX, WSet=XXX
INFO - β Reset successful: WSetEna=0, WSetPct=0
INFO - Control released
Step 2: Verify Status¶
Verify these lines:
π BATTERY POWER (DC)
...
Control Source: Cloud API (aGate native mode) β MUST say this
ποΈ AGATE MODE
OnGridMode: Self-Consumption β Or your preferred mode
If it says "Modbus Control: ACTIVE", repeat Step 1.
Step 3: Health Check¶
Verify:
============================================================
HEALTH CHECK: HEALTHY
============================================================
Checks:
β connection: OK
β model_704: OK
β zombie_state: OK (not in zombie state) β CRITICAL
β can_operate: OK
If zombie_state is not OK, run:
Step 4: Review Test Results¶
# Find latest results
latest=$(ls -t data/test_results_*.json | head -1)
echo "Results: $latest"
# View summary
python -c "
import json
with open('$latest') as f:
d = json.load(f)
s = d['test_session']
print(f\"Tests: {s['passed']}/{s['total_tests']} passed\")
for r in d['results']:
status = 'β
' if r['status'] == 'passed' else 'β'
print(f\" {status} {r['test_name']}: {r['status']}\")
"
π¨ Emergency Procedures¶
If Tests Are Interrupted (Ctrl+C, crash, etc.)¶
-
Check control state:
-
If Modbus Control is ACTIVE:
-
If release fails (zombie state):
If Battery Won't Respond to Cloud/App¶
-
Check if Modbus control is still active:
-
If WSetEna=1, release it:
bash python -c " import sys; sys.path.insert(0, 'src') from franklinwh_modbus import FranklinWHController ctrl = FranklinWHController('YOUR_AGATE_IP') if ctrl.connect(): ctrl.reset_control_state() print('Released') ctrl.disconnect() " -
Verify in FranklinWH app that control is restored
π§ Troubleshooting¶
Test Skipped: "Safety check failed"¶
Check specific issue:
Common causes: - SoC < 10% or > 95% β Wait for natural charge/discharge - Grid disconnected β Check AC connection - Critical alarms β Resolve faults first
Test Failed: "Command failed"¶
-
Check connection:
-
Check for conflicts:
-
May need
--reset-on-startif another controller is active
Results File Not Created¶
Check data directory exists:
Verify permissions:
π Example Test Session¶
# 1. Check current state
$ python franklinwh_cli.py -i YOUR_AGATE_IP --status | head -20
State of Charge: 70.0%
Control Source: Cloud API (aGate native mode) β Good, not in test mode
# 2. Run read-only tests
$ python run_hardware_tests.py --read-only
β
All safety checks passed
Tests: 3/3 passed
β
test_read_all_models: passed
β
test_battery_status_consistency: passed
β
test_alarms_clear: passed
# 3. Run low-power write tests
$ python run_hardware_tests.py --low-power
Do you want to proceed? Type 'yes' to continue: yes
β
TEST PASSED: test_charge_low_power
β
TEST PASSED: test_discharge_low_power
β
TEST PASSED: test_standby
β
TEST PASSED: test_release_control
Tests: 4/4 passed
# 4. MANDATORY: Release control
$ python franklinwh_cli.py -i YOUR_AGATE_IP --stop
INFO - β Reset successful: WSetEna=0
# 5. Verify release
$ python franklinwh_cli.py -i YOUR_AGATE_IP --status | grep "Control Source"
Control Source: Cloud API (aGate native mode) β Confirmed released
# 6. Health check
$ python franklinwh_cli.py -i YOUR_AGATE_IP --healthcheck | grep zombie_state
β zombie_state: OK (not in zombie state) β Confirmed clean
# 7. Review results
$ ls -lt data/test_results_*.json | head -1
-rw-r--r-- 1 user user 2.3K Feb 28 23:45 data/test_results_20260228_234515.json
$ cat data/test_results_20260228_234515.json | python -m json.tool
π Interpreting Results¶
Passed Test Indicators¶
status: "passed"validation_passed: truepost_wset_ena: 0 (after rollback)post_socwithin Β±1% ofpre_soc(short tests don't significantly change SoC)
Failed Test Indicators¶
status: "failed"error_message: Description of failuretraceback: Full stack trace- Common failures: connection timeout, safety check failure, validation mismatch
Skipped Test Indicators¶
status: "skipped"- Reason in test output (e.g., "SoC 96% not in ramping zone")
π― Best Practices¶
- Always start with
--read-onlyto verify system health - Run
--low-powertests first before any high-power testing - Never leave tests running unattended - monitor for failures
- Always run
--stopafter testing even if tests passed - Verify
--healthcheckshows zombie_state OK before considering complete - Keep result files for regression analysis
- Test during stable grid conditions (avoid stormy weather, peak demand)
Remember: The aGate controls your home's energy. Treat it with respect, verify the release, and keep the Cloud API in control for normal operation.