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>
486 lines
12 KiB
Markdown
486 lines
12 KiB
Markdown
# MySolem API — Endpoints
|
|
|
|
All paths relative to `https://qualif.mysolem.com`. All require session cookie unless noted.
|
|
|
|
> **Mobile API**: the iOS apps use a separate `/api/` prefix layer with OAuth2 Bearer auth. See `api-docs/mobile-api.md`.
|
|
|
|
---
|
|
|
|
## Feature flags
|
|
|
|
```
|
|
GET /features
|
|
```
|
|
Returns flat JSON object of boolean flags (`dashboard`, `enableHydraulicSupervision`, …).
|
|
|
|
---
|
|
|
|
## 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/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
|
|
```
|
|
|
|
---
|
|
|
|
## Modules
|
|
|
|
**Search / list:**
|
|
```
|
|
POST /modules/search
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"query": { "id": { "$in": ["moduleId1", "moduleId2"] } },
|
|
"projection": { "name": 1, "type": 1, "serialNumber": 1 }
|
|
}
|
|
```
|
|
Response: `{ result: { results: [...], totalCount: null } }`
|
|
|
|
```
|
|
GET /modules/available?userId={userId}
|
|
```
|
|
|
|
**Paginated modules for a site:**
|
|
```
|
|
GET /canopy-sites/{siteId}/modules?limit=20&skip=0&includeTotalCount=true
|
|
```
|
|
Response: `{items: [...], total: N}`. Note: items use `_id` (MongoDB style), not `id`.
|
|
|
|
**Dashboard chart data:**
|
|
```
|
|
GET /modules/echarts-data?includeCustomers=true&includeAgency=false
|
|
→ {chartModuleTypes: [{name, value}], chartModuleConnectivities: [...]}
|
|
```
|
|
|
|
**SIM card payment check:**
|
|
```
|
|
GET /sim-cards/needsPayment?moduleIds={id1}&moduleIds={id2}
|
|
→ {result: {"moduleId": false, ...}}
|
|
```
|
|
|
|
**Remote state:**
|
|
```
|
|
GET /remote/module/state?moduleId={moduleId}
|
|
GET /remote/module/configuration?moduleId={moduleId}
|
|
```
|
|
|
|
**Module data model (key fields):**
|
|
```json
|
|
{
|
|
"id": "...",
|
|
"name": "...",
|
|
"type": "lr-ag",
|
|
"serialNumber": "6606010709170001",
|
|
"macAddress": "C8:B9:61:...",
|
|
"softwareVersion": "7.2.0",
|
|
"batteryVoltage": 81,
|
|
"battery": 5,
|
|
"rssi": 1,
|
|
"isOnline": true,
|
|
"relay": "relayModuleId",
|
|
"latitude": 43.79417,
|
|
"longitude": 4.09520,
|
|
"lastRadioCommunication": "2026-06-04T17:59:29.000Z"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Sensor / Input data
|
|
|
|
**Batch input config (most efficient way to get all input metadata):**
|
|
```
|
|
GET /inputs?inputId=id1%3Bid2%3Bid3
|
|
```
|
|
Semicolons (`%3B`) separate multiple IDs. Returns array of full input objects with `expression`, `type`, `unit`, `module`, `index`. Much lighter than querying each individually.
|
|
|
|
> **Note**: sensor data can be queried per-input or per-module (all inputs at once).
|
|
|
|
**All inputs for a module — preferred for dashboard-style loads:**
|
|
```
|
|
GET /remote/module/getComputedSensorsData
|
|
?moduleId={moduleId}
|
|
&startDate={iso8601}
|
|
&endDate={iso8601}
|
|
&forceRawOrComputed=computed
|
|
```
|
|
Returns an **array** of input objects, each with a `computedSensorData` field containing ticks.
|
|
More efficient than querying each input individually. Pass `forceRawOrComputed=raw` for raw ADC values.
|
|
|
|
**Single input telemetry:**
|
|
```
|
|
GET /remote/input/getComputedSensorData
|
|
?inputId={inputId}
|
|
&startDate={iso8601}
|
|
&endDate={iso8601}
|
|
&ensurePrevTickForContinuity=true
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"id": "60ca42adad69dac1558f7c66",
|
|
"name": "Plumbago",
|
|
"module": "60ca42adad69dac1558f7c64",
|
|
"type": 12,
|
|
"unit": 10,
|
|
"interval": 60,
|
|
"typeIsMoistureSensor": true,
|
|
"computedSensorData": [
|
|
{ "tickTimestamp": "2026-06-01T00:42:00.000Z", "value": 54.1875 }
|
|
]
|
|
}
|
|
```
|
|
|
|
**Last tick only (no date range — fastest for live dashboards):**
|
|
```
|
|
GET /input/getLastTickFromInput?inputId={inputId}
|
|
```
|
|
Returns `{ "tick": { "input": "...", "value": <raw>, "timestamp": "...", "id": "..." } }`.
|
|
Value is **raw** (before expression). Divide by 100 for pH/temp from LRPS.
|
|
|
|
**Averages:**
|
|
```
|
|
GET /inputs/average?inputIds={id1},{id2}&startDate={iso8601}&endDate={iso8601}
|
|
```
|
|
|
|
**Update input config:**
|
|
```
|
|
POST /input/update
|
|
```
|
|
|
|
**CSV export:**
|
|
```
|
|
GET /input/getComputedInputIpxDataBetweenDatesAsCsv
|
|
?inputId={inputId}&startDate={iso8601}&endDate={iso8601}
|
|
```
|
|
|
|
**Legacy socket.io call** (same route, used from browser JS):
|
|
```
|
|
socket.request({ method: "GET", url: "/remote/module/getComputedSensorsData",
|
|
data: { moduleId, startDate, endDate } }, callback)
|
|
```
|
|
|
|
---
|
|
|
|
## Outputs
|
|
|
|
```
|
|
GET /outputs/modules?moduleIds={moduleId}
|
|
POST /outputs/search { "query": { "module": { "$in": ["moduleId"] } } }
|
|
POST /output/update
|
|
```
|
|
Response: `{ outputs: [{ id, module, index, name, flowRate: { value, unit } }] }`
|
|
Flow rate unit: `1` = L/h, `2` = m³/h.
|
|
|
|
**Output event history:**
|
|
```
|
|
GET /output/events?moduleId={moduleId}
|
|
GET /output/events?outputId={outputId}&startDate={iso8601}&endDate={iso8601}
|
|
```
|
|
|
|
**Watering forecast:**
|
|
```
|
|
GET /output/forecast?moduleId={moduleId}
|
|
GET /output/forecast?outputId={outputId}
|
|
```
|
|
|
|
---
|
|
|
|
## Programs
|
|
|
|
```
|
|
GET /programs/modules?moduleIds={moduleId}
|
|
PUT /programs
|
|
{ "programs": [...], "module": "moduleId" }
|
|
GET /programs/reset?moduleId={moduleId}
|
|
POST /programs/search
|
|
{ "query": { "id": { "$in": [...] } } }
|
|
```
|
|
|
|
Program object:
|
|
```json
|
|
{
|
|
"id": "...",
|
|
"name": "Pelouse Terrasse",
|
|
"module": "...",
|
|
"index": 0,
|
|
"programmingType": "temporal",
|
|
"weekDays": 127,
|
|
"waterBudget": 100,
|
|
"windows": [{ "start": 420, "end": 422, "on": 120, "off": 0 }]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Daily watering
|
|
|
|
```
|
|
GET /garden/dailyData/{moduleId}
|
|
GET /modules/heatmap-data?moduleIds={ids}&startDate=X&endDate=Y
|
|
```
|
|
|
|
---
|
|
|
|
## Users
|
|
|
|
```
|
|
GET /users-info/{userId}?includeUserGroups=true
|
|
GET /users/{userId}/modules
|
|
GET /users/{userId}/sites
|
|
GET /users/{userId}/module-groups
|
|
GET /users/{userId}/subscriptionModules
|
|
POST /users/search
|
|
{ "query": {...}, "projection": {...} }
|
|
```
|
|
|
|
**Flexible module list (POST — supports field projection + filtering):**
|
|
```
|
|
POST /users/{userId}/modules
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"fields": {"name": 1, "type": 1, "serialNumber": 1, "displayType": 1},
|
|
"filters": {},
|
|
"addOwnershipFlag": true,
|
|
"addOwnerIds": false,
|
|
"includeCustomers": true,
|
|
"includeAgency": false,
|
|
"includeTotalCount": true,
|
|
"limit": 20,
|
|
"skip": 0
|
|
}
|
|
```
|
|
Response: `{modules: [...], totalCount: N}`.
|
|
Supported filter fields: `search`, `tagIds`, `displayTypes`, `connectivities`, `powerTypes`.
|
|
|
|
**Similar pattern for grouped resources:**
|
|
```
|
|
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} (associated users)
|
|
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,
|
|
"includeCustomers": true,
|
|
"sortColumn": "timestamp", "sortOrder": -1,
|
|
"fields": {"category": 1, "label": 1, "module": 1, "timestamp": 1}
|
|
}
|
|
→ {records: [...], totalCount: N}
|
|
```
|
|
|
|
**Activity log categories:**
|
|
```
|
|
POST /users/{userId}/records/categories
|
|
{"filters": {"startDate": "2026-05-28T21:51:48.266Z"}, "includeCustomers": true}
|
|
→ [{category, displayCategory}]
|
|
```
|
|
|
|
---
|
|
|
|
## Notifications / journal
|
|
|
|
```
|
|
GET /journal/notifications
|
|
GET /journal/counts
|
|
GET /journal/warnings/accepted
|
|
POST /journal/acknowledge/all
|
|
POST /canopy-sites/{siteId}/notifications/acknowledge-all
|
|
```
|
|
|
|
**Notifications (site-level, POST — supports filtering):**
|
|
```
|
|
POST /canopy-sites/{siteId}/notifications
|
|
{"limit": 1, "includeTotalCount": true, "filters": {"state": {"$in": ["unread", "process"]}}}
|
|
→ {records: [...]}
|
|
```
|
|
|
|
**Journal notifications (cross-site, filtered):**
|
|
```
|
|
POST /journal/notifications
|
|
{
|
|
"canopyIds": ["siteId"],
|
|
"limit": 100, "skip": 0,
|
|
"filters": {"priority": {"$in": ["critical", "high"]}, "state": {"$in": ["unread", "process"]}},
|
|
"projection": {...}
|
|
}
|
|
→ {records: [...], total: N}
|
|
```
|
|
|
|
---
|
|
|
|
## Weather
|
|
|
|
```
|
|
GET /moduleGroups/getCanopyWeather
|
|
?location[latitude]={lat}&location[longitude]={lng}
|
|
```
|
|
Returns 3-day AccuWeather-style forecast.
|
|
|
|
---
|
|
|
|
## Clusters (watering module groups)
|
|
|
|
```
|
|
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}
|
|
```
|
|
|
|
---
|
|
|
|
## Favorites & tags
|
|
|
|
```
|
|
GET /favorites/{userId}
|
|
POST /favorites
|
|
DELETE /favorites/{userId}
|
|
GET /modules/{moduleId}/tags
|
|
POST /module/tags { "module": "moduleId", "tags": [...] }
|
|
```
|
|
|
|
---
|
|
|
|
## Module notebook (event log)
|
|
|
|
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.
|
|
|
|
Record structure:
|
|
```json
|
|
{
|
|
"id": "...",
|
|
"module": "moduleId",
|
|
"category": "manual_command",
|
|
"label": "pHmètre - Seuil bas dépassé",
|
|
"businessLabel": "Le pH est trop bas",
|
|
"priority": "low",
|
|
"state": "archived",
|
|
"timestamp": "2026-06-04T21:00:00.000Z",
|
|
"params": { "action": 4, "index": 1 },
|
|
"input": "inputId",
|
|
"notifications": [],
|
|
"comments": []
|
|
}
|
|
```
|
|
|
|
Known `category` values: `manual_command`, `pool`, `platform_alert`, `firmware_update`
|
|
|
|
The `params` field structure varies by category: for `manual_command` it mirrors the linesControl action/index that triggered the event; for `pool` it contains poolAlertType (e.g. `backwashReminder`, `poolLevelSensorCleaningReminder`). See `api-docs/control.md` for action code definitions.
|
|
|
|
---
|
|
|
|
## Module users
|
|
|
|
```
|
|
GET /modules/{moduleId}/users
|
|
```
|
|
Returns array of full user objects with access to this module (email, push tokens, preferences).
|
|
|
|
---
|
|
|
|
## Linked inputs
|
|
|
|
```
|
|
GET /input/getInputsLinkedModule/{moduleId}
|
|
```
|
|
Returns inputs linked to a module. Returns `[]` for standalone LoRa modules.
|
|
|
|
---
|
|
|
|
## User avatar
|
|
|
|
```
|
|
GET /user/avatar
|
|
GET /user/avatar?t={unix_ms} ← cache-bust variant
|
|
```
|
|
Returns current authenticated user's avatar as `image/png`. Never cached (`no-cache, no-store`).
|
|
|
|
---
|
|
|
|
## SIM subscription
|
|
|
|
```
|
|
GET /users/{userId}/checkSubscriptionSimcardStatus
|
|
```
|
|
Returns ~39-byte JSON — whether the user has an active cellular SIM subscription.
|
|
|
|
---
|
|
|
|
## Field supervision (watering events over time range)
|
|
|
|
```
|
|
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`
|
|
|
|
---
|
|
|
|
## Satellite / NDVI imagery
|
|
|
|
Host: **`sentinel.mysolem.com`** (separate from the main API host).
|
|
|
|
**Check image availability:**
|
|
```
|
|
GET /moduleGroups/{siteId}/fencing/{timestampMs}/images-check
|
|
→ {availableImages: {available: true, reason: "ok"}}
|
|
```
|
|
|
|
**List images:**
|
|
```
|
|
GET /moduleGroups/{siteId}/fencing/{timestampMs}/images
|
|
→ {imagesList: {images: [{name, urlRgb, urlSavi}]}}
|
|
```
|
|
|
|
**Fetch satellite images directly:**
|
|
```
|
|
GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/savi → PNG (SAVI 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`
|