diff --git a/fusion_plating/docs/superpowers/specs/2026-04-22-sub7-iot-polling-design.md b/fusion_plating/docs/superpowers/specs/2026-04-22-sub7-iot-polling-design.md
new file mode 100644
index 00000000..92dce491
--- /dev/null
+++ b/fusion_plating/docs/superpowers/specs/2026-04-22-sub7-iot-polling-design.md
@@ -0,0 +1,206 @@
+# Sub 7 — IoT Polling Interval + Entech Tank Seed
+
+**Date:** 2026-04-22
+**Module scope:** `fusion_plating_iot` (primary — adds the poll interval field, rate-limit, seed data)
+**Status:** Design approved; implementing in this session.
+**Predecessor context:** Fine-Tuning Initiative, entry in `fusion_plating/CLAUDE.md` (Sub 7
+preview: "6–10 active tanks need continuous monitoring, polling 15–30 min, configurable
+per sensor"). Client refined to 25-tank inventory on 2026-04-22.
+
+---
+
+## 1. Scope
+
+### In scope
+
+1. **Per-sensor polling interval** on `fp.tank.sensor.poll_interval_minutes` (Integer,
+ optional). Blank inherits the company-wide default.
+2. **Global default** on `res.company.x_fc_default_poll_interval_minutes` (Integer,
+ default 30), exposed through `res.config.settings`.
+3. **Helper** `fp.tank.sensor._fp_effective_poll_interval_minutes()` returns the
+ effective interval for a sensor (own value > company default). Single lookup point
+ so Sub 6 / Sub 8 additions don't scatter logic.
+4. **Ingest rate-limit** in `/fp/iot/ingest`: readings that arrive inside the sensor's
+ effective interval are dropped; endpoint returns a `skipped` count alongside
+ `stored`.
+5. **Entech tank seed** (25 tanks, 50 sensors, `noupdate=1`):
+ - 5 small tanks (`Small Tank #1–5`), all active.
+ - 20 big tanks (`Big Tank #1–20`); first 10 active, last 10 `active=False`.
+ - Each tank → 1 temperature + 1 pH sensor. Inactive tank → inactive sensors.
+ - Sensors land with **no `device_serial`** — hookup pending Ethernet delivery.
+6. **View exposure:** polling interval + display on sensor form / tree + settings block.
+
+### Out of scope
+
+- Pi-side poller changes (`/etc/fp-iot/poller.conf` stays its own concern; Odoo-side
+ rate-limit is the single source of truth for what actually lands in the DB).
+- Scheduled polling job inside Odoo (sensors are push-driven).
+- Dashboard / trend changes.
+- Alerting on missed polls.
+
+---
+
+## 2. Data Model
+
+### 2.1 `fp.tank.sensor` additions
+
+```python
+poll_interval_minutes = fields.Integer(
+ string='Polling Interval (minutes)',
+ help='How often readings from this sensor should be stored. Leave '
+ 'blank to inherit the company default. Incoming readings that '
+ 'arrive faster than this interval are dropped by the ingest '
+ 'endpoint so the log stays clean even if a polling agent is '
+ 'set to a shorter cadence.',
+)
+poll_interval_display = fields.Char(
+ string='Effective Interval',
+ compute='_compute_poll_interval_display',
+ help='Human-readable form of the effective interval — "30 min "
+ '(default)" when blank, "15 min (override)" when set.',
+)
+```
+
+### 2.2 `res.company` default
+
+```python
+x_fc_default_poll_interval_minutes = fields.Integer(
+ string='IoT default polling interval (minutes)',
+ default=30,
+ help='Applied to any fp.tank.sensor that does not set its own '
+ 'polling interval.',
+)
+```
+
+### 2.3 `res.config.settings` mirror
+
+`x_fc_default_poll_interval_minutes = fields.Integer(related='company_id.x_fc_default_poll_interval_minutes', readonly=False)`
+
+### 2.4 Helper + compute
+
+```python
+def _fp_effective_poll_interval_minutes(self):
+ self.ensure_one()
+ if self.poll_interval_minutes and self.poll_interval_minutes > 0:
+ return self.poll_interval_minutes
+ return (self.env.company.x_fc_default_poll_interval_minutes or 30)
+
+@api.depends('poll_interval_minutes')
+def _compute_poll_interval_display(self):
+ default = self.env.company.x_fc_default_poll_interval_minutes or 30
+ for rec in self:
+ if rec.poll_interval_minutes and rec.poll_interval_minutes > 0:
+ rec.poll_interval_display = _('%d min (override)') % rec.poll_interval_minutes
+ else:
+ rec.poll_interval_display = _('%d min (default)') % default
+```
+
+---
+
+## 3. Ingest Rate-Limit
+
+In `fusion_plating_iot/controllers/fp_iot_ingest.py`, for each incoming reading:
+
+1. Resolve the `fp.tank.sensor` by `device_serial`.
+2. If the sensor has a `last_reading_at` (either stored on the sensor or via the latest
+ `fp.tank.reading`) and `now - last_reading_at < effective_interval`, **skip** the reading.
+3. Return JSON with `{'stored': n, 'skipped': m, 'errors': [...]}` so the Pi can log it.
+
+The sensor may already track a last-read stamp via `fp.tank.reading` — confirm during
+implementation, and if not, add a `last_read_at` datetime on the sensor that gets
+stamped on every stored reading.
+
+---
+
+## 4. Seed Data
+
+File: `fusion_plating_iot/data/fp_entech_tank_seed.xml`, `noupdate="1"`.
+
+- Generate 25 tank records + 50 sensor records via inline XML.
+- External IDs follow the pattern `tank_small_01..05`, `tank_big_01..20`,
+ `sensor_temp_small_01`, `sensor_ph_small_01`, etc.
+- Tanks use the default facility via `` or reference
+ whatever facility seed exists on entech. Guard: if no facility exists, skip the seed (the
+ admin can rerun the seed after creating one).
+- The 2 existing entech tanks (`TK-EN-01`, `TK-PASS-01`) are **untouched** — no rename,
+ no delete. They keep their existing sensors.
+
+---
+
+## 5. Views
+
+- **fp.tank.sensor form:** `poll_interval_minutes` next to its display Char in a new group
+ "IoT Polling".
+- **fp.tank.sensor tree:** both fields `optional="show"`.
+- **fusion.plating.tank form:** the sensor O2M editor already exists (per the existing
+ inherit); include `poll_interval_display` as a read-only column so tank admins can eyeball
+ the cadence per sensor without opening each one.
+- **Settings form:** new block under Fusion Plating section titled "IoT" with
+ `x_fc_default_poll_interval_minutes` + help text.
+
+---
+
+## 6. Security & Migration
+
+- No security changes — `fp.tank.sensor` ACLs already cover the new field.
+- No migration — new fields have defaults; seed data is `noupdate=1` and keyed by XML ID.
+- `fusion_plating_iot` version bump — current ``19.0.`` → bumped one minor.
+
+---
+
+## 7. Testing
+
+### 7.1 Smoke (entech odoo-shell)
+
+- Seed applied → 25 + 2 = 27 tanks present; 50 + 2 = 52 sensors present (existing entech
+ tanks unchanged).
+- Sensor with blank interval → `_fp_effective_poll_interval_minutes()` returns company
+ default (30).
+- Sensor with `poll_interval_minutes = 5` → helper returns 5, display reads "5 min (override)".
+- Post two readings 10 s apart for a sensor with interval 30 → second is skipped.
+- Post two readings 31 min apart → both stored.
+
+### 7.2 View render
+- Open a seeded tank → both sensors visible with polling-interval display.
+- Open settings → interval field present + editable.
+
+---
+
+## 8. File Manifest
+
+```
+fusion_plating_iot/
+├── __manifest__.py (version bump + data entry for seed)
+├── models/
+│ ├── __init__.py (+ res_company, res_config_settings)
+│ ├── fp_tank_sensor.py (poll fields + helper)
+│ ├── res_company.py NEW
+│ └── res_config_settings.py NEW
+├── controllers/
+│ └── fp_iot_ingest.py (rate-limit logic)
+├── views/
+│ ├── fp_tank_sensor_views.xml (expose new fields)
+│ └── res_config_settings_views.xml NEW (Fusion Plating → IoT block)
+└── data/
+ └── fp_entech_tank_seed.xml NEW
+```
+
+Rough LOC: ~120 Python, ~250 XML (mostly the seed).
+
+---
+
+## 9. Rollout
+
+Update order on entech:
+1. `fusion_plating_iot` — gets new fields, controller rate-limit, seed data.
+2. Nothing else.
+
+Post-deploy verification:
+- Tank list count = 27
+- Sensor list count = 52
+- Settings page shows IoT default = 30
+- Typing `5` into a sensor's interval updates display to "5 min (override)" after save
+
+---
+
+*End of spec.*