Files
myprimal/api-docs/REFERENCE.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

48 KiB
Raw Blame History

MySolem / MyIndygo — Complete API Reference

Reverse-engineered from black-box analysis: mitmproxy captures of iOS apps, Safari Web Inspector recordings, JS bundle static analysis, and live endpoint probing.

Sources: inputs/apps_captured_all.jsonl, inputs/lrpc_post.json, inputs/mysolem_captured_all.jsonl, inputs/indygo_captured_all.jsonl, inputs/captured_all.jsonl, JS bundles at /min/production.min.js and /linker/js/esbuild-bundle.js.


Table of Contents

  1. Platforms & Architecture
  2. Authentication
  3. Key IDs — Domicile Site
  4. Web API — Endpoints
  5. Mobile API — /api/ Endpoints
  6. Device Control
  7. MyIndygo — Pool Platform
  8. Device Reference — Domicile
  9. Write Endpoints Reference
  10. Reference Tables
  11. Security Findings

1. Platforms & Architecture

Platform Base URL Focus
MySOLEM https://qualif.mysolem.com Irrigation, garden sensors
MyIndygo https://qualif.myindygo.com Pool control, water chemistry

Both platforms share the same backend database and API server — same module IDs, input IDs, session cookie, and sensor data. MyIndygo adds a pool admin/management layer on top. All web endpoints work identically on either host.

Backend: Node.js + Sails.js v0.13.8, MongoDB (all IDs are 24-char hex ObjectIds)
Proxy: nginx/1.18.0 (Ubuntu)
Real-time: Socket.IO v3 / Engine.IO v3 (EIO=3), direct WebSocket transport (not long-polling)

wss://qualif.mysolem.com/socket.io/?EIO=3&transport=websocket

Two separate API layers on the same host:

  • Web API — no path prefix, session cookie auth
  • Mobile API/api/ prefix, OAuth2 Bearer token auth

2. Authentication

Cookie-based session (Sails.js). No CSRF token required on JSON endpoints.

# Step 1 — init cookie jar
curl -c cookies.txt https://qualif.mysolem.com/login

# Step 2 — authenticate
curl -c cookies.txt -b cookies.txt \
  -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=YOUR_EMAIL&password=YOUR_PASSWORD" \
  "https://qualif.mysolem.com/login"
# → 302 redirect to / on success

# All subsequent calls
curl -b cookies.txt https://qualif.mysolem.com/features

Cookie name: qualification-mysolem-solem-irrigation-platform.sid
Session TTL: ~7 days
Same cookie works on both qualif.mysolem.com and qualif.myindygo.com.

Python:

import requests

BASE = "https://qualif.mysolem.com"
s = requests.Session()
s.get(f"{BASE}/login")                     # init cookie
s.post(f"{BASE}/login", data={
    "email": "YOUR_EMAIL",
    "password": "YOUR_PASSWORD",
})                                         # 302 → / on success
# s carries the session cookie automatically on all subsequent calls

2.2 Mobile API — OAuth2 Bearer Token

The iOS apps use a completely separate auth layer at /api/ prefix.

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 and validated server-side — not present as explicit headers in captured traffic.

All subsequent mobile API 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}"}

resp = requests.get(f"{BASE}/api/getUser", headers=headers)

3. Key IDs — Domicile Site

Users

Platform Account User ID
MySOLEM arnaud.nelissen.api@icloud.com 5de53bca7ed4c45c2cfe067f
MyIndygo arnaud.nelissen.api@icloud.com 6a21c3db5ebe0fa5f7885460

Site

Resource ID
Site (Domicile) 62b30d26ecbceb08607592c1
Pool (Domicile) 5de53c1c7ed4c45c2cfe0688
Field supervision 643c5134347b27edbf38b173

Modules

Module Type Module ID Serial Number childSerial6
LRMB10-070532 (gateway) lr-mb-10 60ca4276ad69dac1558f7c61 1000000705320001
LRMS-0721DC (sensor station) lr-ms 60ca42adad69dac1558f7c64 7400040721DC0001 0721DC
LR6AG-070917 (irrigation valve) lr-ag 626984f038ec558c598bb6d5 6606010709170001 070917
LRPS-0565C0 (pool analyser) lr-mas 6480da07c2c0896da120ecca 9400030565C00001 0565C0
LRPC-F1DA27 (pool controller) lr-pc 6481bcea8782b78554c02bed 150302F1DA270001 F1DA27
LR2IPECO-0C9282 (IP ECO) lr-ip-eco 6516ca57b7c098e6807cb332 3802020C92820001 0C9282
LRPR-0D6800 (filter pressure) lr-pr 6a17218be0b88ea3e925524f 1D00010D68000001 0D6800
POOL-LVL-0E953B (float switch) pool-level-sensor 6a174dd1e0b88ea3e925531c 0300020E953B0001
LRLEVEL-0D2ED9 (fill valve) lr-niv 6a1c4b153d6c33ab843edc13 3A01020D2ED90001 0D2ED9

All child modules route through gateway 1000000705320001 (LRMB10) via LoRa radio.

Inputs

Sensor Module Input ID Type Unit
Plumbago moisture LRMS 60ca42adad69dac1558f7c66 12 %
Pelouse moisture LRMS 60ca42adad69dac1558f7c67 12 %
Air temperature LRMS 60ca42adad69dac1558f7c69 5 °C
Rain sensor LR6AG 626984f038ec558c598bb6d7 2 0/1
Pool water temperature LRPS 6480da07c2c0896da120eccc 133 °C
Pool pH LRPS 6480da07c2c0896da120eccd 6 pH (raw ÷ 100)
Pool ORP / Redox LRPS 6480da07c2c0896da120ecce 7 mV
Flow meter LR2IPECO 6516ca57b7c098e6807cb334 33 L
Filter pressure LRPR 6a17218be0b88ea3e9255251 8 bar
Pool level switch POOL-LVL 6a174dd1e0b88ea3e925531e 21 0/1
Fill volume LRLEVEL 6a1c4b153d6c33ab843edc15 33 L
Fill valve state LRLEVEL 6a1c4b153d6c33ab843edc16 149 0/1
LRPC temperature LRPC 6481bcea8782b78554c02bef 133 °C

