Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-04-22-sub7-iot-polling-design.md
gsinghpal def9c801fa docs(iot): Sub 7 design spec — per-sensor polling interval + entech seed
Per-sensor override on fp.tank.sensor.poll_interval_minutes, company-
wide default on res.company, ingest controller rate-limits readings
that arrive inside the effective interval. Seeds entech with 25
tanks (5 small, 20 big — 10 active/10 inactive) and 50 sensors
(temp + pH per tank) as noupdate=1 data so admin edits survive
upgrades.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:20:29 -04:00

207 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: "610 active tanks need continuous monitoring, polling 1530 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 #15`), all active.
- 20 big tanks (`Big Tank #120`); 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 `<field name="facility_id" search="[]"/>` 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.<whatever>`` → 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.*