Files
myprimal/api-docs/REFERENCE.md
T
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

1367 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:3022: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: 2028°C |
| pH | `6480da07c2c0896da120eccd` | pH (raw ÷ 100) | Target: 7.27.6 |
| ORP/Redox | `6480da07c2c0896da120ecce` | mV (raw) | Healthy: 650750 mV |
| Filter pressure | `6a17218be0b88ea3e9255251` | bar: `(1.5×raw500)/1000` | Running: ~0.700.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.700.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×raw500)/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 |