Outputs

Output Module Output ID Index Flow rate
AG Station 1 LR6AG 626984f038ec558c598bb6d8 0 30 m³/h
AG Station 2 LR6AG 626984f038ec558c598bb6d9 1 14 m³/h
AG Station 3 LR6AG 626984f038ec558c598bb6da 2 20 m³/h
AG Station 4 LR6AG 626984f038ec558c598bb6db 3 1 m³/h
AG Station 5 LR6AG 626984f038ec558c598bb6dc 4 38 L/h
AG Station 6 LR6AG 626984f038ec558c598bb6dd 5 38 L/h
LRPC Station 1 (filtration pump) LRPC 6481bcea8782b78554c02bf1 0
LRPC Station 2 (pool light) LRPC 6481bcea8782b78554c02bf2 1
LRPC Station 3 LRPC 6481bcea8782b78554c02bf3 2
IPECO Station 1 LR2IPECO 6516ca57b7c098e6807cb336 0
IPECO Station 2 LR2IPECO 6516ca57b7c098e6807cb337 1
LRLEVEL fill valve LRLEVEL 6a1c4b153d6c33ab843edc17 0

Programs

Program Module Program ID Index Type
Filtration (4h/day) LRPC 6481bcea8782b78554c02bf4 0 4 (filtration)
Auxiliary/cyclic LRPC 6481bcea8782b78554c02bf5 1 2 (auxiliary)
Unused LRPC 6481bcea8782b78554c02bf6 2

4. Web API — Endpoints

All paths relative to https://qualif.mysolem.com (or https://qualif.myindygo.com — identical backend). All require session cookie.

4.1 Feature Flags

GET /features

Returns flat JSON object of boolean flags: dashboard, enableHydraulicSupervision, enableBSTSubscriptions, enablePolytropicAPI, enableGraphicalProgrammation, enableNdviSatellite, enableEvapotranspiration, ocedisWaterChemistryCorrection, etc.

4.2 Sites

GET /canopy-sites?userId={userId}

Returns { items: [...], total: N }. Each item includes full modules array and fencing config.

GET /canopy-sites/{siteId}/users
GET /canopy-sites/{siteId}/modules?limit=20&skip=0&includeTotalCount=true
GET /canopy-sites/{siteId}/modules/indicators
GET /canopy-sites/{siteId}/informations
GET /canopy-sites/{siteId}/rules
GET /canopy-sites/{siteId}/map/modules
GET /canopy-sites/{siteId}/modules/eco-mode-compatible
GET /canopy-sites/{siteId}/site-notes
GET /canopy-sites/{siteId}/notifications
GET /canopy/{siteId}/waterBudget

4.3 Modules

Search / list (MongoDB-style filter):

POST /modules/search
Content-Type: application/json

{
  "query": { "owner": "userId" },
  "projection": { "name": 1, "type": 1, "serialNumber": 1 }
}

Response: { result: { results: [...], totalCount: null } }
⚠ Empty query {} returns all 4,607 platform modules — no ownership enforcement.

GET /modules/available?userId={userId}
GET /modules/echarts-data?includeCustomers=true&includeAgency=false
→ {chartModuleTypes: [{name, value}], chartModuleConnectivities: [...]}

GET /sim-cards/needsPayment?moduleIds={id1}&moduleIds={id2}
→ {result: {"moduleId": false, ...}}

Flexible module list:

POST /users/{userId}/modules
Content-Type: application/json

{
  "fields": {"name": 1, "type": 1, "serialNumber": 1, "displayType": 1},
  "filters": {},
  "addOwnershipFlag": true,
  "includeTotalCount": true,
  "limit": 20,
  "skip": 0
}

Response: {modules: [...], totalCount: N}

Remote state (cached + live LoRa):

GET /remote/module/state?moduleId={moduleId}
GET /remote/module/configuration?moduleId={moduleId}
GET /remote/module/configuration/and/programs?moduleId={moduleId}
GET /remote/module/informations?moduleId={relayId}&childrenSerialNumber={childSerial}

Live endpoints trigger a LoRa radio roundtrip — returns module_not_reachable if device is offline.

Module metadata:

GET /modules/{moduleId}/users
GET /modules/{moduleId}/tags
GET /modules/{moduleId}/notebook?startDate={unix_epoch_seconds}
GET /input/getInputsLinkedModule/{moduleId}

User avatar:

GET /user/avatar          → image/png (no-cache)
GET /user/avatar?t={ms}   → cache-bust variant

4.4 Sensor Data (Inputs)

Batch input config:

GET /inputs?inputId=id1%3Bid2%3Bid3

Semicolons (%3B) separate multiple IDs. Returns full input objects with expression, type, unit, module, index.

All inputs for a module (most efficient for dashboard loads):

GET /remote/module/getComputedSensorsData
  ?moduleId={moduleId}
  &startDate={iso8601}
  &endDate={iso8601}
  &forceRawOrComputed=computed

Returns array of input objects, each with computedSensorData ticks. Pass forceRawOrComputed=raw for raw ADC values.

Single input time-series:

GET /remote/input/getComputedSensorData
  ?inputId={inputId}
  &startDate={iso8601}
  &endDate={iso8601}
  &ensurePrevTickForContinuity=true

Response:

{
  "id": "60ca42adad69dac1558f7c66",
  "name": "Plumbago",
  "type": 12,
  "unit": 10,
  "interval": 60,
  "computedSensorData": [
    { "tickTimestamp": "2026-06-01T00:42:00.000Z", "value": 54.1875 }
  ]
}

Last tick only (fastest — no date range needed):

GET /input/getLastTickFromInput?inputId={inputId}

Returns { "tick": { "input": "...", "value": <raw>, "timestamp": "...", "id": "..." } }.
Value is raw (pre-expression). Apply the input's expression to get the computed value.
LRPS pH: raw ÷ 100 → pH. LRPS temp: raw ÷ 100 → °C.

