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>
9.5 KiB
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:
{
"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}
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:
{
"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
5de53c1c7ed4c45c2cfe0688for the Domicile pool — previously only accessible via the mainanelissen@solem.fraccount 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):
{
"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 |