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>
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. Seeapi-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}
startDateis 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