Averages & CSV:

GET /inputs/average?inputIds={id1},{id2}&startDate={iso8601}&endDate={iso8601}
GET /input/getComputedInputIpxDataBetweenDatesAsCsv?inputId={id}&startDate={iso8601}&endDate={iso8601}

Live pool sensor snapshot (Python):

POOL_INPUTS = {
    "temp":     "6480da07c2c0896da120eccc",
    "pH":       "6480da07c2c0896da120eccd",
    "ORP":      "6480da07c2c0896da120ecce",
    "pressure": "6a17218be0b88ea3e9255251",
    "level":    "6a174dd1e0b88ea3e925531e",
}
readings = {}
for name, input_id in POOL_INPUTS.items():
    r = s.get(f"{BASE}/input/getLastTickFromInput", params={"inputId": input_id}).json()
    readings[name] = r.get("tick", {}).get("value")
# pH: readings["pH"] / 100  → pH units
# temp: readings["temp"] / 100  → °C

4.5 Outputs

GET  /outputs/modules?moduleIds={moduleId}
POST /outputs/search
     {"query": {"module": {"$in": ["moduleId"]}}}
POST /output/update

GET /output/events?moduleId={moduleId}
GET /output/events?outputId={outputId}&startDate={iso8601}&endDate={iso8601}
GET /output/forecast?moduleId={moduleId}
GET /output/forecast?outputId={outputId}

Response structure: { outputs: [{ id, module, index, name, flowRate: { value, unit } }] }
Flow rate unit: 1 = L/h, 2 = m³/h.

4.6 Programs

GET  /programs/modules?moduleIds={moduleId}
PUT  /programs         {"programs": [...], "module": "moduleId"}
POST /programs         create new program
DELETE /programs
PATCH /programs/name
POST /programs/search  {"query": {"id": {"$in": [...]}}}
GET  /programs/reset?moduleId={moduleId}
POST /checkAvailableWindows    check time conflicts before saving

Program object:

{
  "id": "...",
  "name": "Pelouse Terrasse",
  "module": "moduleId",
  "index": 0,
  "programmingType": "temporal",
  "weekDays": 127,
  "waterBudget": 100,
  "windows": [{ "start": 420, "end": 422, "on": 120, "off": 0 }]
}

Window times are minutes from midnight. on / off are in seconds. weekDays 127 = every day (bitmask, bit 0 = Monday).

LRPC extended program fields (pool controller):

{
  "programCharacteristics": {
    "programType": 4,
    "mode": 2,
    "onSpeed": 1,
    "speedSequence": 4368,
    "microFilteringTemperature": 5,
    "microFilteringDuration": 10,
    "microFilteringPeriod": 480,
    "frostFreeSpeed": 1
  },
  "windows": [{ "start": 300, "end": 360, "on": 3600, "off": 0, "runningDays": 127 }]
}

programType: 4 = filtration, 2 = auxiliary/cyclic, 0 = disabled.

4.7 Users

GET  /users-info/{userId}?includeUserGroups=true
GET  /users/{userId}/modules
GET  /users/{userId}/sites
GET  /users/{userId}/module-groups
GET  /users/{userId}/subscriptionModules
GET  /users/{userId}/checkSubscriptionSimcardStatus
POST /users/search   {"query": {...}, "projection": {...}}

Grouped resources (paginated POST):

POST /users/{userId}/module-groups           → {moduleGroups, totalCount}
POST /users/{userId}/agricultural-module-groups → {agriculturalModuleGroups, totalCount}
POST /users/{userId}/watering-module-groups  → {wateringModuleGroups, totalCount}
POST /users/{userId}/customers               → {customers, totalCount}
POST /users/{userId}/associated              → {users, totalCount}
POST /users/{userId}/tags                    → {tags}

Activity log:

POST /users/{userId}/records
{
  "limit": 10, "skip": 0,
  "filters": {"startDate": "2026-05-28T21:51:48.266Z", "showAllEvents": true},
  "includeTotalCount": true,
  "sortColumn": "timestamp", "sortOrder": -1,
  "fields": {"category": 1, "label": 1, "module": 1, "timestamp": 1}
}
→ {records: [...], totalCount: N}

POST /users/{userId}/records/categories
{"filters": {"startDate": "..."}, "includeCustomers": true}
→ [{category, displayCategory}]

4.8 Module Notebook

Full chronological history of manual commands, alerts, firmware updates, pool events:

GET /modules/{moduleId}/notebook?startDate={unix_epoch_seconds}

startDate is a Unix timestamp in seconds (not ISO 8601).

Response: { "records": [...] } sorted oldest-first.

{
  "category": "manual_command",
  "label": "...",
  "priority": "low",
  "state": "archived",
  "timestamp": "2026-06-04T21:00:00.000Z",
  "params": { "action": 2, "index": 1, "manualDuration": 1800 },
  "input": "inputId"
}

Known category values: manual_command, pool, platform_alert, firmware_update.
For pool category: params.poolAlertType = backwashReminder, backwashDone, poolLevelSensorCleaningReminder, etc.
For manual_command: params mirrors the linesControl action/index that triggered the event. Note: manualDuration in notebook is stored in seconds internally, regardless of the time:"MM:SS" format sent to the API.

4.9 Notifications & Journal

GET  /journal/notifications
GET  /journal/counts
GET  /journal/warnings/accepted
PUT  /journal/acknowledge/all
POST /canopy-sites/{siteId}/notifications/acknowledge-all

Filtered site notifications:

POST /canopy-sites/{siteId}/notifications
{"limit": 1, "includeTotalCount": true, "filters": {"state": {"$in": ["unread", "process"]}}}
→ {records: [...]}

Cross-site journal:

POST /journal/notifications
{
  "canopyIds": ["siteId"],
  "limit": 100, "skip": 0,
  "filters": {"priority": {"$in": ["critical", "high"]}, "state": {"$in": ["unread", "process"]}},
  "projection": {...}
}
→ {records: [...], total: N}

4.10 Weather

GET /moduleGroups/getCanopyWeather
  ?location[latitude]={lat}&location[longitude]={lng}

