feat: initial myprimal MQTT bridge service
Dynamic MySolem/MyIndygo IoT bridge with MQTT, Home Assistant autodiscovery, and Homebridge EasyMQTT compatibility. - Device discovery via MySolem API on startup (all modules/inputs/outputs) - Configurable poll engine (fast/normal/slow buckets, per-input overrides) - MQTT retained state publishing + HA autodiscovery payloads - Unified manual command routing (linesControl vs sendManualModuleCommand) - Runtime config via MQTT $config/set, persisted to config.json - Expression override system (API-provided > hardcoded > passthrough) - Minimal REST API: /health, /state, /control, /rediscover - Docker deployment (Dockerfile + docker-compose.yml) - 38 unit tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,258 @@
|
||||
# MySolem / MyIndygo API — Device Control
|
||||
|
||||
All commands work on both `https://qualif.mysolem.com` and `https://qualif.myindygo.com`. Requires session cookie.
|
||||
|
||||
---
|
||||
|
||||
## Manual irrigation command
|
||||
|
||||
```
|
||||
POST /module/sendManualModuleCommand
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
Payloads are **form-encoded**. All patterns confirmed via mitmproxy captures. A JSON body variant (same fields) also works — see the example at the end of this section.
|
||||
|
||||
Note: the `command` field is not an action name — it accepts an **output ID string**, `"allStations"`, or a **program ID**.
|
||||
|
||||
### Status commands (enable / disable module)
|
||||
|
||||
```
|
||||
commandType=status&command=on&commandParams=0 → enable (no rain delay)
|
||||
commandType=status&command=off&commandParams=0 → disable
|
||||
commandType=status&command=off&commandParams=1 → 1-day rain delay
|
||||
commandType=status&command=off&commandParams=255 → disable with max rain delay
|
||||
```
|
||||
|
||||
### Station commands
|
||||
|
||||
Run a specific station for a duration:
|
||||
```
|
||||
commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=1
|
||||
```
|
||||
|
||||
Cycling station (on/off intervals + optional agricultural module):
|
||||
```
|
||||
commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0
|
||||
&commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId}
|
||||
```
|
||||
|
||||
Stop a specific station:
|
||||
```
|
||||
commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId}
|
||||
```
|
||||
|
||||
Run all stations for a duration:
|
||||
```
|
||||
commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=2
|
||||
```
|
||||
|
||||
Stop everything immediately (no outputIndex, no commandParams):
|
||||
```
|
||||
commandType=manualStop
|
||||
```
|
||||
|
||||
### Program command (NEW)
|
||||
|
||||
Send a specific program to the module:
|
||||
```
|
||||
commandType=program&command={programId}
|
||||
```
|
||||
|
||||
### JSON body equivalent (original documented form)
|
||||
|
||||
```json
|
||||
{
|
||||
"moduleSerialNumber": "6606010709170001",
|
||||
"commandType": "station",
|
||||
"outputIndex": 0,
|
||||
"command": null,
|
||||
"commandParams": {
|
||||
"hours": 0,
|
||||
"minutes": 15,
|
||||
"on": 0,
|
||||
"off": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `outputIndex`: 0-based index of the output/station
|
||||
- `hours` + `minutes`: duration to water
|
||||
- `on` / `off`: cycle timing (0 = continuous)
|
||||
- `agriculturalModuleId`: required when stopping or cycling a specific station
|
||||
|
||||
---
|
||||
|
||||
## Flush pending commands to device
|
||||
|
||||
Triggers the platform to push all queued commands to a site's modules:
|
||||
|
||||
```
|
||||
POST /canopy/{siteId}/sendModulesCommand
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
Response: `202 Accepted` — `{ "message": "Modules command accepted for processing" }`
|
||||
|
||||
---
|
||||
|
||||
## Remote module rename
|
||||
|
||||
```
|
||||
GET /remote/module/name?moduleId={moduleId}&name={newName}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dissociate module from relay
|
||||
|
||||
```
|
||||
POST /remote/module/dissociate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"relayMsn": "relaySerialNumber",
|
||||
"modulesMsn": ["childSerialNumber"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update module metadata
|
||||
|
||||
```
|
||||
PUT /module/update
|
||||
Content-Type: application/json
|
||||
|
||||
{ "module": { "id": "moduleId", "name": "New Name", ... } }
|
||||
```
|
||||
|
||||
```
|
||||
PUT /module/update/address
|
||||
PUT /module/update/status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Remove module
|
||||
|
||||
```
|
||||
DELETE /module/{moduleId}
|
||||
GET /module/remove/from/my/modules/{moduleId}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Program update (socket.io legacy)
|
||||
|
||||
```javascript
|
||||
io.socket.post("/program/update", {
|
||||
programs: [...],
|
||||
module: moduleId,
|
||||
programmationtype: "temporal"
|
||||
}, callback)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pool Controller Control (LRPC / lr-pc) — MyIndygo
|
||||
|
||||
Pool controllers use a **different control endpoint** with a `linesControl` array structure.
|
||||
|
||||
```
|
||||
POST /remote/module/control
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
Payload (JSON body — `Content-Type: application/json`):
|
||||
```json
|
||||
{
|
||||
"moduleSerialNumber": "150302F1DA270001",
|
||||
"linesControl": [
|
||||
{
|
||||
"index": 0,
|
||||
"action": 2,
|
||||
"time": "60:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Action codes (verified from app mitmproxy captures + live testing):**
|
||||
|
||||
| action | meaning | extra params | notes |
|
||||
|---|---|---|---|
|
||||
| `0` | Stop pump (index 0 only) | — | Returns `erreurtraitement: 6` on other outputs |
|
||||
| `1` | **Stop / cancel** any running command | — | Works on all outputs; cancels both timed and indefinite runs |
|
||||
| `2` | Timed impulse | `time`: `"MM:SS"` string | Also used to override a running timer early |
|
||||
| `3` | Boost filtration | `time`: `"MM:SS"` | Response: `origin:3, info:["boost"]` |
|
||||
| `4` | Start indefinitely | — | Runs until action 1 is sent; common in notebook for evening light use |
|
||||
| `5` | Pause N days | `manualDuration`: N (integer days) | Disables output including scheduled programs; response shows `"pause": N` = days remaining |
|
||||
| `11` | Backwash cycle | `time`: encoded duration, `speed`: pump speed level | |
|
||||
|
||||
**To stop a timed impulse early**: send `action 1` — cancels any running manual command.
|
||||
**To stop a timed output (alternative)**: override with `action 2, time: "00:01"`.
|
||||
|
||||
**Pause semantics**: action 5 suspends the output entirely (manual commands and scheduled programs) for N days. The response pool state includes `"pause": N` showing days remaining. `manualDuration` here is **days**, not seconds.
|
||||
|
||||
**Response timing**: the pool state in the response always reflects the **pre-command device state** — it shows what the device reported before processing the new command. The command takes effect on the next LoRa downlink window.
|
||||
|
||||
Duration format: `"MM:SS"` string, e.g. `"30:00"` = 30 minutes. Verified from real capture.
|
||||
|
||||
**Examples:**
|
||||
|
||||
Stop fill valve (LRLEVEL):
|
||||
```json
|
||||
{"moduleSerialNumber": "3A01020D2ED90001", "linesControl": [{"action": 0, "index": 0}]}
|
||||
```
|
||||
|
||||
Boost filtration 1 min (LRPC):
|
||||
```json
|
||||
{"moduleSerialNumber": "150302F1DA270001", "linesControl": [{"action": 3, "time": "01:00", "index": 0}]}
|
||||
```
|
||||
|
||||
**Response** — full real-time device state, e.g. for LRPC after boost:
|
||||
```json
|
||||
{
|
||||
"dialogTimeStamp": "...",
|
||||
"ackTimestamp": 1780609493,
|
||||
"temperature": 20,
|
||||
"pool": [
|
||||
{"index": 0, "origin": 3, "time": "01:00", "value": 1, "info": ["boost"]},
|
||||
{"index": 1, "time": "00:00", "tempRef": 7, "value": 0}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Python example — timed pool pump impulse (30 min on Station 2):**
|
||||
```python
|
||||
import json
|
||||
s.post(f"{BASE}/remote/module/control",
|
||||
json={
|
||||
"moduleSerialNumber": "150302F1DA270001",
|
||||
"linesControl": [{"index": 1, "action": 2, "time": "30:00"}]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LRPC output IDs (Domicile)
|
||||
|
||||
| Index | Name | Output ID |
|
||||
|---|---|---|
|
||||
| 0 | Station 1 | `6481bcea8782b78554c02bf1` |
|
||||
| 1 | Station 2 | `6481bcea8782b78554c02bf2` |
|
||||
| 2 | Station 3 | `6481bcea8782b78554c02bf3` |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Commands go through the platform's relay infrastructure — the device doesn't need to be directly reachable
|
||||
- Use `POST /canopy/{siteId}/sendModulesCommand` after updating programs via `PUT /programs` to force an immediate push
|
||||
- `moduleSerialNumber` is the `serialNumber` field on the module object (not the `id`)
|
||||
- For irrigation valves (lr-ag, lr-ip-eco): use `POST /module/sendManualModuleCommand`
|
||||
- For pool controllers (lr-pc, lr-pc-vs): use `POST /remote/module/control` with `linesControl`
|
||||
@@ -0,0 +1,485 @@
|
||||
# MySolem API — Endpoints
|
||||
|
||||
All paths relative to `https://qualif.mysolem.com`. All require session cookie unless noted.
|
||||
|
||||
> **Mobile API**: the iOS apps use a separate `/api/` prefix layer with OAuth2 Bearer auth. See `api-docs/mobile-api.md`.
|
||||
|
||||
---
|
||||
|
||||
## Feature flags
|
||||
|
||||
```
|
||||
GET /features
|
||||
```
|
||||
Returns flat JSON object of boolean flags (`dashboard`, `enableHydraulicSupervision`, …).
|
||||
|
||||
---
|
||||
|
||||
## 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/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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modules
|
||||
|
||||
**Search / list:**
|
||||
```
|
||||
POST /modules/search
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"query": { "id": { "$in": ["moduleId1", "moduleId2"] } },
|
||||
"projection": { "name": 1, "type": 1, "serialNumber": 1 }
|
||||
}
|
||||
```
|
||||
Response: `{ result: { results: [...], totalCount: null } }`
|
||||
|
||||
```
|
||||
GET /modules/available?userId={userId}
|
||||
```
|
||||
|
||||
**Paginated modules for a site:**
|
||||
```
|
||||
GET /canopy-sites/{siteId}/modules?limit=20&skip=0&includeTotalCount=true
|
||||
```
|
||||
Response: `{items: [...], total: N}`. Note: items use `_id` (MongoDB style), not `id`.
|
||||
|
||||
**Dashboard chart data:**
|
||||
```
|
||||
GET /modules/echarts-data?includeCustomers=true&includeAgency=false
|
||||
→ {chartModuleTypes: [{name, value}], chartModuleConnectivities: [...]}
|
||||
```
|
||||
|
||||
**SIM card payment check:**
|
||||
```
|
||||
GET /sim-cards/needsPayment?moduleIds={id1}&moduleIds={id2}
|
||||
→ {result: {"moduleId": false, ...}}
|
||||
```
|
||||
|
||||
**Remote state:**
|
||||
```
|
||||
GET /remote/module/state?moduleId={moduleId}
|
||||
GET /remote/module/configuration?moduleId={moduleId}
|
||||
```
|
||||
|
||||
**Module data model (key fields):**
|
||||
```json
|
||||
{
|
||||
"id": "...",
|
||||
"name": "...",
|
||||
"type": "lr-ag",
|
||||
"serialNumber": "6606010709170001",
|
||||
"macAddress": "C8:B9:61:...",
|
||||
"softwareVersion": "7.2.0",
|
||||
"batteryVoltage": 81,
|
||||
"battery": 5,
|
||||
"rssi": 1,
|
||||
"isOnline": true,
|
||||
"relay": "relayModuleId",
|
||||
"latitude": 43.79417,
|
||||
"longitude": 4.09520,
|
||||
"lastRadioCommunication": "2026-06-04T17:59:29.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sensor / Input data
|
||||
|
||||
**Batch input config (most efficient way to get all input metadata):**
|
||||
```
|
||||
GET /inputs?inputId=id1%3Bid2%3Bid3
|
||||
```
|
||||
Semicolons (`%3B`) separate multiple IDs. Returns array of full input objects with `expression`, `type`, `unit`, `module`, `index`. Much lighter than querying each individually.
|
||||
|
||||
> **Note**: sensor data can be queried per-input or per-module (all inputs at once).
|
||||
|
||||
**All inputs for a module — preferred for dashboard-style loads:**
|
||||
```
|
||||
GET /remote/module/getComputedSensorsData
|
||||
?moduleId={moduleId}
|
||||
&startDate={iso8601}
|
||||
&endDate={iso8601}
|
||||
&forceRawOrComputed=computed
|
||||
```
|
||||
Returns an **array** of input objects, each with a `computedSensorData` field containing ticks.
|
||||
More efficient than querying each input individually. Pass `forceRawOrComputed=raw` for raw ADC values.
|
||||
|
||||
**Single input telemetry:**
|
||||
```
|
||||
GET /remote/input/getComputedSensorData
|
||||
?inputId={inputId}
|
||||
&startDate={iso8601}
|
||||
&endDate={iso8601}
|
||||
&ensurePrevTickForContinuity=true
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "60ca42adad69dac1558f7c66",
|
||||
"name": "Plumbago",
|
||||
"module": "60ca42adad69dac1558f7c64",
|
||||
"type": 12,
|
||||
"unit": 10,
|
||||
"interval": 60,
|
||||
"typeIsMoistureSensor": true,
|
||||
"computedSensorData": [
|
||||
{ "tickTimestamp": "2026-06-01T00:42:00.000Z", "value": 54.1875 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Last tick only (no date range — fastest for live dashboards):**
|
||||
```
|
||||
GET /input/getLastTickFromInput?inputId={inputId}
|
||||
```
|
||||
Returns `{ "tick": { "input": "...", "value": <raw>, "timestamp": "...", "id": "..." } }`.
|
||||
Value is **raw** (before expression). Divide by 100 for pH/temp from LRPS.
|
||||
|
||||
**Averages:**
|
||||
```
|
||||
GET /inputs/average?inputIds={id1},{id2}&startDate={iso8601}&endDate={iso8601}
|
||||
```
|
||||
|
||||
**Update input config:**
|
||||
```
|
||||
POST /input/update
|
||||
```
|
||||
|
||||
**CSV export:**
|
||||
```
|
||||
GET /input/getComputedInputIpxDataBetweenDatesAsCsv
|
||||
?inputId={inputId}&startDate={iso8601}&endDate={iso8601}
|
||||
```
|
||||
|
||||
**Legacy socket.io call** (same route, used from browser JS):
|
||||
```
|
||||
socket.request({ method: "GET", url: "/remote/module/getComputedSensorsData",
|
||||
data: { moduleId, startDate, endDate } }, callback)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Outputs
|
||||
|
||||
```
|
||||
GET /outputs/modules?moduleIds={moduleId}
|
||||
POST /outputs/search { "query": { "module": { "$in": ["moduleId"] } } }
|
||||
POST /output/update
|
||||
```
|
||||
Response: `{ outputs: [{ id, module, index, name, flowRate: { value, unit } }] }`
|
||||
Flow rate unit: `1` = L/h, `2` = m³/h.
|
||||
|
||||
**Output event history:**
|
||||
```
|
||||
GET /output/events?moduleId={moduleId}
|
||||
GET /output/events?outputId={outputId}&startDate={iso8601}&endDate={iso8601}
|
||||
```
|
||||
|
||||
**Watering forecast:**
|
||||
```
|
||||
GET /output/forecast?moduleId={moduleId}
|
||||
GET /output/forecast?outputId={outputId}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Programs
|
||||
|
||||
```
|
||||
GET /programs/modules?moduleIds={moduleId}
|
||||
PUT /programs
|
||||
{ "programs": [...], "module": "moduleId" }
|
||||
GET /programs/reset?moduleId={moduleId}
|
||||
POST /programs/search
|
||||
{ "query": { "id": { "$in": [...] } } }
|
||||
```
|
||||
|
||||
Program object:
|
||||
```json
|
||||
{
|
||||
"id": "...",
|
||||
"name": "Pelouse Terrasse",
|
||||
"module": "...",
|
||||
"index": 0,
|
||||
"programmingType": "temporal",
|
||||
"weekDays": 127,
|
||||
"waterBudget": 100,
|
||||
"windows": [{ "start": 420, "end": 422, "on": 120, "off": 0 }]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Daily watering
|
||||
|
||||
```
|
||||
GET /garden/dailyData/{moduleId}
|
||||
GET /modules/heatmap-data?moduleIds={ids}&startDate=X&endDate=Y
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Users
|
||||
|
||||
```
|
||||
GET /users-info/{userId}?includeUserGroups=true
|
||||
GET /users/{userId}/modules
|
||||
GET /users/{userId}/sites
|
||||
GET /users/{userId}/module-groups
|
||||
GET /users/{userId}/subscriptionModules
|
||||
POST /users/search
|
||||
{ "query": {...}, "projection": {...} }
|
||||
```
|
||||
|
||||
**Flexible module list (POST — supports field projection + filtering):**
|
||||
```
|
||||
POST /users/{userId}/modules
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"fields": {"name": 1, "type": 1, "serialNumber": 1, "displayType": 1},
|
||||
"filters": {},
|
||||
"addOwnershipFlag": true,
|
||||
"addOwnerIds": false,
|
||||
"includeCustomers": true,
|
||||
"includeAgency": false,
|
||||
"includeTotalCount": true,
|
||||
"limit": 20,
|
||||
"skip": 0
|
||||
}
|
||||
```
|
||||
Response: `{modules: [...], totalCount: N}`.
|
||||
Supported filter fields: `search`, `tagIds`, `displayTypes`, `connectivities`, `powerTypes`.
|
||||
|
||||
**Similar pattern for grouped resources:**
|
||||
```
|
||||
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} (associated users)
|
||||
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,
|
||||
"includeCustomers": true,
|
||||
"sortColumn": "timestamp", "sortOrder": -1,
|
||||
"fields": {"category": 1, "label": 1, "module": 1, "timestamp": 1}
|
||||
}
|
||||
→ {records: [...], totalCount: N}
|
||||
```
|
||||
|
||||
**Activity log categories:**
|
||||
```
|
||||
POST /users/{userId}/records/categories
|
||||
{"filters": {"startDate": "2026-05-28T21:51:48.266Z"}, "includeCustomers": true}
|
||||
→ [{category, displayCategory}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notifications / journal
|
||||
|
||||
```
|
||||
GET /journal/notifications
|
||||
GET /journal/counts
|
||||
GET /journal/warnings/accepted
|
||||
POST /journal/acknowledge/all
|
||||
POST /canopy-sites/{siteId}/notifications/acknowledge-all
|
||||
```
|
||||
|
||||
**Notifications (site-level, POST — supports filtering):**
|
||||
```
|
||||
POST /canopy-sites/{siteId}/notifications
|
||||
{"limit": 1, "includeTotalCount": true, "filters": {"state": {"$in": ["unread", "process"]}}}
|
||||
→ {records: [...]}
|
||||
```
|
||||
|
||||
**Journal notifications (cross-site, filtered):**
|
||||
```
|
||||
POST /journal/notifications
|
||||
{
|
||||
"canopyIds": ["siteId"],
|
||||
"limit": 100, "skip": 0,
|
||||
"filters": {"priority": {"$in": ["critical", "high"]}, "state": {"$in": ["unread", "process"]}},
|
||||
"projection": {...}
|
||||
}
|
||||
→ {records: [...], total: N}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weather
|
||||
|
||||
```
|
||||
GET /moduleGroups/getCanopyWeather
|
||||
?location[latitude]={lat}&location[longitude]={lng}
|
||||
```
|
||||
Returns 3-day AccuWeather-style forecast.
|
||||
|
||||
---
|
||||
|
||||
## Clusters (watering module groups)
|
||||
|
||||
```
|
||||
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}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Favorites & tags
|
||||
|
||||
```
|
||||
GET /favorites/{userId}
|
||||
POST /favorites
|
||||
DELETE /favorites/{userId}
|
||||
GET /modules/{moduleId}/tags
|
||||
POST /module/tags { "module": "moduleId", "tags": [...] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module notebook (event log)
|
||||
|
||||
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.
|
||||
|
||||
Record structure:
|
||||
```json
|
||||
{
|
||||
"id": "...",
|
||||
"module": "moduleId",
|
||||
"category": "manual_command",
|
||||
"label": "pHmètre - Seuil bas dépassé",
|
||||
"businessLabel": "Le pH est trop bas",
|
||||
"priority": "low",
|
||||
"state": "archived",
|
||||
"timestamp": "2026-06-04T21:00:00.000Z",
|
||||
"params": { "action": 4, "index": 1 },
|
||||
"input": "inputId",
|
||||
"notifications": [],
|
||||
"comments": []
|
||||
}
|
||||
```
|
||||
|
||||
Known `category` values: `manual_command`, `pool`, `platform_alert`, `firmware_update`
|
||||
|
||||
The `params` field structure varies by category: for `manual_command` it mirrors the linesControl action/index that triggered the event; for `pool` it contains poolAlertType (e.g. `backwashReminder`, `poolLevelSensorCleaningReminder`). See `api-docs/control.md` for action code definitions.
|
||||
|
||||
---
|
||||
|
||||
## Module users
|
||||
|
||||
```
|
||||
GET /modules/{moduleId}/users
|
||||
```
|
||||
Returns array of full user objects with access to this module (email, push tokens, preferences).
|
||||
|
||||
---
|
||||
|
||||
## Linked inputs
|
||||
|
||||
```
|
||||
GET /input/getInputsLinkedModule/{moduleId}
|
||||
```
|
||||
Returns inputs linked to a module. Returns `[]` for standalone LoRa modules.
|
||||
|
||||
---
|
||||
|
||||
## User avatar
|
||||
|
||||
```
|
||||
GET /user/avatar
|
||||
GET /user/avatar?t={unix_ms} ← cache-bust variant
|
||||
```
|
||||
Returns current authenticated user's avatar as `image/png`. Never cached (`no-cache, no-store`).
|
||||
|
||||
---
|
||||
|
||||
## SIM subscription
|
||||
|
||||
```
|
||||
GET /users/{userId}/checkSubscriptionSimcardStatus
|
||||
```
|
||||
Returns ~39-byte JSON — whether the user has an active cellular SIM subscription.
|
||||
|
||||
---
|
||||
|
||||
## Field supervision (watering events over time range)
|
||||
|
||||
```
|
||||
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`
|
||||
|
||||
---
|
||||
|
||||
## Satellite / NDVI imagery
|
||||
|
||||
Host: **`sentinel.mysolem.com`** (separate from the main API host).
|
||||
|
||||
**Check image availability:**
|
||||
```
|
||||
GET /moduleGroups/{siteId}/fencing/{timestampMs}/images-check
|
||||
→ {availableImages: {available: true, reason: "ok"}}
|
||||
```
|
||||
|
||||
**List images:**
|
||||
```
|
||||
GET /moduleGroups/{siteId}/fencing/{timestampMs}/images
|
||||
→ {imagesList: {images: [{name, urlRgb, urlSavi}]}}
|
||||
```
|
||||
|
||||
**Fetch satellite images directly:**
|
||||
```
|
||||
GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/savi → PNG (SAVI 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`
|
||||
@@ -0,0 +1,101 @@
|
||||
# LR6AG — Agricultural Valve Controller (LR6AG-070917)
|
||||
|
||||
**Module ID**: `626984f038ec558c598bb6d5`
|
||||
**Serial**: `6606010709170001`
|
||||
**Type**: `lr-ag`
|
||||
**Role**: Controls 6 irrigation stations, has 1 rain sensor input
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
| Input ID | Name | Type | Unit | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `626984f038ec558c598bb6d7` | *(unnamed)* | 2 (TOR/on-off) | raw | Rain sensor — value 1=rain, 0=clear |
|
||||
|
||||
### Rain sensor — recent state
|
||||
```
|
||||
2026-06-04T15:37 value=1 (rain detected)
|
||||
2026-06-03T10:57 value=0 (clear)
|
||||
2026-06-02T12:19 value=1 (rain detected)
|
||||
2026-05-28T19:06 value=0 (clear)
|
||||
2026-05-28T19:05 value=0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output ID | Name | Index | Flow rate |
|
||||
|---|---|---|---|
|
||||
| `626984f038ec558c598bb6d8` | Station 1 | 0 | 30 m³/h |
|
||||
| `626984f038ec558c598bb6d9` | Station 2 | 1 | 14 m³/h |
|
||||
| `626984f038ec558c598bb6da` | Station 3 | 2 | 20 m³/h |
|
||||
| `626984f038ec558c598bb6db` | Station 4 | 3 | 1 m³/h |
|
||||
| `626984f038ec558c598bb6dc` | Station 5 | 4 | 38 L/h |
|
||||
| `626984f038ec558c598bb6dd` | Station 6 | 5 | 38 L/h |
|
||||
|
||||
Flow rate unit: `1` = L/h, `2` = m³/h
|
||||
|
||||
---
|
||||
|
||||
## Programs
|
||||
|
||||
6 programs (one per station), all **reset on 2026-06-04 with no active windows**.
|
||||
Programs are named Station 1–6 (type 3 = standard irrigation).
|
||||
|
||||
```
|
||||
GET /programs/modules?moduleIds=626984f038ec558c598bb6d5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Remote state
|
||||
|
||||
```
|
||||
GET /remote/module/state?moduleId=626984f038ec558c598bb6d5
|
||||
```
|
||||
|
||||
- Battery: 81%
|
||||
- Last radio contact: 2026-06-04T19:57:05Z
|
||||
- Relay: `60ca4276ad69dac1558f7c61` (LRMB10)
|
||||
|
||||
**Live output status** (from `state.ag.outputs`):
|
||||
| Station | State | Rain delay |
|
||||
|---|---|---|
|
||||
| 0 | off | 255 days |
|
||||
| 1 | off | 255 days |
|
||||
| 2 | off | 0 |
|
||||
| 3 | off | 0 |
|
||||
| 4 | off | 0 |
|
||||
| 5 | off | 0 |
|
||||
|
||||
Stations 0 and 1 have a rain delay of 255 — effectively suspended indefinitely.
|
||||
`state` field: `0` = off, `1` = on/running.
|
||||
|
||||
---
|
||||
|
||||
## Manual control
|
||||
|
||||
```
|
||||
POST /module/sendManualModuleCommand
|
||||
{
|
||||
"moduleSerialNumber": "6606010709170001",
|
||||
"commandType": "station",
|
||||
"outputIndex": 0,
|
||||
"command": null,
|
||||
"commandParams": { "hours": 0, "minutes": 15, "on": 0, "off": 0 }
|
||||
}
|
||||
```
|
||||
|
||||
Rain delay (suspend station):
|
||||
```json
|
||||
{
|
||||
"moduleSerialNumber": "6606010709170001",
|
||||
"commandType": "status",
|
||||
"command": "off",
|
||||
"commandParams": 7,
|
||||
"outputIndex": 0
|
||||
}
|
||||
```
|
||||
`commandParams` = number of days to delay (255 = maximum/indefinite).
|
||||
@@ -0,0 +1,79 @@
|
||||
# LR2IPECO — IP ECO Controller (LR2IPECO-0C9282)
|
||||
|
||||
**Module ID**: `6516ca57b7c098e6807cb332`
|
||||
**Serial**: `3802020C92820001`
|
||||
**Type**: `lr-ip-eco`
|
||||
**Role**: 2-station irrigation controller with flow meter input
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
| Input ID | Name | Type | Unit | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `6516ca57b7c098e6807cb334` | *(unnamed)* | 33 (flow) | L | Flow meter — cumulative per minute |
|
||||
|
||||
### Flow meter — recent readings (2026-06-04)
|
||||
```
|
||||
06:12 0 L/min
|
||||
06:11 2.12 L/min
|
||||
06:10 0 L/min
|
||||
06:09 0 L/min
|
||||
06:08 1.97 L/min
|
||||
```
|
||||
Pulse-based flow meter, alternates between values and 0 (on/off pattern suggests short irrigation bursts).
|
||||
|
||||
---
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output ID | Name | Index |
|
||||
|---|---|---|
|
||||
| `6516ca57b7c098e6807cb336` | Station 1 | 0 |
|
||||
| `6516ca57b7c098e6807cb337` | Station 2 | 1 |
|
||||
|
||||
No flow rates configured (useSensor: false).
|
||||
|
||||
---
|
||||
|
||||
## Programs
|
||||
|
||||
3 programs: Programme A (index 0), Programme B (index 1), Programme C (index 2).
|
||||
All have **no active windows** (unconfigured).
|
||||
|
||||
```
|
||||
GET /programs/modules?moduleIds=6516ca57b7c098e6807cb332
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Remote state
|
||||
|
||||
```
|
||||
GET /remote/module/state?moduleId=6516ca57b7c098e6807cb332
|
||||
```
|
||||
|
||||
- Primary battery: 89%
|
||||
- Secondary battery: 3200 mV (`secondaryBatteryVoltage`) — secondary power state: 0
|
||||
- Last radio contact: 2026-06-04T19:43:08Z
|
||||
- Relay: `60ca4276ad69dac1558f7c61` (LRMB10)
|
||||
|
||||
Live watering status (`state.watering`):
|
||||
```json
|
||||
{
|
||||
"origin": 0,
|
||||
"runningProgram": 0,
|
||||
"runningStation": 0,
|
||||
"time": "00:00",
|
||||
"state": 1
|
||||
}
|
||||
```
|
||||
`state: 1` = currently running / active (irrigation in progress at last poll).
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Remote config returns `module_not_reachable` — LoRa-only communication
|
||||
- The `secondaryBatteryVoltage` field is unique to this module (likely a backup/solar battery)
|
||||
- Output events endpoint returns no historical records — watering history only inferrable from flow meter data
|
||||
@@ -0,0 +1,32 @@
|
||||
# LRMB10 — LoRa Gateway (LRMB10-070532)
|
||||
|
||||
**Module ID**: `60ca4276ad69dac1558f7c61`
|
||||
**Serial**: `1000000705320001`
|
||||
**Type**: `lr-mb-10`
|
||||
**Role**: LoRa relay — all other modules communicate through this gateway
|
||||
|
||||
---
|
||||
|
||||
## Hardware info (from `remote/module/configuration`)
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Firmware | 6.5.0 |
|
||||
| Hardware | 68.0.0 |
|
||||
| BLE | 1.0.13 |
|
||||
| WiFi MAC | `10:52:1C:01:2D:F4` |
|
||||
| BT MAC | `C8:B9:61:07:05:32` |
|
||||
| Ethernet MAC | `FF:FF:FF:FF:FF:FF` (not used) |
|
||||
| WiFi SSID | `Freebox-2A5E5D` |
|
||||
| Links | WiFi ✓, Bluetooth ✓, Ethernet ✗, Modem ✗ |
|
||||
| Server | `qualif.mysolem.com` / `10.iot` |
|
||||
| Uptime | 775361 s (~9 days) |
|
||||
| Last reboot count | 11 |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- No programs or outputs — pure relay/gateway
|
||||
- All LoRa child modules (`LRMS`, `LRAG`, `LRPS`, `LRPC`) reference this via their `relay` field
|
||||
- Remote config endpoint works directly: `GET /remote/module/configuration?moduleId=60ca4276ad69dac1558f7c61`
|
||||
@@ -0,0 +1,73 @@
|
||||
# LRMS — Sensor Station (LRMS-0721DC)
|
||||
|
||||
**Module ID**: `60ca42adad69dac1558f7c64`
|
||||
**Serial**: `7400040721DC0001`
|
||||
**Type**: `lr-ms`
|
||||
**Role**: Pure sensor station — no outputs, no programs
|
||||
|
||||
---
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input ID | Name | Type | Unit | Interval | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `60ca42adad69dac1558f7c66` | Plumbago | 12 (soil moisture) | % | 60 min | Named sensor |
|
||||
| `60ca42adad69dac1558f7c67` | Pelouse | 12 (soil moisture) | % | 60 min | Named sensor |
|
||||
| `60ca42adad69dac1558f7c69` | *(unnamed)* | 5 (temperature) | °C | 15 min | Air/soil temp |
|
||||
|
||||
### Latest readings (2026-06-04)
|
||||
|
||||
**Plumbago** (moisture)
|
||||
```
|
||||
19:42 71.19%
|
||||
18:42 74.06%
|
||||
17:42 71.63% ← irrigation spike visible
|
||||
16:42 44.64%
|
||||
15:42 41.83%
|
||||
```
|
||||
|
||||
**Pelouse** (moisture)
|
||||
```
|
||||
19:42 78.25%
|
||||
18:42 79.63%
|
||||
17:42 70.81% ← irrigation spike visible
|
||||
16:42 58.50%
|
||||
14:42 56.19%
|
||||
```
|
||||
|
||||
**Temperature (unnamed)**
|
||||
```
|
||||
19:42 16.10°C
|
||||
19:27 16.44°C
|
||||
18:42 16.66°C
|
||||
18:12 16.54°C
|
||||
17:42 16.31°C
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Remote state
|
||||
|
||||
```
|
||||
GET /remote/module/state?moduleId=60ca42adad69dac1558f7c64
|
||||
```
|
||||
|
||||
- Battery: 78% (batteryVoltage field)
|
||||
- Last radio contact: 2026-06-04T19:51:50Z
|
||||
- Relay: `60ca4276ad69dac1558f7c61` (LRMB10)
|
||||
- `relayIsLongPolling: true`
|
||||
|
||||
---
|
||||
|
||||
## Sensor data endpoint
|
||||
|
||||
```
|
||||
GET /remote/input/getComputedSensorData
|
||||
?inputId={inputId}
|
||||
&startDate={iso8601}
|
||||
&endDate={iso8601}
|
||||
&ensurePrevTickForContinuity=true
|
||||
```
|
||||
|
||||
The moisture sensor uses a computed expression to convert raw ADC values to %.
|
||||
Moisture spikes (>10% jump) correspond to irrigation events — useful to infer watering history even without direct output events.
|
||||
@@ -0,0 +1,82 @@
|
||||
# LRLEVEL — Pool Fill Valve Controller (LRLEVEL-0D2ED9)
|
||||
|
||||
**Module ID**: `6a1c4b153d6c33ab843edc13`
|
||||
**Type**: `lr-niv` (niveau = level)
|
||||
**Role**: Controls pool water fill valve; measures volume added and fill state
|
||||
|
||||
---
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input ID | Type | Unit | Description |
|
||||
|---|---|---|---|
|
||||
| `6a1c4b153d6c33ab843edc15` | 33 (flow) | L | Volume of water added per fill cycle |
|
||||
| `6a1c4b153d6c33ab843edc16` | 149 (level state) | raw | Fill valve / level state (0/1) |
|
||||
|
||||
### Input edc15 — Water volume (fill meter)
|
||||
|
||||
Expression: `javascript:if(x<=0){0.0}else if(x>=80000){68.76}else{-5.01×10⁻¹⁰×...}` (polynomial curve for pulse-to-litre conversion)
|
||||
|
||||
Recent readings — all **0 L** for the past 5 ticks (no fill events since 2026-06-03):
|
||||
```
|
||||
2026-06-04T19:12 0 L
|
||||
2026-06-04T13:11 0 L
|
||||
2026-06-04T07:10 0 L
|
||||
2026-06-04T01:09 0 L
|
||||
2026-06-03T19:08 0 L
|
||||
```
|
||||
Pool level currently stable — no refill needed.
|
||||
|
||||
### Input edc16 — Level state
|
||||
|
||||
Type 149 (new), raw 0/1. Consistently reads **1** in all recent ticks (every 6h):
|
||||
```
|
||||
2026-06-04T15:12 1
|
||||
2026-06-04T09:11 1
|
||||
2026-06-04T03:10 1
|
||||
2026-06-03T21:09 1
|
||||
2026-06-03T15:08 1
|
||||
```
|
||||
`1` = fill valve closed / level OK.
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
| Output ID | Name | Index |
|
||||
|---|---|---|
|
||||
| `6a1c4b153d6c33ab843edc17` | Station 1 | 0 |
|
||||
|
||||
Controls the physical fill valve. Program index 0 ("Station 1") exists but has no active windows — fill is triggered automatically by the level sensor, not by schedule.
|
||||
|
||||
---
|
||||
|
||||
## Remote state
|
||||
|
||||
- Battery: **94%**
|
||||
- Last radio contact: 2026-06-04T20:12:35Z
|
||||
- Relay: `60ca4276ad69dac1558f7c61` (LRMB10)
|
||||
|
||||
---
|
||||
|
||||
## Fill event detection
|
||||
|
||||
To detect when the pool was last filled and how much water was added, query the flow input over a wider date range and filter for ticks where `value > 0`:
|
||||
|
||||
```python
|
||||
data = session.get(f"{BASE}/remote/input/getComputedSensorData", params={
|
||||
"inputId": "6a1c4b153d6c33ab843edc15",
|
||||
"startDate": "2026-01-01T00:00:00.000Z",
|
||||
"endDate": "2026-06-04T23:59:59.000Z",
|
||||
"ensurePrevTickForContinuity": "true",
|
||||
}).json()
|
||||
|
||||
fill_events = [t for t in data["computedSensorData"] if t["value"] > 0]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Type 149 is a new input type not seen in other modules (likely lr-niv specific level state)
|
||||
- The system works as: `pool-level-sensor` (float) → signals low → `LRLEVEL` opens fill valve (output 0) → `edc15` measures L added → valve closes when `pool-level-sensor` returns 1
|
||||
@@ -0,0 +1,121 @@
|
||||
# LRPC — Pool Controller (LRPC-F1DA27)
|
||||
|
||||
**Module ID**: `6481bcea8782b78554c02bed`
|
||||
**Serial**: `150302F1DA270001`
|
||||
**Type**: `lr-pc`
|
||||
|
||||
---
|
||||
|
||||
## Pool configuration
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Pool ID | `5de53c1c7ed4c45c2cfe0688` |
|
||||
| Volume | 25 m³ |
|
||||
| Shape | rectangular 6 × 3 m, depth 1.4 m |
|
||||
| Filter | glass filter |
|
||||
| Treatment | stabilised chlorine tablets |
|
||||
| Chlorine regulation | manual |
|
||||
| pH regulation | automatic |
|
||||
| Installation | outdoor, no heating, no cover |
|
||||
|
||||
Endpoint:
|
||||
```
|
||||
GET /maintenance-logs-data?moduleId=6481bcea8782b78554c02bed
|
||||
```
|
||||
Returns 25+ pool alerts, maintenance history, and `messages[0].pool` with full config.
|
||||
|
||||
---
|
||||
|
||||
## Sensors / Inputs
|
||||
|
||||
| Input ID | Metric | Unit | Type code |
|
||||
|---|---|---|---|
|
||||
| `6481bcea8782b78554c02bef` | Water temperature | °C | 133 |
|
||||
|
||||
> Sensor telemetry endpoint: `GET /remote/input/getComputedSensorData?inputId=...`
|
||||
> Live readings (temp, pH, ORP, pressure) are in `maintenance-logs-data` under `pool.status`.
|
||||
|
||||
---
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output ID | Name | Index |
|
||||
|---|---|---|
|
||||
| `6481bcea8782b78554c02bf1` | Station 1 (filtration pump) | 0 |
|
||||
| `6481bcea8782b78554c02bf2` | Station 2 | 1 |
|
||||
| `6481bcea8782b78554c02bf3` | Station 3 | 2 |
|
||||
|
||||
---
|
||||
|
||||
## Filtration programs
|
||||
|
||||
### Program 0 — Filtration (type 4, index 0)
|
||||
|
||||
ID: `6481bcea8782b78554c02bf4`
|
||||
Runs every day (weekDays: 127).
|
||||
|
||||
| Window | Time range | Duration |
|
||||
|---|---|---|
|
||||
| Morning | 05:00–06:00 | 1h |
|
||||
| Midday | 12:00–14:00 | 2h |
|
||||
| Evening | 22:00–23:00 | 1h |
|
||||
| **Total/day** | | **4h** |
|
||||
|
||||
Special features:
|
||||
- **Micro-filtration** (frost mode): triggers every 480 min for 10 min when temp < 5°C
|
||||
- **Temperature-adaptive schedules**: 2 schedules active (96 days + 31 days/year), each with 12 thresholds that adjust run windows based on pool temperature
|
||||
|
||||
### Program 1 — Auxiliary/cyclic (type 2, index 1)
|
||||
|
||||
ID: `6481bcea8782b78554c02bf5`
|
||||
Active window: 21:30–22:30 (1h slot), cycle duration 5 min.
|
||||
Likely electrolyser or secondary pump.
|
||||
|
||||
### Program 2 — Unused (index 2)
|
||||
|
||||
ID: `6481bcea8782b78554c02bf6`
|
||||
No active windows configured.
|
||||
|
||||
---
|
||||
|
||||
## Reading filtration schedule
|
||||
|
||||
```
|
||||
GET /programs/modules?moduleIds=6481bcea8782b78554c02bed
|
||||
```
|
||||
|
||||
Window times are in **minutes from midnight**. Duration (`on`) is in **seconds**.
|
||||
|
||||
```python
|
||||
def mins_to_time(m): return f"{m//60:02d}:{m%60:02d}"
|
||||
def secs_to_human(s): h,m = divmod(s,3600); return f"{h}h{m//60:02d}m"
|
||||
|
||||
for prog in data['programs']:
|
||||
for w in prog['windows']:
|
||||
if w['runningDays'] > 0:
|
||||
print(f"{mins_to_time(w['start'])}–{mins_to_time(w['end'])} {secs_to_human(w['on'])}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Alerts & maintenance history
|
||||
|
||||
```
|
||||
GET /maintenance-logs-data?moduleId=6481bcea8782b78554c02bed
|
||||
```
|
||||
|
||||
Returns `{ code, messages: [...] }`. Each message has:
|
||||
- `poolAlertType`: `backwashReminder`, `backwashDone`, `poolLevelSensorCleaningReminder`, …
|
||||
- `priority`: `low` / `medium` / `high`
|
||||
- `state`: `read` / `unread`
|
||||
- `timestamp`: ISO 8601
|
||||
- `pool`: full pool config object (only on messages that have pool context)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `remote/module/configuration` and `remote/module/informations` return `module_not_reachable` — the LRPC communicates via LoRa relay, not direct HTTP
|
||||
- `output/events` returns empty — past filtration run history is not stored server-side per event; the program schedule is the source of truth
|
||||
- The LRPC temperature input (type 133, unit 2) is distinct from the LRPS pool sensor temperature; both track pool water temp but from different probes
|
||||
@@ -0,0 +1,66 @@
|
||||
# LRPR — Filter Pressure Sensor (LRPR-0D6800)
|
||||
|
||||
**Module ID**: `6a17218be0b88ea3e925524f`
|
||||
**Serial**: `1D00010D68000001`
|
||||
**MAC**: `C8:B9:61:0D:68:00`
|
||||
**UUID**: `1D0058B9610D680000000001000D6800`
|
||||
**Firmware**: 6.3.0 | **Hardware**: A | **Manufacturer**: `1D`
|
||||
**Type**: `lr-pr`
|
||||
**Role**: Measures filter pressure — indicates filter cleanliness / pump running state
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
| Input ID | Type | Unit | Interval |
|
||||
|---|---|---|---|
|
||||
| `6a17218be0b88ea3e9255251` | 8 (pressure) | bar (unit code 6) | 1 min |
|
||||
|
||||
Raw-to-bar expression: `(1.5 × raw − 500) / 1000`
|
||||
|
||||
### Latest readings (2026-06-04)
|
||||
|
||||
```
|
||||
16:01 0.091 bar ← pump stopped / off
|
||||
16:00 0.715 bar
|
||||
15:59 0.741 bar
|
||||
15:58 0.730 bar
|
||||
18:41 0.081 bar ← pump off
|
||||
```
|
||||
|
||||
Normal operating pressure ~0.70–0.75 bar. Drop to ~0.08 bar signals pump has stopped.
|
||||
This sensor can be used to detect filtration start/stop and detect dirty filter (rising pressure).
|
||||
|
||||
---
|
||||
|
||||
## Remote state
|
||||
|
||||
- Battery: **94%**
|
||||
- Last radio contact: 2026-06-04T20:05:14Z
|
||||
- Relay: `60ca4276ad69dac1558f7c61` (LRMB10)
|
||||
- No programs, no outputs — pure sensor
|
||||
|
||||
---
|
||||
|
||||
## Pressure Alert Configuration
|
||||
|
||||
From `poolPressureAlertsVariables` in the input object (retrieved via mobile API):
|
||||
|
||||
| Parameter | Value | Meaning |
|
||||
|---|---|---|
|
||||
| `staticPressure` | 0.919 bar | Baseline pressure at pump start |
|
||||
| `nominalPressure` | 1.7275 bar | Expected operating pressure |
|
||||
| `maximalPressure` | 1.0615 bar | Upper alert threshold |
|
||||
| `atmosphericPressure` | 1.015 bar | Calibration reference |
|
||||
| `filterCloggedMargin` | 0.5 bar | Rise above static = filter clogged alert |
|
||||
| `filterPreCloggedMargin` | 0.3 bar | Rise above static = pre-clog warning |
|
||||
| `activeFilterCloggedAlert` | true | Filter clog alert enabled |
|
||||
| `activeWaterLevelLowAlert` | true | Low water level alert enabled |
|
||||
| `activeSuctionValveClosedAlert` | true | Suction valve closed alert enabled |
|
||||
| `waterLevelLowCount` | 6 | Ticks below threshold before alert |
|
||||
|
||||
## Notes
|
||||
|
||||
- `secondaryBatteryVoltage: 80` — secondary power source present
|
||||
- Unit code 6 = bar; type 8 = pressure sensor
|
||||
- Backwash reminder alerts fire every ~15 days (shared with pool notebook)
|
||||
@@ -0,0 +1,69 @@
|
||||
# LRPS — Pool Analyser / Master Valve (LRPS-0565C0)
|
||||
|
||||
**Module ID**: `6480da07c2c0896da120ecca`
|
||||
**Serial**: `9400030565C00001`
|
||||
**Type**: `lr-mas`
|
||||
**Role**: Pool water quality analyser (temperature, pH, ORP/redox)
|
||||
|
||||
---
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input ID | Metric | Type | Unit | Interval |
|
||||
|---|---|---|---|---|
|
||||
| `6480da07c2c0896da120eccc` | Water temperature | 5 | °C | ~30 min |
|
||||
| `6480da07c2c0896da120eccd` | pH | 6 | — | varies |
|
||||
| `6480da07c2c0896da120ecce` | ORP / Redox | 7 | mV | varies |
|
||||
|
||||
### Latest readings (2026-06-04)
|
||||
|
||||
**Temperature**
|
||||
```
|
||||
19:03 23.43°C
|
||||
18:03 23.56°C
|
||||
17:03 23.75°C
|
||||
16:33 23.87°C
|
||||
15:33 24.12°C
|
||||
```
|
||||
Peak today: 24.62°C at 12:33, cooling through afternoon.
|
||||
|
||||
**pH**
|
||||
```
|
||||
2026-06-04T01:03 6.35
|
||||
2026-06-03T12:33 6.30
|
||||
2026-06-03T03:03 6.35
|
||||
2026-06-02T18:03 6.30
|
||||
2026-06-01T20:03 6.25
|
||||
```
|
||||
Slowly rising from 5.98 (2026-05-30). Ideal range: 7.2–7.6. Currently below target.
|
||||
|
||||
**ORP / Redox**
|
||||
```
|
||||
2026-06-04T16:33 646 mV
|
||||
2026-06-04T15:33 666 mV
|
||||
2026-06-04T15:03 661 mV
|
||||
2026-06-04T14:33 651 mV
|
||||
2026-06-04T14:03 646 mV
|
||||
```
|
||||
Healthy range: 650–750 mV. Currently borderline.
|
||||
|
||||
---
|
||||
|
||||
## Remote state
|
||||
|
||||
```
|
||||
GET /remote/module/state?moduleId=6480da07c2c0896da120ecca
|
||||
```
|
||||
|
||||
- Battery: 60% (low — alerts were sent Feb 2026)
|
||||
- Last radio contact: 2026-06-04T20:06:55Z
|
||||
- Relay: `60ca4276ad69dac1558f7c61` (LRMB10)
|
||||
|
||||
---
|
||||
|
||||
## Alerts (from LRPC maintenance-logs-data)
|
||||
|
||||
| Date | Alert |
|
||||
|---|---|
|
||||
| 2026-03-18 | Analyzer LRPS-0565C0 needs calibration |
|
||||
| 2026-02-03 | Battery too low — measurements may be unreliable |
|
||||
@@ -0,0 +1,343 @@
|
||||
# Mobile App API (iOS — MySOLEM & MyIndygo apps)
|
||||
|
||||
**Base URL**: `https://qualif.mysolem.com`
|
||||
**Prefix**: `/api/` (distinct from the web platform's unprefixed routes)
|
||||
**Auth**: OAuth2 Bearer token (not session cookie)
|
||||
**Source**: mitmproxy capture of both iOS apps (`inputs/apps_captured_all.jsonl`)
|
||||
|
||||
The mobile apps use a cleaner, more structured REST API than the web platform. Key differences:
|
||||
- Bearer token instead of cookies
|
||||
- `/api/` prefix on all endpoints
|
||||
- Gateway-routed module access: `GET /api/module/{gatewaySerial}/{action}/{childSerial6}`
|
||||
- High-level composite endpoints (`getPoolStatus`, `getUserWithHisModules`)
|
||||
|
||||
---
|
||||
|
||||
## Authentication — OAuth2
|
||||
|
||||
```
|
||||
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. The server accepts `client_credentials` without explicit client authentication headers visible in the capture — likely validated by the app's TLS client certificate or embedded credentials.
|
||||
|
||||
All subsequent requests:
|
||||
```
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
```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}"}
|
||||
|
||||
# Then use headers= on every request
|
||||
resp = requests.get(f"{BASE}/api/getUser", headers=headers)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User
|
||||
|
||||
```
|
||||
GET /api/getUser
|
||||
```
|
||||
Returns current authenticated user with `agency`, `distributor`, `manufacturer` fields not present in the web API.
|
||||
|
||||
```
|
||||
GET /api/getUserWithHisModules
|
||||
```
|
||||
Returns user object with full `modules` array embedded.
|
||||
|
||||
```
|
||||
GET /api/getUserPools
|
||||
```
|
||||
Returns array of pool configuration records. Each pool has:
|
||||
```json
|
||||
{
|
||||
"id": "5de53c1c7ed4c45c2cfe0688",
|
||||
"owner": "5de53bca7ed4c45c2cfe067f",
|
||||
"adminUsers": ["5de53bca7ed4c45c2cfe067f"],
|
||||
"address": "30 Rue de Florette, 30250 Villevieille, France",
|
||||
"latitude": 43.79417821332963,
|
||||
"longitude": 4.095361891348177,
|
||||
"volume": 25,
|
||||
"installationType": "outdoor",
|
||||
"filteringType": "filterGlass",
|
||||
"waterTreatmentType": "chlorineStabilizedTablet",
|
||||
"phRegulationMode": "automatic",
|
||||
"chlorineRegulationMode": "manual",
|
||||
"heatingSystemType": "none",
|
||||
"coverType": "none",
|
||||
"cyanuricAcidRate": 0,
|
||||
"model": "Autre"
|
||||
}
|
||||
```
|
||||
|
||||
> This endpoint reveals the pool ID `5de53c1c7ed4c45c2cfe0688` for the Domicile pool — previously only accessible via the main `anelissen@solem.fr` account on the web. Via the mobile API + OAuth2 it is readable.
|
||||
|
||||
```
|
||||
GET /api/getUserWateringModuleGroups → {"wateringModuleGroups": []}
|
||||
GET /api/getUserAgriculturalModuleGroups → {"agriculturalModuleGroups": []}
|
||||
GET /api/getCustomers → {"err": {"code": "user_is_not_professional"}} (pro-only)
|
||||
GET /api/users/{userId}/canopy-sites
|
||||
GET /api/users/{userId}/access-requests
|
||||
GET /api/users/{userId}/module-access-requests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pool
|
||||
|
||||
```
|
||||
GET /api/getPoolStatus?attributesToPopulate[]=modules&poolIdentifier={poolId}
|
||||
```
|
||||
Returns pool status including all associated modules with full config. Pool ID for Domicile: `5de53c1c7ed4c45c2cfe0688`.
|
||||
|
||||
```
|
||||
GET /api/getUserPools (see above)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modules
|
||||
|
||||
```
|
||||
GET /api/getModuleWithHisUsers?module={moduleId}
|
||||
```
|
||||
Returns module object with full `users` array.
|
||||
|
||||
```
|
||||
GET /api/getModuleInventory
|
||||
?module={gatewaySerial}
|
||||
&startDate={iso8601}
|
||||
&endDate={iso8601}
|
||||
&data=1
|
||||
```
|
||||
Returns `{children: [...]}` — all child modules registered under this gateway with full config, battery, connectivity state. Most efficient way to enumerate a site's devices.
|
||||
|
||||
```
|
||||
GET /api/getModuleBackups?module={moduleId}
|
||||
```
|
||||
Returns array of saved program backups (automatic + manual snapshots).
|
||||
|
||||
```
|
||||
PUT /api/updateModule
|
||||
Content-Type: application/json
|
||||
|
||||
{"module": {"id": "moduleId", "batteryVoltage": 77, "powerState": 0, "status": {...}, ...}}
|
||||
```
|
||||
Response: full module object including `inputs` and `outputs` arrays.
|
||||
Used by the app to sync device status back to the server after a LoRa radio round-trip. The `status` field contains the device's reported state (e.g. `ag.outputs`, `watering`).
|
||||
|
||||
---
|
||||
|
||||
## Gateway-routed module access
|
||||
|
||||
All real-time commands go through the gateway module (LRMB10). The URL encodes:
|
||||
- `{gatewaySerial}` = full gateway serial, e.g. `1000000705320001`
|
||||
- `{childSerial6}` = **last 6 hex chars** of the child module's serial
|
||||
|
||||
**Domicile module routing table:**
|
||||
|
||||
| Child module | childSerial6 | Full serial |
|
||||
|---|---|---|
|
||||
| LRPC-F1DA27 (pool controller) | `F1DA27` | `150302F1DA270001` |
|
||||
| LR6AG-070917 (valve) | `070917` | `6606010709170001` |
|
||||
| LRMS-0721DC (sensor station) | `0721DC` | `7400040721DC0001` |
|
||||
| LR2IPECO-0C9282 (IP ECO) | `0C9282` | `3802020C92820001` |
|
||||
| LRLEVEL-0D2ED9 (fill valve) | `0D2ED9` | `3A01020D2ED90001` |
|
||||
| LRPR-0D6800 (pressure) | `0D6800` | (lr-pr type) |
|
||||
|
||||
### Read endpoints
|
||||
|
||||
```
|
||||
GET /api/module/{gatewaySerial}/information ← gateway itself
|
||||
GET /api/module/{gatewaySerial}/information/{child6} ← child module real-time info
|
||||
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 (same as getModuleInventory)
|
||||
```
|
||||
|
||||
Gateway information response (`GET /api/module/1000000705320001/information`):
|
||||
```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",
|
||||
"dmn": "qualif.mysolem.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Write endpoints
|
||||
|
||||
```
|
||||
POST /api/module/{gatewaySerial}/status/{child6}
|
||||
Content-Type: application/json
|
||||
|
||||
{"ag": [{"index": 0, "rainDelay": 0}, {"index": 1, "rainDelay": 0}, ...]}
|
||||
```
|
||||
Sets rain delay for each output. Response: full device status including `dialogTimeStamp`, `ackTimestamp`, `temperature`, `battery`, `ag.sensor`, `ag.outputs`.
|
||||
|
||||
```
|
||||
POST /api/module/{gatewaySerial}/programs/{child6}
|
||||
Content-Type: application/json
|
||||
|
||||
{programs: [...], programmationType: 1}
|
||||
```
|
||||
Sends programs directly to device over LoRa.
|
||||
|
||||
```
|
||||
POST /api/module/{gatewaySerial}/manual/{child6}
|
||||
Content-Type: application/json
|
||||
|
||||
{...command body...}
|
||||
```
|
||||
Sends a manual command to the device over LoRa.
|
||||
|
||||
---
|
||||
|
||||
## Sensor data & chart
|
||||
|
||||
```
|
||||
GET /api/getInputs?id={moduleId}
|
||||
```
|
||||
Returns all inputs for a module with full `expression`, `expressionInverse`, alert config, `pushAlert`, `mailAlert`.
|
||||
|
||||
```
|
||||
GET /api/getComputedChartData
|
||||
?inputId={inputId}
|
||||
&startDate={iso8601}
|
||||
&endDate={iso8601}
|
||||
&numberOfBar={N}
|
||||
&locale=fr
|
||||
```
|
||||
Returns input data bucketed into N bars over the date range. Response includes `expression`, `expressionInverse`, and `computedSensorData` array.
|
||||
|
||||
```
|
||||
GET /api/getComputedOutputActivationHistory
|
||||
?moduleId={moduleId}
|
||||
&outputIndex={0|1|2...}
|
||||
&startDate={iso8601}
|
||||
&endDate={iso8601}
|
||||
&numberOfBar={N}
|
||||
```
|
||||
Returns per-period activation state: `[{currentPeriodStartDate, currentPeriodEndDate, value}]`.
|
||||
With `numberOfBar=1440` over 24h = 1 bar per minute.
|
||||
|
||||
---
|
||||
|
||||
## Programs
|
||||
|
||||
```
|
||||
GET /api/getPrograms?id={moduleId}
|
||||
```
|
||||
Full program objects including `startTimesCycles`, `stationsDuration` arrays not present in the web API.
|
||||
|
||||
```
|
||||
PUT /api/updatePrograms
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"programmationType": 1,
|
||||
"programs": [{
|
||||
"id": "...",
|
||||
"name": "...",
|
||||
"waterBudget": 100,
|
||||
"windows": [...],
|
||||
"programCharacteristics": {...}
|
||||
}]
|
||||
}
|
||||
```
|
||||
Response: `{updatedPrograms: [...]}`.
|
||||
|
||||
```
|
||||
POST /api/library/program-templates
|
||||
{"limit": 10, "moduleId": "moduleId", "offset": 0}
|
||||
→ {"programTemplates": []}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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}}]
|
||||
```
|
||||
BLE advertising profiles: `default`, `ecoMode`, etc.
|
||||
|
||||
```
|
||||
GET /api/getAllBanishedSerialNumbers
|
||||
→ ["120C0200750001", ...]
|
||||
```
|
||||
Blacklisted/revoked device serial numbers.
|
||||
|
||||
---
|
||||
|
||||
## Push notifications
|
||||
|
||||
```
|
||||
POST /api/addApnsTokenToUser
|
||||
{"isSandbox": false, "topic": "com.mysolem.qualif", "user": "{userId}", "token": "{apnsToken}"}
|
||||
→ {"code": "ok"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reporting (app telemetry)
|
||||
|
||||
These are called by the app after sending data to devices — informational only.
|
||||
|
||||
```
|
||||
POST /api/reportModuleDatasSent {"module": "moduleId"}
|
||||
POST /api/reportProgramsDatasSent {"programs": ["programId", ...], "module": "moduleId"}
|
||||
POST /api/reportManualCommandSent {"route": "http", "command": {...}, "id": "moduleId"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key differences vs web API
|
||||
|
||||
| | Web (cookie auth) | Mobile (Bearer token) |
|
||||
|---|---|---|
|
||||
| Auth | Session cookie | OAuth2 Bearer |
|
||||
| Module routing | Direct by ID | Via gateway serial |
|
||||
| Pool access | Admin-only / main account | `GET /api/getUserPools` |
|
||||
| Sensor data | `getComputedSensorData` (ticks) | `getComputedChartData` (bars) |
|
||||
| Output history | `output/events` | `getComputedOutputActivationHistory` |
|
||||
| Programs | `programs/modules` | `getPrograms` (richer fields) |
|
||||
| Module inventory | `modules/search` | `getModuleInventory` via gateway |
|
||||
| Backups | — | `getModuleBackups` |
|
||||
@@ -0,0 +1,367 @@
|
||||
# MyIndygo — Platform Overview & API Reference
|
||||
|
||||
**Base URL**: `https://qualif.myindygo.com`
|
||||
**Purpose**: Pool-focused platform — filtration scheduling, water chemistry, pool level monitoring
|
||||
**Relation to MySOLEM**: Same backend database and Sails.js server. Both platforms share all module data, sensor readings, and programs. MyIndygo adds a pool admin/management layer on top.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Identical to MySOLEM — same cookie name, same credentials work on both platforms.
|
||||
|
||||
```bash
|
||||
curl -c cookies.txt https://qualif.myindygo.com/login
|
||||
curl -c cookies.txt -b cookies.txt \
|
||||
-X POST \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "email=YOUR_EMAIL&password=YOUR_PASSWORD" \
|
||||
"https://qualif.myindygo.com/login"
|
||||
```
|
||||
|
||||
Cookie: `qualification-mysolem-solem-irrigation-platform.sid` (same name as MySOLEM)
|
||||
|
||||
---
|
||||
|
||||
## User IDs — MyIndygo vs MySOLEM
|
||||
|
||||
The test API account (`arnaud.nelissen.api@icloud.com`) has **different ObjectIds** on each platform:
|
||||
|
||||
| Platform | User ID |
|
||||
|---|---|
|
||||
| MySOLEM | `5de53bca7ed4c45c2cfe067f` |
|
||||
| MyIndygo | `6a21c3db5ebe0fa5f7885460` |
|
||||
|
||||
The MyIndygo user ID appears in the page HTML:
|
||||
```html
|
||||
<script>const userId = "6a21c3db5ebe0fa5f7885460"</script>
|
||||
```
|
||||
|
||||
The pool/site ID `5de53c1c7ed4c45c2cfe0688` is the main account's pool record on MyIndygo — it's only accessible when logged in as the main account (`anelissen@solem.fr`), not the test API account.
|
||||
|
||||
---
|
||||
|
||||
## Feature flags (MyIndygo-specific)
|
||||
|
||||
`GET /features` returns pool-focused flags including:
|
||||
|
||||
| Flag | Value | Meaning |
|
||||
|---|---|---|
|
||||
| `ocedisWaterChemistryCorrection` | `true` | Water chemistry correction (OCEDIS dosing) |
|
||||
| `authenticationMyIndygoUIV2` | `true` | MyIndygo-specific login UI |
|
||||
| `enableBSTSubscriptions` | `true` | BST subscription management |
|
||||
| `enablePWAIndygoInstallation` | `true` | Pool app PWA install |
|
||||
| `clusterTurfOpened` | `true` | Turf cluster watering |
|
||||
| `enablePolytropicAPI` | `true` | Polytropic pump integration |
|
||||
| `enableGraphicalProgrammation` | `true` | Visual program editor |
|
||||
| `enableNdviSatellite` | `true` | NDVI satellite data |
|
||||
| `enableEvapotranspiration` | `true` | ET-based watering |
|
||||
| `enableHPA` | `true` | HPA (high-pressure assist?) |
|
||||
|
||||
---
|
||||
|
||||
## Pool Admin System (B2B)
|
||||
|
||||
MyIndygo has a pool dealer admin layer for managing multiple customer pools. These endpoints are **admin-only** — not accessible with a standard user account.
|
||||
|
||||
```
|
||||
GET /pools/admin list all managed pools
|
||||
POST /pools/admin/search search pools (DataTables format)
|
||||
POST /pools/admin/sendMessage send a message to a pool customer
|
||||
GET /pools/admin/getMessages get admin messages
|
||||
DELETE /pools/admin/dissociateAndRemove/{id} remove pool from admin
|
||||
```
|
||||
|
||||
Pool data is fetched via:
|
||||
```
|
||||
GET /user/pools current user's pools (returns {"pools": []} for test account)
|
||||
GET /user/pools?userId= another user's pools (IDOR — no ownership check)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pool-Specific Endpoints
|
||||
|
||||
### Last sensor tick (no date range needed)
|
||||
```
|
||||
GET /input/getLastTickFromInput?inputId={inputId}
|
||||
```
|
||||
Returns the single most-recent raw tick for an input. Much lighter than `getComputedSensorData`.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"tick": {
|
||||
"input": "6480da07c2c0896da120eccd",
|
||||
"value": 635,
|
||||
"timestamp": "2026-06-04T01:03:00.000Z",
|
||||
"createdAt": "2026-06-04T01:03:11.000Z",
|
||||
"id": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: `value` is **raw** (not expression-computed). For LRPS pH: `635` = 6.35 pH (divide by 100). For LRPS temp: `2331` = 23.31°C.
|
||||
|
||||
### Live pool sensor snapshot
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE = "https://qualif.myindygo.com"
|
||||
s = requests.Session()
|
||||
s.get(f"{BASE}/login")
|
||||
s.post(f"{BASE}/login", data={"email": "...", "password": "..."})
|
||||
|
||||
POOL_INPUTS = {
|
||||
"temp": "6480da07c2c0896da120eccc",
|
||||
"pH": "6480da07c2c0896da120eccd",
|
||||
"ORP": "6480da07c2c0896da120ecce",
|
||||
"pressure": "6a17218be0b88ea3e9255251",
|
||||
"level": "6a174dd1e0b88ea3e925531e",
|
||||
}
|
||||
|
||||
readings = {}
|
||||
for name, input_id in POOL_INPUTS.items():
|
||||
r = s.get(f"{BASE}/input/getLastTickFromInput", params={"inputId": input_id}).json()
|
||||
readings[name] = r.get("tick", {})
|
||||
|
||||
# Current values (raw):
|
||||
# temp → divide by 100 → °C
|
||||
# pH → divide by 100 → pH units
|
||||
# ORP → raw mV
|
||||
# pressure → raw ADC (apply expression for bar)
|
||||
# level → 0=low/fill needed, 1=OK
|
||||
```
|
||||
|
||||
### Output events history
|
||||
```
|
||||
GET /output/events?moduleId={moduleId}
|
||||
GET /output/events?outputId={outputId}
|
||||
GET /output/events?moduleId={id}&startDate={iso8601}&endDate={iso8601}
|
||||
```
|
||||
Returns `[]` if no events or empty. Tracks when outputs have been triggered.
|
||||
|
||||
### Output watering forecast
|
||||
```
|
||||
GET /output/forecast?moduleId={moduleId}
|
||||
GET /output/forecast?outputId={outputId}
|
||||
```
|
||||
Returns upcoming scheduled watering windows.
|
||||
|
||||
### Real-time module informations (via LoRa)
|
||||
```
|
||||
GET /remote/module/informations
|
||||
?moduleId={relayModuleId}
|
||||
&childrenSerialNumber={childSerialNumber}
|
||||
```
|
||||
Or via socket.io (as seen in JS):
|
||||
```js
|
||||
io.socket.request({
|
||||
method: "get",
|
||||
url: "/remote/module/informations",
|
||||
data: {
|
||||
moduleId: relayId, // LRMB10 gateway ID
|
||||
moduleUuid: uuid,
|
||||
moduleSerialNumber: serialNumber
|
||||
}
|
||||
}, callback)
|
||||
```
|
||||
Returns real-time device state polled over LoRa. Fails with `module_not_reachable` if device is offline.
|
||||
|
||||
### Real-time combined config + programs
|
||||
```
|
||||
GET /remote/module/configuration/and/programs?moduleId={moduleId}
|
||||
```
|
||||
Returns configuration + programs in a single LoRa roundtrip. Requires device online.
|
||||
|
||||
### Pool controller control (linesControl)
|
||||
JSON body (verified from real network capture):
|
||||
```
|
||||
POST /remote/module/control
|
||||
Content-Type: application/json
|
||||
```
|
||||
```json
|
||||
{
|
||||
"moduleSerialNumber": "150302F1DA270001",
|
||||
"linesControl": [
|
||||
{
|
||||
"index": 1,
|
||||
"action": 2,
|
||||
"time": "30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Known `action` codes for LRPC pool controller:
|
||||
|
||||
| action | meaning | extra params |
|
||||
|---|---|---|
|
||||
| `0` | Stop pump output (index 0 only) | — |
|
||||
| `1` | **Stop / cancel** any running manual command | — |
|
||||
| `2` | Timed impulse | `time`: `"MM:SS"` string (e.g. `"30:00"` = 30 minutes) |
|
||||
| `3` | Boost filtration | `time`: `"MM:SS"` |
|
||||
| `4` | Start indefinitely | — |
|
||||
| `5` | Pause N days (blocks manual + scheduled) | `manualDuration`: N (integer days) |
|
||||
| `11` | Backwash cycle | `time`: encoded duration string, `speed`: pump speed level |
|
||||
|
||||
Response pool state includes `"pause": N` (days remaining) when an output is paused.
|
||||
Response always reflects **pre-command state** — the command takes effect on the next LoRa downlink.
|
||||
|
||||
For backwash, duration is encoded using `encodeBackwashDuration(durationMinutes, minMinutes, maxMinutes)`.
|
||||
|
||||
> **Corrected from JS analysis**: duration is `time: "MM:SS"` (string), NOT `manualDuration` in seconds. Verified from real Safari capture.
|
||||
|
||||
### Module advertising profile
|
||||
```
|
||||
GET /remote/module/advertisingProfile?moduleId={moduleId}
|
||||
GET /remote/modules/advertisingProfile (bulk)
|
||||
```
|
||||
BLE advertising profile data. Likely used for Bluetooth pairing.
|
||||
|
||||
### Team pool access
|
||||
```
|
||||
GET /teams/pools/{poolId}/membersAccessGranted
|
||||
```
|
||||
Returns `{ teamMembersAlreadyGrantedPool: [...] }` — team members with access to a specific pool.
|
||||
|
||||
---
|
||||
|
||||
## LRPC Pool Controller — Program Structure (MyIndygo extended)
|
||||
|
||||
MyIndygo exposes more program fields than MySOLEM for `lr-pc` modules:
|
||||
|
||||
```
|
||||
GET /programs/modules?moduleIds={moduleId}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"programs": [
|
||||
{
|
||||
"id": "6481bcea8782b78554c02bf4",
|
||||
"name": "",
|
||||
"module": "6481bcea8782b78554c02bed",
|
||||
"index": 0,
|
||||
"weekDays": 127,
|
||||
"numberOfDays": 2,
|
||||
"synchroDay": 0,
|
||||
"waterBudget": 100,
|
||||
"startTimes": [-1, -1, -1, -1, -1, -1, -1, -1],
|
||||
"programCharacteristics": {
|
||||
"programType": 4,
|
||||
"mode": 2,
|
||||
"onSpeed": 1,
|
||||
"speedSequence": 4368,
|
||||
"microFilteringTemperature": 5,
|
||||
"microFilteringDuration": 10,
|
||||
"microFilteringPeriod": 480,
|
||||
"microFilteringSpeed": 1,
|
||||
"auxiliaryCyclicDuration": 0,
|
||||
"frostFreeSpeed": 1,
|
||||
"defaultProgramSpeed": 1,
|
||||
"adaptOffset": 0,
|
||||
"isShutterDependant": false,
|
||||
"isRandom": false,
|
||||
"isUsingButton": false
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"start": 300, "end": 360,
|
||||
"on": 3600, "off": 0,
|
||||
"runningDays": 127
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**`programType` codes** (pool controller):
|
||||
|
||||
| code | meaning |
|
||||
|---|---|
|
||||
| `4` | Main filtration (pump runs on schedule) |
|
||||
| `2` | Auxiliary / cyclic (treatment dosing, UV, etc.) |
|
||||
| `0` | Disabled / empty slot |
|
||||
|
||||
**`mode`**: `2` = active, `0` = disabled
|
||||
**`speedSequence`**: pump speed bitmask for variable-speed pumps (`4368` = 0x1110)
|
||||
**`windows[].runningDays`**: per-window day bitmask (127 = every day, unlike mysolem where weekDays is per-program)
|
||||
**`startTimes`**: array of 8 fixed start times in minutes from midnight (-1 = unused)
|
||||
|
||||
### Domicile LRPC filtration schedule
|
||||
|
||||
| Program | Type | Windows | Total |
|
||||
|---|---|---|---|
|
||||
| 0 (Filtration) | 4 | 05:00–06:00 (1h), 12:00–14:00 (2h), 22:00–23:00 (1h) | 4h/day |
|
||||
| 1 (Auxiliary) | 2 | 21:30–22:30 (1h) | 1h/day |
|
||||
| 2 | — | empty | — |
|
||||
|
||||
---
|
||||
|
||||
## LRPC Outputs (Pool Controller)
|
||||
|
||||
| Output ID | Name | Index |
|
||||
|---|---|---|
|
||||
| `6481bcea8782b78554c02bf1` | Station 1 | 0 |
|
||||
| `6481bcea8782b78554c02bf2` | Station 2 | 1 |
|
||||
| `6481bcea8782b78554c02bf3` | Station 3 | 2 |
|
||||
|
||||
---
|
||||
|
||||
## New Module Types (found via modules/search on MyIndygo)
|
||||
|
||||
| Type | Name | Description |
|
||||
|---|---|---|
|
||||
| `lr-pc-vs` | LRPCVS | Pool controller with variable speed pump |
|
||||
| `lr-mv` | LRMV/LRPM1 | Motor valve (multi-output) |
|
||||
| `lr-mb-30` | LRMB30 | LoRa gateway v30 |
|
||||
| `lr-mb-100` | LRMB100 | LoRa gateway v100 |
|
||||
| `lr-ip-fl` | LR1IPFL | IP flow metering |
|
||||
| `lr-ip-fl-v2` | LR6IP | IP flow v2 |
|
||||
| `lr-ip-wan-v2` | LRIP6 WAN | IP WAN v2 |
|
||||
| `lr-is` | LR6IS/LR12IS | LoRa input sensor (multi-input) |
|
||||
| `lr-is-city` | LR6IS | City-variant input sensor |
|
||||
| `lr-is-city-wan` | LR12IS | City WAN sensor |
|
||||
| `lr-ms-eco` | LRMS_ECO | Eco sensor station |
|
||||
| `lr-ms-rs485` | LRMSRS485 | RS485 sensor station |
|
||||
| `lr-ms-v2` | LRMS v2 | Sensor station v2 |
|
||||
| `lr-ol` | LROL4 | LoRa OL (lighting?) |
|
||||
| `lr-bst-100` | LRBST100 | BST 100 (booster?) |
|
||||
| `lr-bst-react-4g` | LR-BST R4G | BST 4G reactive (booster 4G) |
|
||||
| `joro` / `joro-v2` | WB-xxxx | Joro water balance meter |
|
||||
| `smart-is` | SmartIS | Smart input sensor |
|
||||
| `smart-is-lcd` | VILLA-/SMT-HOME | Smart IS with LCD display |
|
||||
| `bl-ip-v2` | BL6IP | Bluetooth IP v2 |
|
||||
| `ipx` | GEN_xxxx | IPX controller (generic) |
|
||||
|
||||
---
|
||||
|
||||
## Shared Endpoints (same on both MySOLEM and MyIndygo)
|
||||
|
||||
All endpoints from `api-docs/endpoints.md` work identically on `https://qualif.myindygo.com`. The backend database is shared — module IDs, input IDs, programs, and sensor data are identical on both platforms.
|
||||
|
||||
Key shared endpoints that work well for pool monitoring:
|
||||
- `GET /remote/input/getComputedSensorData?inputId=...` — time-series sensor data
|
||||
- `GET /remote/module/getComputedSensorsData?moduleId=...` — all inputs for a module
|
||||
- `POST /module/sendManualModuleCommand` — send commands to devices
|
||||
- `GET /modules/{moduleId}/notebook?startDate={unix_epoch}` — event log
|
||||
- `GET /programs/modules?moduleIds=...` — read/write programs
|
||||
|
||||
---
|
||||
|
||||
## Pool Monitoring Quick Reference
|
||||
|
||||
Current pool sensor IDs and live values (2026-06-04):
|
||||
|
||||
| Sensor | Input ID | Last Raw Value | Computed |
|
||||
|---|---|---|---|
|
||||
| Water temp (LRPS) | `6480da07c2c0896da120eccc` | 2331 | 23.31 °C |
|
||||
| pH (LRPS) | `6480da07c2c0896da120eccd` | 635 | 6.35 (low — target 7.2–7.6) |
|
||||
| ORP/Redox (LRPS) | `6480da07c2c0896da120ecce` | 646 | 646 mV |
|
||||
| Filter pressure (LRPR) | `6a17218be0b88ea3e9255251` | 956 | — |
|
||||
| Pool level switch | `6a174dd1e0b88ea3e925531e` | 1 | OK (1=full) |
|
||||
| Fill flow volume | `6a1c4b153d6c33ab843edc15` | 0 | 0 L (no active fill) |
|
||||
| Fill valve state | `6a1c4b153d6c33ab843edc16` | 1 | Closed |
|
||||
@@ -0,0 +1,139 @@
|
||||
# MySolem / MyIndygo API — Overview
|
||||
|
||||
| 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** — same module IDs, input IDs, session cookie. MyIndygo adds pool-specific management endpoints on top.
|
||||
See `api-docs/myindygo.md` for MyIndygo-specific docs.
|
||||
|
||||
**Backend**: Node.js + Sails.js v0.13.8 socket client, MongoDB (all IDs are 24-char hex ObjectIds)
|
||||
**Real-time**: Socket.IO v3 / Engine.IO v3 (`EIO=3`), direct WebSocket transport (not long-polling), `wss://qualif.mysolem.com/socket.io/`
|
||||
**Proxy**: nginx/1.18.0 (Ubuntu) in front
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Two separate auth systems exist on the same server:
|
||||
|
||||
- **Web API** (no path prefix): session cookie (Sails.js), no CSRF required on JSON endpoints
|
||||
- **Mobile API** (`/api/` prefix): OAuth2 Bearer token (`grant_type: client_credentials`)
|
||||
|
||||
See `api-docs/mobile-api.md` for the mobile API auth flow.
|
||||
|
||||
### Web login flow
|
||||
|
||||
```bash
|
||||
# Step 1: initialize 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"
|
||||
|
||||
# All subsequent calls
|
||||
curl -b cookies.txt https://qualif.mysolem.com/features
|
||||
```
|
||||
|
||||
Cookie name: `qualification-mysolem-solem-irrigation-platform.sid`
|
||||
Session TTL: ~7 days
|
||||
No CSRF token required on JSON endpoints.
|
||||
|
||||
### Python example
|
||||
|
||||
```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": "...",
|
||||
"password": "...",
|
||||
}) # 302 → / on success
|
||||
|
||||
# Now s carries the session cookie automatically
|
||||
sites = s.get(f"{BASE}/canopy-sites", params={"userId": USER_ID}).json()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key IDs (test account)
|
||||
|
||||
| Resource | Name | ID | Serial Number |
|
||||
|---|---|---|---|
|
||||
| User (MySOLEM) | Arnaud Nelissen | `5de53bca7ed4c45c2cfe067f` | — |
|
||||
| User (MyIndygo) | Arnaud Nelissen (API account) | `6a21c3db5ebe0fa5f7885460` | — |
|
||||
| Site | Domicile | `62b30d26ecbceb08607592c1` | — |
|
||||
| Gateway module | LRMB10-070532 | `60ca4276ad69dac1558f7c61` | `1000000705320001` |
|
||||
| Sensor station module | LRMS-0721DC | `60ca42adad69dac1558f7c64` | `7400040721DC0001` |
|
||||
| Agricultural valve module | LR6AG-070917 | `626984f038ec558c598bb6d5` | `6606010709170001` |
|
||||
| Pool analyser module | LRPS-0565C0 | `6480da07c2c0896da120ecca` | `9400030565C00001` |
|
||||
| Pool ctrl module | LRPC-F1DA27 | `6481bcea8782b78554c02bed` | `150302F1DA270001` |
|
||||
| IP ECO module | LR2IPECO-0C9282 | `6516ca57b7c098e6807cb332` | `3802020C92820001` |
|
||||
| Filter pressure sensor | LRPR-0D6800 | `6a17218be0b88ea3e925524f` | `1D00010D68000001` |
|
||||
| Pool level switch | POOL-LVL-0E953B | `6a174dd1e0b88ea3e925531c` | `0300020E953B0001` |
|
||||
| Level ctrl module | LRLEVEL-0D2ED9 | `6a1c4b153d6c33ab843edc13` | `3A01020D2ED90001` |
|
||||
| Input: Soil moisture (Plumbago) | — | `60ca42adad69dac1558f7c66` |
|
||||
| Input: Sensor 2 | — | `60ca42adad69dac1558f7c67` |
|
||||
| Input: Sensor 3 | — | `60ca42adad69dac1558f7c69` |
|
||||
| Input: Rain sensor (AG) | — | `626984f038ec558c598bb6d7` |
|
||||
| Input: Pool temperature (LRPS) | — | `6480da07c2c0896da120eccc` |
|
||||
| Input: Pool pH (LRPS) | — | `6480da07c2c0896da120eccd` |
|
||||
| Input: Pool ORP/Redox (LRPS) | — | `6480da07c2c0896da120ecce` |
|
||||
| Input: Rain sensor (LRAG) | — | `626984f038ec558c598bb6d7` |
|
||||
| Input: Moisture — Plumbago (LRMS) | — | `60ca42adad69dac1558f7c66` |
|
||||
| Input: Moisture — Pelouse (LRMS) | — | `60ca42adad69dac1558f7c67` |
|
||||
| Input: Temperature (LRMS) | — | `60ca42adad69dac1558f7c69` |
|
||||
| Input: Flow meter (LRIPECO) | — | `6516ca57b7c098e6807cb334` |
|
||||
| Input: Filter pressure (LRPR) | — | `6a17218be0b88ea3e9255251` |
|
||||
| Input: Pool level switch (POOL-LVL) | — | `6a174dd1e0b88ea3e925531e` |
|
||||
| Input: Pool level switch aux (POOL-LVL) | — | `6a174dd1e0b88ea3e925531f` |
|
||||
| Input: Fill volume (LRLEVEL) | — | `6a1c4b153d6c33ab843edc15` |
|
||||
| Input: Fill valve state (LRLEVEL) | — | `6a1c4b153d6c33ab843edc16` |
|
||||
| Output: Station 1 (AG) | Station 1 | `626984f038ec558c598bb6d8` |
|
||||
| Output: Station 2 (AG) | Station 2 | `626984f038ec558c598bb6d9` |
|
||||
|
||||
---
|
||||
|
||||
## Module types
|
||||
|
||||
| Type string | Description |
|
||||
|---|---|
|
||||
| `lr-mb-10` | LoRa relay / gateway |
|
||||
| `lr-ms` | LoRa sensor station |
|
||||
| `lr-ag` | LoRa agricultural valve |
|
||||
| `lr-mas` | LoRa master valve |
|
||||
| `lr-pc` | LoRa pool controller |
|
||||
| `lr-ip-eco` | LoRa IP ECO |
|
||||
| `lr-pr` | LoRa pressure sensor (filter) |
|
||||
| `pool-level-sensor` | Pool float level switch |
|
||||
| `lr-niv` | LoRa level controller (fill valve) |
|
||||
| `wf-mb` | WiFi gateway |
|
||||
| `wf-is` / `bl-is` | WiFi/BT sensor |
|
||||
| `bl-ip` | BT IP |
|
||||
| `wf-ol` | WiFi online |
|
||||
|
||||
---
|
||||
|
||||
## Input type codes
|
||||
|
||||
| Code | Meaning | Unit |
|
||||
|---|---|---|
|
||||
| `2` | TOR (on/off, e.g. rain sensor) | — |
|
||||
| `5` | Air/soil temperature | °C |
|
||||
| `6` | pH | — (raw ÷ 100) |
|
||||
| `7` | ORP / Redox | mV |
|
||||
| `8` | Pressure | bar (unit `6`, expression: `(1.5×raw−500)/1000`) |
|
||||
| `12` | Soil moisture | % (unit `10`) |
|
||||
| `20` | Air moisture | — |
|
||||
| `21` | Pool level TOR switch | raw 0/1 |
|
||||
| `33` | Volumetric / flow | L (unit `1`) |
|
||||
| `133` | Water temperature | °C (unit `2`) |
|
||||
| `149` | Fill valve / level state | raw 0/1 |
|
||||
@@ -0,0 +1,53 @@
|
||||
# pool-level-sensor — Pool Level Float Switch (POOL-LVL-0E953B)
|
||||
|
||||
**Module ID**: `6a174dd1e0b88ea3e925531c`
|
||||
**Serial**: `0300020E953B0001`
|
||||
**MAC**: `C8:B9:61:0E:95:3B`
|
||||
**UUID**: `030058B9610E953B00000001000E953B`
|
||||
**Firmware**: 7.3.1 | **Hardware**: B | **Manufacturer**: `03`
|
||||
**Type**: `pool-level-sensor`
|
||||
**Role**: Detects pool water level — triggers fill valve via LRLEVEL (lr-niv)
|
||||
|
||||
---
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input ID | Index | Type | Unit | Interval | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `6a174dd1e0b88ea3e925531e` | 1 | 21 (level TOR) | raw (0/1) | 1 min | Main level switch |
|
||||
| `6a174dd1e0b88ea3e925531f` | 2 | 0 | raw | 0 (event-driven) | Secondary / aux (no telemetry recorded) |
|
||||
|
||||
Both inputs: expression `x` (raw passthrough). `metadata.equipmentType: "waterLevel"` on input 1.
|
||||
|
||||
### Recent readings
|
||||
|
||||
```
|
||||
2026-05-31T14:54 1 (level OK / above threshold)
|
||||
2026-05-30T18:27 1 (level OK)
|
||||
2026-05-29T16:18 0 (level LOW — fill triggered)
|
||||
2026-05-29T16:17 1
|
||||
2026-05-29T15:59 0 (level LOW)
|
||||
```
|
||||
|
||||
`0` = water below threshold (fill needed)
|
||||
`1` = water level OK
|
||||
|
||||
Only 11 events recorded over the last month — sensor only transmits on state change.
|
||||
|
||||
---
|
||||
|
||||
## Remote state
|
||||
|
||||
- Battery: **41%** — low, needs replacement
|
||||
- Last radio contact: 2026-06-04T20:14:07Z
|
||||
- Relay: `60ca4276ad69dac1558f7c61` (LRMB10)
|
||||
|
||||
The `poolLevelSensorCleaningReminder` alert (2026-05-28) confirms this device needs cleaning.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Works in tandem with `LRLEVEL-0D2ED9` (lr-niv): when this sensor reads 0, LRLEVEL opens the fill valve
|
||||
- No programs or outputs — pure sensor
|
||||
- Type 21 = pool level TOR switch (new type, not seen in other modules)
|
||||
@@ -0,0 +1,172 @@
|
||||
# MySolem API — Security Critique
|
||||
|
||||
Assessment based on black-box reverse engineering of `https://qualif.mysolem.com`.
|
||||
|
||||
---
|
||||
|
||||
## Critical
|
||||
|
||||
### 1. `/modules/search` leaks the entire platform's device database
|
||||
|
||||
`POST /modules/search` with an empty query (`"query": {}`) returned **4,607 modules** belonging to all users on the platform. Confirmed exposed fields: `name`, `serialNumber`, `macAddress`, `softwareVersion`, `seenAt` (last-seen timestamp), `batteryAlarm`, `uuid`, `type`, and `createdAt`.
|
||||
|
||||
Any authenticated user can enumerate every device on the platform. This is a broken object-level authorization (BOLA / IDOR) vulnerability — the endpoint enforces authentication but not ownership scoping.
|
||||
|
||||
**Impact**: full device inventory of all platform customers — device names, serial numbers, MAC addresses, firmware versions, and last-seen timestamps across all tenants.
|
||||
|
||||
---
|
||||
|
||||
### 2. `POST /users/search` exposes full user records including password hashes and account takeover tokens
|
||||
|
||||
`POST /users/search` accepts an arbitrary MongoDB-style query with no ownership enforcement. Querying by any known user ID returns the complete user document, including:
|
||||
|
||||
- Email address and full name
|
||||
- **Hashed password** (`password` field — 64-character hex string, consistent with unsalted SHA-256; notably not bcrypt which is Sails.js's default — warrants further analysis)
|
||||
- **Active password reset token** (`resetPasswordToken`) — can be used directly to take over the account without knowing the current password
|
||||
- **APNs and FCM push notification tokens** — allows sending arbitrary push notifications to the user's devices
|
||||
- Address fields, notification preferences, and app version history
|
||||
|
||||
User IDs are trivially obtained from finding #1: every module document in the `/modules/search` dump contains the owning user's ID in its relational fields.
|
||||
|
||||
**Proof of concept**: querying `{"query": {"id": "641c3250efed32199bed0166"}}` returned the full record for `jdheilly@solem.fr` including password hash, reset token, and 11 push tokens across iOS and Android.
|
||||
|
||||
**Impact**: complete account takeover of any platform user via the exposed reset token, plus credential exposure (unsalted SHA-256 hashes are trivially crackable). Push token exposure enables notification spam or phishing. This is the most severe finding in this assessment.
|
||||
|
||||
---
|
||||
|
||||
### 3. `POST /outputs/search` and `POST /programs/search` expose full platform watering configuration
|
||||
|
||||
Both search endpoints accept an empty MongoDB query with no ownership enforcement:
|
||||
|
||||
- `POST /outputs/search` with `{"query": {}}` returned **12,034 outputs** (valve/station definitions) across all tenants, including names, flow rates, and module associations.
|
||||
- `POST /programs/search` with `{"query": {}}` returned **6,073 irrigation programs** across all tenants, including full schedules, station durations, start times, water-budget percentages, and weekday patterns.
|
||||
|
||||
**Impact**: complete read access to every customer's watering configuration. Combined with finding #4 (device control), this gives an attacker enough information to silently modify or disrupt any installation on the platform.
|
||||
|
||||
---
|
||||
|
||||
### 4. `GET /remote/module/configuration` exposes device internals for any module
|
||||
|
||||
The endpoint accepts any `moduleId` without ownership verification. Probing a foreign active module (`67697cc9673ef4bfbb4fdbbc`) returned full device configuration including:
|
||||
|
||||
- WiFi SSID and all MAC addresses (WiFi, Bluetooth, Ethernet)
|
||||
- Firmware, hardware, and BLE versions
|
||||
- Connected network domain and IoT subdomain
|
||||
- Uptime counter and reset count
|
||||
- Active link states (WiFi, Bluetooth, modem)
|
||||
|
||||
**Impact**: network topology and credential-adjacent data (WiFi SSID + MAC) for any online device on the platform, without owning it.
|
||||
|
||||
---
|
||||
|
||||
### 5. No authorization check on `/remote/input/getComputedSensorData`
|
||||
|
||||
The sensor telemetry endpoint accepts any `inputId`. If an attacker enumerates input IDs (trivially derived from the module search above — they are sequential MongoDB ObjectIds), they can pull historical sensor data for any device on the platform without owning it.
|
||||
|
||||
**Impact**: silent exfiltration of long-term environmental data (soil moisture trends, rain events, watering schedules) for any customer site.
|
||||
|
||||
---
|
||||
|
||||
### 7. `POST /module/sendManualModuleCommand` — no apparent ownership check
|
||||
|
||||
The control endpoint takes a `moduleSerialNumber`, not a session-scoped module ID. Serial numbers are visible in the `/modules/search` dump. It is likely (not confirmed) that a valid session can send commands to modules owned by other users.
|
||||
|
||||
**Impact**: if confirmed, any authenticated user could start or stop irrigation on any device on the platform — including commercial agricultural installations.
|
||||
|
||||
---
|
||||
|
||||
## High
|
||||
|
||||
### 8. Google Maps API key hard-coded in HTML
|
||||
|
||||
The key `AIzaSyDh8fCC9vTEuN9eKewxgqWXGP9ENb5an1c` is embedded in every page load with no referrer restriction visible. It can be extracted and abused for Maps API billing fraud against the platform owner.
|
||||
|
||||
---
|
||||
|
||||
### 9. Session cookie has no observed expiry enforcement beyond 7 days
|
||||
|
||||
Sails.js default session TTL. There is no `SameSite` attribute confirmed in the `Set-Cookie` header, which means the cookie is sent on cross-site navigated requests — making CSRF possible for state-changing endpoints despite the lack of a CSRF token check (both conditions compound each other).
|
||||
|
||||
A `SameSite=Lax` or `SameSite=Strict` attribute would mitigate this without requiring a token.
|
||||
|
||||
---
|
||||
|
||||
### 10. No rate limiting observed on `/login`
|
||||
|
||||
The login endpoint accepted repeated requests without delay, lockout, or CAPTCHA. Credential stuffing and brute-force attacks are uninhibited.
|
||||
|
||||
---
|
||||
|
||||
### 11. MongoDB ObjectIds as primary keys are sequentially guessable
|
||||
|
||||
MongoDB ObjectIds encode a timestamp + machine ID + process ID + counter. They are not random. An attacker who knows one valid ID can narrow the search space for adjacent resources created around the same time. Combined with findings #1–2, this accelerates enumeration significantly.
|
||||
|
||||
---
|
||||
|
||||
## Medium
|
||||
|
||||
### 12. CORS is misconfigured (blank `Access-Control-Allow-Origin`)
|
||||
|
||||
The server returns `Access-Control-Allow-Origin: ` (empty string) rather than omitting the header or setting a specific origin. This is a misconfiguration — browsers treat it as invalid, but it signals the header is being generated dynamically and may be inconsistently applied across routes.
|
||||
|
||||
---
|
||||
|
||||
### 13. No `Content-Security-Policy` header
|
||||
|
||||
The React frontend loads without a CSP. If an XSS vulnerability exists anywhere in the app, the attacker has full access to the DOM and cookie-less session (cookies are `HttpOnly`, but XSS can still make authenticated API calls).
|
||||
|
||||
---
|
||||
|
||||
### 14. Verbose server fingerprinting
|
||||
|
||||
`X-Powered-By: Sails <sailsjs.org>` is returned on every response. Attackers can immediately target known Sails.js CVEs without fingerprinting effort. This header should be removed.
|
||||
|
||||
---
|
||||
|
||||
### 15. Module serial numbers and MAC addresses exposed in bulk
|
||||
|
||||
The `/modules/search` dump includes full `serialNumber`, `uuid`, `macAddress`, and `manufacturerType` for every device. These are used as authentication tokens in some IoT protocols (LoRa device EUIs, BLE MAC-based pairing). Bulk exposure reduces the bar for physical-layer attacks.
|
||||
|
||||
---
|
||||
|
||||
## Low / Informational
|
||||
|
||||
### 16. Qualification environment uses real production data
|
||||
|
||||
The `qualif.mysolem.com` environment appears to share the same database as production (or a live mirror), given that real device GPS coordinates, real customer addresses, and live telemetry timestamps are visible. Qualification/staging environments should use anonymized or synthetic data.
|
||||
|
||||
---
|
||||
|
||||
### 17. Input IDs discoverable via HTML scraping
|
||||
|
||||
Input IDs are embedded as `data-input-id` attributes in the module detail page. This is not a vulnerability on its own, but combined with #1 (module enumeration) and #2 (unrestricted telemetry), it completes the chain without requiring any guessing.
|
||||
|
||||
---
|
||||
|
||||
## Summary table
|
||||
|
||||
| # | Finding | Confirmed | Severity | OWASP / CWE |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `/modules/search` — 4,607 devices platform-wide | Yes | Critical | API1:2023 BOLA |
|
||||
| 2 | `/users/search` — password hashes, reset tokens, push tokens | Yes | Critical | API1:2023 BOLA / CWE-312 |
|
||||
| 3 | `/outputs/search` + `/programs/search` — 12K outputs, 6K programs | Yes | Critical | API1:2023 BOLA |
|
||||
| 4 | `/remote/module/configuration` — WiFi/MAC/firmware for any device | Yes | Critical | API1:2023 BOLA |
|
||||
| 5 | Sensor telemetry readable for any input ID | Likely | Critical | API1:2023 BOLA |
|
||||
| 6 | Sensor history readable for any module ID | Likely | High | API1:2023 BOLA |
|
||||
| 7 | Device control likely has no ownership check | Unconfirmed | Critical | API1:2023 BOLA |
|
||||
| 8 | Google Maps API key in HTML, no restriction | Yes | High | CWE-312 |
|
||||
| 9 | Session cookie missing `SameSite` attribute | Yes | High | CWE-352 CSRF |
|
||||
| 10 | No login rate limiting | Yes | High | CWE-307 |
|
||||
| 11 | Sequential ObjectIds accelerate enumeration | Yes | High | CWE-330 |
|
||||
| 12 | Blank CORS header (misconfigured) | Yes | Medium | CWE-942 |
|
||||
| 13 | No Content-Security-Policy | Yes | Medium | CWE-1021 |
|
||||
| 14 | `X-Powered-By` header leaks framework | Yes | Low | CWE-200 |
|
||||
| 15 | MAC/serial bulk exposure | Yes | Medium | CWE-200 |
|
||||
| 16 | Staging uses real data | Yes | Medium | — |
|
||||
| 17 | Input IDs in HTML source | Yes | Info | — |
|
||||
|
||||
---
|
||||
|
||||
## Responsible disclosure note
|
||||
|
||||
These findings were discovered through authorized testing of an account owned by the researcher. The critical BOLA issues (#1–7) should be reported to the MySolem security team before any public disclosure, as they affect all platform customers.
|
||||
@@ -0,0 +1,249 @@
|
||||
# Write-Method Endpoints (POST / PUT / PATCH / DELETE)
|
||||
|
||||
All endpoints that mutate state, sorted by risk. Sources:
|
||||
- Static analysis of `/min/production.min.js` (8MB) and `/linker/js/esbuild-bundle.js` (16MB)
|
||||
- Socket.io call extraction from both bundles
|
||||
- `inputs/lrpc_post.json` Safari capture (real POST payloads)
|
||||
|
||||
**Do not call destructive or control endpoints without intent.** Safe read-only probing stays on GET.
|
||||
|
||||
---
|
||||
|
||||
## Device control (high impact — sends LoRa radio commands)
|
||||
|
||||
### Pool controller impulse / start / stop
|
||||
```
|
||||
POST /remote/module/control
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"moduleSerialNumber": "150302F1DA270001",
|
||||
"linesControl": [
|
||||
{ "index": 0, "action": 0 } // stop output
|
||||
{ "index": 1, "action": 1 } // start (indefinitely)
|
||||
{ "index": 1, "action": 2, "time": "30:00" } // timed impulse MM:SS
|
||||
{ "index": 0, "action": 3, "time": "01:00" } // boost filtration
|
||||
{ "index": 0, "action": 5, "manualDuration": 1 } // pause (?)
|
||||
{ "index": 0, "action": 11, "time": "...", "speed": 1 } // backwash
|
||||
]
|
||||
}
|
||||
```
|
||||
Verified real payloads (mitmproxy captures):
|
||||
- `{"action":0,"index":0}` → stop fill valve (LRLEVEL serial `3A01020D2ED90001`)
|
||||
- `{"action":1,"index":1}` → Station 2, start indefinitely
|
||||
- `{"action":2,"time":"30:00","index":1}` → Station 2, 30 min impulse
|
||||
- `{"action":3,"time":"01:00","index":0}` → boost filtration 1 min (LRPC serial `150302F1DA270001`)
|
||||
|
||||
### Irrigation valve start/stop (form-encoded — confirmed via mitmproxy)
|
||||
```
|
||||
POST /module/sendManualModuleCommand
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
Confirmed payload patterns:
|
||||
|
||||
| Intent | Params |
|
||||
|---|---|
|
||||
| Run station N minutes | `commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=N` |
|
||||
| Cycling station (on/off) | `commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0&commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId}` |
|
||||
| Stop station | `commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId}` |
|
||||
| All stations N minutes | `commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=N` |
|
||||
| Stop everything | `commandType=manualStop` |
|
||||
| Enable (no rain delay) | `commandType=status&command=on&commandParams=0` |
|
||||
| Disable | `commandType=status&command=off&commandParams=0` |
|
||||
| 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** (not an action name).
|
||||
- `agriculturalModuleId` is required for stop and cycling station commands.
|
||||
- `commandType=manualStop` takes no `outputIndex` or `commandParams`.
|
||||
|
||||
### Flush commands to device
|
||||
```
|
||||
POST /canopy/{siteId}/sendModulesCommand {}
|
||||
```
|
||||
|
||||
### Module LoRa data push (forces sync)
|
||||
```
|
||||
POST /modules/sendDataViaLoRaWAN
|
||||
POST /agriculturalModuleGroup/sendDataViaLoRaWAN
|
||||
POST /wateringModuleGroup/sendDataViaLoRaWAN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Program / schedule changes (persistent, survives reboot)
|
||||
|
||||
```
|
||||
PUT /programs { "programs": [...], "module": "moduleId" }
|
||||
POST /programs create new program
|
||||
DELETE /programs delete program
|
||||
PATCH /programs/name rename
|
||||
POST /program/update socket.io variant (immediate push to device)
|
||||
PUT /program/update REST variant
|
||||
POST /program/reportProgramsDataSent acknowledge sync
|
||||
POST /checkAvailableWindows check time conflicts before saving
|
||||
```
|
||||
|
||||
Program window format for `PUT /programs`:
|
||||
```json
|
||||
{
|
||||
"programs": [{
|
||||
"id": "...",
|
||||
"module": "moduleId",
|
||||
"index": 0,
|
||||
"weekDays": 127,
|
||||
"waterBudget": 100,
|
||||
"windows": [{ "start": 300, "end": 360, "on": 3600, "off": 0, "runningDays": 127 }],
|
||||
"programCharacteristics": { "programType": 4, "mode": 2, "onSpeed": 1 }
|
||||
}],
|
||||
"module": "moduleId"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module configuration (persistent device settings)
|
||||
|
||||
```
|
||||
POST /remote/module/configuration socket.io: update device config
|
||||
POST /remote/module/configuration/and/programs config + programs in one push
|
||||
POST /input/update update input settings (name, thresholds)
|
||||
PUT /output/update update output settings (name, flowrate)
|
||||
POST /remote/module/name rename via LoRa
|
||||
PUT /module/update update module metadata
|
||||
PUT /module/update/address
|
||||
PUT /module/update/status
|
||||
POST /module/update/name
|
||||
POST /module/update/securityMode
|
||||
POST /module/reportModuleDataSent acknowledge device sync
|
||||
POST /remote/module/advertisingProfile BLE advertising config
|
||||
POST /remote/modules/advertisingProfile bulk
|
||||
POST /remote/module/dissociate { relayMsn, modulesMsn } — removes pairing
|
||||
PUT /module/dissociate/{moduleId}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Site / pool management
|
||||
|
||||
```
|
||||
POST /canopy-sites create new site
|
||||
PUT /pools/update/address update pool address
|
||||
POST /pools/admin/sendMessage send admin message to pool customer
|
||||
PUT /teams/handleAccessGrantedPools grant pool access to team
|
||||
POST /teams/members/add
|
||||
POST /teams/members/create
|
||||
POST /agency/team/members/add
|
||||
POST /addAgriculturalGroupToUser
|
||||
POST /wateringModuleGroup/create
|
||||
PUT /wateringModuleGroup/update
|
||||
PUT /wateringModuleGroup/update/name
|
||||
PUT /wateringModuleGroup/updateFlowRate
|
||||
DELETE /wateringModuleGroup/delete
|
||||
POST /wateringModuleGroup/remove
|
||||
POST /updateAgriculturalModuleGroup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Journal / notifications
|
||||
|
||||
```
|
||||
PUT /journal/acknowledge/all
|
||||
PUT /journal/acknowledge/records
|
||||
POST /canopy-sites/{siteId}/notifications/acknowledge-all
|
||||
POST /journal/notifications (write variant — mark read?)
|
||||
POST /journal/counts
|
||||
POST /journal/warnings/accepted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content / documents (safe-ish)
|
||||
|
||||
```
|
||||
PUT /favorites
|
||||
PUT /favorites/
|
||||
POST /module/tags { module, tags: [...] }
|
||||
DELETE /document/remove
|
||||
DELETE /canopy/v2/deleteImage
|
||||
POST /hpaform/create
|
||||
POST /library/program-template
|
||||
POST /library/programming-template
|
||||
POST /notices/remove
|
||||
POST /polytropic/setIpxData
|
||||
POST /updateOutputIpxData
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin-only (requires admin role — will 403 on standard account)
|
||||
|
||||
```
|
||||
POST /admin/modules/search
|
||||
DELETE /admin/sim-card/delete
|
||||
POST /admin/sim-card/update
|
||||
POST /manufacturer/upload-blacklist
|
||||
POST /firmwares/removeModuleFirmware
|
||||
POST /pools/admin/sendMessage
|
||||
POST /pools/admin/search
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to discover more (without executing)
|
||||
|
||||
Three complementary techniques, all read-only:
|
||||
|
||||
**1. Static JS analysis** (already done above):
|
||||
```bash
|
||||
# All $.ajax write calls
|
||||
grep -oE 'type:"(POST|PUT|PATCH|DELETE)"[^}]{0,200}url:"[^"]*"' /tmp/indygo_prod.js
|
||||
|
||||
# All fetch() write calls
|
||||
grep -oE 'fetch\("[^"]*"[^)]{0,200}method:"(POST|PUT|PATCH|DELETE)"' /tmp/indygo_prod.js
|
||||
|
||||
# Socket.io write calls
|
||||
grep -oE 'io\.socket\.(post|put|patch|delete)\("[^"]*"' /tmp/indygo_prod.js
|
||||
```
|
||||
|
||||
**2. Safari Web Inspector recording** — record a session where you perform the action in the real app, then parse `inputs/*.json`:
|
||||
```python
|
||||
import json
|
||||
with open("inputs/lrpc_post.json") as f:
|
||||
data = json.load(f)
|
||||
records = data["recording"]["records"]
|
||||
for rec in records:
|
||||
req = rec.get("entry", {}).get("request", {})
|
||||
if req.get("method") in ("POST","PUT","PATCH","DELETE"):
|
||||
print(req["method"], req["url"])
|
||||
print(" ", req.get("postData", {}).get("text", "")[:200])
|
||||
```
|
||||
|
||||
**3. Mitmproxy / Charles** (more complete than Safari — captures socket.io frames):
|
||||
```bash
|
||||
mitmproxy --mode transparent --ssl-insecure -s dump_writes.py
|
||||
# dump_writes.py: filter on method != GET, log url + body
|
||||
```
|
||||
Safari doesn't capture WebSocket frames (socket.io messages), so LoRa push commands that go via socket.io are invisible in recordings.
|
||||
|
||||
---
|
||||
|
||||
## Socket.io write calls (discovered via bundle grep)
|
||||
|
||||
These bypass the normal HTTP layer — standard Safari recording captures neither the URL nor body:
|
||||
```
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user