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:
@@ -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` |
|
||||
Reference in New Issue
Block a user