Returns 3-day AccuWeather-style forecast.

4.11 Field Supervision

GET /canopy/{siteId}/field-supervisions/modules
→ {canopyModules: [{type, modules: [{serialNumber, name, outputs: [{id, index, name}]}]}]}

GET /canopy/{siteId}/field-supervision/{supervisionId}/resume
→ {allPrograms: [...]}

GET /moduleGroups/{siteId}/field-supervision/{supervisionId}/events/{startISO}/{endISO}?timezone=Europe/Paris
→ {events: [{eventId, moduleId, moduleName, identifier, name, start, end, flowRate, index}]}

Field supervision ID for Domicile: 643c5134347b27edbf38b173

4.12 Satellite / NDVI Imagery

Host: sentinel.mysolem.com (separate subdomain).

GET /moduleGroups/{siteId}/fencing/{timestampMs}/images-check
→ {availableImages: {available: true, reason: "ok"}}

GET /moduleGroups/{siteId}/fencing/{timestampMs}/images
→ {imagesList: {images: [{name, urlRgb, urlSavi}]}}

GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/savi  → PNG (vegetation index)
GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/rgb   → PNG (true color)

Timestamp format: YYYYMMDDTHHmmss, e.g. 20220417T103631.
Domicile plot ID: 62f4efcb4a6dd563203558c3, fencing timestamp: 1656939129360.

4.13 Clusters & Daily Data

GET  /clusters/{clusterId}/data
GET  /clusters/{clusterId}/modules
GET  /clusters/{clusterId}/programs
PUT  /clusters/{clusterId}/programs
PATCH /clusters/{clusterId}/name
PATCH /clusters/{clusterId}/flowrate
DELETE /clusters/{clusterId}

GET /garden/dailyData/{moduleId}
GET /modules/heatmap-data?moduleIds={ids}&startDate=X&endDate=Y

4.14 Pool-Specific (MyIndygo)

GET /user/pools                     → current user's pools (⚠ returns [] for API test account)
GET /user/pools?userId={id}         → another user's pools (IDOR — no ownership check)
GET /maintenance-logs-data?moduleId={moduleId}

maintenance-logs-data returns { code, messages: [...] }. Each message has poolAlertType, priority, state, timestamp, and a pool object with full pool config and pool.status (live sensor values, last pressure, last pH, etc.).

Pool admin (admin-only, 403 on standard account):

GET    /pools/admin
POST   /pools/admin/search
POST   /pools/admin/sendMessage
GET    /pools/admin/getMessages
DELETE /pools/admin/dissociateAndRemove/{id}
GET /teams/pools/{poolId}/membersAccessGranted
→ {teamMembersAlreadyGrantedPool: [...]}

5. Mobile API — /api/ Endpoints

Base URL: https://qualif.mysolem.com (same host, different path prefix).
Auth: Authorization: Bearer {token} on all requests.

5.1 User

GET /api/getUser
GET /api/getUserWithHisModules
GET /api/getUserPools
GET /api/getUserWateringModuleGroups   → {"wateringModuleGroups": []}
GET /api/getUserAgriculturalModuleGroups → {"agriculturalModuleGroups": []}
GET /api/getCustomers                  → {"err": {"code": "user_is_not_professional"}}
GET /api/users/{userId}/canopy-sites
GET /api/users/{userId}/access-requests
GET /api/users/{userId}/module-access-requests

GET /api/getUserPools is the only endpoint that exposes the pool record 5de53c1c7ed4c45c2cfe0688 via the API test account — the web platform /user/pools returns empty for that account.

5.2 Pool

GET /api/getPoolStatus?attributesToPopulate[]=modules&poolIdentifier={poolId}

Returns pool status with full modules array. Pool ID for Domicile: 5de53c1c7ed4c45c2cfe0688.

Pool config fields: volume (25 m³), installationType, filteringType, waterTreatmentType, phRegulationMode, chlorineRegulationMode, heatingSystemType, coverType, cyanuricAcidRate, model, latitude, longitude, address.

5.3 Modules

GET /api/getModuleWithHisUsers?module={moduleId}
GET /api/getModuleInventory?module={gatewaySerial}&startDate={iso8601}&endDate={iso8601}&data=1
→ {children: [...]}   ← all child modules under this gateway with full config
GET /api/getModuleBackups?module={moduleId}
→ array of saved program snapshots
GET /api/getPrograms?id={moduleId}
→ full program objects including startTimesCycles, stationsDuration
PUT /api/updateModule
Content-Type: application/json
{"module": {"id": "moduleId", "batteryVoltage": 77, "powerState": 0, "status": {...}}}

5.4 Gateway-Routed Module Access

All real-time commands go through the LRMB10 gateway. URL pattern:

/api/module/{gatewaySerial}/{action}/{childSerial6}
  • gatewaySerial = full 16-char serial, e.g. 1000000705320001
  • childSerial6 = last 6 hex chars of the child module's serial (case-insensitive)

Domicile routing:

Child childSerial6 Full serial
LRPC-F1DA27 F1DA27 150302F1DA270001
LR6AG-070917 070917 6606010709170001
LRMS-0721DC 0721DC 7400040721DC0001
LR2IPECO-0C9282 0C9282 3802020C92820001
LRLEVEL-0D2ED9 0D2ED9 3A01020D2ED90001
LRPR-0D6800 0D6800

Read endpoints:

GET /api/module/{gatewaySerial}/information              ← gateway itself
GET /api/module/{gatewaySerial}/information/{child6}    ← child real-time info (LoRa roundtrip)
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 (= getModuleInventory)

Gateway information response example:

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

Write endpoints:

POST /api/module/{gatewaySerial}/status/{child6}
Content-Type: application/json
{"ag": [{"index": 0, "rainDelay": 0}, {"index": 1, "rainDelay": 0}]}
→ sets rain delay per output; response includes full device status

POST /api/module/{gatewaySerial}/programs/{child6}
Content-Type: application/json
{"programs": [...], "programmationType": 1}
→ sends programs to device over LoRa

