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>
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_iotall 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 viascripts/fp_iot_setup_live_sensor.pyat 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— broadcastsweb.base.urlchanges to paired IoT boxes via the internal IoT channel (not the internet)iot_box.py.version_commit_url— cosmetic link to odoo/odoo on GitHubcontrollers/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 toiot.deviceif the sensor comes in via Pi proxytank_id/bath_id— where the sensor livesparameter_id— what bath parameter it reports (temperature, pH, etc.)alert_min_override/alert_max_override— per-sensor spec override; else inherits fromfusion.plating.bath.parameter.target_min/max- Cached
last_reading_value/last_reading_at/last_reading_in_specfor 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.holdONCE 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-Tokenheader OR"token"key in JSON body, compared toir.config_parameter[fusion_plating_iot.ingest_token]usinghmac.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— forfusion.plating.tank+fusion.plating.bath.parameterfusion_plating_quality— forfusion.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_driverson 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