941abd98d5
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>
250 lines
8.6 KiB
Markdown
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
|
|
```
|