Files
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

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 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):

{
  "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