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>
7.6 KiB
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
- Per-sensor polling interval on
fp.tank.sensor.poll_interval_minutes(Integer, optional). Blank inherits the company-wide default. - Global default on
res.company.x_fc_default_poll_interval_minutes(Integer, default 30), exposed throughres.config.settings. - 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. - Ingest rate-limit in
/fp/iot/ingest: readings that arrive inside the sensor's effective interval are dropped; endpoint returns askippedcount alongsidestored. - 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 10active=False. - Each tank → 1 temperature + 1 pH sensor. Inactive tank → inactive sensors.
- Sensors land with no
device_serial— hookup pending Ethernet delivery.
- 5 small tanks (
- View exposure: polling interval + display on sensor form / tree + settings block.
Out of scope
- Pi-side poller changes (
/etc/fp-iot/poller.confstays 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
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
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
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:
- Resolve the
fp.tank.sensorbydevice_serial. - If the sensor has a
last_reading_at(either stored on the sensor or via the latestfp.tank.reading) andnow - last_reading_at < effective_interval, skip the reading. - 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_minutesnext 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_displayas 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.sensorACLs already cover the new field. - No migration — new fields have defaults; seed data is
noupdate=1and keyed by XML ID. fusion_plating_iotversion bump — current19.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:
fusion_plating_iot— gets new fields, controller rate-limit, seed data.- Nothing else.
Post-deploy verification:
- Tank list count = 27
- Sensor list count = 52
- Settings page shows IoT default = 30
- Typing
5into a sensor's interval updates display to "5 min (override)" after save
End of spec.