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:
@@ -0,0 +1,91 @@
|
||||
"""Sub 7 smoke test — runs inside odoo-shell on entech."""
|
||||
env = env
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
Param = env['ir.config_parameter'].sudo()
|
||||
Sensor = env['fp.tank.sensor']
|
||||
Tank = env['fusion.plating.tank']
|
||||
Reading = env['fp.tank.reading']
|
||||
|
||||
# ---- Field presence + helper -----------------------------------------
|
||||
assert 'poll_interval_minutes' in Sensor._fields
|
||||
assert 'poll_interval_display' in Sensor._fields
|
||||
assert 'x_fc_default_poll_interval_minutes' in env['res.company']._fields
|
||||
print('[OK] Fields present')
|
||||
|
||||
default = env.company.x_fc_default_poll_interval_minutes
|
||||
assert default == 30, f'default should be 30, got {default}'
|
||||
print(f'[OK] Company default polling = {default} min')
|
||||
|
||||
# Pick any existing sensor
|
||||
any_sensor = Sensor.search([], limit=1)
|
||||
assert any_sensor
|
||||
assert any_sensor._fp_effective_poll_interval_minutes() == 30
|
||||
print(f'[OK] Helper returns default for blank sensor')
|
||||
|
||||
any_sensor.poll_interval_minutes = 5
|
||||
any_sensor.invalidate_recordset()
|
||||
assert any_sensor._fp_effective_poll_interval_minutes() == 5
|
||||
assert '(override)' in any_sensor.poll_interval_display
|
||||
print(f'[OK] Override respected: {any_sensor.poll_interval_display}')
|
||||
|
||||
any_sensor.poll_interval_minutes = 0
|
||||
any_sensor.invalidate_recordset()
|
||||
assert '(default)' in any_sensor.poll_interval_display
|
||||
print(f'[OK] Blank interval shows default: {any_sensor.poll_interval_display}')
|
||||
|
||||
# ---- Seed the 25 tanks + 50 sensors via the hook ----------------------
|
||||
TankAll = Tank.with_context(active_test=False)
|
||||
SensorAll = Sensor.with_context(active_test=False)
|
||||
tanks_before = TankAll.search_count([])
|
||||
sensors_before = SensorAll.search_count([])
|
||||
Param.set_param('fusion_plating_iot.seed_entech_tanks', '1')
|
||||
from odoo.addons.fusion_plating_iot.hooks import _seed_entech_tanks_and_sensors
|
||||
_seed_entech_tanks_and_sensors(env)
|
||||
tanks_after = TankAll.search_count([])
|
||||
sensors_after = SensorAll.search_count([])
|
||||
print(f'[OK] Seed ran. Tanks: {tanks_before} → {tanks_after} (+{tanks_after - tanks_before})')
|
||||
print(f'[OK] Seed ran. Sensors: {sensors_before} → {sensors_after} (+{sensors_after - sensors_before})')
|
||||
assert tanks_after - tanks_before == 25, 'expected +25 tanks'
|
||||
assert sensors_after - sensors_before == 50, 'expected +50 sensors'
|
||||
|
||||
# Active / inactive breakdown
|
||||
small_active = TankAll.search_count([('code', 'like', 'SMALL-%'), ('active', '=', True)])
|
||||
big_active = TankAll.search_count([('code', 'like', 'BIG-%'), ('active', '=', True)])
|
||||
big_inactive = TankAll.search_count([('code', 'like', 'BIG-%'), ('active', '=', False)])
|
||||
assert small_active == 5, f'expected 5 small active, got {small_active}'
|
||||
assert big_active == 10, f'expected 10 big active, got {big_active}'
|
||||
assert big_inactive == 10, f'expected 10 big inactive, got {big_inactive}'
|
||||
print(f'[OK] Active/Inactive split: small={small_active}, big_active={big_active}, big_inactive={big_inactive}')
|
||||
|
||||
# Per-tank sensor counts
|
||||
sample = TankAll.search([('code', '=', 'BIG-01')], limit=1)
|
||||
sample_sensors = SensorAll.search([('tank_id', '=', sample.id)])
|
||||
types = sample_sensors.mapped('parameter_id.parameter_type')
|
||||
assert 'temperature' in types and 'ph' in types
|
||||
print(f'[OK] Big Tank #1 has sensors for: {types}')
|
||||
|
||||
# Idempotency — re-run seed, counts don't change
|
||||
_seed_entech_tanks_and_sensors(env)
|
||||
assert TankAll.search_count([]) == tanks_after, 'seed not idempotent (tanks)'
|
||||
assert SensorAll.search_count([]) == sensors_after, 'seed not idempotent (sensors)'
|
||||
print('[OK] Seed is idempotent')
|
||||
|
||||
# ---- Rate-limit: simulate two readings too close together -----------
|
||||
rate_sensor = Sensor.search([('code', '!=', False)], limit=1) if False else any_sensor
|
||||
rate_sensor.poll_interval_minutes = 30
|
||||
rate_sensor.last_reading_at = datetime.now() - timedelta(minutes=5) # 5 min ago
|
||||
print(f'[OK] Setup: interval=30min, last_reading=5min ago')
|
||||
# The ingest controller does the rate-limit. Simulate its check here.
|
||||
effective = rate_sensor._fp_effective_poll_interval_minutes()
|
||||
elapsed = datetime.now() - rate_sensor.last_reading_at
|
||||
assert elapsed < timedelta(minutes=effective), 'rate-limit check should fire'
|
||||
print('[OK] Rate-limit condition holds — reading would be skipped')
|
||||
|
||||
rate_sensor.last_reading_at = datetime.now() - timedelta(minutes=35) # 35 min ago
|
||||
elapsed = datetime.now() - rate_sensor.last_reading_at
|
||||
assert elapsed >= timedelta(minutes=effective), 'rate-limit should allow after interval'
|
||||
print('[OK] Post-interval condition holds — reading would be stored')
|
||||
|
||||
env.cr.rollback()
|
||||
print('\n=== SUB 7 SMOKE PASS — all assertions held ===')
|
||||
Reference in New Issue
Block a user