Files
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

6.9 KiB

MySolem / MyIndygo API — Device Control

All commands work on both https://qualif.mysolem.com and https://qualif.myindygo.com. Requires session cookie.


Manual irrigation command

POST /module/sendManualModuleCommand
Content-Type: application/x-www-form-urlencoded

Payloads are form-encoded. All patterns confirmed via mitmproxy captures. A JSON body variant (same fields) also works — see the example at the end of this section.

Note: the command field is not an action name — it accepts an output ID string, "allStations", or a program ID.

Status commands (enable / disable module)

commandType=status&command=on&commandParams=0          → enable (no rain delay)
commandType=status&command=off&commandParams=0         → disable
commandType=status&command=off&commandParams=1         → 1-day rain delay
commandType=status&command=off&commandParams=255       → disable with max rain delay

Station commands

Run a specific station for a duration:

commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=1

Cycling station (on/off intervals + optional agricultural module):

commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0
  &commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId}

Stop a specific station:

commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId}

Run all stations for a duration:

commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=2

Stop everything immediately (no outputIndex, no commandParams):

commandType=manualStop

Program command (NEW)

Send a specific program to the module:

commandType=program&command={programId}

JSON body equivalent (original documented form)

{
  "moduleSerialNumber": "6606010709170001",
  "commandType": "station",
  "outputIndex": 0,
  "command": null,
  "commandParams": {
    "hours": 0,
    "minutes": 15,
    "on": 0,
    "off": 0
  }
}
  • outputIndex: 0-based index of the output/station
  • hours + minutes: duration to water
  • on / off: cycle timing (0 = continuous)
  • agriculturalModuleId: required when stopping or cycling a specific station

Flush pending commands to device

Triggers the platform to push all queued commands to a site's modules:

POST /canopy/{siteId}/sendModulesCommand
Content-Type: application/json

{}

Response: 202 Accepted{ "message": "Modules command accepted for processing" }


Remote module rename

GET /remote/module/name?moduleId={moduleId}&name={newName}

Dissociate module from relay

POST /remote/module/dissociate
Content-Type: application/json

{
  "relayMsn": "relaySerialNumber",
  "modulesMsn": ["childSerialNumber"]
}

Update module metadata

PUT /module/update
Content-Type: application/json

{ "module": { "id": "moduleId", "name": "New Name", ... } }
PUT /module/update/address
PUT /module/update/status

Remove module

DELETE /module/{moduleId}
GET    /module/remove/from/my/modules/{moduleId}

Program update (socket.io legacy)

io.socket.post("/program/update", {
  programs: [...],
  module: moduleId,
  programmationtype: "temporal"
}, callback)

Pool Controller Control (LRPC / lr-pc) — MyIndygo

Pool controllers use a different control endpoint with a linesControl array structure.

POST /remote/module/control
Content-Type: application/x-www-form-urlencoded

Payload (JSON body — Content-Type: application/json):

{
  "moduleSerialNumber": "150302F1DA270001",
  "linesControl": [
    {
      "index": 0,
      "action": 2,
      "time": "60:00"
    }
  ]
}

Action codes (verified from app mitmproxy captures + live testing):

action meaning extra params notes
0 Stop pump (index 0 only) Returns erreurtraitement: 6 on other outputs
1 Stop / cancel any running command Works on all outputs; cancels both timed and indefinite runs
2 Timed impulse time: "MM:SS" string Also used to override a running timer early
3 Boost filtration time: "MM:SS" Response: origin:3, info:["boost"]
4 Start indefinitely Runs until action 1 is sent; common in notebook for evening light use
5 Pause N days manualDuration: N (integer days) Disables output including scheduled programs; response shows "pause": N = days remaining
11 Backwash cycle time: encoded duration, speed: pump speed level

To stop a timed impulse early: send action 1 — cancels any running manual command.
To stop a timed output (alternative): override with action 2, time: "00:01".

Pause semantics: action 5 suspends the output entirely (manual commands and scheduled programs) for N days. The response pool state includes "pause": N showing days remaining. manualDuration here is days, not seconds.

Response timing: the pool state in the response always reflects the pre-command device state — it shows what the device reported before processing the new command. The command takes effect on the next LoRa downlink window.

Duration format: "MM:SS" string, e.g. "30:00" = 30 minutes. Verified from real capture.

Examples:

Stop fill valve (LRLEVEL):

{"moduleSerialNumber": "3A01020D2ED90001", "linesControl": [{"action": 0, "index": 0}]}

Boost filtration 1 min (LRPC):

{"moduleSerialNumber": "150302F1DA270001", "linesControl": [{"action": 3, "time": "01:00", "index": 0}]}

Response — full real-time device state, e.g. for LRPC after boost:

{
  "dialogTimeStamp": "...",
  "ackTimestamp": 1780609493,
  "temperature": 20,
  "pool": [
    {"index": 0, "origin": 3, "time": "01:00", "value": 1, "info": ["boost"]},
    {"index": 1, "time": "00:00", "tempRef": 7, "value": 0}
  ]
}

Python example — timed pool pump impulse (30 min on Station 2):

import json
s.post(f"{BASE}/remote/module/control",
    json={
        "moduleSerialNumber": "150302F1DA270001",
        "linesControl": [{"index": 1, "action": 2, "time": "30:00"}]
    }
)

LRPC output IDs (Domicile)

Index Name Output ID
0 Station 1 6481bcea8782b78554c02bf1
1 Station 2 6481bcea8782b78554c02bf2
2 Station 3 6481bcea8782b78554c02bf3

Notes

  • Commands go through the platform's relay infrastructure — the device doesn't need to be directly reachable
  • Use POST /canopy/{siteId}/sendModulesCommand after updating programs via PUT /programs to force an immediate push
  • moduleSerialNumber is the serialNumber field on the module object (not the id)
  • For irrigation valves (lr-ag, lr-ip-eco): use POST /module/sendManualModuleCommand
  • For pool controllers (lr-pc, lr-pc-vs): use POST /remote/module/control with linesControl