941abd98d5
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>
1367 lines
48 KiB
Markdown
1367 lines
48 KiB
Markdown
# MySolem / MyIndygo — Complete API Reference
|
||
|
||
Reverse-engineered from black-box analysis: mitmproxy captures of iOS apps, Safari Web Inspector recordings, JS bundle static analysis, and live endpoint probing.
|
||
|
||
**Sources**: `inputs/apps_captured_all.jsonl`, `inputs/lrpc_post.json`, `inputs/mysolem_captured_all.jsonl`, `inputs/indygo_captured_all.jsonl`, `inputs/captured_all.jsonl`, JS bundles at `/min/production.min.js` and `/linker/js/esbuild-bundle.js`.
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Platforms & Architecture](#1-platforms--architecture)
|
||
2. [Authentication](#2-authentication)
|
||
3. [Key IDs — Domicile Site](#3-key-ids--domicile-site)
|
||
4. [Web API — Endpoints](#4-web-api--endpoints)
|
||
5. [Mobile API — /api/ Endpoints](#5-mobile-api--api-endpoints)
|
||
6. [Device Control](#6-device-control)
|
||
7. [MyIndygo — Pool Platform](#7-myindygo--pool-platform)
|
||
8. [Device Reference — Domicile](#8-device-reference--domicile)
|
||
9. [Write Endpoints Reference](#9-write-endpoints-reference)
|
||
10. [Reference Tables](#10-reference-tables)
|
||
11. [Security Findings](#11-security-findings)
|
||
|
||
---
|
||
|
||
## 1. Platforms & Architecture
|
||
|
||
| Platform | Base URL | Focus |
|
||
|---|---|---|
|
||
| MySOLEM | `https://qualif.mysolem.com` | Irrigation, garden sensors |
|
||
| MyIndygo | `https://qualif.myindygo.com` | Pool control, water chemistry |
|
||
|
||
Both platforms share **the same backend database and API server** — same module IDs, input IDs, session cookie, and sensor data. MyIndygo adds a pool admin/management layer on top. All web endpoints work identically on either host.
|
||
|
||
**Backend**: Node.js + Sails.js v0.13.8, MongoDB (all IDs are 24-char hex ObjectIds)
|
||
**Proxy**: nginx/1.18.0 (Ubuntu)
|
||
**Real-time**: Socket.IO v3 / Engine.IO v3 (`EIO=3`), direct WebSocket transport (not long-polling)
|
||
```
|
||
wss://qualif.mysolem.com/socket.io/?EIO=3&transport=websocket
|
||
```
|
||
|
||
**Two separate API layers on the same host**:
|
||
- **Web API** — no path prefix, session cookie auth
|
||
- **Mobile API** — `/api/` prefix, OAuth2 Bearer token auth
|
||
|
||
---
|
||
|
||
## 2. Authentication
|
||
|
||
### 2.1 Web API — Session Cookie
|
||
|
||
Cookie-based session (Sails.js). No CSRF token required on JSON endpoints.
|
||
|
||
```bash
|
||
# Step 1 — init cookie jar
|
||
curl -c cookies.txt https://qualif.mysolem.com/login
|
||
|
||
# Step 2 — authenticate
|
||
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.mysolem.com/login"
|
||
# → 302 redirect to / on success
|
||
|
||
# All subsequent calls
|
||
curl -b cookies.txt https://qualif.mysolem.com/features
|
||
```
|
||
|
||
**Cookie name**: `qualification-mysolem-solem-irrigation-platform.sid`
|
||
**Session TTL**: ~7 days
|
||
Same cookie works on both `qualif.mysolem.com` and `qualif.myindygo.com`.
|
||
|
||
**Python:**
|
||
```python
|
||
import requests
|
||
|
||
BASE = "https://qualif.mysolem.com"
|
||
s = requests.Session()
|
||
s.get(f"{BASE}/login") # init cookie
|
||
s.post(f"{BASE}/login", data={
|
||
"email": "YOUR_EMAIL",
|
||
"password": "YOUR_PASSWORD",
|
||
}) # 302 → / on success
|
||
# s carries the session cookie automatically on all subsequent calls
|
||
```
|
||
|
||
### 2.2 Mobile API — OAuth2 Bearer Token
|
||
|
||
The iOS apps use a completely separate auth layer at `/api/` prefix.
|
||
|
||
```
|
||
POST /oauth2/token
|
||
Content-Type: application/json
|
||
|
||
{"scope": "*", "grant_type": "client_credentials"}
|
||
```
|
||
|
||
Response:
|
||
```json
|
||
{
|
||
"access_token": "6wtff8dVlhjn7y...",
|
||
"expires_in": 5184000,
|
||
"token_type": "Bearer"
|
||
}
|
||
```
|
||
|
||
`expires_in` = 5,184,000 seconds = **60 days**.
|
||
|
||
The `client_id` and `client_secret` are embedded in the app binary and validated server-side — not present as explicit headers in captured traffic.
|
||
|
||
All subsequent mobile API requests:
|
||
```
|
||
Authorization: Bearer {access_token}
|
||
```
|
||
|
||
**Python:**
|
||
```python
|
||
import requests
|
||
|
||
BASE = "https://qualif.mysolem.com"
|
||
r = requests.post(f"{BASE}/oauth2/token",
|
||
json={"scope": "*", "grant_type": "client_credentials"})
|
||
token = r.json()["access_token"]
|
||
headers = {"Authorization": f"Bearer {token}"}
|
||
|
||
resp = requests.get(f"{BASE}/api/getUser", headers=headers)
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Key IDs — Domicile Site
|
||
|
||
### Users
|
||
|
||
| Platform | Account | User ID |
|
||
|---|---|---|
|
||
| MySOLEM | arnaud.nelissen.api@icloud.com | `5de53bca7ed4c45c2cfe067f` |
|
||
| MyIndygo | arnaud.nelissen.api@icloud.com | `6a21c3db5ebe0fa5f7885460` |
|
||
|
||
### Site
|
||
|
||
| Resource | ID |
|
||
|---|---|
|
||
| Site (Domicile) | `62b30d26ecbceb08607592c1` |
|
||
| Pool (Domicile) | `5de53c1c7ed4c45c2cfe0688` |
|
||
| Field supervision | `643c5134347b27edbf38b173` |
|
||
|
||
### Modules
|
||
|
||
| Module | Type | Module ID | Serial Number | childSerial6 |
|
||
|---|---|---|---|---|
|
||
| LRMB10-070532 (gateway) | `lr-mb-10` | `60ca4276ad69dac1558f7c61` | `1000000705320001` | — |
|
||
| LRMS-0721DC (sensor station) | `lr-ms` | `60ca42adad69dac1558f7c64` | `7400040721DC0001` | `0721DC` |
|
||
| LR6AG-070917 (irrigation valve) | `lr-ag` | `626984f038ec558c598bb6d5` | `6606010709170001` | `070917` |
|
||
| LRPS-0565C0 (pool analyser) | `lr-mas` | `6480da07c2c0896da120ecca` | `9400030565C00001` | `0565C0` |
|
||
| LRPC-F1DA27 (pool controller) | `lr-pc` | `6481bcea8782b78554c02bed` | `150302F1DA270001` | `F1DA27` |
|
||
| LR2IPECO-0C9282 (IP ECO) | `lr-ip-eco` | `6516ca57b7c098e6807cb332` | `3802020C92820001` | `0C9282` |
|
||
| LRPR-0D6800 (filter pressure) | `lr-pr` | `6a17218be0b88ea3e925524f` | `1D00010D68000001` | `0D6800` |
|
||
| POOL-LVL-0E953B (float switch) | `pool-level-sensor` | `6a174dd1e0b88ea3e925531c` | `0300020E953B0001` | — |
|
||
| LRLEVEL-0D2ED9 (fill valve) | `lr-niv` | `6a1c4b153d6c33ab843edc13` | `3A01020D2ED90001` | `0D2ED9` |
|
||
|
||
All child modules route through gateway `1000000705320001` (LRMB10) via LoRa radio.
|
||
|
||
### Inputs
|
||
|
||
| Sensor | Module | Input ID | Type | Unit |
|
||
|---|---|---|---|---|
|
||
| Plumbago moisture | LRMS | `60ca42adad69dac1558f7c66` | 12 | % |
|
||
| Pelouse moisture | LRMS | `60ca42adad69dac1558f7c67` | 12 | % |
|
||
| Air temperature | LRMS | `60ca42adad69dac1558f7c69` | 5 | °C |
|
||
| Rain sensor | LR6AG | `626984f038ec558c598bb6d7` | 2 | 0/1 |
|
||
| Pool water temperature | LRPS | `6480da07c2c0896da120eccc` | 133 | °C |
|
||
| Pool pH | LRPS | `6480da07c2c0896da120eccd` | 6 | pH (raw ÷ 100) |
|
||
| Pool ORP / Redox | LRPS | `6480da07c2c0896da120ecce` | 7 | mV |
|
||
| Flow meter | LR2IPECO | `6516ca57b7c098e6807cb334` | 33 | L |
|
||
| Filter pressure | LRPR | `6a17218be0b88ea3e9255251` | 8 | bar |
|
||
| Pool level switch | POOL-LVL | `6a174dd1e0b88ea3e925531e` | 21 | 0/1 |
|
||
| Fill volume | LRLEVEL | `6a1c4b153d6c33ab843edc15` | 33 | L |
|
||
| Fill valve state | LRLEVEL | `6a1c4b153d6c33ab843edc16` | 149 | 0/1 |
|
||
| LRPC temperature | LRPC | `6481bcea8782b78554c02bef` | 133 | °C |
|
||
|
||
### Outputs
|
||
|
||
| Output | Module | Output ID | Index | Flow rate |
|
||
|---|---|---|---|---|
|
||
| AG Station 1 | LR6AG | `626984f038ec558c598bb6d8` | 0 | 30 m³/h |
|
||
| AG Station 2 | LR6AG | `626984f038ec558c598bb6d9` | 1 | 14 m³/h |
|
||
| AG Station 3 | LR6AG | `626984f038ec558c598bb6da` | 2 | 20 m³/h |
|
||
| AG Station 4 | LR6AG | `626984f038ec558c598bb6db` | 3 | 1 m³/h |
|
||
| AG Station 5 | LR6AG | `626984f038ec558c598bb6dc` | 4 | 38 L/h |
|
||
| AG Station 6 | LR6AG | `626984f038ec558c598bb6dd` | 5 | 38 L/h |
|
||
| LRPC Station 1 (filtration pump) | LRPC | `6481bcea8782b78554c02bf1` | 0 | — |
|
||
| LRPC Station 2 (pool light) | LRPC | `6481bcea8782b78554c02bf2` | 1 | — |
|
||
| LRPC Station 3 | LRPC | `6481bcea8782b78554c02bf3` | 2 | — |
|
||
| IPECO Station 1 | LR2IPECO | `6516ca57b7c098e6807cb336` | 0 | — |
|
||
| IPECO Station 2 | LR2IPECO | `6516ca57b7c098e6807cb337` | 1 | — |
|
||
| LRLEVEL fill valve | LRLEVEL | `6a1c4b153d6c33ab843edc17` | 0 | — |
|
||
|
||
### Programs
|
||
|
||
| Program | Module | Program ID | Index | Type |
|
||
|---|---|---|---|---|
|
||
| Filtration (4h/day) | LRPC | `6481bcea8782b78554c02bf4` | 0 | 4 (filtration) |
|
||
| Auxiliary/cyclic | LRPC | `6481bcea8782b78554c02bf5` | 1 | 2 (auxiliary) |
|
||
| Unused | LRPC | `6481bcea8782b78554c02bf6` | 2 | — |
|
||
|
||
---
|
||
|
||
## 4. Web API — Endpoints
|
||
|
||
All paths relative to `https://qualif.mysolem.com` (or `https://qualif.myindygo.com` — identical backend). All require session cookie.
|
||
|
||
### 4.1 Feature Flags
|
||
|
||
```
|
||
GET /features
|
||
```
|
||
Returns flat JSON object of boolean flags: `dashboard`, `enableHydraulicSupervision`, `enableBSTSubscriptions`, `enablePolytropicAPI`, `enableGraphicalProgrammation`, `enableNdviSatellite`, `enableEvapotranspiration`, `ocedisWaterChemistryCorrection`, etc.
|
||
|
||
### 4.2 Sites
|
||
|
||
```
|
||
GET /canopy-sites?userId={userId}
|
||
```
|
||
Returns `{ items: [...], total: N }`. Each item includes full `modules` array and `fencing` config.
|
||
|
||
```
|
||
GET /canopy-sites/{siteId}/users
|
||
GET /canopy-sites/{siteId}/modules?limit=20&skip=0&includeTotalCount=true
|
||
GET /canopy-sites/{siteId}/modules/indicators
|
||
GET /canopy-sites/{siteId}/informations
|
||
GET /canopy-sites/{siteId}/rules
|
||
GET /canopy-sites/{siteId}/map/modules
|
||
GET /canopy-sites/{siteId}/modules/eco-mode-compatible
|
||
GET /canopy-sites/{siteId}/site-notes
|
||
GET /canopy-sites/{siteId}/notifications
|
||
GET /canopy/{siteId}/waterBudget
|
||
```
|
||
|
||
### 4.3 Modules
|
||
|
||
**Search / list (MongoDB-style filter):**
|
||
```
|
||
POST /modules/search
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"query": { "owner": "userId" },
|
||
"projection": { "name": 1, "type": 1, "serialNumber": 1 }
|
||
}
|
||
```
|
||
Response: `{ result: { results: [...], totalCount: null } }`
|
||
⚠ Empty query `{}` returns **all 4,607 platform modules** — no ownership enforcement.
|
||
|
||
```
|
||
GET /modules/available?userId={userId}
|
||
GET /modules/echarts-data?includeCustomers=true&includeAgency=false
|
||
→ {chartModuleTypes: [{name, value}], chartModuleConnectivities: [...]}
|
||
|
||
GET /sim-cards/needsPayment?moduleIds={id1}&moduleIds={id2}
|
||
→ {result: {"moduleId": false, ...}}
|
||
```
|
||
|
||
**Flexible module list:**
|
||
```
|
||
POST /users/{userId}/modules
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"fields": {"name": 1, "type": 1, "serialNumber": 1, "displayType": 1},
|
||
"filters": {},
|
||
"addOwnershipFlag": true,
|
||
"includeTotalCount": true,
|
||
"limit": 20,
|
||
"skip": 0
|
||
}
|
||
```
|
||
Response: `{modules: [...], totalCount: N}`
|
||
|
||
**Remote state (cached + live LoRa):**
|
||
```
|
||
GET /remote/module/state?moduleId={moduleId}
|
||
GET /remote/module/configuration?moduleId={moduleId}
|
||
GET /remote/module/configuration/and/programs?moduleId={moduleId}
|
||
GET /remote/module/informations?moduleId={relayId}&childrenSerialNumber={childSerial}
|
||
```
|
||
Live endpoints trigger a LoRa radio roundtrip — returns `module_not_reachable` if device is offline.
|
||
|
||
**Module metadata:**
|
||
```
|
||
GET /modules/{moduleId}/users
|
||
GET /modules/{moduleId}/tags
|
||
GET /modules/{moduleId}/notebook?startDate={unix_epoch_seconds}
|
||
GET /input/getInputsLinkedModule/{moduleId}
|
||
```
|
||
|
||
**User avatar:**
|
||
```
|
||
GET /user/avatar → image/png (no-cache)
|
||
GET /user/avatar?t={ms} → cache-bust variant
|
||
```
|
||
|
||
### 4.4 Sensor Data (Inputs)
|
||
|
||
**Batch input config:**
|
||
```
|
||
GET /inputs?inputId=id1%3Bid2%3Bid3
|
||
```
|
||
Semicolons (`%3B`) separate multiple IDs. Returns full input objects with `expression`, `type`, `unit`, `module`, `index`.
|
||
|
||
**All inputs for a module (most efficient for dashboard loads):**
|
||
```
|
||
GET /remote/module/getComputedSensorsData
|
||
?moduleId={moduleId}
|
||
&startDate={iso8601}
|
||
&endDate={iso8601}
|
||
&forceRawOrComputed=computed
|
||
```
|
||
Returns array of input objects, each with `computedSensorData` ticks. Pass `forceRawOrComputed=raw` for raw ADC values.
|
||
|
||
**Single input time-series:**
|
||
```
|
||
GET /remote/input/getComputedSensorData
|
||
?inputId={inputId}
|
||
&startDate={iso8601}
|
||
&endDate={iso8601}
|
||
&ensurePrevTickForContinuity=true
|
||
```
|
||
Response:
|
||
```json
|
||
{
|
||
"id": "60ca42adad69dac1558f7c66",
|
||
"name": "Plumbago",
|
||
"type": 12,
|
||
"unit": 10,
|
||
"interval": 60,
|
||
"computedSensorData": [
|
||
{ "tickTimestamp": "2026-06-01T00:42:00.000Z", "value": 54.1875 }
|
||
]
|
||
}
|
||
```
|
||
|
||
**Last tick only (fastest — no date range needed):**
|
||
```
|
||
GET /input/getLastTickFromInput?inputId={inputId}
|
||
```
|
||
Returns `{ "tick": { "input": "...", "value": <raw>, "timestamp": "...", "id": "..." } }`.
|
||
Value is **raw** (pre-expression). Apply the input's `expression` to get the computed value.
|
||
LRPS pH: `raw ÷ 100` → pH. LRPS temp: `raw ÷ 100` → °C.
|
||
|
||
**Averages & CSV:**
|
||
```
|
||
GET /inputs/average?inputIds={id1},{id2}&startDate={iso8601}&endDate={iso8601}
|
||
GET /input/getComputedInputIpxDataBetweenDatesAsCsv?inputId={id}&startDate={iso8601}&endDate={iso8601}
|
||
```
|
||
|
||
**Live pool sensor snapshot (Python):**
|
||
```python
|
||
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", {}).get("value")
|
||
# pH: readings["pH"] / 100 → pH units
|
||
# temp: readings["temp"] / 100 → °C
|
||
```
|
||
|
||
### 4.5 Outputs
|
||
|
||
```
|
||
GET /outputs/modules?moduleIds={moduleId}
|
||
POST /outputs/search
|
||
{"query": {"module": {"$in": ["moduleId"]}}}
|
||
POST /output/update
|
||
|
||
GET /output/events?moduleId={moduleId}
|
||
GET /output/events?outputId={outputId}&startDate={iso8601}&endDate={iso8601}
|
||
GET /output/forecast?moduleId={moduleId}
|
||
GET /output/forecast?outputId={outputId}
|
||
```
|
||
Response structure: `{ outputs: [{ id, module, index, name, flowRate: { value, unit } }] }`
|
||
Flow rate unit: `1` = L/h, `2` = m³/h.
|
||
|
||
### 4.6 Programs
|
||
|
||
```
|
||
GET /programs/modules?moduleIds={moduleId}
|
||
PUT /programs {"programs": [...], "module": "moduleId"}
|
||
POST /programs create new program
|
||
DELETE /programs
|
||
PATCH /programs/name
|
||
POST /programs/search {"query": {"id": {"$in": [...]}}}
|
||
GET /programs/reset?moduleId={moduleId}
|
||
POST /checkAvailableWindows check time conflicts before saving
|
||
```
|
||
|
||
**Program object:**
|
||
```json
|
||
{
|
||
"id": "...",
|
||
"name": "Pelouse Terrasse",
|
||
"module": "moduleId",
|
||
"index": 0,
|
||
"programmingType": "temporal",
|
||
"weekDays": 127,
|
||
"waterBudget": 100,
|
||
"windows": [{ "start": 420, "end": 422, "on": 120, "off": 0 }]
|
||
}
|
||
```
|
||
Window times are **minutes from midnight**. `on` / `off` are in **seconds**. `weekDays` 127 = every day (bitmask, bit 0 = Monday).
|
||
|
||
**LRPC extended program fields** (pool controller):
|
||
```json
|
||
{
|
||
"programCharacteristics": {
|
||
"programType": 4,
|
||
"mode": 2,
|
||
"onSpeed": 1,
|
||
"speedSequence": 4368,
|
||
"microFilteringTemperature": 5,
|
||
"microFilteringDuration": 10,
|
||
"microFilteringPeriod": 480,
|
||
"frostFreeSpeed": 1
|
||
},
|
||
"windows": [{ "start": 300, "end": 360, "on": 3600, "off": 0, "runningDays": 127 }]
|
||
}
|
||
```
|
||
`programType`: 4 = filtration, 2 = auxiliary/cyclic, 0 = disabled.
|
||
|
||
### 4.7 Users
|
||
|
||
```
|
||
GET /users-info/{userId}?includeUserGroups=true
|
||
GET /users/{userId}/modules
|
||
GET /users/{userId}/sites
|
||
GET /users/{userId}/module-groups
|
||
GET /users/{userId}/subscriptionModules
|
||
GET /users/{userId}/checkSubscriptionSimcardStatus
|
||
POST /users/search {"query": {...}, "projection": {...}}
|
||
```
|
||
|
||
**Grouped resources (paginated POST):**
|
||
```
|
||
POST /users/{userId}/module-groups → {moduleGroups, totalCount}
|
||
POST /users/{userId}/agricultural-module-groups → {agriculturalModuleGroups, totalCount}
|
||
POST /users/{userId}/watering-module-groups → {wateringModuleGroups, totalCount}
|
||
POST /users/{userId}/customers → {customers, totalCount}
|
||
POST /users/{userId}/associated → {users, totalCount}
|
||
POST /users/{userId}/tags → {tags}
|
||
```
|
||
|
||
**Activity log:**
|
||
```
|
||
POST /users/{userId}/records
|
||
{
|
||
"limit": 10, "skip": 0,
|
||
"filters": {"startDate": "2026-05-28T21:51:48.266Z", "showAllEvents": true},
|
||
"includeTotalCount": true,
|
||
"sortColumn": "timestamp", "sortOrder": -1,
|
||
"fields": {"category": 1, "label": 1, "module": 1, "timestamp": 1}
|
||
}
|
||
→ {records: [...], totalCount: N}
|
||
|
||
POST /users/{userId}/records/categories
|
||
{"filters": {"startDate": "..."}, "includeCustomers": true}
|
||
→ [{category, displayCategory}]
|
||
```
|
||
|
||
### 4.8 Module Notebook
|
||
|
||
Full chronological history of manual commands, alerts, firmware updates, pool events:
|
||
|
||
```
|
||
GET /modules/{moduleId}/notebook?startDate={unix_epoch_seconds}
|
||
```
|
||
|
||
`startDate` is a **Unix timestamp in seconds** (not ISO 8601).
|
||
|
||
Response: `{ "records": [...] }` sorted oldest-first.
|
||
|
||
```json
|
||
{
|
||
"category": "manual_command",
|
||
"label": "...",
|
||
"priority": "low",
|
||
"state": "archived",
|
||
"timestamp": "2026-06-04T21:00:00.000Z",
|
||
"params": { "action": 2, "index": 1, "manualDuration": 1800 },
|
||
"input": "inputId"
|
||
}
|
||
```
|
||
|
||
Known `category` values: `manual_command`, `pool`, `platform_alert`, `firmware_update`.
|
||
For `pool` category: `params.poolAlertType` = `backwashReminder`, `backwashDone`, `poolLevelSensorCleaningReminder`, etc.
|
||
For `manual_command`: `params` mirrors the linesControl action/index that triggered the event. Note: `manualDuration` in notebook is stored in **seconds** internally, regardless of the `time:"MM:SS"` format sent to the API.
|
||
|
||
### 4.9 Notifications & Journal
|
||
|
||
```
|
||
GET /journal/notifications
|
||
GET /journal/counts
|
||
GET /journal/warnings/accepted
|
||
PUT /journal/acknowledge/all
|
||
POST /canopy-sites/{siteId}/notifications/acknowledge-all
|
||
```
|
||
|
||
**Filtered site notifications:**
|
||
```
|
||
POST /canopy-sites/{siteId}/notifications
|
||
{"limit": 1, "includeTotalCount": true, "filters": {"state": {"$in": ["unread", "process"]}}}
|
||
→ {records: [...]}
|
||
```
|
||
|
||
**Cross-site journal:**
|
||
```
|
||
POST /journal/notifications
|
||
{
|
||
"canopyIds": ["siteId"],
|
||
"limit": 100, "skip": 0,
|
||
"filters": {"priority": {"$in": ["critical", "high"]}, "state": {"$in": ["unread", "process"]}},
|
||
"projection": {...}
|
||
}
|
||
→ {records: [...], total: N}
|
||
```
|
||
|
||
### 4.10 Weather
|
||
|
||
```
|
||
GET /moduleGroups/getCanopyWeather
|
||
?location[latitude]={lat}&location[longitude]={lng}
|
||
```
|
||
Returns 3-day AccuWeather-style forecast.
|
||
|
||
### 4.11 Field Supervision
|
||
|
||
```
|
||
GET /canopy/{siteId}/field-supervisions/modules
|
||
→ {canopyModules: [{type, modules: [{serialNumber, name, outputs: [{id, index, name}]}]}]}
|
||
|
||
GET /canopy/{siteId}/field-supervision/{supervisionId}/resume
|
||
→ {allPrograms: [...]}
|
||
|
||
GET /moduleGroups/{siteId}/field-supervision/{supervisionId}/events/{startISO}/{endISO}?timezone=Europe/Paris
|
||
→ {events: [{eventId, moduleId, moduleName, identifier, name, start, end, flowRate, index}]}
|
||
```
|
||
|
||
Field supervision ID for Domicile: `643c5134347b27edbf38b173`
|
||
|
||
### 4.12 Satellite / NDVI Imagery
|
||
|
||
Host: **`sentinel.mysolem.com`** (separate subdomain).
|
||
|
||
```
|
||
GET /moduleGroups/{siteId}/fencing/{timestampMs}/images-check
|
||
→ {availableImages: {available: true, reason: "ok"}}
|
||
|
||
GET /moduleGroups/{siteId}/fencing/{timestampMs}/images
|
||
→ {imagesList: {images: [{name, urlRgb, urlSavi}]}}
|
||
|
||
GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/savi → PNG (vegetation index)
|
||
GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/rgb → PNG (true color)
|
||
```
|
||
|
||
Timestamp format: `YYYYMMDDTHHmmss`, e.g. `20220417T103631`.
|
||
Domicile plot ID: `62f4efcb4a6dd563203558c3`, fencing timestamp: `1656939129360`.
|
||
|
||
### 4.13 Clusters & Daily Data
|
||
|
||
```
|
||
GET /clusters/{clusterId}/data
|
||
GET /clusters/{clusterId}/modules
|
||
GET /clusters/{clusterId}/programs
|
||
PUT /clusters/{clusterId}/programs
|
||
PATCH /clusters/{clusterId}/name
|
||
PATCH /clusters/{clusterId}/flowrate
|
||
DELETE /clusters/{clusterId}
|
||
|
||
GET /garden/dailyData/{moduleId}
|
||
GET /modules/heatmap-data?moduleIds={ids}&startDate=X&endDate=Y
|
||
```
|
||
|
||
### 4.14 Pool-Specific (MyIndygo)
|
||
|
||
```
|
||
GET /user/pools → current user's pools (⚠ returns [] for API test account)
|
||
GET /user/pools?userId={id} → another user's pools (IDOR — no ownership check)
|
||
GET /maintenance-logs-data?moduleId={moduleId}
|
||
```
|
||
|
||
`maintenance-logs-data` returns `{ code, messages: [...] }`. Each message has `poolAlertType`, `priority`, `state`, `timestamp`, and a `pool` object with full pool config and `pool.status` (live sensor values, last pressure, last pH, etc.).
|
||
|
||
**Pool admin (admin-only, 403 on standard account):**
|
||
```
|
||
GET /pools/admin
|
||
POST /pools/admin/search
|
||
POST /pools/admin/sendMessage
|
||
GET /pools/admin/getMessages
|
||
DELETE /pools/admin/dissociateAndRemove/{id}
|
||
```
|
||
|
||
```
|
||
GET /teams/pools/{poolId}/membersAccessGranted
|
||
→ {teamMembersAlreadyGrantedPool: [...]}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Mobile API — /api/ Endpoints
|
||
|
||
Base URL: `https://qualif.mysolem.com` (same host, different path prefix).
|
||
Auth: `Authorization: Bearer {token}` on all requests.
|
||
|
||
### 5.1 User
|
||
|
||
```
|
||
GET /api/getUser
|
||
GET /api/getUserWithHisModules
|
||
GET /api/getUserPools
|
||
GET /api/getUserWateringModuleGroups → {"wateringModuleGroups": []}
|
||
GET /api/getUserAgriculturalModuleGroups → {"agriculturalModuleGroups": []}
|
||
GET /api/getCustomers → {"err": {"code": "user_is_not_professional"}}
|
||
GET /api/users/{userId}/canopy-sites
|
||
GET /api/users/{userId}/access-requests
|
||
GET /api/users/{userId}/module-access-requests
|
||
```
|
||
|
||
`GET /api/getUserPools` is the only endpoint that exposes the pool record `5de53c1c7ed4c45c2cfe0688` via the API test account — the web platform `/user/pools` returns empty for that account.
|
||
|
||
### 5.2 Pool
|
||
|
||
```
|
||
GET /api/getPoolStatus?attributesToPopulate[]=modules&poolIdentifier={poolId}
|
||
```
|
||
Returns pool status with full modules array. Pool ID for Domicile: `5de53c1c7ed4c45c2cfe0688`.
|
||
|
||
Pool config fields: `volume` (25 m³), `installationType`, `filteringType`, `waterTreatmentType`, `phRegulationMode`, `chlorineRegulationMode`, `heatingSystemType`, `coverType`, `cyanuricAcidRate`, `model`, `latitude`, `longitude`, `address`.
|
||
|
||
### 5.3 Modules
|
||
|
||
```
|
||
GET /api/getModuleWithHisUsers?module={moduleId}
|
||
GET /api/getModuleInventory?module={gatewaySerial}&startDate={iso8601}&endDate={iso8601}&data=1
|
||
→ {children: [...]} ← all child modules under this gateway with full config
|
||
GET /api/getModuleBackups?module={moduleId}
|
||
→ array of saved program snapshots
|
||
GET /api/getPrograms?id={moduleId}
|
||
→ full program objects including startTimesCycles, stationsDuration
|
||
PUT /api/updateModule
|
||
Content-Type: application/json
|
||
{"module": {"id": "moduleId", "batteryVoltage": 77, "powerState": 0, "status": {...}}}
|
||
```
|
||
|
||
### 5.4 Gateway-Routed Module Access
|
||
|
||
All real-time commands go through the LRMB10 gateway. URL pattern:
|
||
```
|
||
/api/module/{gatewaySerial}/{action}/{childSerial6}
|
||
```
|
||
- `gatewaySerial` = full 16-char serial, e.g. `1000000705320001`
|
||
- `childSerial6` = **last 6 hex chars** of the child module's serial (case-insensitive)
|
||
|
||
**Domicile routing:**
|
||
|
||
| Child | childSerial6 | Full serial |
|
||
|---|---|---|
|
||
| LRPC-F1DA27 | `F1DA27` | `150302F1DA270001` |
|
||
| LR6AG-070917 | `070917` | `6606010709170001` |
|
||
| LRMS-0721DC | `0721DC` | `7400040721DC0001` |
|
||
| LR2IPECO-0C9282 | `0C9282` | `3802020C92820001` |
|
||
| LRLEVEL-0D2ED9 | `0D2ED9` | `3A01020D2ED90001` |
|
||
| LRPR-0D6800 | `0D6800` | — |
|
||
|
||
**Read endpoints:**
|
||
```
|
||
GET /api/module/{gatewaySerial}/information ← gateway itself
|
||
GET /api/module/{gatewaySerial}/information/{child6} ← child real-time info (LoRa roundtrip)
|
||
GET /api/module/{gatewaySerial}/status/{child6} ← current output/input states
|
||
GET /api/module/{gatewaySerial}/configuration/{child6} ← device configuration
|
||
GET /api/module/{gatewaySerial}/programs/{child6} ← programs stored on device
|
||
GET /api/module/{gatewaySerial}/inventory ← all children (= getModuleInventory)
|
||
```
|
||
|
||
Gateway information response example:
|
||
```json
|
||
{
|
||
"mtype": "10",
|
||
"msn": "1000000705320001",
|
||
"name": "LRMB10-070532 (Perso)",
|
||
"vsoft": "6.5.0",
|
||
"ble": "1.0.13",
|
||
"ssid": "Freebox-2A5E5D",
|
||
"bluetoothLink": true,
|
||
"wifiLink": true,
|
||
"ethernetLink": false,
|
||
"modemLink": false,
|
||
"up": 782800,
|
||
"cdate": "2026-06-05T00:11:38+02:00"
|
||
}
|
||
```
|
||
|
||
**Write endpoints:**
|
||
```
|
||
POST /api/module/{gatewaySerial}/status/{child6}
|
||
Content-Type: application/json
|
||
{"ag": [{"index": 0, "rainDelay": 0}, {"index": 1, "rainDelay": 0}]}
|
||
→ sets rain delay per output; response includes full device status
|
||
|
||
POST /api/module/{gatewaySerial}/programs/{child6}
|
||
Content-Type: application/json
|
||
{"programs": [...], "programmationType": 1}
|
||
→ sends programs to device over LoRa
|
||
|
||
POST /api/module/{gatewaySerial}/manual/{child6}
|
||
Content-Type: application/json
|
||
{...command body...}
|
||
→ sends a manual command to device over LoRa
|
||
```
|
||
|
||
### 5.5 Sensor & Chart Data
|
||
|
||
```
|
||
GET /api/getInputs?id={moduleId}
|
||
→ all inputs for module with expression, expressionInverse, alert config
|
||
|
||
GET /api/getComputedChartData
|
||
?inputId={inputId}
|
||
&startDate={iso8601}
|
||
&endDate={iso8601}
|
||
&numberOfBar={N}
|
||
&locale=fr
|
||
→ input data bucketed into N bars; includes computedSensorData array
|
||
|
||
GET /api/getComputedOutputActivationHistory
|
||
?moduleId={moduleId}
|
||
&outputIndex={0|1|2}
|
||
&startDate={iso8601}
|
||
&endDate={iso8601}
|
||
&numberOfBar={N}
|
||
→ [{currentPeriodStartDate, currentPeriodEndDate, value}]
|
||
value=1 means output was active during that period
|
||
numberOfBar=1440 over 24h = 1 bar per minute
|
||
```
|
||
|
||
### 5.6 Programs (Mobile)
|
||
|
||
```
|
||
GET /api/getPrograms?id={moduleId}
|
||
PUT /api/updatePrograms
|
||
Content-Type: application/json
|
||
{
|
||
"programmationType": 1,
|
||
"programs": [{"id": "...", "name": "...", "waterBudget": 100, "windows": [...], "programCharacteristics": {...}}]
|
||
}
|
||
→ {updatedPrograms: [...]}
|
||
|
||
POST /api/library/program-templates
|
||
{"limit": 10, "moduleId": "moduleId", "offset": 0}
|
||
→ {"programTemplates": []}
|
||
```
|
||
|
||
### 5.7 Documents & Metadata
|
||
|
||
```
|
||
GET /api/getDocument?documentType=installation-guide&product-type={moduleType}
|
||
→ {"url": "https://qualif.mysolem.com/files/notices/{type}/solem/fr-installation-guide.pdf"}
|
||
|
||
GET /api/getAdvertisingProfilePresets
|
||
→ [{name, ble: {windows: [...]}, loRa: {interval}}]
|
||
Profiles: default, ecoMode, etc.
|
||
|
||
GET /api/getAllBanishedSerialNumbers
|
||
→ ["120C0200750001", ...]
|
||
Blacklisted/revoked device serial numbers.
|
||
```
|
||
|
||
### 5.8 Reporting (App Telemetry)
|
||
|
||
Called by the app after sending data to devices — informational, do not depend on responses.
|
||
|
||
```
|
||
POST /api/reportModuleDatasSent {"module": "moduleId"}
|
||
POST /api/reportProgramsDatasSent {"programs": ["programId", ...], "module": "moduleId"}
|
||
POST /api/reportManualCommandSent {"route": "http", "command": {...}, "id": "moduleId"}
|
||
```
|
||
|
||
### 5.9 Push Notifications
|
||
|
||
```
|
||
POST /api/addApnsTokenToUser
|
||
{"isSandbox": false, "topic": "com.mysolem.qualif", "user": "{userId}", "token": "{apnsToken}"}
|
||
→ {"code": "ok"}
|
||
```
|
||
|
||
### 5.10 Web vs Mobile API Comparison
|
||
|
||
| Feature | Web (cookie) | Mobile (Bearer) |
|
||
|---|---|---|
|
||
| Auth | Session cookie | OAuth2 Bearer |
|
||
| Pool access | Admin-only / main account | `GET /api/getUserPools` |
|
||
| Module routing | Direct by ID | Via gateway serial + childSerial6 |
|
||
| Sensor data | `getComputedSensorData` (raw ticks) | `getComputedChartData` (bucketed) |
|
||
| Output history | `output/events` | `getComputedOutputActivationHistory` |
|
||
| Programs | `programs/modules` | `getPrograms` (richer fields) |
|
||
| Module inventory | `modules/search` | `getModuleInventory` via gateway |
|
||
| Program backups | — | `getModuleBackups` |
|
||
|
||
---
|
||
|
||
## 6. Device Control
|
||
|
||
All control commands work on both `qualif.mysolem.com` and `qualif.myindygo.com`. All require session cookie.
|
||
|
||
### 6.1 Pool Controller (LRPC) — linesControl
|
||
|
||
Used for LRPC (`lr-pc`), LRLEVEL (`lr-niv`), and other pool devices.
|
||
|
||
```
|
||
POST /remote/module/control
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"moduleSerialNumber": "150302F1DA270001",
|
||
"linesControl": [
|
||
{"index": 0, "action": 1}
|
||
]
|
||
}
|
||
```
|
||
|
||
`moduleSerialNumber` is the device's `serialNumber` field (not the MongoDB `id`).
|
||
Multiple entries in `linesControl` can be sent in one call.
|
||
|
||
**Action codes (verified from app mitmproxy captures + live testing):**
|
||
|
||
| action | meaning | extra params |
|
||
|---|---|---|
|
||
| `0` | Stop pump (index 0 only) | — |
|
||
| `1` | **Stop / cancel** any running command | — |
|
||
| `2` | Timed impulse | `time`: `"MM:SS"` string (e.g. `"30:00"` = 30 min) |
|
||
| `3` | Boost filtration | `time`: `"MM:SS"` |
|
||
| `4` | Start indefinitely (runs until action 1) | — |
|
||
| `5` | Pause N days (blocks manual + scheduled programs) | `manualDuration`: N (integer days) |
|
||
| `11` | Backwash cycle | `time`: encoded duration, `speed`: pump speed level |
|
||
|
||
**Important notes:**
|
||
- Action 0 returns `{"erreurtraitement": 6}` if called on a timed output that's running.
|
||
- To stop a running timed output: send `action 1`, or override with `action 2, time: "00:01"`.
|
||
- Action 5 `manualDuration` is **days** (not seconds). Response includes `"pause": N` (days remaining).
|
||
- **Response shows pre-command device state** — the new command takes effect on the next LoRa downlink window.
|
||
|
||
**Response structure (LRPC):**
|
||
```json
|
||
{
|
||
"dialogTimeStamp": "2026-06-04T22:35:16Z",
|
||
"ackTimestamp": 1780612516,
|
||
"temperature": 19,
|
||
"pool": [
|
||
{"index": 0, "time": "00:00", "tempRef": 7, "value": 0},
|
||
{"index": 1, "origin": 2, "time": "30:00", "value": 1, "info": ["man"]},
|
||
{"index": 2, "time": "00:00", "tempRef": 7, "value": 0}
|
||
],
|
||
"sensorState": [
|
||
{"index": 0, "value": 2318},
|
||
{"index": 1, "value": 2147483647}
|
||
],
|
||
"inputsAlerts": [],
|
||
"radio": ["cmd"]
|
||
}
|
||
```
|
||
|
||
`pool[n].value`: 1 = running, 0 = stopped.
|
||
`pool[n].origin`: 2 = manual, 3 = boost.
|
||
`pool[n].info`: `["man"]` = manual, `["boost"]` = boost mode.
|
||
`pool[n].pause`: days remaining on a pause (only present when paused).
|
||
`radio: ["cmd"]` = command queued for LoRa transmission.
|
||
|
||
**Common examples:**
|
||
|
||
```bash
|
||
# Start pool light (index 1) for 30 minutes
|
||
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":2,"time":"30:00"}]}'
|
||
|
||
# Stop pool light
|
||
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":1}]}'
|
||
|
||
# Boost filtration for 2 hours
|
||
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":0,"action":3,"time":"02:00"}]}'
|
||
|
||
# Pause pool light for 3 days (disables manual + programs)
|
||
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":5,"manualDuration":3}]}'
|
||
|
||
# Stop fill valve (LRLEVEL, index 0)
|
||
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"moduleSerialNumber":"3A01020D2ED90001","linesControl":[{"action":0,"index":0}]}'
|
||
```
|
||
|
||
### 6.2 Irrigation Valve (LR6AG) — sendManualModuleCommand
|
||
|
||
Used for `lr-ag`, `lr-ip-eco`, and other irrigation modules.
|
||
|
||
```
|
||
POST /module/sendManualModuleCommand
|
||
Content-Type: application/x-www-form-urlencoded
|
||
```
|
||
|
||
All patterns confirmed via mitmproxy captures.
|
||
|
||
| Intent | Form params |
|
||
|---|---|
|
||
| Run station N minutes | `commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=N` |
|
||
| Run all stations N minutes | `commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=N` |
|
||
| Cycling station (on/off intervals) | `commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0&commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId}` |
|
||
| Stop specific station | `commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId}` |
|
||
| Stop everything | `commandType=manualStop` |
|
||
| Enable (no rain delay) | `commandType=status&command=on&commandParams=0` |
|
||
| Disable / 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:**
|
||
- `command` accepts an output ID string, `"allStations"`, or a program ID.
|
||
- `agriculturalModuleId` is required for stop and cycling station commands.
|
||
- `commandType=manualStop` takes no `outputIndex` or `commandParams`.
|
||
|
||
**JSON body variant (also accepted):**
|
||
```json
|
||
{
|
||
"moduleSerialNumber": "6606010709170001",
|
||
"commandType": "station",
|
||
"outputIndex": 0,
|
||
"command": null,
|
||
"commandParams": {"hours": 0, "minutes": 15, "on": 0, "off": 0}
|
||
}
|
||
```
|
||
|
||
Rain delay example (suspend station 0 indefinitely):
|
||
```json
|
||
{
|
||
"moduleSerialNumber": "6606010709170001",
|
||
"commandType": "status",
|
||
"command": "off",
|
||
"commandParams": 255,
|
||
"outputIndex": 0
|
||
}
|
||
```
|
||
|
||
### 6.3 Flush Commands to Device
|
||
|
||
Triggers the platform to push all queued commands to a site's modules immediately:
|
||
|
||
```
|
||
POST /canopy/{siteId}/sendModulesCommand
|
||
Content-Type: application/json
|
||
{}
|
||
```
|
||
Response: `202 Accepted` — `{ "message": "Modules command accepted for processing" }`
|
||
|
||
Use after `PUT /programs` to force immediate LoRa push.
|
||
|
||
### 6.4 LoRa Data Push (Force Sync)
|
||
|
||
```
|
||
POST /modules/sendDataViaLoRaWAN
|
||
POST /agriculturalModuleGroup/sendDataViaLoRaWAN
|
||
POST /wateringModuleGroup/sendDataViaLoRaWAN
|
||
```
|
||
|
||
### 6.5 Module Rename
|
||
|
||
```
|
||
GET /remote/module/name?moduleId={moduleId}&name={newName}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. MyIndygo — Pool Platform
|
||
|
||
MyIndygo (`https://qualif.myindygo.com`) is the pool-focused frontend on the same backend. All endpoints from section 4 work identically.
|
||
|
||
### 7.1 Pool Auth Note
|
||
|
||
The test API account (`arnaud.nelissen.api@icloud.com`) has different user IDs on each platform:
|
||
- MySOLEM: `5de53bca7ed4c45c2cfe067f`
|
||
- MyIndygo: `6a21c3db5ebe0fa5f7885460`
|
||
|
||
The pool ID `5de53c1c7ed4c45c2cfe0688` is accessible via the mobile API (`GET /api/getUserPools`) or when logged in as the main account (`anelissen@solem.fr`) on the web.
|
||
|
||
### 7.2 LRPC Filtration Schedule (Domicile)
|
||
|
||
Current program 0 (filtration, type 4), runs every day:
|
||
|
||
| Window | Start | End | Duration |
|
||
|---|---|---|---|
|
||
| Morning | 05:00 | 06:00 | 1h |
|
||
| Midday | 12:00 | 14:00 | 2h |
|
||
| Evening | 22:00 | 23:00 | 1h |
|
||
| **Total** | | | **4h/day** |
|
||
|
||
Special features:
|
||
- **Micro-filtration** (frost mode): activates every 480 min for 10 min when temp < 5°C, speed 1
|
||
- **Temperature-adaptive**: 2 schedules (96d + 31d/year) with 12 temperature thresholds adjusting run windows
|
||
|
||
Program 1 (auxiliary, type 2): 21:30–22:30 active window, 5-min cycles — likely electrolyser or secondary treatment pump.
|
||
|
||
### 7.3 Pool Sensor Quick Reference
|
||
|
||
| Sensor | Input ID | Unit | Notes |
|
||
|---|---|---|---|
|
||
| Water temp | `6480da07c2c0896da120eccc` | °C (raw ÷ 100) | Healthy: 20–28°C |
|
||
| pH | `6480da07c2c0896da120eccd` | pH (raw ÷ 100) | Target: 7.2–7.6 |
|
||
| ORP/Redox | `6480da07c2c0896da120ecce` | mV (raw) | Healthy: 650–750 mV |
|
||
| Filter pressure | `6a17218be0b88ea3e9255251` | bar: `(1.5×raw−500)/1000` | Running: ~0.70–0.75 bar; off: ~0.08 bar |
|
||
| Pool level switch | `6a174dd1e0b88ea3e925531e` | 0/1 | 0 = fill needed, 1 = OK |
|
||
| Fill volume | `6a1c4b153d6c33ab843edc15` | L | Volume added per fill cycle |
|
||
| Fill valve state | `6a1c4b153d6c33ab843edc16` | 0/1 | 1 = valve closed |
|
||
|
||
### 7.4 Fill System Logic
|
||
|
||
```
|
||
pool-level-sensor (float switch) reads 0 (low)
|
||
→ LRLEVEL (lr-niv) opens fill valve (output 0, Station 1)
|
||
→ edc15 input measures litres added
|
||
→ valve closes when pool-level-sensor returns 1 (OK)
|
||
```
|
||
|
||
Filter pressure (`LRPR`) can be used to detect filtration start/stop and dirty filter (pressure rises above ~0.80 bar when filter needs backwash). Backwash reminder fires every ~15 days.
|
||
|
||
### 7.5 MyIndygo Feature Flags
|
||
|
||
Pool-specific flags returned by `GET /features` on `qualif.myindygo.com`:
|
||
|
||
| Flag | Meaning |
|
||
|---|---|
|
||
| `ocedisWaterChemistryCorrection` | OCEDIS chemical dosing correction |
|
||
| `enableBSTSubscriptions` | BST subscription management |
|
||
| `enablePolytropicAPI` | Variable-speed pump (Polytropic) integration |
|
||
| `enableGraphicalProgrammation` | Visual program schedule editor |
|
||
| `enableNdviSatellite` | NDVI satellite imagery |
|
||
| `enableEvapotranspiration` | ET-based automated watering |
|
||
| `clusterTurfOpened` | Turf cluster watering mode |
|
||
|
||
---
|
||
|
||
## 8. Device Reference — Domicile
|
||
|
||
### 8.1 LRMB10-070532 — LoRa Gateway
|
||
|
||
**ID**: `60ca4276ad69dac1558f7c61` | **Serial**: `1000000705320001`
|
||
All other modules communicate through this gateway via LoRa radio.
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Firmware | 6.5.0 |
|
||
| BLE | 1.0.13 |
|
||
| WiFi MAC | `10:52:1C:01:2D:F4` |
|
||
| BT MAC | `C8:B9:61:07:05:32` |
|
||
| WiFi SSID | `Freebox-2A5E5D` |
|
||
| Links | WiFi ✓, Bluetooth ✓, Ethernet ✗, Modem ✗ |
|
||
|
||
`remote/module/configuration` works directly (gateway is WiFi-connected, not LoRa-only).
|
||
|
||
### 8.2 LRPC-F1DA27 — Pool Controller
|
||
|
||
**ID**: `6481bcea8782b78554c02bed` | **Serial**: `150302F1DA270001` | **Type**: `lr-pc`
|
||
Controls filtration pump (index 0), pool light (index 1), and a third output (index 2, unused).
|
||
|
||
Pool config: 25 m³, rectangular 6×3 m depth 1.4 m, outdoor, glass filter, stabilised chlorine tablets, automatic pH regulation, manual chlorine regulation.
|
||
|
||
Programs: see section 7.2.
|
||
Alert history: `GET /maintenance-logs-data?moduleId=6481bcea8782b78554c02bed`
|
||
`remote/module/configuration` returns `module_not_reachable` — LoRa relay only.
|
||
|
||
### 8.3 LRPS-0565C0 — Pool Analyser
|
||
|
||
**ID**: `6480da07c2c0896da120ecca` | **Serial**: `9400030565C00001` | **Type**: `lr-mas`
|
||
Measures pool water temperature, pH, and ORP/redox. Battery at 60% — alerts fired Feb 2026. Calibration needed (flagged Mar 2026).
|
||
|
||
| Input | ID | Type | Interval |
|
||
|---|---|---|---|
|
||
| Water temperature | `6480da07c2c0896da120eccc` | 5 | ~30 min |
|
||
| pH | `6480da07c2c0896da120eccd` | 6 | varies |
|
||
| ORP/Redox | `6480da07c2c0896da120ecce` | 7 | varies |
|
||
|
||
No outputs — pure analyser.
|
||
|
||
### 8.4 LR6AG-070917 — Irrigation Valve Controller
|
||
|
||
**ID**: `626984f038ec558c598bb6d5` | **Serial**: `6606010709170001` | **Type**: `lr-ag`
|
||
6 irrigation stations, 1 rain sensor input. Battery: 81%. Relay: LRMB10.
|
||
|
||
All 6 programs reset on 2026-06-04 with no active windows.
|
||
Stations 0 and 1 currently have 255-day rain delay (suspended indefinitely).
|
||
|
||
| Station | Output ID | Index | Flow rate |
|
||
|---|---|---|---|
|
||
| Station 1 | `626984f038ec558c598bb6d8` | 0 | 30 m³/h |
|
||
| Station 2 | `626984f038ec558c598bb6d9` | 1 | 14 m³/h |
|
||
| Station 3 | `626984f038ec558c598bb6da` | 2 | 20 m³/h |
|
||
| Station 4 | `626984f038ec558c598bb6db` | 3 | 1 m³/h |
|
||
| Station 5 | `626984f038ec558c598bb6dc` | 4 | 38 L/h |
|
||
| Station 6 | `626984f038ec558c598bb6dd` | 5 | 38 L/h |
|
||
|
||
Rain sensor (input `626984f038ec558c598bb6d7`, type 2): 1 = rain, 0 = clear.
|
||
|
||
### 8.5 LRMS-0721DC — Sensor Station
|
||
|
||
**ID**: `60ca42adad69dac1558f7c64` | **Serial**: `7400040721DC0001` | **Type**: `lr-ms`
|
||
Pure sensor — no outputs, no programs. Battery: 78%. `relayIsLongPolling: true`.
|
||
|
||
| Input | ID | Type | Interval |
|
||
|---|---|---|---|
|
||
| Plumbago (soil moisture) | `60ca42adad69dac1558f7c66` | 12 | 60 min |
|
||
| Pelouse (soil moisture) | `60ca42adad69dac1558f7c67` | 12 | 60 min |
|
||
| Air/soil temperature | `60ca42adad69dac1558f7c69` | 5 | 15 min |
|
||
|
||
Moisture spikes (>10% jump within an hour) correlate with irrigation events — useful to infer watering history when `output/events` returns no records.
|
||
|
||
### 8.6 LR2IPECO-0C9282 — IP ECO Controller
|
||
|
||
**ID**: `6516ca57b7c098e6807cb332` | **Serial**: `3802020C92820001` | **Type**: `lr-ip-eco`
|
||
2 irrigation stations + pulse flow meter. Battery: 89% primary, secondary battery: 3200 mV. All 3 programs have no active windows.
|
||
|
||
| Input | ID | Type |
|
||
|---|---|---|
|
||
| Flow meter | `6516ca57b7c098e6807cb334` | 33 (flow, L) |
|
||
|
||
Flow meter alternates: values are cumulative per minute, pattern suggests short irrigation bursts.
|
||
`watering.state: 1` in remote state = currently active at last poll.
|
||
|
||
### 8.7 LRPR-0D6800 — Filter Pressure Sensor
|
||
|
||
**ID**: `6a17218be0b88ea3e925524f` | **childSerial6**: `0D6800` | **Type**: `lr-pr`
|
||
Pure pressure sensor. Battery: 94%. 1-minute reporting interval.
|
||
|
||
Input `6a17218be0b88ea3e9255251`, type 8. Expression: `(1.5 × raw − 500) / 1000` → bar.
|
||
|
||
Normal operating pressure: **0.70–0.75 bar** (pump running). Drop to ~0.08 bar = pump stopped.
|
||
Rising pressure above ~0.80 bar = dirty filter — backwash needed.
|
||
|
||
### 8.8 pool-level-sensor — Float Level Switch
|
||
|
||
**ID**: `6a174dd1e0b88ea3e925531c` | **Type**: `pool-level-sensor`
|
||
Binary float switch, only transmits on state change (~11 events per month). Battery: **41% — needs replacement soon**. Cleaning reminder fired 2026-05-28.
|
||
|
||
Input `6a174dd1e0b88ea3e925531e`, type 21. Raw passthrough: 0 = below threshold (fill needed), 1 = OK.
|
||
|
||
### 8.9 LRLEVEL-0D2ED9 — Pool Fill Valve Controller
|
||
|
||
**ID**: `6a1c4b153d6c33ab843edc13` | **Serial**: `3A01020D2ED90001` | **Type**: `lr-niv`
|
||
Controls pool fill valve automatically based on pool-level-sensor. Battery: 94%.
|
||
|
||
| Input | ID | Type | Notes |
|
||
|---|---|---|---|
|
||
| Fill volume | `6a1c4b153d6c33ab843edc15` | 33 | L added per fill cycle; polynomial expression |
|
||
| Level state | `6a1c4b153d6c33ab843edc16` | 149 | 1 = valve closed, 0 = open |
|
||
|
||
Output `6a1c4b153d6c33ab843edc17` (Station 1, index 0) — fill valve. No scheduled program; triggered by level sensor.
|
||
|
||
To query fill history: `GET /remote/input/getComputedSensorData?inputId=6a1c4b153d6c33ab843edc15` over wide date range, filter ticks where `value > 0`.
|
||
|
||
---
|
||
|
||
## 9. Write Endpoints Reference
|
||
|
||
All mutation endpoints grouped by risk. All require session cookie unless noted.
|
||
|
||
### 9.1 Device Control (high impact — sends LoRa radio commands)
|
||
|
||
```
|
||
POST /remote/module/control pool controller linesControl (see section 6.1)
|
||
POST /module/sendManualModuleCommand irrigation valve commands (see section 6.2)
|
||
POST /canopy/{siteId}/sendModulesCommand flush commands to device
|
||
POST /modules/sendDataViaLoRaWAN
|
||
POST /agriculturalModuleGroup/sendDataViaLoRaWAN
|
||
POST /wateringModuleGroup/sendDataViaLoRaWAN
|
||
```
|
||
|
||
### 9.2 Programs (persistent — survives device reboot)
|
||
|
||
```
|
||
PUT /programs {"programs": [...], "module": "moduleId"}
|
||
POST /programs create new program
|
||
DELETE /programs
|
||
PATCH /programs/name
|
||
POST /program/update socket.io variant (immediate push to device)
|
||
PUT /program/update REST variant
|
||
POST /checkAvailableWindows check time conflicts before saving
|
||
PUT /api/updatePrograms mobile API variant
|
||
```
|
||
|
||
### 9.3 Module Configuration
|
||
|
||
```
|
||
POST /remote/module/configuration
|
||
POST /remote/module/configuration/and/programs
|
||
POST /input/update update input settings (name, thresholds, alerts)
|
||
PUT /output/update update output settings (name, flow rate)
|
||
GET /remote/module/name?moduleId=...&name=... rename via LoRa
|
||
PUT /module/update
|
||
PUT /module/update/address
|
||
PUT /module/update/status
|
||
POST /module/update/name
|
||
POST /module/update/securityMode
|
||
POST /remote/module/advertisingProfile
|
||
POST /remote/module/dissociate {"relayMsn": "...", "modulesMsn": ["..."]}
|
||
PUT /module/dissociate/{moduleId}
|
||
```
|
||
|
||
### 9.4 Site / Pool Management
|
||
|
||
```
|
||
POST /canopy-sites
|
||
PUT /pools/update/address
|
||
POST /pools/admin/sendMessage
|
||
POST /wateringModuleGroup/create
|
||
PUT /wateringModuleGroup/update
|
||
DELETE /wateringModuleGroup/delete
|
||
POST /addAgriculturalGroupToUser
|
||
PUT /teams/handleAccessGrantedPools
|
||
POST /teams/members/add
|
||
```
|
||
|
||
### 9.5 Journal / Notifications
|
||
|
||
```
|
||
PUT /journal/acknowledge/all
|
||
PUT /journal/acknowledge/records
|
||
POST /canopy-sites/{siteId}/notifications/acknowledge-all
|
||
POST /journal/warnings/accepted
|
||
```
|
||
|
||
### 9.6 Socket.io Write Calls
|
||
|
||
These bypass the normal HTTP layer — not captured by Safari recordings:
|
||
|
||
```
|
||
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
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Reference Tables
|
||
|
||
### Module Types
|
||
|
||
| Type string | Device name | Description |
|
||
|---|---|---|
|
||
| `lr-mb-10` | LRMB10 | LoRa gateway v10 |
|
||
| `lr-mb-30` | LRMB30 | LoRa gateway v30 |
|
||
| `lr-mb-100` | LRMB100 | LoRa gateway v100 |
|
||
| `lr-ms` | LRMS | LoRa sensor station |
|
||
| `lr-ms-eco` | LRMS_ECO | Eco sensor station |
|
||
| `lr-ms-v2` | LRMS v2 | Sensor station v2 |
|
||
| `lr-ms-rs485` | LRMSRS485 | RS485 sensor station |
|
||
| `lr-ag` | LR6AG | LoRa agricultural valve (6 stations) |
|
||
| `lr-mas` | LRPS | LoRa master valve / pool analyser |
|
||
| `lr-pc` | LRPC | LoRa pool controller |
|
||
| `lr-pc-vs` | LRPCVS | Pool controller with variable-speed pump |
|
||
| `lr-ip-eco` | LR2IPECO | LoRa IP ECO (2-station + flow meter) |
|
||
| `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-niv` | LRLEVEL | LoRa level controller (fill valve) |
|
||
| `lr-pr` | LRPR | LoRa pressure sensor |
|
||
| `lr-mv` | LRMV/LRPM1 | LoRa motor valve |
|
||
| `lr-ol` | LROL4 | LoRa OL (lighting control) |
|
||
| `lr-bst-100` | LRBST100 | BST 100 booster |
|
||
| `lr-bst-react-4g` | LR-BST R4G | BST 4G reactive booster |
|
||
| `pool-level-sensor` | POOL-LVL | Pool float level switch |
|
||
| `wf-mb` | WiFi gateway | WiFi gateway |
|
||
| `wf-is` / `bl-is` | WiFi/BT sensor | WiFi or Bluetooth sensor |
|
||
| `bl-ip` / `bl-ip-v2` | BL6IP | Bluetooth IP |
|
||
| `wf-ol` | WiFi OL | WiFi online |
|
||
| `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 |
|
||
| `ipx` | GEN_xxxx | IPX controller |
|
||
|
||
### Input Type Codes
|
||
|
||
| Code | Meaning | Unit | Notes |
|
||
|---|---|---|---|
|
||
| `2` | TOR (on/off) | — | Rain sensor: 1=rain, 0=clear |
|
||
| `5` | Air/soil temperature | °C | LRMS ambient sensor |
|
||
| `6` | pH | — | Raw ÷ 100 = pH units |
|
||
| `7` | ORP / Redox | mV | Raw = mV |
|
||
| `8` | Pressure | bar | Expression: `(1.5×raw−500)/1000` |
|
||
| `12` | Soil moisture | % (unit `10`) | LRMS capacitive probe |
|
||
| `20` | Air moisture | — | |
|
||
| `21` | Pool level TOR switch | 0/1 | 0=low/fill needed, 1=OK |
|
||
| `33` | Volumetric / flow | L (unit `1`) | Cumulative, pulse-based |
|
||
| `133` | Water temperature | °C (unit `2`) | LRPC/LRPS probe |
|
||
| `149` | Fill valve / level state | 0/1 | LRLEVEL: 1=closed |
|
||
|
||
### linesControl Action Codes (LRPC)
|
||
|
||
| action | meaning | params | verified |
|
||
|---|---|---|---|
|
||
| `0` | Stop pump (index 0 only) | — | Yes (LRLEVEL) |
|
||
| `1` | Stop / cancel any running command | — | Yes (app capture) |
|
||
| `2` | Timed impulse | `time: "MM:SS"` | Yes (app capture + live) |
|
||
| `3` | Boost filtration | `time: "MM:SS"` | Yes (Safari capture) |
|
||
| `4` | Start indefinitely | — | Yes (notebook pattern) |
|
||
| `5` | Pause N days | `manualDuration: N` (days) | Yes (app capture) |
|
||
| `11` | Backwash cycle | `time`, `speed` | Partial (JS analysis) |
|
||
|
||
---
|
||
|
||
## 11. Security Findings
|
||
|
||
Assessment from black-box analysis. All findings verified on `qualif.mysolem.com`. Reported for awareness; not exploited beyond authorized testing scope.
|
||
|
||
### Critical
|
||
|
||
| # | Endpoint | Finding |
|
||
|---|---|---|
|
||
| 1 | `POST /modules/search` | Empty query returns **4,607 modules** with name, serial, MAC, firmware, GPS — no ownership enforcement (BOLA) |
|
||
| 2 | `POST /users/search` | Returns full user documents including **password hashes** (unsalted SHA-256), **active reset tokens** (direct account takeover), APNs/FCM push tokens — BOLA |
|
||
| 3 | `POST /outputs/search` + `POST /programs/search` | Empty query returns 12,034 outputs and 6,073 programs platform-wide — no ownership enforcement |
|
||
| 4 | `GET /remote/module/configuration` | Returns WiFi SSID, MACs, firmware for **any module ID** — no ownership check |
|
||
| 5 | `GET /remote/input/getComputedSensorData` | Any `inputId` accepted — full sensor history exfiltration across tenants |
|
||
| 6 | `POST /module/sendManualModuleCommand` | Takes `moduleSerialNumber` (obtainable from #1) — likely no ownership check, unconfirmed |
|
||
|
||
### High
|
||
|
||
| # | Finding |
|
||
|---|---|
|
||
| 7 | Google Maps API key `AIzaSyDh8fCC9vTEuN9eKewxgqWXGP9ENb5an1c` hard-coded in HTML, no referrer restriction |
|
||
| 8 | Session cookie missing `SameSite` attribute → CSRF possible on state-changing endpoints |
|
||
| 9 | No rate limiting on `POST /login` — credential stuffing uninhibited |
|
||
| 10 | MongoDB ObjectIds are sequentially guessable (timestamp + machine + counter), accelerating enumeration |
|
||
|
||
### Medium / Low
|
||
|
||
| # | Finding |
|
||
|---|---|
|
||
| 11 | `Access-Control-Allow-Origin: ` (empty string) — CORS misconfiguration |
|
||
| 12 | No `Content-Security-Policy` header on React frontend |
|
||
| 13 | `X-Powered-By: Sails <sailsjs.org>` on every response — unnecessary fingerprinting |
|
||
| 14 | Qualification environment appears to use live production data (real GPS, real addresses, live telemetry) |
|
||
| 15 | Input IDs embedded as `data-input-id` HTML attributes — completes enumeration chain without guessing |
|