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>
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user