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

259 lines
6.9 KiB
Markdown

# 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)
```json
{
"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)
```javascript
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`):
```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):
```json
{"moduleSerialNumber": "3A01020D2ED90001", "linesControl": [{"action": 0, "index": 0}]}
```
Boost filtration 1 min (LRPC):
```json
{"moduleSerialNumber": "150302F1DA270001", "linesControl": [{"action": 3, "time": "01:00", "index": 0}]}
```
**Response** — full real-time device state, e.g. for LRPC after boost:
```json
{
"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):**
```python
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`