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>
159 lines
6.7 KiB
Markdown
159 lines
6.7 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
|
|
├── 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-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
|
|
```
|