POST /api/module/{gatewaySerial}/manual/{child6}
Content-Type: application/json
{...command body...}
→ sends a manual command to device over LoRa

5.5 Sensor & Chart Data

GET /api/getInputs?id={moduleId}
→ all inputs for module with expression, expressionInverse, alert config

GET /api/getComputedChartData
  ?inputId={inputId}
  &startDate={iso8601}
  &endDate={iso8601}
  &numberOfBar={N}
  &locale=fr
→ input data bucketed into N bars; includes computedSensorData array

GET /api/getComputedOutputActivationHistory
  ?moduleId={moduleId}
  &outputIndex={0|1|2}
  &startDate={iso8601}
  &endDate={iso8601}
  &numberOfBar={N}
→ [{currentPeriodStartDate, currentPeriodEndDate, value}]
   value=1 means output was active during that period
   numberOfBar=1440 over 24h = 1 bar per minute

5.6 Programs (Mobile)

GET /api/getPrograms?id={moduleId}
PUT /api/updatePrograms
Content-Type: application/json
{
  "programmationType": 1,
  "programs": [{"id": "...", "name": "...", "waterBudget": 100, "windows": [...], "programCharacteristics": {...}}]
}
→ {updatedPrograms: [...]}

POST /api/library/program-templates
{"limit": 10, "moduleId": "moduleId", "offset": 0}
→ {"programTemplates": []}

5.7 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}}]
Profiles: default, ecoMode, etc.

GET /api/getAllBanishedSerialNumbers
→ ["120C0200750001", ...]
Blacklisted/revoked device serial numbers.

5.8 Reporting (App Telemetry)

Called by the app after sending data to devices — informational, do not depend on responses.

POST /api/reportModuleDatasSent    {"module": "moduleId"}
POST /api/reportProgramsDatasSent  {"programs": ["programId", ...], "module": "moduleId"}
POST /api/reportManualCommandSent  {"route": "http", "command": {...}, "id": "moduleId"}

5.9 Push Notifications

POST /api/addApnsTokenToUser
{"isSandbox": false, "topic": "com.mysolem.qualif", "user": "{userId}", "token": "{apnsToken}"}
→ {"code": "ok"}

5.10 Web vs Mobile API Comparison

Feature Web (cookie) Mobile (Bearer)
Auth Session cookie OAuth2 Bearer
Pool access Admin-only / main account GET /api/getUserPools
Module routing Direct by ID Via gateway serial + childSerial6
Sensor data getComputedSensorData (raw ticks) getComputedChartData (bucketed)
Output history output/events getComputedOutputActivationHistory
Programs programs/modules getPrograms (richer fields)
Module inventory modules/search getModuleInventory via gateway
Program backups getModuleBackups

6. Device Control

All control commands work on both qualif.mysolem.com and qualif.myindygo.com. All require session cookie.

6.1 Pool Controller (LRPC) — linesControl

Used for LRPC (lr-pc), LRLEVEL (lr-niv), and other pool devices.

POST /remote/module/control
Content-Type: application/json

{
  "moduleSerialNumber": "150302F1DA270001",
  "linesControl": [
    {"index": 0, "action": 1}
  ]
}

moduleSerialNumber is the device's serialNumber field (not the MongoDB id).
Multiple entries in linesControl can be sent in one call.

Action codes (verified from app mitmproxy captures + live testing):

action meaning extra params
0 Stop pump (index 0 only)
1 Stop / cancel any running command
2 Timed impulse time: "MM:SS" string (e.g. "30:00" = 30 min)
3 Boost filtration time: "MM:SS"
4 Start indefinitely (runs until action 1)
5 Pause N days (blocks manual + scheduled programs) manualDuration: N (integer days)
11 Backwash cycle time: encoded duration, speed: pump speed level

Important notes:

  • Action 0 returns {"erreurtraitement": 6} if called on a timed output that's running.
  • To stop a running timed output: send action 1, or override with action 2, time: "00:01".
  • Action 5 manualDuration is days (not seconds). Response includes "pause": N (days remaining).
  • Response shows pre-command device state — the new command takes effect on the next LoRa downlink window.

Response structure (LRPC):

{
  "dialogTimeStamp": "2026-06-04T22:35:16Z",
  "ackTimestamp": 1780612516,
  "temperature": 19,
  "pool": [
    {"index": 0, "time": "00:00", "tempRef": 7, "value": 0},
    {"index": 1, "origin": 2, "time": "30:00", "value": 1, "info": ["man"]},
    {"index": 2, "time": "00:00", "tempRef": 7, "value": 0}
  ],
  "sensorState": [
    {"index": 0, "value": 2318},
    {"index": 1, "value": 2147483647}
  ],
  "inputsAlerts": [],
  "radio": ["cmd"]
}

pool[n].value: 1 = running, 0 = stopped.
pool[n].origin: 2 = manual, 3 = boost.
pool[n].info: ["man"] = manual, ["boost"] = boost mode.
pool[n].pause: days remaining on a pause (only present when paused).
radio: ["cmd"] = command queued for LoRa transmission.

Common examples:

# Start pool light (index 1) for 30 minutes
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
  -H "Content-Type: application/json" \
  -d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":2,"time":"30:00"}]}'

# Stop pool light
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
  -H "Content-Type: application/json" \
  -d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":1}]}'

# Boost filtration for 2 hours
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
  -H "Content-Type: application/json" \
  -d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":0,"action":3,"time":"02:00"}]}'

# Pause pool light for 3 days (disables manual + programs)
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
  -H "Content-Type: application/json" \
  -d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":5,"manualDuration":3}]}'

# Stop fill valve (LRLEVEL, index 0)
curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \
  -H "Content-Type: application/json" \
  -d '{"moduleSerialNumber":"3A01020D2ED90001","linesControl":[{"action":0,"index":0}]}'

6.2 Irrigation Valve (LR6AG) — sendManualModuleCommand

Used for lr-ag, lr-ip-eco, and other irrigation modules.

POST /module/sendManualModuleCommand
Content-Type: application/x-www-form-urlencoded

All patterns confirmed via mitmproxy captures.

