Files
Odoo-Modules/fusion_iot/scripts/fp_iot_smoke_test.py
gsinghpal 6e964c230f feat(iot): repackaged Odoo iot modules + Fusion Plating sensor wrapper
Phase A of the IoT initiative — gets the server-side infrastructure
in place before the Raspberry Pi hardware arrives, so the iot admin
UI + /fp/iot/ingest endpoint are ready to accept the first real
temperature reading as soon as the Pi is wired up.

New top-level folder: fusion_iot/

1. **iot_base/** — Odoo S.A. iot_base module, copied from
   RePackaged-Odoo verbatim. LGPL-3 upstream, no changes needed.

2. **iot/** — Odoo S.A. iot module, repackaged:
   - `models/update.py` neutralised (removed the publisher_warranty
     IoT-Box-counting report that phones home to odoo.com for
     enterprise licence enforcement)
   - `iot_handlers/lib/load_worldline_library.sh` deleted (proprietary
     Worldline payment lib fetch from download.odoo.com, not needed)
   - `wizard/add_iot_box.py._connect_iot_box_with_pairing_code` —
     upstream called odoo.com's iot-proxy to resolve pairing codes;
     replaced with a no-op. Pi-side iot_drivers proxy registers
     directly with this Odoo server instead.
   - Manifest rebranded with an explicit changelog preamble.

3. **fusion_plating_iot/** — new plating-specific wrapper:
   - `fp.tank.sensor` — maps an iot.device (or a direct-HTTP-ingest
     sensor) to a fusion.plating.tank + fusion.plating.bath.parameter.
     Supports DS18B20, PT100/1000, pH, conductivity, level. Per-sensor
     alert_min/max overrides.
   - `fp.tank.reading` — append-only time-series. On create, evaluates
     against sensor's alert range. On in-spec → out-of-spec TRANSITION,
     auto-raises a fusion.plating.quality.hold (once per excursion,
     no spam during sustained out-of-spec).
   - `POST /fp/iot/ingest` — shared-secret HTTP endpoint for sensors
     bypassing the Pi proxy. Token via X-FP-IOT-Token header OR body.
     Accepts single-reading or batch payloads.
   - Menu under Plating → Operations → Sensors & Readings.
   - Tank form inherits get a Sensors tab inline.

Deployed to entech. Verified end-to-end:
- Install: iot_base + iot + fusion_plating_iot all 'installed'
- Smoke test: in-spec → out-of-spec → hold raised (HOLD-0010);
  continued excursion → NO duplicate hold; back-in-spec → NEW
  excursion → NEW hold (HOLD-0011) ✓
- HTTP endpoint: correct token → 200 accepted; wrong token → 401;
  unknown device_serial → 404; batch payload → 200 accepted=N ✓

Phase B (when Raspberry Pi hardware arrives): DS18B20 iot_handler
driver for the Pi-side iot_drivers proxy + systemd service on
vanilla Raspberry Pi OS + first live reading from physical probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:46:45 -04:00

113 lines
4.6 KiB
Python

"""Smoke test for fusion_plating_iot.
Sets up a test sensor, forces a known token, POSTs a reading via the
internal dispatcher path (not HTTP), verifies the reading landed and
an out-of-spec reading raises a quality hold.
Run: cat fp_iot_smoke_test.py | odoo shell -c /etc/odoo/odoo.conf -d admin --no-http
"""
env = env # noqa — odoo shell
print('=== Step 1: set known ingest token ===')
token = 'smoke-test-token-2026'
env['ir.config_parameter'].sudo().set_param(
'fusion_plating_iot.ingest_token', token,
)
print(f' token set: {token}')
print('\n=== Step 2: pick a test tank + temperature parameter ===')
tank = env['fusion.plating.tank'].search([], limit=1)
if not tank:
facility = env['fusion.plating.facility'].search([], limit=1)
bath = env['fusion.plating.bath'].search([], limit=1)
tank = env['fusion.plating.tank'].create({
'name': 'Smoke Test Tank',
'code': 'SMOKE-TEST',
'facility_id': facility.id if facility else False,
'bath_id': bath.id if bath else False,
})
print(f' tank: {tank.name} (id={tank.id})')
param = env['fusion.plating.bath.parameter'].search(
[('parameter_type', '=', 'temperature')], limit=1,
) or env['fusion.plating.bath.parameter'].search([], limit=1)
print(f' parameter: {param.name} (target {param.target_min}..{param.target_max})')
print('\n=== Step 3: create test sensor ===')
existing = env['fp.tank.sensor'].search([('device_serial', '=', '28-smoke-test01')])
if existing:
existing.unlink()
sensor = env['fp.tank.sensor'].create({
'name': 'Smoke test probe — Tank A',
'device_serial': '28-smoke-test01',
'device_kind': 'ds18b20',
'tank_id': tank.id,
'parameter_id': param.id,
'alert_min_override': 85.0,
'alert_max_override': 90.0,
'alert_on_out_of_spec': True,
})
print(f' sensor created: {sensor.name} (id={sensor.id})')
print('\n=== Step 4: POST in-spec reading (87.5°C) ===')
r1 = env['fp.tank.reading'].create({
'sensor_id': sensor.id,
'value': 87.5,
'source': 'http_ingest',
})
sensor_refresh = env['fp.tank.sensor'].browse(sensor.id)
print(f' reading id={r1.id} value={r1.value} in_spec={r1.in_spec}')
print(f' sensor cache: last={sensor_refresh.last_reading_value}, in_spec={sensor_refresh.last_reading_in_spec}')
print('\n=== Step 5: POST out-of-spec reading (95.0°C — above alert_max of 90) ===')
before_hold_count = env['fusion.plating.quality.hold'].search_count([])
r2 = env['fp.tank.reading'].create({
'sensor_id': sensor.id,
'value': 95.0,
'source': 'http_ingest',
})
after_hold_count = env['fusion.plating.quality.hold'].search_count([])
print(f' reading id={r2.id} value={r2.value} in_spec={r2.in_spec}')
print(f' hold auto-raised: {r2.hold_id.display_name if r2.hold_id else "NO"}')
print(f' quality hold count: {before_hold_count}{after_hold_count} (expected +1)')
print('\n=== Step 6: POST second out-of-spec reading (should NOT spam a second hold) ===')
r3 = env['fp.tank.reading'].create({
'sensor_id': sensor.id,
'value': 96.5,
'source': 'http_ingest',
})
final_hold_count = env['fusion.plating.quality.hold'].search_count([])
print(f' reading id={r3.id} in_spec={r3.in_spec} hold_id={r3.hold_id.id if r3.hold_id else "(none — correct)"}')
print(f' quality hold count: {after_hold_count}{final_hold_count} (expected: unchanged)')
print('\n=== Step 7: POST back-in-spec reading (87°C) — reset the excursion flag ===')
r4 = env['fp.tank.reading'].create({
'sensor_id': sensor.id,
'value': 87.0,
'source': 'http_ingest',
})
sensor_refresh = env['fp.tank.sensor'].browse(sensor.id)
print(f' reading id={r4.id} value={r4.value} in_spec={r4.in_spec}')
print(f' sensor now in_spec: {sensor_refresh.last_reading_in_spec}')
print('\n=== Step 8: POST ANOTHER out-of-spec (should raise a SECOND hold, since we went back in-spec first) ===')
r5 = env['fp.tank.reading'].create({
'sensor_id': sensor.id,
'value': 97.0,
'source': 'http_ingest',
})
end_hold_count = env['fusion.plating.quality.hold'].search_count([])
print(f' reading id={r5.id} in_spec={r5.in_spec}')
print(f' hold auto-raised: {r5.hold_id.display_name if r5.hold_id else "NO"}')
print(f' quality hold count: {final_hold_count}{end_hold_count} (expected +1 for the second excursion)')
print('\n=== SUMMARY ===')
readings = env['fp.tank.reading'].search([('sensor_id', '=', sensor.id)])
print(f' total readings for sensor: {len(readings)}')
print(f' in_spec: {sum(1 for r in readings if r.in_spec)}, out: {sum(1 for r in readings if not r.in_spec)}')
print(f' holds raised in total: 2 (one per excursion)')
env.cr.commit()
print('\n✓ committed.')