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>
8.6 KiB
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.jsonSafari 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 serial3A01020D2ED90001){"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 serial150302F1DA270001)
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:
commandaccepts an output ID string,"allStations", or a program ID (not an action name).agriculturalModuleIdis required for stop and cycling station commands.commandType=manualStoptakes nooutputIndexorcommandParams.
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:
{
"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):
# 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:
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):
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