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

7.6 KiB
Raw Blame History

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

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:

  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.