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>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
# myprimal
|
||||
|
||||
MQTT bridge for the MySolem / MyIndygo IoT platform (irrigation + pool). Reverse-engineered client that dynamically discovers all devices and exposes them via MQTT with Home Assistant autodiscovery and Homebridge EasyMQTT compatibility.
|
||||
|
||||
## Features
|
||||
|
||||
- Auto-discovers all modules, sensors, and outputs on startup
|
||||
- Polls sensor readings on configurable intervals
|
||||
- Publishes to MQTT with retained state
|
||||
- Home Assistant MQTT autodiscovery (sensors + switches)
|
||||
- Compatible with Homebridge [EasyMQTT](https://github.com/Shaquu/homebridge-easymqtt) plugin
|
||||
- Manual commands via MQTT (`/set` topics)
|
||||
- Runtime poll interval tuning via MQTT
|
||||
- REST API for health, state snapshot, and direct control
|
||||
- Docker deployment
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Configure
|
||||
|
||||
Copy `.env.example` to `.env` and fill in credentials:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Required:
|
||||
- `SOLEM_EMAIL` / `SOLEM_PASSWORD` — MySolem account credentials
|
||||
- `MQTT_URL` — your MQTT broker (e.g. `mqtt://192.168.1.10:1883`)
|
||||
|
||||
### 2. Run locally
|
||||
|
||||
```bash
|
||||
npm install
|
||||
node src/index.js
|
||||
```
|
||||
|
||||
### 3. Run with Docker
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
On first run, `config.json` is created in the Docker volume for persistent poll interval settings.
|
||||
|
||||
## MQTT Topics
|
||||
|
||||
All topics are prefixed with `MQTT_TOPIC_PREFIX` (default: `myprimal`).
|
||||
|
||||
### Sensor readings (retained)
|
||||
|
||||
```
|
||||
myprimal/{module_slug}/sensor/{metric}
|
||||
```
|
||||
|
||||
Payload: `{"value": 24.5, "unit": "°C", "ts": "2026-06-28T10:00:00.000Z"}`
|
||||
|
||||
Examples:
|
||||
```
|
||||
myprimal/lrpc_f1da27/sensor/temperature {"value": 24.5, "unit": "°C", ...}
|
||||
myprimal/lrps_0565c0/sensor/ph {"value": 7.35, "unit": "pH", ...}
|
||||
myprimal/lrps_0565c0/sensor/orp_redox {"value": 690, "unit": "mV", ...}
|
||||
myprimal/lr6ag_070917/sensor/plumbago {"value": 54, "unit": "%", ...}
|
||||
```
|
||||
|
||||
### Output state (retained)
|
||||
|
||||
```
|
||||
myprimal/{module_slug}/output/{index}/state
|
||||
```
|
||||
|
||||
Payload: `{"value": 0, "ts": "..."}` — `value` 1 = running, 0 = stopped
|
||||
|
||||
### Commands
|
||||
|
||||
```
|
||||
myprimal/{module_slug}/output/{index}/set
|
||||
```
|
||||
|
||||
Payload:
|
||||
```json
|
||||
{ "action": "run", "duration": 30 } // run for 30 minutes
|
||||
{ "action": "stop" } // stop immediately
|
||||
{ "action": "boost", "duration": 120 } // boost (pool pump only)
|
||||
{ "action": "pause", "days": 3 } // pause/rain delay N days
|
||||
```
|
||||
|
||||
### Availability (retained)
|
||||
|
||||
```
|
||||
myprimal/{module_slug}/availability "online" | "offline"
|
||||
```
|
||||
|
||||
### Config (retained, runtime-tunable)
|
||||
|
||||
Read current config:
|
||||
```
|
||||
myprimal/$config
|
||||
```
|
||||
|
||||
Update poll intervals (seconds):
|
||||
```
|
||||
myprimal/$config/set {"poll": {"fast": 30, "normal": 120}}
|
||||
```
|
||||
|
||||
Update interval for a specific sensor:
|
||||
```
|
||||
myprimal/$config/set {"poll": {"overrides": {"lrps_0565c0/ph": 30}}}
|
||||
```
|
||||
|
||||
Changes take effect immediately and persist across restarts.
|
||||
|
||||
### Poll buckets (defaults)
|
||||
|
||||
| Bucket | Default | Sensor types |
|
||||
|---|---|---|
|
||||
| `fast` | 60s | pH, ORP, pressure, water temperature |
|
||||
| `normal` | 300s | Soil moisture, air temperature, flow |
|
||||
| `slow` | 900s | Binary sensors (rain, level switch, valve state) |
|
||||
|
||||
## Home Assistant
|
||||
|
||||
Entities are auto-created via MQTT discovery on startup. Check **Settings → Devices & Services → MQTT** — all Solem devices appear automatically.
|
||||
|
||||
Discovery topics: `homeassistant/{sensor|binary_sensor|switch}/myprimal_{id}/config`
|
||||
|
||||
No manual configuration required.
|
||||
|
||||
## Homebridge (EasyMQTT)
|
||||
|
||||
Install [homebridge-easymqtt](https://github.com/Shaquu/homebridge-easymqtt) and configure accessories pointing to the MQTT topics above.
|
||||
|
||||
Example for pool light switch:
|
||||
```json
|
||||
{
|
||||
"accessory": "EasyMQTT",
|
||||
"name": "Pool Light",
|
||||
"type": "Switch",
|
||||
"mqttGetTopic": "myprimal/lrpc_f1da27/output/1/state",
|
||||
"mqttSetTopic": "myprimal/lrpc_f1da27/output/1/set",
|
||||
"mqttGetTransformation": "return JSON.parse(message).value === 1 ? 'true' : 'false';",
|
||||
"mqttSetTransformation": "return value === 'true' ? JSON.stringify({action:'run',duration:30}) : JSON.stringify({action:'stop'});"
|
||||
}
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| `GET` | `/health` | Service health + counts |
|
||||
| `GET` | `/state` | All cached sensor readings |
|
||||
| `GET` | `/state/:moduleId` | Readings for one module |
|
||||
| `GET` | `/registry` | Full device registry |
|
||||
| `POST` | `/control/:moduleId/:outputIndex` | Send manual command |
|
||||
| `POST` | `/rediscover` | Re-discover all devices |
|
||||
|
||||
### Control example
|
||||
|
||||
```bash
|
||||
# Run pool light (LRPC output 1) for 30 minutes
|
||||
curl -X POST http://localhost:3001/control/6481bcea8782b78554c02bed/1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"action":"run","duration":30}'
|
||||
|
||||
# Stop
|
||||
curl -X POST http://localhost:3001/control/6481bcea8782b78554c02bed/1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"action":"stop"}'
|
||||
```
|
||||
|
||||
## Module Slug Reference (Domicile)
|
||||
|
||||
| Module | Slug | Type |
|
||||
|---|---|---|
|
||||
| LRPC-F1DA27 (pool controller) | `lrpc_f1da27` | Filtration pump (output 0), light (output 1) |
|
||||
| LR6AG-070917 (irrigation) | `lr6ag_070917` | 6 irrigation stations (outputs 0–5) |
|
||||
| LRMS-0721DC (sensor station) | `lrms_0721dc` | Soil moisture × 2, air temp |
|
||||
| LRPS-0565C0 (pool analyser) | `lr_mas_0565c0` | Water temp, pH, ORP |
|
||||
| LRPR-0D6800 (filter pressure) | `lr_pr_0d6800` | Filter pressure |
|
||||
| LRLEVEL-0D2ED9 (fill valve) | `lr_niv_0d2ed9` | Fill valve control |
|
||||
| LR2IPECO-0C9282 | `lr_ip_eco_0c9282` | Flow meter |
|
||||
| POOL-LVL-0E953B (level switch) | `pool_level_sensor_0e953b` | Pool level binary |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
MySolem API ──► solem.js (auth + HTTP)
|
||||
│
|
||||
registry.js (device discovery on startup)
|
||||
│
|
||||
poller.js (per-bucket poll timers)
|
||||
│
|
||||
state.js (in-memory cache, EventEmitter)
|
||||
│
|
||||
┌─────────┴──────────┐
|
||||
mqtt/publish.js api/routes.js
|
||||
mqtt/discovery.js (REST API)
|
||||
mqtt/commands.js ──► control.js ──► MySolem API
|
||||
mqtt/config-bridge.js
|
||||
```
|
||||
Reference in New Issue
Block a user