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:
@@ -31,7 +31,7 @@ Returns 200 + `{ok: true, accepted: N}` on success, 401 on auth fail,
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request, Response
|
||||
@@ -109,6 +109,7 @@ class FpIotIngestController(http.Controller):
|
||||
Sensor = request.env['fp.tank.sensor'].sudo()
|
||||
Reading = request.env['fp.tank.reading'].sudo()
|
||||
accepted = 0
|
||||
skipped_interval = 0
|
||||
unknown_serials = []
|
||||
for r in readings:
|
||||
serial = (r.get('device_serial') or '').strip()
|
||||
@@ -122,6 +123,23 @@ class FpIotIngestController(http.Controller):
|
||||
value = float(r.get('value'))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
# Sub 7 — per-sensor rate-limit. Drop readings that arrive
|
||||
# inside the sensor's effective polling interval so the Pi
|
||||
# agent can happily poll every 30 s while the log only
|
||||
# retains a row every 15–30 min. Inactive sensors also
|
||||
# dropped so disabled tanks don't clutter the log.
|
||||
if not sensor.active:
|
||||
skipped_interval += 1
|
||||
continue
|
||||
interval = sensor._fp_effective_poll_interval_minutes()
|
||||
if interval > 0 and sensor.last_reading_at:
|
||||
read_at = _parse_read_at(r.get('read_at'))
|
||||
elapsed = read_at - sensor.last_reading_at
|
||||
if elapsed < timedelta(minutes=interval):
|
||||
skipped_interval += 1
|
||||
continue
|
||||
|
||||
Reading.create({
|
||||
'sensor_id': sensor.id,
|
||||
'value': value,
|
||||
@@ -130,10 +148,11 @@ class FpIotIngestController(http.Controller):
|
||||
})
|
||||
accepted += 1
|
||||
|
||||
status = 200 if accepted else (404 if unknown_serials else 400)
|
||||
status = 200 if (accepted or skipped_interval) else (404 if unknown_serials else 400)
|
||||
payload = {
|
||||
'ok': accepted > 0,
|
||||
'ok': (accepted + skipped_interval) > 0,
|
||||
'accepted': accepted,
|
||||
'skipped': skipped_interval,
|
||||
}
|
||||
if unknown_serials:
|
||||
payload['unknown_serials'] = unknown_serials
|
||||
|
||||
Reference in New Issue
Block a user