Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
All notable changes to this project will be documented in this file.

## 2026-10
- Add a hardware-tested Midea/Clivet R32 Modbus RTU bridge with exactly nine Virtual Components, guarded zone-1/DHW/Silent Mode control, fresh masked writes, physical readback, setup documentation and 33 RPC/Modbus tests
- Add a Local Network Messaging starter collection for firmware 2.0+: message diagnostics, remote switch mirroring, received button control, input-driven multicast group commands, and a TV power follower with configurable hysteresis and delayed OFF control, plus complete setup and troubleshooting documentation
- Add the Shelly Plus Plug S LED power visualization from upstream PR #204, with fixed threshold states or a green-to-red gradient, repository-standard headers, and manifest/README integration
- Add `modbus/Growatt/SPF6000/spf6000_vc.shelly.js` for the Growatt SPF 6000 ES Plus off-grid inverter, reusing the input-register map from `SFP5000` (same off-grid family, different power rating). No official protocol document for this exact model was found, so the register addresses are explicitly flagged as unverified in the file header and `@status under development` until confirmed on real hardware
Expand Down
20 changes: 18 additions & 2 deletions examples-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,22 @@
"pv"
]
},
{
"fname": "modbus/Midea/midea-clivet-r32-pro-modbus-vc.shelly.js",
"title": "Midea/Clivet R32 Modbus RTU bridge",
"description": "Local zone-1, DHW and Silent Mode control with nine Virtual Components. Requires firmware 2.0.0+ and Shelly Pro Modbus Add-on.",
"doc": "modbus/Midea/README.md",
"tags": [
"modbus-controller",
"modbus-rtu",
"hvac",
"midea",
"clivet",
"r32",
"heat-pump",
"virtual-components"
]
},
{
"fname": "modbus/Sigenergy/SigenStor/sigenstor_plant_vc.shelly.js",
"title": "Sigenergy SigenStor MODBUS-RTU plant monitor",
Expand Down Expand Up @@ -1824,7 +1840,7 @@
{
"fname": "power-energy/automatic-transfer-switch/iats_grid_backup_controller.shelly.js",
"title": "iATS two-channel grid/backup automatic transfer switch controller",
"description": "Controls K1 (grid) and K2 (backup) contactors on a two-channel Shelly device (Pro 2, Pro 2PM, Plus 2PM, 2PM Gen3/Gen4 in switch profile). SW1/input:0 = grid available; SW2/input:1 = backup available. Grid is preferred when both sources are present. Physical inputs are detached from outputs on boot \u2014 this script is the sole owner of switch:0 (K2) and switch:1 (K1). Both outputs are opened before a transfer; a configurable delay prevents contactor overlap. Optional manual override is received from the Sensor Add-on helper via KVS; a stale-sequence watchdog reverts to AUTO when the helper stops publishing.",
"description": "Controls K1 (grid) and K2 (backup) contactors on a two-channel Shelly device (Pro 2, Pro 2PM, Plus 2PM, 2PM Gen3/Gen4 in switch profile). SW1/input:0 = grid available; SW2/input:1 = backup available. Grid is preferred when both sources are present. Physical inputs are detached from outputs on boot — this script is the sole owner of switch:0 (K2) and switch:1 (K1). Both outputs are opened before a transfer; a configurable delay prevents contactor overlap. Optional manual override is received from the Sensor Add-on helper via KVS; a stale-sequence watchdog reverts to AUTO when the helper stops publishing.",
"doc": "power-energy/automatic-transfer-switch/README.md",
"tags": [
"energy",
Expand All @@ -1838,7 +1854,7 @@
{
"fname": "power-energy/automatic-transfer-switch/iats_sensor_addon_manual_control.shelly.js",
"title": "iATS Sensor Add-on manual control helper",
"description": "Companion script for the iATS controller, running on the Shelly Pro Sensor Add-on. DIN0 (input:100) selects AUTO vs MANUAL mode; DIN1 (input:101) selects GRID vs PV target. Publishes the current selection to KVS at a configurable interval. Does not touch relay outputs \u2014 iats_grid_backup_controller.shelly.js remains the sole owner of switch:0/1 and validates source availability before acting on any manual request.",
"description": "Companion script for the iATS controller, running on the Shelly Pro Sensor Add-on. DIN0 (input:100) selects AUTO vs MANUAL mode; DIN1 (input:101) selects GRID vs PV target. Publishes the current selection to KVS at a configurable interval. Does not touch relay outputs — iats_grid_backup_controller.shelly.js remains the sole owner of switch:0/1 and validates source availability before acting on any manual request.",
"doc": "power-energy/automatic-transfer-switch/README.md",
"tags": [
"energy",
Expand Down
16 changes: 16 additions & 0 deletions modbus/Midea/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Midea/Clivet R32 Modbus RTU bridge
Copyright 2026 Georgi Germanov and contributors
SPDX-License-Identifier: Apache-2.0

This standalone script adapts the transport, deferred-callback, startup
synchronization and readback architecture of lg-therma-v-pro-em50_vc.shelly.js
published in drHouse-gif/lg-therma-v-shelly and ALLTERCO/shelly-script-examples.
The Midea register handling, fresh masked writes, nine-component layout,
non-destructive provisioning and test harness were added for this solution.

The register data was cross-checked against the R32 profile maintained in
Mosibi/Midea-heat-pump-ESPHome and the 171H120F device configuration in
timlaing/modbus_local_gateway. Technical references are listed in
TECHNICAL_NOTES.md. No manufacturer endorsement is implied.

Distributed under the repository's Apache License, Version 2.0.
183 changes: 183 additions & 0 deletions modbus/Midea/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Midea/Clivet R32 via Shelly Pro Modbus Add-on

Local RS485 / Modbus RTU monitoring and control for Midea R32 hydronic heat
pumps and compatible Clivet controllers. The standalone script provisions
**exactly nine Virtual Components** and uses the original heat-pump controller.

A successful real-device test was confirmed by the contributor on 2026-10-10.
The production profile remains scoped to the controller/register layout below.

- Script: [midea-clivet-r32-pro-modbus-vc.shelly.js](midea-clivet-r32-pro-modbus-vc.shelly.js)
- Protocol and source details: [TECHNICAL_NOTES.md](TECHNICAL_NOTES.md)
- Software checks: [tests/bridge.test.cjs](tests/bridge.test.cjs)

## Profile and connection

The profile targets the documented Midea R32 / 171H120F-compatible register
layout with **zone 1 under leaving-water control**. Check the exact wired
controller and its service manual. Brand names alone do not select the map.
R290 controllers, Clivet SWAN-2 water heaters and room-control installations
require their own profiles.

On the controller described by this profile:

| Controller | RS485 Add-on |
| --- | --- |
| H2 / A+ | A |
| H1 / B- | B |
| Signal reference, if specified | Follow the controller's wiring manual |

Do not substitute another two-wire bus or A/B power terminals for H1/H2.
Use twisted-pair cable, one Modbus client on the line, and the termination and
shield arrangement specified for the installation. The original controller
and the heat pump's built-in protections remain in service.

Use a compatible Shelly Pro host, its Pro Modbus Add-on, and firmware 2.0.0 or
later with `Serial`, `MbRtuClient`, scripting and Virtual Components. Select
the RS485 add-on in the device configuration and reboot if requested.

## Nine components

Default `vcBase: 220` produces the following components:

| Key | Name | Purpose |
| --- | --- | --- |
| `boolean:220` | Zone 1 Enable | Water-control enable flag for zone 1 |
| `boolean:221` | DHW Enable | Domestic hot water request |
| `boolean:222` | Silent Mode | Silent-mode enable flag |
| `number:223` | Zone 1 Water Target | Zone-1 target, integer degrees C |
| `number:224` | DHW Target | Tank target, integer degrees C |
| `number:225` | Inlet Temperature | Water return temperature |
| `number:226` | Outlet Temperature | Water leaving temperature |
| `number:227` | Tank Temperature | T5 sensor, when configured |
| `text:228` | Status | Online/read-only/control, actual mode, fault and version |

Operating mode is selected on the original controller. Its actual operating
mode is included in Status. Zone 1 Enable is not a mains switch or a global
override of zone 2. The script does not change the silent-mode level.

No groups are created. Existing unrelated components are not modified or
deleted. The script checks capacity before provisioning; nine slots are needed
within the device's ten-component limit. An ID/name conflict stops setup.
Change `vcBase` to a free range or dedicate a host to this bridge.

## Configuration and first synchronization

Create one Shelly script, paste the complete `.shelly.js` file and edit `CONFIG`:

```javascript
serialId: 100,
slaveId: 1,
baud: 9600,
format: '8N1',
enableWrites: false,
expectedControllerVersion: null,
hasDhwTank: false,
allowCooling: false
```

`serialId` identifies the Shelly Serial/MbRtuClient instance. `slaveId` is the
address configured on the heat-pump controller. They are different identifiers.
Address 1 is an example; some controllers use a different address, including 16.
Find the Serial instance with `Shelly.GetComponents` if its ID differs from 100.

Set `hasDhwTank: true` only when the tank and T5 sensor are present and correctly
configured. Otherwise tank-temperature and DHW-limit reads are omitted, DHW
writes are blocked, and the tank-temperature tile is unused. The default
zero in that unused tile is not a physical measurement.

Start in read-only mode. The script creates/reuses its components, configures
`mode: 'mb_client'`, reads the physical values, and publishes them. It logs:

```text
[Midea] Controller version (register 131) = <reported word>
[Midea] READY: 9 components synchronized; read-only mode
```

Compare temperatures, flags and targets with the original controller and
verify the register layout. Set `expectedControllerVersion` to the reported
word, configure the installed temperature limits, then set `enableWrites: true`
and restart. A matching version is an additional guard, not automatic model
identification. Configured controls become toggles/fields after restart.

## Temperature limits and controller behavior

The default configured heating/DHW ceiling is 60 C. Change limits only to
values permitted by the installed system. Target writes must satisfy both
the configured bounds and the controller's live bounds:

- Heating zone 1: low bytes of registers 203/204, upper/lower respectively.
- Cooling zone 1: low bytes of registers 201/202, upper/lower respectively.
- DHW: registers 207/208, upper/lower respectively.

Select Heat or Cool on the original controller for manual water-target control.
Cooling requests require `allowCooling: true` and an installation commissioned
for cooling. A manual water target is blocked in Auto or when the zone-1
weather curve is active. The integration does not disable the factory curve.
Room-control flag bit 0 of register 0 and an active fault block device writes.

## Register map used by this script

All reads use **Holding Registers, FC03**, including temperatures and faults.
Addresses below are wire offsets, not 4xxxx reference numbers.

| Address | Meaning | Handling |
| --- | --- | --- |
| 0, bit 1 | Zone-1 water enable | Mask 0x0002 |
| 0, bit 2 | DHW enable | Mask 0x0004 |
| 1 | Configured mode | Read only: 1 Auto, 2 Cool, 3 Heat |
| 2, low byte | Zone-1 water target | Mask 0x00ff; preserve zone-2 high byte |
| 4 | DHW target | Integer C |
| 5, bit 6 | Silent Mode | Mask 0x0040; preserve other bits |
| 100/101 | Compressor frequency / actual mode | Status diagnostics |
| 104/105 | Inlet/outlet water temperature | Signed 16-bit, integer C |
| 115 | Tank temperature | Signed 16-bit, integer C; optional |
| 124 | Current fault | Raw fault number in Status |
| 128, bit 1 | Defrost | Included in Status |
| 131 | Controller version | Read and match configured version |
| 201-204 | Cooling/heating limits | Zone-1 low bytes |
| 207/208 | DHW upper/lower limits | Optional when tank is configured |

Writes use FC06 by default. Set `writeFunction: 16` only if the exact controller
requires FC16; the script then writes a one-word list. There is no automatic
write-function fallback or blind write retry.

## Command and reconnect behavior

The first successful physical synchronization establishes the baseline. Values
left in the app or changed while the bridge is offline are not replayed on
startup/reconnect. Every changed control is checked, relevant guards refreshed,
and the target register read again before writing.

Registers 0, 2 and 5 use a masked read-modify-write. For example, changing the
zone-1 target keeps the latest zone-2 high byte. After each write the script
waits `settleMs`, reads back the register and publishes the reported value.
Unconfirmed writes are logged rather than presented as successful commands.

On a communication error, Status shows `OFFLINE | last readings stale`, commands
are blocked, and a full physical synchronization is required before control
resumes. Commands are sequential, with `gapMs` between operations and `pollMs`
between completed cycles. Normal defaults are 100 ms gaps, 700 ms settling,
and a 10-second polling pause.

## Troubleshooting and software checks

- No response: check H1/H2, polarity, configured server address, baud/format,
add-on selection, Serial ID and competing Modbus clients.
- Illegal address: verify the controller family and register table.
- Implausible temperatures: this map uses signed **whole degrees**, not LG's
tenths-of-a-degree conversion.
- Read-only status: check `enableWrites`, expected version and room-control flag.
- Rejected targets: check active faults, mode, weather curve and both sets of limits.
- Setup stops: check component capacity and ID/name conflicts.

Run the deterministic RPC/Modbus tests from the repository root:

```bash
node --check modbus/Midea/midea-clivet-r32-pro-modbus-vc.shelly.js
node --test modbus/Midea/tests/bridge.test.cjs
```

The harness checks nine-component provisioning, fresh bit/byte preservation,
signed temperatures, target limits, readback, timeout/reconnect handling and
UI synchronization. Details are recorded in [TECHNICAL_NOTES.md](TECHNICAL_NOTES.md).
60 changes: 60 additions & 0 deletions modbus/Midea/TECHNICAL_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Midea R32 profile notes

The script implements zone-1 leaving-water control using the R32 register layout.
Mode selection, zone 2, room-temperature control, weather-curve changes,
forced heaters, disinfection, energy counters and installer parameters are
outside the command surface of this nine-component bridge.

## References

- LG transport/startup/readback base: [drHouse-gif/lg-therma-v-shelly](https://github.com/drHouse-gif/lg-therma-v-shelly/blob/0a18acdafed25b9abe617cfda339fc47b0e3731b/upstream/lg-therma-v-pro-em50_vc.shelly.js).
- R32 register definitions: [Mosibi/Midea-heat-pump-ESPHome](https://github.com/Mosibi/Midea-heat-pump-ESPHome/blob/385ab6e698f6fd640abfa8c310edf257d2a94ece/models/R32-generic.yaml).
- 171H120F configuration: [timlaing/modbus_local_gateway](https://github.com/timlaing/modbus_local_gateway/blob/main/custom_components/modbus_local_gateway/device_configs/midea_heat_pump.yaml), inspected blob `75b80e19efd51b55581e6384de11603bf2f1d539`.
- [Shelly Serial API](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Serial/).
- [Shelly Modbus RTU Client API](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/MbRtuClient/).
- [Virtual Components](https://shelly-api-docs.shelly.cloud/gen2/DynamicComponents/Virtual/).
- [Shelly scripting language](https://shelly-api-docs.shelly.cloud/gen2/Scripts/LanguageReference/).

The host runtime uses supported synchronous component lookups, callback-based
RPC and single-shot timers. There are no imports, promises, async functions,
classes, external services or manually constructed RTU frames.

## Software verification

On 2026-10-07, Node.js 24 ran 33 deterministic RPC/Modbus integration tests with
33 passes. Syntax validation used `node --check`. The harness executes the
complete standalone script with a register-server emulator and injected
transport faults. It checks the resulting Modbus writes and UI values.

The checks cover startup/reconnect read-before-write, register-0 bit preservation,
register-2 high-byte preservation, register-5 function-bit preservation, signed
temperature conversion, current fault and curve guards, changed controller mode,
controller version, configured/live limits, explicit FC16, missing DHW hardware,
capacity/ownership and commands replaced during a preparatory read.

## Real-device validation

On 2026-10-10, the contributor confirmed that this bridge passed a real-device
test. The script is marked `production` and included in the example manifest.
This confirmation applies to the integration's documented R32 profile, not to
every heat pump sold under the Midea or Clivet brand.

The validated script retains its fixed-ID, self-provisioned nine-component
layout. Its ownership and capacity preflight deliberately stops on an unrelated
component conflict instead of deleting or replacing another integration's VCs.
It starts Modbus operations only after all nine components have been prepared.

## Integration boundaries

Version register 131 is a compatibility guard; it is not a unique hardware ID.
R32, R290 and water-heater maps must not be selected by brand name alone.
An absent sensor can return a plausible constant, so `hasDhwTank` is explicit.
FC03 is required for the diagnostic values as well as writable words.

There is one Modbus client per serial line. The script serializes its own
operations. A read-modify-write preserves the word sampled immediately before
the write; a controller changing that same word concurrently can still cause a
race because the interface does not provide an atomic masked-write operation.

Virtual Component updates can trigger scenes/webhooks. Keep scene conditions
idempotent and do not create a feedback loop between reported values and commands.
Loading
Loading