Files
myprimal/api-docs/mobile-api.md
T
arnaudne 941abd98d5 feat: initial myprimal MQTT bridge service
Dynamic MySolem/MyIndygo IoT bridge with MQTT, Home Assistant
autodiscovery, and Homebridge EasyMQTT compatibility.

- Device discovery via MySolem API on startup (all modules/inputs/outputs)
- Configurable poll engine (fast/normal/slow buckets, per-input overrides)
- MQTT retained state publishing + HA autodiscovery payloads
- Unified manual command routing (linesControl vs sendManualModuleCommand)
- Runtime config via MQTT $config/set, persisted to config.json
- Expression override system (API-provided > hardcoded > passthrough)
- Minimal REST API: /health, /state, /control, /rediscover
- Docker deployment (Dockerfile + docker-compose.yml)
- 38 unit tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 01:05:24 +02:00

344 lines
9.5 KiB
Markdown

# 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` |