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:
2026-06-28 01:05:24 +02:00
commit 941abd98d5
48 changed files with 12118 additions and 0 deletions
+367
View File
@@ -0,0 +1,367 @@
# MyIndygo — Platform Overview & API Reference
**Base URL**: `https://qualif.myindygo.com`
**Purpose**: Pool-focused platform — filtration scheduling, water chemistry, pool level monitoring
**Relation to MySOLEM**: Same backend database and Sails.js server. Both platforms share all module data, sensor readings, and programs. MyIndygo adds a pool admin/management layer on top.
---
## Authentication
Identical to MySOLEM — same cookie name, same credentials work on both platforms.
```bash
curl -c cookies.txt https://qualif.myindygo.com/login
curl -c cookies.txt -b cookies.txt \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=YOUR_EMAIL&password=YOUR_PASSWORD" \
"https://qualif.myindygo.com/login"
```
Cookie: `qualification-mysolem-solem-irrigation-platform.sid` (same name as MySOLEM)
---
## User IDs — MyIndygo vs MySOLEM
The test API account (`arnaud.nelissen.api@icloud.com`) has **different ObjectIds** on each platform:
| Platform | User ID |
|---|---|
| MySOLEM | `5de53bca7ed4c45c2cfe067f` |
| MyIndygo | `6a21c3db5ebe0fa5f7885460` |
The MyIndygo user ID appears in the page HTML:
```html
<script>const userId = "6a21c3db5ebe0fa5f7885460"</script>
```
The pool/site ID `5de53c1c7ed4c45c2cfe0688` is the main account's pool record on MyIndygo — it's only accessible when logged in as the main account (`anelissen@solem.fr`), not the test API account.
---
## Feature flags (MyIndygo-specific)
`GET /features` returns pool-focused flags including:
| Flag | Value | Meaning |
|---|---|---|
| `ocedisWaterChemistryCorrection` | `true` | Water chemistry correction (OCEDIS dosing) |
| `authenticationMyIndygoUIV2` | `true` | MyIndygo-specific login UI |
| `enableBSTSubscriptions` | `true` | BST subscription management |
| `enablePWAIndygoInstallation` | `true` | Pool app PWA install |
| `clusterTurfOpened` | `true` | Turf cluster watering |
| `enablePolytropicAPI` | `true` | Polytropic pump integration |
| `enableGraphicalProgrammation` | `true` | Visual program editor |
| `enableNdviSatellite` | `true` | NDVI satellite data |
| `enableEvapotranspiration` | `true` | ET-based watering |
| `enableHPA` | `true` | HPA (high-pressure assist?) |
---
## Pool Admin System (B2B)
MyIndygo has a pool dealer admin layer for managing multiple customer pools. These endpoints are **admin-only** — not accessible with a standard user account.
```
GET /pools/admin list all managed pools
POST /pools/admin/search search pools (DataTables format)
POST /pools/admin/sendMessage send a message to a pool customer
GET /pools/admin/getMessages get admin messages
DELETE /pools/admin/dissociateAndRemove/{id} remove pool from admin
```
Pool data is fetched via:
```
GET /user/pools current user's pools (returns {"pools": []} for test account)
GET /user/pools?userId= another user's pools (IDOR — no ownership check)
```
---
## Pool-Specific Endpoints
### Last sensor tick (no date range needed)
```
GET /input/getLastTickFromInput?inputId={inputId}
```
Returns the single most-recent raw tick for an input. Much lighter than `getComputedSensorData`.
Response:
```json
{
"tick": {
"input": "6480da07c2c0896da120eccd",
"value": 635,
"timestamp": "2026-06-04T01:03:00.000Z",
"createdAt": "2026-06-04T01:03:11.000Z",
"id": "..."
}
}
```
Note: `value` is **raw** (not expression-computed). For LRPS pH: `635` = 6.35 pH (divide by 100). For LRPS temp: `2331` = 23.31°C.
### Live pool sensor snapshot
```python
import requests
BASE = "https://qualif.myindygo.com"
s = requests.Session()
s.get(f"{BASE}/login")
s.post(f"{BASE}/login", data={"email": "...", "password": "..."})
POOL_INPUTS = {
"temp": "6480da07c2c0896da120eccc",
"pH": "6480da07c2c0896da120eccd",
"ORP": "6480da07c2c0896da120ecce",
"pressure": "6a17218be0b88ea3e9255251",
"level": "6a174dd1e0b88ea3e925531e",
}
readings = {}
for name, input_id in POOL_INPUTS.items():
r = s.get(f"{BASE}/input/getLastTickFromInput", params={"inputId": input_id}).json()
readings[name] = r.get("tick", {})
# Current values (raw):
# temp → divide by 100 → °C
# pH → divide by 100 → pH units
# ORP → raw mV
# pressure → raw ADC (apply expression for bar)
# level → 0=low/fill needed, 1=OK
```
### Output events history
```
GET /output/events?moduleId={moduleId}
GET /output/events?outputId={outputId}
GET /output/events?moduleId={id}&startDate={iso8601}&endDate={iso8601}
```
Returns `[]` if no events or empty. Tracks when outputs have been triggered.
### Output watering forecast
```
GET /output/forecast?moduleId={moduleId}
GET /output/forecast?outputId={outputId}
```
Returns upcoming scheduled watering windows.
### Real-time module informations (via LoRa)
```
GET /remote/module/informations
?moduleId={relayModuleId}
&childrenSerialNumber={childSerialNumber}
```
Or via socket.io (as seen in JS):
```js
io.socket.request({
method: "get",
url: "/remote/module/informations",
data: {
moduleId: relayId, // LRMB10 gateway ID
moduleUuid: uuid,
moduleSerialNumber: serialNumber
}
}, callback)
```
Returns real-time device state polled over LoRa. Fails with `module_not_reachable` if device is offline.
### Real-time combined config + programs
```
GET /remote/module/configuration/and/programs?moduleId={moduleId}
```
Returns configuration + programs in a single LoRa roundtrip. Requires device online.
### Pool controller control (linesControl)
JSON body (verified from real network capture):
```
POST /remote/module/control
Content-Type: application/json
```
```json
{
"moduleSerialNumber": "150302F1DA270001",
"linesControl": [
{
"index": 1,
"action": 2,
"time": "30:00"
}
]
}
```
Known `action` codes for LRPC pool controller:
| action | meaning | extra params |
|---|---|---|
| `0` | Stop pump output (index 0 only) | — |
| `1` | **Stop / cancel** any running manual command | — |
| `2` | Timed impulse | `time`: `"MM:SS"` string (e.g. `"30:00"` = 30 minutes) |
| `3` | Boost filtration | `time`: `"MM:SS"` |
| `4` | Start indefinitely | — |
| `5` | Pause N days (blocks manual + scheduled) | `manualDuration`: N (integer days) |
| `11` | Backwash cycle | `time`: encoded duration string, `speed`: pump speed level |
Response pool state includes `"pause": N` (days remaining) when an output is paused.
Response always reflects **pre-command state** — the command takes effect on the next LoRa downlink.
For backwash, duration is encoded using `encodeBackwashDuration(durationMinutes, minMinutes, maxMinutes)`.
> **Corrected from JS analysis**: duration is `time: "MM:SS"` (string), NOT `manualDuration` in seconds. Verified from real Safari capture.
### Module advertising profile
```
GET /remote/module/advertisingProfile?moduleId={moduleId}
GET /remote/modules/advertisingProfile (bulk)
```
BLE advertising profile data. Likely used for Bluetooth pairing.
### Team pool access
```
GET /teams/pools/{poolId}/membersAccessGranted
```
Returns `{ teamMembersAlreadyGrantedPool: [...] }` — team members with access to a specific pool.
---
## LRPC Pool Controller — Program Structure (MyIndygo extended)
MyIndygo exposes more program fields than MySOLEM for `lr-pc` modules:
```
GET /programs/modules?moduleIds={moduleId}
```
```json
{
"programs": [
{
"id": "6481bcea8782b78554c02bf4",
"name": "",
"module": "6481bcea8782b78554c02bed",
"index": 0,
"weekDays": 127,
"numberOfDays": 2,
"synchroDay": 0,
"waterBudget": 100,
"startTimes": [-1, -1, -1, -1, -1, -1, -1, -1],
"programCharacteristics": {
"programType": 4,
"mode": 2,
"onSpeed": 1,
"speedSequence": 4368,
"microFilteringTemperature": 5,
"microFilteringDuration": 10,
"microFilteringPeriod": 480,
"microFilteringSpeed": 1,
"auxiliaryCyclicDuration": 0,
"frostFreeSpeed": 1,
"defaultProgramSpeed": 1,
"adaptOffset": 0,
"isShutterDependant": false,
"isRandom": false,
"isUsingButton": false
},
"windows": [
{
"start": 300, "end": 360,
"on": 3600, "off": 0,
"runningDays": 127
}
]
}
]
}
```
**`programType` codes** (pool controller):
| code | meaning |
|---|---|
| `4` | Main filtration (pump runs on schedule) |
| `2` | Auxiliary / cyclic (treatment dosing, UV, etc.) |
| `0` | Disabled / empty slot |
**`mode`**: `2` = active, `0` = disabled
**`speedSequence`**: pump speed bitmask for variable-speed pumps (`4368` = 0x1110)
**`windows[].runningDays`**: per-window day bitmask (127 = every day, unlike mysolem where weekDays is per-program)
**`startTimes`**: array of 8 fixed start times in minutes from midnight (-1 = unused)
### Domicile LRPC filtration schedule
| Program | Type | Windows | Total |
|---|---|---|---|
| 0 (Filtration) | 4 | 05:0006:00 (1h), 12:0014:00 (2h), 22:0023:00 (1h) | 4h/day |
| 1 (Auxiliary) | 2 | 21:3022:30 (1h) | 1h/day |
| 2 | — | empty | — |
---
## LRPC Outputs (Pool Controller)
| Output ID | Name | Index |
|---|---|---|
| `6481bcea8782b78554c02bf1` | Station 1 | 0 |
| `6481bcea8782b78554c02bf2` | Station 2 | 1 |
| `6481bcea8782b78554c02bf3` | Station 3 | 2 |
---
## New Module Types (found via modules/search on MyIndygo)
| Type | Name | Description |
|---|---|---|
| `lr-pc-vs` | LRPCVS | Pool controller with variable speed pump |
| `lr-mv` | LRMV/LRPM1 | Motor valve (multi-output) |
| `lr-mb-30` | LRMB30 | LoRa gateway v30 |
| `lr-mb-100` | LRMB100 | LoRa gateway v100 |
| `lr-ip-fl` | LR1IPFL | IP flow metering |
| `lr-ip-fl-v2` | LR6IP | IP flow v2 |
| `lr-ip-wan-v2` | LRIP6 WAN | IP WAN v2 |
| `lr-is` | LR6IS/LR12IS | LoRa input sensor (multi-input) |
| `lr-is-city` | LR6IS | City-variant input sensor |
| `lr-is-city-wan` | LR12IS | City WAN sensor |
| `lr-ms-eco` | LRMS_ECO | Eco sensor station |
| `lr-ms-rs485` | LRMSRS485 | RS485 sensor station |
| `lr-ms-v2` | LRMS v2 | Sensor station v2 |
| `lr-ol` | LROL4 | LoRa OL (lighting?) |
| `lr-bst-100` | LRBST100 | BST 100 (booster?) |
| `lr-bst-react-4g` | LR-BST R4G | BST 4G reactive (booster 4G) |
| `joro` / `joro-v2` | WB-xxxx | Joro water balance meter |
| `smart-is` | SmartIS | Smart input sensor |
| `smart-is-lcd` | VILLA-/SMT-HOME | Smart IS with LCD display |
| `bl-ip-v2` | BL6IP | Bluetooth IP v2 |
| `ipx` | GEN_xxxx | IPX controller (generic) |
---
## Shared Endpoints (same on both MySOLEM and MyIndygo)
All endpoints from `api-docs/endpoints.md` work identically on `https://qualif.myindygo.com`. The backend database is shared — module IDs, input IDs, programs, and sensor data are identical on both platforms.
Key shared endpoints that work well for pool monitoring:
- `GET /remote/input/getComputedSensorData?inputId=...` — time-series sensor data
- `GET /remote/module/getComputedSensorsData?moduleId=...` — all inputs for a module
- `POST /module/sendManualModuleCommand` — send commands to devices
- `GET /modules/{moduleId}/notebook?startDate={unix_epoch}` — event log
- `GET /programs/modules?moduleIds=...` — read/write programs
---
## Pool Monitoring Quick Reference
Current pool sensor IDs and live values (2026-06-04):
| Sensor | Input ID | Last Raw Value | Computed |
|---|---|---|---|
| Water temp (LRPS) | `6480da07c2c0896da120eccc` | 2331 | 23.31 °C |
| pH (LRPS) | `6480da07c2c0896da120eccd` | 635 | 6.35 (low — target 7.27.6) |
| ORP/Redox (LRPS) | `6480da07c2c0896da120ecce` | 646 | 646 mV |
| Filter pressure (LRPR) | `6a17218be0b88ea3e9255251` | 956 | — |
| Pool level switch | `6a174dd1e0b88ea3e925531e` | 1 | OK (1=full) |
| Fill flow volume | `6a1c4b153d6c33ab843edc15` | 0 | 0 L (no active fill) |
| Fill valve state | `6a1c4b153d6c33ab843edc16` | 1 | Closed |