# 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.*