Intent Form params
Run station N minutes commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=N
Run all stations N minutes commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=N
Cycling station (on/off intervals) commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0&commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId}
Stop specific station commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId}
Stop everything commandType=manualStop
Enable (no rain delay) commandType=status&command=on&commandParams=0
Disable / rain delay N days commandType=status&command=off&commandParams=N
Max rain delay commandType=status&command=off&commandParams=255
Run program commandType=program&command={programId}

Notes:

  • command accepts an output ID string, "allStations", or a program ID.
  • agriculturalModuleId is required for stop and cycling station commands.
  • commandType=manualStop takes no outputIndex or commandParams.

JSON body variant (also accepted):

{
  "moduleSerialNumber": "6606010709170001",
  "commandType": "station",
  "outputIndex": 0,
  "command": null,
  "commandParams": {"hours": 0, "minutes": 15, "on": 0, "off": 0}
}

Rain delay example (suspend station 0 indefinitely):

{
  "moduleSerialNumber": "6606010709170001",
  "commandType": "status",
  "command": "off",
  "commandParams": 255,
  "outputIndex": 0
}

6.3 Flush Commands to Device

Triggers the platform to push all queued commands to a site's modules immediately:

POST /canopy/{siteId}/sendModulesCommand
Content-Type: application/json
{}

Response: 202 Accepted{ "message": "Modules command accepted for processing" }

Use after PUT /programs to force immediate LoRa push.

6.4 LoRa Data Push (Force Sync)

POST /modules/sendDataViaLoRaWAN
POST /agriculturalModuleGroup/sendDataViaLoRaWAN
POST /wateringModuleGroup/sendDataViaLoRaWAN

6.5 Module Rename

GET /remote/module/name?moduleId={moduleId}&name={newName}

7. MyIndygo — Pool Platform

