# 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': , 'parameter_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 ```