Files
myprimal/api-docs/write-endpoints.md
T
arnaudne 941abd98d5 feat: initial myprimal MQTT bridge service
Dynamic MySolem/MyIndygo IoT bridge with MQTT, Home Assistant
autodiscovery, and Homebridge EasyMQTT compatibility.

- Device discovery via MySolem API on startup (all modules/inputs/outputs)
- Configurable poll engine (fast/normal/slow buckets, per-input overrides)
- MQTT retained state publishing + HA autodiscovery payloads
- Unified manual command routing (linesControl vs sendManualModuleCommand)
- Runtime config via MQTT $config/set, persisted to config.json
- Expression override system (API-provided > hardcoded > passthrough)
- Minimal REST API: /health, /state, /control, /rediscover
- Docker deployment (Dockerfile + docker-compose.yml)
- 38 unit tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 01:05:24 +02:00

250 lines
8.6 KiB
Markdown

# Write-Method Endpoints (POST / PUT / PATCH / DELETE)
All endpoints that mutate state, sorted by risk. Sources:
- Static analysis of `/min/production.min.js` (8MB) and `/linker/js/esbuild-bundle.js` (16MB)
- Socket.io call extraction from both bundles
- `inputs/lrpc_post.json` Safari capture (real POST payloads)
**Do not call destructive or control endpoints without intent.** Safe read-only probing stays on GET.
---
## Device control (high impact — sends LoRa radio commands)
### Pool controller impulse / start / stop
```
POST /remote/module/control
Content-Type: application/json
{
"moduleSerialNumber": "150302F1DA270001",
"linesControl": [
{ "index": 0, "action": 0 } // stop output
{ "index": 1, "action": 1 } // start (indefinitely)
{ "index": 1, "action": 2, "time": "30:00" } // timed impulse MM:SS
{ "index": 0, "action": 3, "time": "01:00" } // boost filtration
{ "index": 0, "action": 5, "manualDuration": 1 } // pause (?)
{ "index": 0, "action": 11, "time": "...", "speed": 1 } // backwash
]
}
```
Verified real payloads (mitmproxy captures):
- `{"action":0,"index":0}` → stop fill valve (LRLEVEL serial `3A01020D2ED90001`)
- `{"action":1,"index":1}` → Station 2, start indefinitely
- `{"action":2,"time":"30:00","index":1}` → Station 2, 30 min impulse
- `{"action":3,"time":"01:00","index":0}` → boost filtration 1 min (LRPC serial `150302F1DA270001`)
### Irrigation valve start/stop (form-encoded — confirmed via mitmproxy)
```
POST /module/sendManualModuleCommand
Content-Type: application/x-www-form-urlencoded
```
Confirmed payload patterns:
| Intent | Params |
|---|---|
| Run station N minutes | `commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=N` |
| Cycling station (on/off) | `commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0&commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId}` |
| Stop station | `commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId}` |
| All stations N minutes | `commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=N` |
| Stop everything | `commandType=manualStop` |
| Enable (no rain delay) | `commandType=status&command=on&commandParams=0` |
| Disable | `commandType=status&command=off&commandParams=0` |
| Rain delay N days | `commandType=status&command=off&commandParams=N` |
| Max rain delay | `commandType=status&command=off&commandParams=255` |
| Run program | `commandType=program&command={programId}` |
Notes:
- `command` accepts an **output ID string**, `"allStations"`, or a **program ID** (not an action name).
- `agriculturalModuleId` is required for stop and cycling station commands.
- `commandType=manualStop` takes no `outputIndex` or `commandParams`.
### Flush commands to device
```
POST /canopy/{siteId}/sendModulesCommand {}
```
### Module LoRa data push (forces sync)
```
POST /modules/sendDataViaLoRaWAN
POST /agriculturalModuleGroup/sendDataViaLoRaWAN
POST /wateringModuleGroup/sendDataViaLoRaWAN
```
---
## Program / schedule changes (persistent, survives reboot)
```
PUT /programs { "programs": [...], "module": "moduleId" }
POST /programs create new program
DELETE /programs delete program
PATCH /programs/name rename
POST /program/update socket.io variant (immediate push to device)
PUT /program/update REST variant
POST /program/reportProgramsDataSent acknowledge sync
POST /checkAvailableWindows check time conflicts before saving
```
Program window format for `PUT /programs`:
```json
{
"programs": [{
"id": "...",
"module": "moduleId",
"index": 0,
"weekDays": 127,
"waterBudget": 100,
"windows": [{ "start": 300, "end": 360, "on": 3600, "off": 0, "runningDays": 127 }],
"programCharacteristics": { "programType": 4, "mode": 2, "onSpeed": 1 }
}],
"module": "moduleId"
}
```
---
## Module configuration (persistent device settings)
```
POST /remote/module/configuration socket.io: update device config
POST /remote/module/configuration/and/programs config + programs in one push
POST /input/update update input settings (name, thresholds)
PUT /output/update update output settings (name, flowrate)
POST /remote/module/name rename via LoRa
PUT /module/update update module metadata
PUT /module/update/address
PUT /module/update/status
POST /module/update/name
POST /module/update/securityMode
POST /module/reportModuleDataSent acknowledge device sync
POST /remote/module/advertisingProfile BLE advertising config
POST /remote/modules/advertisingProfile bulk
POST /remote/module/dissociate { relayMsn, modulesMsn } — removes pairing
PUT /module/dissociate/{moduleId}
```
---
## Site / pool management
```
POST /canopy-sites create new site
PUT /pools/update/address update pool address
POST /pools/admin/sendMessage send admin message to pool customer
PUT /teams/handleAccessGrantedPools grant pool access to team
POST /teams/members/add
POST /teams/members/create
POST /agency/team/members/add
POST /addAgriculturalGroupToUser
POST /wateringModuleGroup/create
PUT /wateringModuleGroup/update
PUT /wateringModuleGroup/update/name
PUT /wateringModuleGroup/updateFlowRate
DELETE /wateringModuleGroup/delete
POST /wateringModuleGroup/remove
POST /updateAgriculturalModuleGroup
```
---
## Journal / notifications
```
PUT /journal/acknowledge/all
PUT /journal/acknowledge/records
POST /canopy-sites/{siteId}/notifications/acknowledge-all
POST /journal/notifications (write variant — mark read?)
POST /journal/counts
POST /journal/warnings/accepted
```
---
## Content / documents (safe-ish)
```
PUT /favorites
PUT /favorites/
POST /module/tags { module, tags: [...] }
DELETE /document/remove
DELETE /canopy/v2/deleteImage
POST /hpaform/create
POST /library/program-template
POST /library/programming-template
POST /notices/remove
POST /polytropic/setIpxData
POST /updateOutputIpxData
```
---
## Admin-only (requires admin role — will 403 on standard account)
```
POST /admin/modules/search
DELETE /admin/sim-card/delete
POST /admin/sim-card/update
POST /manufacturer/upload-blacklist
POST /firmwares/removeModuleFirmware
POST /pools/admin/sendMessage
POST /pools/admin/search
```
---
## How to discover more (without executing)
Three complementary techniques, all read-only:
**1. Static JS analysis** (already done above):
```bash
# All $.ajax write calls
grep -oE 'type:"(POST|PUT|PATCH|DELETE)"[^}]{0,200}url:"[^"]*"' /tmp/indygo_prod.js
# All fetch() write calls
grep -oE 'fetch\("[^"]*"[^)]{0,200}method:"(POST|PUT|PATCH|DELETE)"' /tmp/indygo_prod.js
# Socket.io write calls
grep -oE 'io\.socket\.(post|put|patch|delete)\("[^"]*"' /tmp/indygo_prod.js
```
**2. Safari Web Inspector recording** — record a session where you perform the action in the real app, then parse `inputs/*.json`:
```python
import json
with open("inputs/lrpc_post.json") as f:
data = json.load(f)
records = data["recording"]["records"]
for rec in records:
req = rec.get("entry", {}).get("request", {})
if req.get("method") in ("POST","PUT","PATCH","DELETE"):
print(req["method"], req["url"])
print(" ", req.get("postData", {}).get("text", "")[:200])
```
**3. Mitmproxy / Charles** (more complete than Safari — captures socket.io frames):
```bash
mitmproxy --mode transparent --ssl-insecure -s dump_writes.py
# dump_writes.py: filter on method != GET, log url + body
```
Safari doesn't capture WebSocket frames (socket.io messages), so LoRa push commands that go via socket.io are invisible in recordings.
---
## Socket.io write calls (discovered via bundle grep)
These bypass the normal HTTP layer — standard Safari recording captures neither the URL nor body:
```
socket.post /checkAvailableWindows
socket.post /input/update
socket.post /program/update
socket.post /remote/module/configuration
socket.post /remote/module/configuration/and/programs
socket.post /remote/module/name
socket.put /module/update
socket.put /module/update/address
socket.put /pools/update/address
socket.put /program/update
```