MyIndygo (https://qualif.myindygo.com) is the pool-focused frontend on the same backend. All endpoints from section 4 work identically.

7.1 Pool Auth Note

The test API account (arnaud.nelissen.api@icloud.com) has different user IDs on each platform:

  • MySOLEM: 5de53bca7ed4c45c2cfe067f
  • MyIndygo: 6a21c3db5ebe0fa5f7885460

The pool ID 5de53c1c7ed4c45c2cfe0688 is accessible via the mobile API (GET /api/getUserPools) or when logged in as the main account (anelissen@solem.fr) on the web.

7.2 LRPC Filtration Schedule (Domicile)

Current program 0 (filtration, type 4), runs every day:

Window Start End Duration
Morning 05:00 06:00 1h
Midday 12:00 14:00 2h
Evening 22:00 23:00 1h
Total 4h/day

Special features:

  • Micro-filtration (frost mode): activates every 480 min for 10 min when temp < 5°C, speed 1
  • Temperature-adaptive: 2 schedules (96d + 31d/year) with 12 temperature thresholds adjusting run windows

Program 1 (auxiliary, type 2): 21:3022:30 active window, 5-min cycles — likely electrolyser or secondary treatment pump.

7.3 Pool Sensor Quick Reference

Sensor Input ID Unit Notes
Water temp 6480da07c2c0896da120eccc °C (raw ÷ 100) Healthy: 2028°C
pH 6480da07c2c0896da120eccd pH (raw ÷ 100) Target: 7.27.6
ORP/Redox 6480da07c2c0896da120ecce mV (raw) Healthy: 650750 mV
Filter pressure 6a17218be0b88ea3e9255251 bar: (1.5×raw500)/1000 Running: ~0.700.75 bar; off: ~0.08 bar
Pool level switch 6a174dd1e0b88ea3e925531e 0/1 0 = fill needed, 1 = OK
Fill volume 6a1c4b153d6c33ab843edc15 L Volume added per fill cycle
Fill valve state 6a1c4b153d6c33ab843edc16 0/1 1 = valve closed

7.4 Fill System Logic

pool-level-sensor (float switch) reads 0 (low)
  → LRLEVEL (lr-niv) opens fill valve (output 0, Station 1)
    → edc15 input measures litres added
      → valve closes when pool-level-sensor returns 1 (OK)

Filter pressure (LRPR) can be used to detect filtration start/stop and dirty filter (pressure rises above ~0.80 bar when filter needs backwash). Backwash reminder fires every ~15 days.

7.5 MyIndygo Feature Flags

Pool-specific flags returned by GET /features on qualif.myindygo.com:

Flag Meaning
ocedisWaterChemistryCorrection OCEDIS chemical dosing correction
enableBSTSubscriptions BST subscription management
enablePolytropicAPI Variable-speed pump (Polytropic) integration
enableGraphicalProgrammation Visual program schedule editor
enableNdviSatellite NDVI satellite imagery
enableEvapotranspiration ET-based automated watering
clusterTurfOpened Turf cluster watering mode

8. Device Reference — Domicile

8.1 LRMB10-070532 — LoRa Gateway

ID: 60ca4276ad69dac1558f7c61 | Serial: 1000000705320001
All other modules communicate through this gateway via LoRa radio.

Field Value
Firmware 6.5.0
BLE 1.0.13
WiFi MAC 10:52:1C:01:2D:F4
BT MAC C8:B9:61:07:05:32
WiFi SSID Freebox-2A5E5D
Links WiFi ✓, Bluetooth ✓, Ethernet ✗, Modem ✗

remote/module/configuration works directly (gateway is WiFi-connected, not LoRa-only).

8.2 LRPC-F1DA27 — Pool Controller

ID: 6481bcea8782b78554c02bed | Serial: 150302F1DA270001 | Type: lr-pc
Controls filtration pump (index 0), pool light (index 1), and a third output (index 2, unused).

Pool config: 25 m³, rectangular 6×3 m depth 1.4 m, outdoor, glass filter, stabilised chlorine tablets, automatic pH regulation, manual chlorine regulation.

Programs: see section 7.2.
Alert history: GET /maintenance-logs-data?moduleId=6481bcea8782b78554c02bed
remote/module/configuration returns module_not_reachable — LoRa relay only.

8.3 LRPS-0565C0 — Pool Analyser

ID: 6480da07c2c0896da120ecca | Serial: 9400030565C00001 | Type: lr-mas
Measures pool water temperature, pH, and ORP/redox. Battery at 60% — alerts fired Feb 2026. Calibration needed (flagged Mar 2026).

Input ID Type Interval
Water temperature 6480da07c2c0896da120eccc 5 ~30 min
pH 6480da07c2c0896da120eccd 6 varies
ORP/Redox 6480da07c2c0896da120ecce 7 varies

No outputs — pure analyser.

8.4 LR6AG-070917 — Irrigation Valve Controller

ID: 626984f038ec558c598bb6d5 | Serial: 6606010709170001 | Type: lr-ag
6 irrigation stations, 1 rain sensor input. Battery: 81%. Relay: LRMB10.

All 6 programs reset on 2026-06-04 with no active windows.
Stations 0 and 1 currently have 255-day rain delay (suspended indefinitely).

Station Output ID Index Flow rate
Station 1 626984f038ec558c598bb6d8 0 30 m³/h
Station 2 626984f038ec558c598bb6d9 1 14 m³/h
Station 3 626984f038ec558c598bb6da 2 20 m³/h
Station 4 626984f038ec558c598bb6db 3 1 m³/h
Station 5 626984f038ec558c598bb6dc 4 38 L/h
Station 6 626984f038ec558c598bb6dd 5 38 L/h

Rain sensor (input 626984f038ec558c598bb6d7, type 2): 1 = rain, 0 = clear.

8.5 LRMS-0721DC — Sensor Station

ID: 60ca42adad69dac1558f7c64 | Serial: 7400040721DC0001 | Type: lr-ms
Pure sensor — no outputs, no programs. Battery: 78%. relayIsLongPolling: true.

Input ID Type Interval
Plumbago (soil moisture) 60ca42adad69dac1558f7c66 12 60 min
Pelouse (soil moisture) 60ca42adad69dac1558f7c67 12 60 min
Air/soil temperature 60ca42adad69dac1558f7c69 5 15 min

Moisture spikes (>10% jump within an hour) correlate with irrigation events — useful to infer watering history when output/events returns no records.

8.6 LR2IPECO-0C9282 — IP ECO Controller

ID: 6516ca57b7c098e6807cb332 | Serial: 3802020C92820001 | Type: lr-ip-eco
2 irrigation stations + pulse flow meter. Battery: 89% primary, secondary battery: 3200 mV. All 3 programs have no active windows.

Input ID Type
Flow meter 6516ca57b7c098e6807cb334 33 (flow, L)

Flow meter alternates: values are cumulative per minute, pattern suggests short irrigation bursts.
watering.state: 1 in remote state = currently active at last poll.

8.7 LRPR-0D6800 — Filter Pressure Sensor

ID: 6a17218be0b88ea3e925524f | childSerial6: 0D6800 | Type: lr-pr
Pure pressure sensor. Battery: 94%. 1-minute reporting interval.

Input 6a17218be0b88ea3e9255251, type 8. Expression: (1.5 × raw 500) / 1000 → bar.

Normal operating pressure: 0.700.75 bar (pump running). Drop to ~0.08 bar = pump stopped.
Rising pressure above ~0.80 bar = dirty filter — backwash needed.

8.8 pool-level-sensor — Float Level Switch

ID: 6a174dd1e0b88ea3e925531c | Type: pool-level-sensor
Binary float switch, only transmits on state change (~11 events per month). Battery: 41% — needs replacement soon. Cleaning reminder fired 2026-05-28.

Input 6a174dd1e0b88ea3e925531e, type 21. Raw passthrough: 0 = below threshold (fill needed), 1 = OK.

8.9 LRLEVEL-0D2ED9 — Pool Fill Valve Controller

ID: 6a1c4b153d6c33ab843edc13 | Serial: 3A01020D2ED90001 | Type: lr-niv
Controls pool fill valve automatically based on pool-level-sensor. Battery: 94%.

Input ID Type Notes
Fill volume 6a1c4b153d6c33ab843edc15 33 L added per fill cycle; polynomial expression
Level state 6a1c4b153d6c33ab843edc16 149 1 = valve closed, 0 = open

Output 6a1c4b153d6c33ab843edc17 (Station 1, index 0) — fill valve. No scheduled program; triggered by level sensor.

To query fill history: GET /remote/input/getComputedSensorData?inputId=6a1c4b153d6c33ab843edc15 over wide date range, filter ticks where value > 0.


9. Write Endpoints Reference

All mutation endpoints grouped by risk. All require session cookie unless noted.

9.1 Device Control (high impact — sends LoRa radio commands)

POST /remote/module/control                   pool controller linesControl (see section 6.1)
POST /module/sendManualModuleCommand          irrigation valve commands (see section 6.2)
POST /canopy/{siteId}/sendModulesCommand      flush commands to device
POST /modules/sendDataViaLoRaWAN
POST /agriculturalModuleGroup/sendDataViaLoRaWAN
POST /wateringModuleGroup/sendDataViaLoRaWAN

9.2 Programs (persistent — survives device reboot)

PUT  /programs                    {"programs": [...], "module": "moduleId"}
POST /programs                    create new program
DELETE /programs
PATCH /programs/name
POST /program/update              socket.io variant (immediate push to device)
PUT  /program/update              REST variant
POST /checkAvailableWindows       check time conflicts before saving
PUT  /api/updatePrograms          mobile API variant

9.3 Module Configuration

POST /remote/module/configuration
POST /remote/module/configuration/and/programs
POST /input/update                update input settings (name, thresholds, alerts)
PUT  /output/update               update output settings (name, flow rate)
GET  /remote/module/name?moduleId=...&name=...   rename via LoRa
PUT  /module/update
PUT  /module/update/address
PUT  /module/update/status
POST /module/update/name
POST /module/update/securityMode
POST /remote/module/advertisingProfile
POST /remote/module/dissociate    {"relayMsn": "...", "modulesMsn": ["..."]}
PUT  /module/dissociate/{moduleId}

9.4 Site / Pool Management

POST /canopy-sites
PUT  /pools/update/address
POST /pools/admin/sendMessage
POST /wateringModuleGroup/create
PUT  /wateringModuleGroup/update
DELETE /wateringModuleGroup/delete
POST /addAgriculturalGroupToUser
PUT  /teams/handleAccessGrantedPools
POST /teams/members/add

9.5 Journal / Notifications

PUT  /journal/acknowledge/all
PUT  /journal/acknowledge/records
POST /canopy-sites/{siteId}/notifications/acknowledge-all
POST /journal/warnings/accepted

9.6 Socket.io Write Calls

These bypass the normal HTTP layer — not captured by Safari recordings:

socket.post  /checkAvailableWindows
socket.post  /input/update
socket.post  /program/update
socket.post  /remote/module/configuration
socket.post  /remote/module/configuration/and/programs
socket.post  /remote/module/name
socket.put   /module/update
socket.put   /module/update/address
socket.put   /pools/update/address
socket.put   /program/update

10. Reference Tables

Module Types

Type string Device name Description
lr-mb-10 LRMB10 LoRa gateway v10
lr-mb-30 LRMB30 LoRa gateway v30
lr-mb-100 LRMB100 LoRa gateway v100
lr-ms LRMS LoRa sensor station
lr-ms-eco LRMS_ECO Eco sensor station
lr-ms-v2 LRMS v2 Sensor station v2
lr-ms-rs485 LRMSRS485 RS485 sensor station
lr-ag LR6AG LoRa agricultural valve (6 stations)
lr-mas LRPS LoRa master valve / pool analyser
lr-pc LRPC LoRa pool controller
lr-pc-vs LRPCVS Pool controller with variable-speed pump
lr-ip-eco LR2IPECO LoRa IP ECO (2-station + flow meter)
lr-ip-fl LR1IPFL IP flow metering
lr-ip-fl-v2 LR6IP IP flow v2
lr-ip-wan-v2 LRIP6 WAN IP WAN v2
lr-is LR6IS/LR12IS LoRa input sensor (multi-input)
lr-is-city LR6IS City variant input sensor
lr-niv LRLEVEL LoRa level controller (fill valve)
lr-pr LRPR LoRa pressure sensor
lr-mv LRMV/LRPM1 LoRa motor valve
lr-ol LROL4 LoRa OL (lighting control)
lr-bst-100 LRBST100 BST 100 booster
lr-bst-react-4g LR-BST R4G BST 4G reactive booster
pool-level-sensor POOL-LVL Pool float level switch
wf-mb WiFi gateway WiFi gateway
wf-is / bl-is WiFi/BT sensor WiFi or Bluetooth sensor
bl-ip / bl-ip-v2 BL6IP Bluetooth IP
wf-ol WiFi OL WiFi online
joro / joro-v2 WB-xxxx Joro water balance meter
smart-is SmartIS Smart input sensor
smart-is-lcd VILLA-/SMT-HOME Smart IS with LCD
ipx GEN_xxxx IPX controller

Input Type Codes

Code Meaning Unit Notes
2 TOR (on/off) Rain sensor: 1=rain, 0=clear
5 Air/soil temperature °C LRMS ambient sensor
6 pH Raw ÷ 100 = pH units
7 ORP / Redox mV Raw = mV
8 Pressure bar Expression: (1.5×raw500)/1000
12 Soil moisture % (unit 10) LRMS capacitive probe
20 Air moisture
21 Pool level TOR switch 0/1 0=low/fill needed, 1=OK
33 Volumetric / flow L (unit 1) Cumulative, pulse-based
133 Water temperature °C (unit 2) LRPC/LRPS probe
149 Fill valve / level state 0/1 LRLEVEL: 1=closed

linesControl Action Codes (LRPC)

action meaning params verified
0 Stop pump (index 0 only) Yes (LRLEVEL)
1 Stop / cancel any running command Yes (app capture)
2 Timed impulse time: "MM:SS" Yes (app capture + live)
3 Boost filtration time: "MM:SS" Yes (Safari capture)
4 Start indefinitely Yes (notebook pattern)
5 Pause N days manualDuration: N (days) Yes (app capture)
11 Backwash cycle time, speed Partial (JS analysis)

11. Security Findings

Assessment from black-box analysis. All findings verified on qualif.mysolem.com. Reported for awareness; not exploited beyond authorized testing scope.

Critical

# Endpoint Finding
1 POST /modules/search Empty query returns 4,607 modules with name, serial, MAC, firmware, GPS — no ownership enforcement (BOLA)
2 POST /users/search Returns full user documents including password hashes (unsalted SHA-256), active reset tokens (direct account takeover), APNs/FCM push tokens — BOLA
3 POST /outputs/search + POST /programs/search Empty query returns 12,034 outputs and 6,073 programs platform-wide — no ownership enforcement
4 GET /remote/module/configuration Returns WiFi SSID, MACs, firmware for any module ID — no ownership check
5 GET /remote/input/getComputedSensorData Any inputId accepted — full sensor history exfiltration across tenants
6 POST /module/sendManualModuleCommand Takes moduleSerialNumber (obtainable from #1) — likely no ownership check, unconfirmed

High

# Finding
7 Google Maps API key AIzaSyDh8fCC9vTEuN9eKewxgqWXGP9ENb5an1c hard-coded in HTML, no referrer restriction
8 Session cookie missing SameSite attribute → CSRF possible on state-changing endpoints
9 No rate limiting on POST /login — credential stuffing uninhibited
10 MongoDB ObjectIds are sequentially guessable (timestamp + machine + counter), accelerating enumeration

Medium / Low

# Finding
11 Access-Control-Allow-Origin: (empty string) — CORS misconfiguration
12 No Content-Security-Policy header on React frontend
13 X-Powered-By: Sails <sailsjs.org> on every response — unnecessary fingerprinting
14 Qualification environment appears to use live production data (real GPS, real addresses, live telemetry)
15 Input IDs embedded as data-input-id HTML attributes — completes enumeration chain without guessing