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

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`