941abd98d5
Dynamic MySolem/MyIndygo IoT bridge with MQTT, Home Assistant autodiscovery, and Homebridge EasyMQTT compatibility. - Device discovery via MySolem API on startup (all modules/inputs/outputs) - Configurable poll engine (fast/normal/slow buckets, per-input overrides) - MQTT retained state publishing + HA autodiscovery payloads - Unified manual command routing (linesControl vs sendManualModuleCommand) - Runtime config via MQTT $config/set, persisted to config.json - Expression override system (API-provided > hardcoded > passthrough) - Minimal REST API: /health, /state, /control, /rediscover - Docker deployment (Dockerfile + docker-compose.yml) - 38 unit tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
344 lines
9.5 KiB
Markdown
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` |
|