feat(iot): Sub 7 — per-sensor polling interval + rate-limit + entech seed
Per-sensor override on fp.tank.sensor.poll_interval_minutes with a
company-wide default (res.company.x_fc_default_poll_interval_minutes,
default 30) exposed in Settings → Fusion Plating → IoT. Single
lookup helper _fp_effective_poll_interval_minutes keeps downstream
call sites simple. Read-only poll_interval_display Char ("30 min
(default)" / "15 min (override)") keeps units unambiguous per user
request.
Ingest endpoint /fp/iot/ingest drops readings that arrive inside a
sensor's effective interval, returning {accepted, skipped} so the Pi
agent can log it. Pi-side interval stays its own concern.
Post-init hook seeds 5 small tanks + 20 big tanks (10 active, 10
inactive) with 1 temperature + 1 pH sensor each → 25 tanks, 50
sensors. Idempotent (keyed by tank.code, with_context(active_test=
False)). Opt-in via ir.config_parameter
fusion_plating_iot.seed_entech_tanks = '1' so a fresh install
elsewhere doesn't auto-seed. Flag set on entech today; 27 tanks / 52
sensors now live (2 pilot + 25 seeded).
Smoke on entech: 14/14 assertions pass including idempotency and
rate-limit conditions.
fusion_plating_iot → 19.0.2.0.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import fp_sensor_type
|
||||
from . import fp_sensor_dashboard
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import fp_tank_sensor
|
||||
from . import fp_tank_reading
|
||||
from . import fusion_plating_tank
|
||||
|
||||
@@ -163,6 +163,53 @@ class FpTankSensor(models.Model):
|
||||
'specific sensor.',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Sub 7 — per-sensor polling interval
|
||||
#
|
||||
# Blank on the sensor = inherit res.company default. The effective
|
||||
# value gates the ingest endpoint so too-frequent readings are
|
||||
# dropped even when a Pi agent polls at a shorter cadence.
|
||||
# ------------------------------------------------------------------
|
||||
poll_interval_minutes = fields.Integer(
|
||||
string='Polling Interval (minutes)',
|
||||
help='How often a reading from this sensor should be stored. '
|
||||
'Leave blank to inherit the company-wide default. Readings '
|
||||
'that arrive inside this interval are dropped by the '
|
||||
'ingest endpoint — the database stays clean even if the '
|
||||
'Pi agent polls more often.',
|
||||
)
|
||||
poll_interval_display = fields.Char(
|
||||
string='Effective Interval',
|
||||
compute='_compute_poll_interval_display',
|
||||
help='Human-readable form of the sensor\'s effective polling '
|
||||
'interval. Shows "(override)" when the sensor carries its '
|
||||
'own value, "(default)" when it inherits.',
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
def _fp_effective_poll_interval_minutes(self):
|
||||
"""Return the effective polling interval for this sensor.
|
||||
|
||||
Single lookup point. Call this rather than reading the raw
|
||||
field so later additions (per-tank override in Sub 8, per-
|
||||
customer override in Sub 6) only touch this helper.
|
||||
"""
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Effective target — resolves override → parameter default → 0
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
21
fusion_iot/fusion_plating_iot/models/res_company.py
Normal file
21
fusion_iot/fusion_plating_iot/models/res_company.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
#
|
||||
# Sub 7 — company-wide default for the IoT sensor polling interval.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
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. Used by the ingest endpoint to drop '
|
||||
'readings that arrive inside the interval, keeping the '
|
||||
'database clean even when a Pi agent polls more often.',
|
||||
)
|
||||
19
fusion_iot/fusion_plating_iot/models/res_config_settings.py
Normal file
19
fusion_iot/fusion_plating_iot/models/res_config_settings.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
#
|
||||
# Sub 7 — Expose the IoT polling default on the Fusion Plating
|
||||
# Settings page so admins manage it alongside other plating settings.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
x_fc_default_poll_interval_minutes = fields.Integer(
|
||||
related='company_id.x_fc_default_poll_interval_minutes',
|
||||
readonly=False,
|
||||
string='IoT default polling interval (minutes)',
|
||||
)
|
||||
Reference in New Issue
Block a user