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

12 KiB

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

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

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

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

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