Files
Odoo-Modules/fusion_iot/CLAUDE.md
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

131 lines
5.5 KiB
Markdown

# Fusion IoT — Claude Code Instructions
## Purpose
Fusion IoT lets Fusion Apps products ingest live sensor readings from
hardware mounted on a shop floor — initially tank temperature probes
for Fusion Plating, with room to grow into label printers, scales,
and any other device Odoo's IoT framework supports.
## Folder contents
```
fusion_iot/
├── iot_base/ # Repackaged from Odoo S.A. — shared JS utils
├── iot/ # Repackaged from Odoo S.A. — IoT Box mgmt models + UI
└── fusion_plating_iot/ # Our wrapper — sensor→tank mapping + out-of-spec holds
```
## Repackaging notes — `iot_base` + `iot`
Both copied as-is from `/Users/gurpreet/Github/RePackaged-Odoo/_dependencies/`
(tag Odoo 19). Both are already LGPL-3 upstream — no license flip needed.
**Gutted phone-home**:
| File | Change |
|------|--------|
| `iot/models/update.py` | `Publisher_WarrantyContract._get_message` override REMOVED (no more IoT-Box counting-back to Odoo S.A. for enterprise licensing) |
| `iot/iot_handlers/lib/load_worldline_library.sh` | DELETED (proprietary Worldline payment lib fetch from download.odoo.com — we don't use Worldline) |
**Left intact** (NOT phone-home, don't remove):
- `ir_config_parameter.py` — broadcasts `web.base.url` changes to paired IoT boxes via the internal IoT channel (not the internet)
- `iot_box.py.version_commit_url` — cosmetic link to odoo/odoo on GitHub
- `controllers/main.py` — serves the iot handlers zip to the Pi (this is the point of the module)
## `fusion_plating_iot` — the wrapper
### Models
**`fp.tank.sensor`** — maps a physical sensor to a tank + parameter
- `device_serial` — hardware unique ID (e.g. DS18B20 1-Wire address)
- `iot_device_id` — optional link to `iot.device` if the sensor comes in via Pi proxy
- `tank_id` / `bath_id` — where the sensor lives
- `parameter_id` — what bath parameter it reports (temperature, pH, etc.)
- `alert_min_override` / `alert_max_override` — per-sensor spec override; else inherits from `fusion.plating.bath.parameter.target_min/max`
- Cached `last_reading_value` / `last_reading_at` / `last_reading_in_spec` for fast list views
**`fp.tank.reading`** — time-series log of every reading
- Append-only — never updated/deleted. The compliance record of bath history.
- `create()` evaluates each reading against the sensor's alert range
- Raises a `fusion.plating.quality.hold` ONCE on the transition from in-spec → out-of-spec (no spam)
**`fusion.plating.tank`** — extended with `x_fc_sensor_ids` o2m + `x_fc_has_out_of_spec` bool for the tank form.
### Endpoint — `POST /fp/iot/ingest`
For sensors that skip the Pi proxy and POST directly over HTTP.
- Auth: `X-FP-IOT-Token` header OR `"token"` key in JSON body, compared to `ir.config_parameter[fusion_plating_iot.ingest_token]` using `hmac.compare_digest`
- Seeded token value: `CHANGE-ME-AFTER-INSTALL`**MUST be rotated immediately after install** via Settings → Technical → System Parameters
- Payload: single `{device_serial, value, read_at}` OR batch `{readings: [...]}`
- Response: 200 + `{ok: true, accepted: N}`, 401 on auth fail, 404 if device_serial unknown
### Dependencies
- `iot` — the server-side Odoo IoT module (in this same folder, needs to be installed first)
- `fusion_plating` — for `fusion.plating.tank` + `fusion.plating.bath.parameter`
- `fusion_plating_quality` — for `fusion.plating.quality.hold`
### Not yet — Phase B (when Pi hardware arrives)
- DS18B20 handler module for `iot_drivers` (the Pi-side proxy)
- Systemd service config for running `iot_drivers` on vanilla Raspberry Pi OS
- Pi firmware README
## Deployment to entech (LXC 111)
```bash
# 1. Sync all three modules
rsync -av fusion_iot/iot_base/ pve-worker5:/tmp/iot_base/
rsync -av fusion_iot/iot/ pve-worker5:/tmp/iot/
rsync -av fusion_iot/fusion_plating_iot/ pve-worker5:/tmp/fpi/
ssh pve-worker5 "pct exec 111 -- bash -c '
mv /tmp/iot_base /mnt/extra-addons/custom/
mv /tmp/iot /mnt/extra-addons/custom/
mv /tmp/fpi /mnt/extra-addons/custom/fusion_plating_iot
chown -R odoo:odoo /mnt/extra-addons/custom/iot_base /mnt/extra-addons/custom/iot /mnt/extra-addons/custom/fusion_plating_iot
'"
# 2. Install modules (order matters)
ssh pve-worker5 "pct exec 111 -- su - odoo -s /bin/bash -c \
\"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -i iot_base,iot,fusion_plating_iot --stop-after-init\""
# 3. Verify
# - Settings → Technical → IoT menu appears
# - Plating → Operations → Sensors & Readings menu appears
# - curl test against /fp/iot/ingest (see README)
```
## Test commands
```bash
# Set a known token
odoo shell> env['ir.config_parameter'].set_param('fusion_plating_iot.ingest_token', 'test-secret-123')
# Create a sensor manually
odoo shell> env['fp.tank.sensor'].create({
'name': 'Test probe',
'device_serial': '28-test000001',
'device_kind': 'ds18b20',
'tank_id': <some_tank.id>,
'parameter_id': <temperature_param.id>,
})
# POST a reading
curl -X POST http://entech:8069/fp/iot/ingest \
-H 'Content-Type: application/json' \
-H 'X-FP-IOT-Token: test-secret-123' \
-d '{"device_serial":"28-test000001","value":87.3}'
# → {"ok":true,"accepted":1}
# Simulate out-of-spec reading (assuming target_max=90)
curl -X POST http://entech:8069/fp/iot/ingest \
-H 'Content-Type: application/json' \
-H 'X-FP-IOT-Token: test-secret-123' \
-d '{"device_serial":"28-test000001","value":95.0}'
# → reading created + fusion.plating.quality.hold auto-raised
```