Files
Odoo-Modules/fusion_iot/CLAUDE.md
gsinghpal efb11df983 docs(fusion_iot): Tailscale + SSH cheat sheet for the pilot Pi
fp-iot-01 is now on Tailscale at 100.108.41.97. SSH config on the
Mac aliases `ssh fp-iot-01` to the Tailscale IP with key-based auth
(no more sshpass + password flying around in shell history).

Also noted the Pi-side folder structure (pi/ + scripts/) and the
live deployment facts (probe serial, systemd unit, config path)
so future sessions can pick up from zero without re-investigating.

Verified end-to-end with real hardware:
- Physical probe heated to 79.94°C → auto-raised HOLD-0015
- 30 subsequent out-of-spec readings → no duplicate holds (as designed)
- hold_id correctly linked back to the triggering reading

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:47:18 -04:00

6.7 KiB

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
├── pi/                      # Pi-side: lightweight systemd poller (no iot_drivers)
│   ├── fp_iot_poller.py
│   └── fp-iot-poller.service
└── scripts/                 # One-shot setup + smoke tests

Live deployments

Pi #1 — pilot probe (DS18B20, Tank TK-EN-01)

Attribute Value
Hostname fp-iot-01
LAN IP 192.168.10.112
Tailscale IP 100.108.41.97
SSH ssh fp-iot-01 (aliased in ~/.ssh/config, key-based via Tailscale — no password, no sshpass)
User fp
Probe serial 28-000000b276e4 (DS18B20)
Poller service fp-iot-poller.service — posts to entech every 30s
Poller config /etc/fp-iot/poller.conf
Poller logs journalctl -u fp-iot-poller -f

Tailscale auth: pre-authed to the gurpreet6672@ tailnet. Survives reboots (tailscaled enabled).

entech LXC (Odoo server)

  • iot_base + iot + fusion_plating_iot all installed
  • Ingest endpoint: POST http://10.200.1.26:8069/fp/iot/ingest
  • Token lives in ir.config_parameter['fusion_plating_iot.ingest_token'] — rotated via scripts/fp_iot_setup_live_sensor.py at setup time; rotate again in Settings → Technical → System Parameters as needed

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-INSTALLMUST 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)

# 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

# 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