Configure the Modbus RTU slave from the web UI instead of the console, the
same way MQTT is set up.
- a "Modbus RTU" button on the Configuration menu, next to "MQTT", built
only with USE_MIEL_HVAC_MODBUS_SLAVE on ESP32
- the page has an "Enable Modbus RTU" checkbox plus address, baudrate and
serial config, laid out like the MQTT and Logging pages. Save runs the
HVACModbus* commands, which apply live, and returns to the Configuration
menu. No reboot. A wrapped note appears when the MB Rx/Tx GPIOs are
unassigned, without widening the page.
- a "Modbus" sticker on the main-page status line while the slave is running
No core files touched. The console commands are unchanged.
Follow-up to #25004. Since that PR copies an outgoing 0x41 set-settings
request into sc_settings immediately, the confirming 0x62 0x02 normally
equals sc_settings, so the memcmp() in miel_hvac_input_settings() no
longer fires and HVACSettings / SENSOR were not published after an
HVACSet* command until the next TelePeriod (reported by @mamrai1).
Publish HVACSettings + SENSOR right after the optimistic apply. If the
unit rejects or modifies the request its 0x62 0x02 still differs from
sc_settings and the existing comparison republishes the corrected state.
Poll and parse the 0x04 Get Error State response so a faulted indoor unit is
visible from SENSOR / MQTT and over the Modbus slave.
- MIEL_HVAC_REQUEST_ERROR / MIEL_HVAC_DATA_T_ERROR 0x04, struct
miel_hvac_data_error (code bytes 4-5, packed two-char code byte 6), sc_error
slot, parser case and one poll-rotation slot after room temp. Units that do
not implement 0x04 time out via the existing p_tmo path.
- SENSOR: ErrorState (on unless code 8000), ErrorCode (packed BCD, 8000 = no
error, 6999 = bad indoor-unit comms per spec), ErrorShort (decoded two-char
code) and ErrorHex. miel_hvac_error_shortcode() decodes frame byte 6.
- Modbus: input regs 0x0054/0x0055 (PLC 30085/30086) and their FC03 mirror
0x0018/0x0019 (PLC 40025/40026). miel_hvac_mb_bcd16() converts the packed-BCD
wire value so a PLC reads 8000, not 0x8000.
- The register-map comment block now gives every Modbus address in both the raw
0-based offset and the 5-digit Modicon / PLC form.
The web control panel's Mode segment has its own Off button, so the
generic power toggle button and the ON/OFF state row are duplicates.
Hide them with a scoped stylesheet from FUNC_WEB_ADD_MAIN_BUTTON; the
POWER command and its MQTT/HA state sync are unchanged.
Co-authored-by: Theo Arends <11044339+arendst@users.noreply.github.com>
- HVACSetProhibit (and the Modbus prohibit register) never reached the
unit: the 0x41 set-settings update flag was 1<<13 (0x0020 on the wire)
but must be 1<<14 (0x0040), and the lock byte belongs at payload
offset 11, not 8.
- add a Prohibit select to the web control panel; give
miel_hvac_web_optlabel() a '_'->space fallback.
- after sending a 0x41 settings update, reflect the sent fields in
sc_settings right away so the web panel sync, the Modbus registers and
SENSOR show the intent instead of the pre-change state for the ~1s
until the unit confirms - fixes a control (or a PLC reading back over
Modbus) snapping to its previous value. The unit's next 0x62 wins if
it rejects the change.
* Keep the backtrace going when the crash pc is not code
An instruction fetch exception leaves pc pointing at the bad address the CPU
jumped to, so the executable check on the first frame declares the backtrace
corrupted and Status 12 reports a single call chain entry that only repeats EPC.
a0 and a1 are still valid there, so the walk can recover the callers, which is
exactly what identifies the code that used the bad pointer.
esp_backtrace_print_from_frame() skips the same check for EXCCAUSE_INSTR_PROHIBITED;
do the same here and include EXCCAUSE_INSTR_ERROR, seen on a device that jumped
into DRAM.
* Fix use after free in the Zigbee deferred timer list
Z_Devices::runTimer() runs the timer callbacks while iterating _deferred.
Publishing attributes from Z_PublishAttributes() runs the Rules and the Berry
handlers, which can send a Zigbee command; zigbeeZCLSendCmd() then re-arms the
read-back and reachability timers of the target device, and setTimer() clears
the previous ones of the same category. Freeing an element leaves the next
pointer cached by the LList iterator dangling, so the next turn of the loop
reads a freed node and calls whatever the allocator left in func.
On a ZBBridge Pro with 55 devices and rules that send a command on ZbReceived
this crashed about once a day, always as an instruction fetch to an address that
is nowhere in the image:
Exception 20 InstrFetchProhibited EPC 0xc88f8924
CallChain 488f8921 4012271e 400df4b8 400da20a 40157d20
bad ptr, XdrvCall, XdrvXsnsCall, Scheduler, loopTask
Detach the due timers into a local list first and run them afterwards, so no
iterator is held across a callback. The removal in the first loop only touches
the current element, which the iterator already handles.
* MIELHVAC New features and bug fixes
## Changelog
### Bug Fixes
**Memory leak in `miel_hvac_pre_init()`** — `goto del` jumped past the `free(sc)` label, leaking the allocated struct on serial init failure. Replaced with explicit `delete`/`free`/`return`.
**`remotetemp_clear` semantics inverted** — variable was `true` on boot causing a CLR frame to be sent before any sensor registered. Renamed to `remotetemp_active`, initialised `false`.
**`HVACSetPurify` used wrong map** — `airdirection_map` was passed instead of `purifier_map`.
**`0x08` Set Run State flags wrong byte order** — flags are little-endian on the wire (ref: muart-group/muart-group.github.io#17). Removed `htons()` from all `0x08` flag assignments.
**`widevane_isee` false positives** — values like `0x84`/`0x85`/`0x8c` (ISEE bit + position nibble) incorrectly reported `AirDirection:"even"`. Reverted to exact-match for `0x80`, `0x28`, `0xaa`.
---
### New Features
**`0x42` HVAC Options polling** — added `miel_hvac_data_hvac_options` struct and `MIEL_HVAC_REQUEST_HVAC_OPTIONS` request. The unit is polled for Purifier, NightMode and EconoCool state. Requires short request form (len=1). Results stored in `sc_hvac_options` and published in SENSOR and HVACSETTINGS when `cap_run_state=true`.
**Run State commands (`0x41 0x08`)** — new commands `HVACSetPurify`, `HVACSetNightMode`, `HVACSetEconoCool` and `HVACSetAirDirection` sent via `0x08` on units that report `cap_run_state=true`. Optimistic update to `sc_hvac_options` applied before confirmation.
**EconoCool** — `0x08` byte 14, flag `0x10`, COOL mode only. Command `HVACSetEconoCool on|off`.
**Base Capabilities (`0x5B 0xC9`)** — queried once after connecting. Parsed into `miel_hvac_capabilities`. Results published flat inside `MiElHVAC` as `*Supported` fields. Temperature ranges published as °C. Raw packet in `CapabilitiesHex`.
**Capabilities-aware command validation** — commands blocked when unit reports feature unavailable: modes (heat/dry/fan), temperature range from capabilities, fan speeds, run state commands return `NotSupported` or `ControlNotSupported`.
**`0x42` polling skip** — skipped on units with `cap_run_state=false`, eliminating recurring timeouts.
**`miel_hvac_append_settings_json()`** — shared helper replacing ~80 lines of duplicated JSON code between SENSOR and HVACSETTINGS topics.
---
### JSON Changes
- `Power` (W) and `Energy` (kWh) published inside `MiElHVAC` and also in a separate `ENERGY{}` object outside `MiElHVAC` for Home Assistant auto-discovery
- `Purifier`, `NightMode`, `EconoCool` shown in SENSOR and HVACSETTINGS when `cap_run_state=true`; otherwise omitted
- `AirDirection` always shows current state from `0x62 0x02` regardless of control support
- `*Supported` capability fields published flat inside `MiElHVAC`: `HeatSupported`, `DrySupported`, `FanSupported`, `VaneVSupported`, `SwingSupported`, `AutoFanSupported`, `OutdoorTempSupported`, `AirDirectionSupported`, `PurifierSupported`, `NightModeSupported`, `EconoCoolSupported`
---
### Tested on
MSZ-LN25VG2W — `cap_run_state=false`, temp ranges 16–31 °C cool/auto, 10–31 °C heat, 5 fan speeds.
* Extend AirDirection capability, added web ui energy sensor, cleanup
### AirDirection — respect i-See sensor presence
`AirDirection` now requires three conditions instead of just the vertical vane capability:
- `cap_vane_v` — unit has a vertical vane
- `sc_has_isee` — i-See sensor observed at runtime (widevane ever seen with bit `0x80`, or value `0x28`/`0xaa`)
- `cap_run_state` — unit supports `0x08` for control
Resulting behaviour:
| `cap_vane_v` | `sc_has_isee` | `cap_run_state` | `AirDirection` visible | `AirDirectionSupported` |
|:-:|:-:|:-:|:-:|:-:|
| ❌ | — | — | hidden | `not_supported` |
| ✅ | ❌ | — | hidden | `not_supported` |
| ✅ | ✅ | ❌ | shown | `control_not_supported` |
| ✅ | ✅ | ✅ | shown | `on` |
Units with vertical + horizontal vanes but no i-See sensor now correctly report `AirDirectionSupported:"not_supported"` and omit `AirDirection` from both `MiElHVAC` and `HVACSettings` JSON.
### Capability field renames
For naming consistency across the `*Supported` fields:
- `cap_heat` → `cap_mode_heat`, `HeatSupported` → `ModeHeatSupported`
- `cap_dry` → `cap_mode_dry`, `DrySupported` → `ModeDrySupported`
- `cap_fan_mode` → `cap_mode_fan`, `FanSupported` → `ModeFanSupported`
- `cap_auto_fan` → `cap_fan_auto`, `AutoFanSupported` → `FanAutoSupported`
- `OutdoorTempSupported` → `OutdoorTemperatureSupported`
- `TempCool` / `TempHeat` / `TempAuto` → `SetTemperatureCoolMinMax` / `SetTemperatureHeatMinMax` / `SetTemperatureAutoMinMax`
### Energy values
- `Power` (W) and `Energy` (kWh) published both inside `MiElHVAC` and in a standard Tasmota `ENERGY{}` sub-object (`Power`/`Total`) for Home Assistant auto-discovery.
- Added Web UI rows via `FUNC_WEB_SENSOR` showing instantaneous power (W) and cumulative total (kWh) on the Tasmota main page.
### Runtime i-See detection
New `sc_has_isee` flag in `miel_hvac_softc`, set in `miel_hvac_input_settings` on the first widevane value indicating i-See state. Once set, stays set for the session.
**Practical note:** after boot, the flag starts `false`. Units with i-See that has never been activated since Tasmota started will show `AirDirection` as hidden until i-See is first used (via IR remote or `HVACSetAirDirection`). Safe default — prevents showing misleading values on units without i-See.
* MiElHVAC Full support of AirDirection control
* MiELHVAC fix globals, VLA, duplicate JSON fields and float formatting
## Summary
- **Remove file-scope globals** (`temp_type`, `remotetemp_active`,
`remotetemp_auto_clear_time`, `remotetemp_last_call_time`,
`remotetemp_half`): moved into `miel_hvac_softc` as `sc_temp_type`
and `sc_remotetemp_*`. All references updated. Eliminates potential
conflict with other drivers sharing the same translation unit.
- **Fix VLA in `miel_hvac_send`**: `char hex_d[(len + 1) * 2]` replaced
with fixed-size `(MIEL_HVAC_DATABUFLEN + 1) * 2`. Variable-length
arrays on the stack are disallowed by `-Wvla` and unreliable on
embedded targets.
- **Make `temp_type` explicit**: helper functions `miel_hvac_deg2temp`,
`miel_hvac_temp2deg`, `miel_hvac_roomtemp2deg` now take `bool
temp_type` as a parameter. Detection of the extended encoding moved
from render functions to `miel_hvac_input_data()`, removing a hidden
side effect inside JSON serialisation code.
- **Remove duplicate JSON fields**: `Purifier`, `NightMode`, `EconoCool`
were emitted twice (from both the settings block and the options
block). Options block now only emits `OptionsHex`.
- **Fix `RemoteTemperatureSensorAutoClearTime` JSON type**: was
serialised as a string (`"10000"`), now a proper JSON number (`10000`).
- **Bool consistency**: assignments `= 1` / `= 0` on `bool` fields
replaced with `= true` / `= false`.
* MiElHVAC auto-enable i-See widevane when setting AirDirection
### Problem
`HVACSetAirDirection` (`indirect` / `direct` / `even`) sets the i-See airflow
direction via a `0x41 0x08` runstate packet (flag `0x2000`). The unit only
honors that value once i-See airflow control is enabled, which requires
`widevane=0x80` sent in a `0x01` settings packet.
Until now the user had to manually run `HVACSetWideVane isee` **before**
`HVACSetAirDirection`, otherwise the direction change was silently ignored.
### Change
`HVACSetAirDirection` now sets the widevane to
`MIEL_HVAC_SETTINGS_WIDEVANE_ISEE` (`0x80`) in the same call.
The dispatcher always sends the settings (`0x01`) packet before the runstate
(`0x41`) packet, so the unit receives the i-See enable first and the direction
value on the following tick — no manual ordering required.
### Notes
- `off` is unchanged (still expressed as `widevane=0x8c` via `0x01`).
- The existing capability guard (`cap_vane_v` + `sc_has_isee`) is unchanged.
* Fix GPIO name table offset from duplicate MODBUSRELAY entry
kSensorNames had the four D_MODBUSRELAY_* tokens twice while the
UserSelectablePins enum lists GPIO_MODBUSRELAY_* once, shifting every
following name by four - the GPIO dropdown showed MiEl HVAC MB Tx/Rx/DE
and CC1101 CS as 'MbsRelay ...'. Drop the duplicate row.
* MiEL HVAC accept fan_only as alias for fan mode
Home Assistant's climate integration uses 'fan_only'; map it to the same
value as 'fan' so HVACSetMode/HVACSetHAMode accept it. Reporting is
unchanged (miel_hvac_map_byval still returns 'fan').
* MiEL HVAC Modbus: mirror sensor values on FC03 holding registers
Add holding registers 0x0010..0x0017 as read-only mirrors of room
temperature, compressor, instantaneous power, stage operation/fan/mode
and the requests/CRC-error diagnostic counters, so a master that only
speaks FC03 can read them. Writes to these addresses still return
exception 0x02.
* MiEL HVAC Modbus: start FC03 mirror block at 0x000f, no gap after control block
* MiEL HVAC Modbus: queue writes when HVAC link is not up yet
Return to the master with a normal response and queue the update
(same as the HVACSet* console commands) instead of Modbus exception
0x04, so a PLC can pre-load settings and bench testing works without
the AC connected.
* MiEL HVAC Modbus: frame incoming requests by length, not the T3.5 gap
A FUNC_LOOP poll cannot measure the 3.5-char inter-frame gap reliably
above 9600 baud, so a frame split across two polls was processed as two
truncated frames (CRC error count exceeded the request count on a busy
19200 8E1 bus). Compute the expected request length from the function
code and dispatch as soon as it is buffered; keep the idle gap only to
flush a stuck partial frame or retry an unknown function code. Also skip
requests addressed to other slaves without a CRC check.
* MiEL HVAC Modbus: byte-level resync, larger RX buffer, no flush on auto-direction
CrcErrors was still ~1:1 with Requests on a busy 19200 8E1 multi-drop bus.
- resync one byte at a time on a framing/CRC mismatch instead of dropping
the whole assumed frame and waiting for bus idle
- only count a CRC error when the frame claims our own address
- 1 kB RX buffer so a late FUNC_LOOP poll does not lose bytes to a UART
FIFO overflow (TasmotaSerial cannot report the overflow on ESP32)
- bulk serial read instead of byte-at-a-time
- skip flush() when no DE pin is configured - on ESP32 flush() also
discards the RX buffer, dropping the next request on a shared bus
* MiEL HVAC Modbus: add link status to the FC03 sensor mirror
Holding register 0x0017 (PLC 40024) mirrors input 0x0001 - connected to
the HVAC unit (0/1).
* MiEL HVAC Modbus: wait the RTU turnaround silence before answering
Intermittent corrupted responses (master saw e.g. 80 C8 ...) on an
auto-direction transceiver at 19200: the slave started transmitting
before the master's line had turned around. Hold quiet for up to 3.5
char times after the last received byte before sending the response.
Track the last-byte time per read so the wait is accurate.
* MiEL HVAC Modbus: do not send a late reply once the master has re-polled
When a FUNC_LOOP stall makes the response miss the master's timeout
(Siemens S7 MB_MASTER STATUS 0x80C8), a newer request is already
buffered behind the stale one. Apply the stale request but skip its
response so it cannot land on the master's next transaction and make
that one time out too.
* MiEL HVAC Modbus: strip the i-See bit from the swingH register
Input 0x0015 / holding 0x0005 (40006) returned the raw wide-vane byte,
so a unit with an i-See sensor reported e.g. 131 (0x83 = 0x80 | center).
Report the position only (1..8, 12), or 0x80 when the byte encodes
i-See direction mode - matching the SwingH field in the SENSOR JSON.
* MiEL HVAC: document the driver and Modbus slave in the file header
Move the Modbus RTU slave detail out of the changelog into a doc block
at the head of xdrv_44_miel_hvac.ino, per the PR #24993 review.
* MIELHVAC New features and bug fixes
## Changelog
### Bug Fixes
**Memory leak in `miel_hvac_pre_init()`** — `goto del` jumped past the `free(sc)` label, leaking the allocated struct on serial init failure. Replaced with explicit `delete`/`free`/`return`.
**`remotetemp_clear` semantics inverted** — variable was `true` on boot causing a CLR frame to be sent before any sensor registered. Renamed to `remotetemp_active`, initialised `false`.
**`HVACSetPurify` used wrong map** — `airdirection_map` was passed instead of `purifier_map`.
**`0x08` Set Run State flags wrong byte order** — flags are little-endian on the wire (ref: muart-group/muart-group.github.io#17). Removed `htons()` from all `0x08` flag assignments.
**`widevane_isee` false positives** — values like `0x84`/`0x85`/`0x8c` (ISEE bit + position nibble) incorrectly reported `AirDirection:"even"`. Reverted to exact-match for `0x80`, `0x28`, `0xaa`.
---
### New Features
**`0x42` HVAC Options polling** — added `miel_hvac_data_hvac_options` struct and `MIEL_HVAC_REQUEST_HVAC_OPTIONS` request. The unit is polled for Purifier, NightMode and EconoCool state. Requires short request form (len=1). Results stored in `sc_hvac_options` and published in SENSOR and HVACSETTINGS when `cap_run_state=true`.
**Run State commands (`0x41 0x08`)** — new commands `HVACSetPurify`, `HVACSetNightMode`, `HVACSetEconoCool` and `HVACSetAirDirection` sent via `0x08` on units that report `cap_run_state=true`. Optimistic update to `sc_hvac_options` applied before confirmation.
**EconoCool** — `0x08` byte 14, flag `0x10`, COOL mode only. Command `HVACSetEconoCool on|off`.
**Base Capabilities (`0x5B 0xC9`)** — queried once after connecting. Parsed into `miel_hvac_capabilities`. Results published flat inside `MiElHVAC` as `*Supported` fields. Temperature ranges published as °C. Raw packet in `CapabilitiesHex`.
**Capabilities-aware command validation** — commands blocked when unit reports feature unavailable: modes (heat/dry/fan), temperature range from capabilities, fan speeds, run state commands return `NotSupported` or `ControlNotSupported`.
**`0x42` polling skip** — skipped on units with `cap_run_state=false`, eliminating recurring timeouts.
**`miel_hvac_append_settings_json()`** — shared helper replacing ~80 lines of duplicated JSON code between SENSOR and HVACSETTINGS topics.
---
### JSON Changes
- `Power` (W) and `Energy` (kWh) published inside `MiElHVAC` and also in a separate `ENERGY{}` object outside `MiElHVAC` for Home Assistant auto-discovery
- `Purifier`, `NightMode`, `EconoCool` shown in SENSOR and HVACSETTINGS when `cap_run_state=true`; otherwise omitted
- `AirDirection` always shows current state from `0x62 0x02` regardless of control support
- `*Supported` capability fields published flat inside `MiElHVAC`: `HeatSupported`, `DrySupported`, `FanSupported`, `VaneVSupported`, `SwingSupported`, `AutoFanSupported`, `OutdoorTempSupported`, `AirDirectionSupported`, `PurifierSupported`, `NightModeSupported`, `EconoCoolSupported`
---
### Tested on
MSZ-LN25VG2W — `cap_run_state=false`, temp ranges 16–31 °C cool/auto, 10–31 °C heat, 5 fan speeds.
* Extend AirDirection capability, added web ui energy sensor, cleanup
### AirDirection — respect i-See sensor presence
`AirDirection` now requires three conditions instead of just the vertical vane capability:
- `cap_vane_v` — unit has a vertical vane
- `sc_has_isee` — i-See sensor observed at runtime (widevane ever seen with bit `0x80`, or value `0x28`/`0xaa`)
- `cap_run_state` — unit supports `0x08` for control
Resulting behaviour:
| `cap_vane_v` | `sc_has_isee` | `cap_run_state` | `AirDirection` visible | `AirDirectionSupported` |
|:-:|:-:|:-:|:-:|:-:|
| ❌ | — | — | hidden | `not_supported` |
| ✅ | ❌ | — | hidden | `not_supported` |
| ✅ | ✅ | ❌ | shown | `control_not_supported` |
| ✅ | ✅ | ✅ | shown | `on` |
Units with vertical + horizontal vanes but no i-See sensor now correctly report `AirDirectionSupported:"not_supported"` and omit `AirDirection` from both `MiElHVAC` and `HVACSettings` JSON.
### Capability field renames
For naming consistency across the `*Supported` fields:
- `cap_heat` → `cap_mode_heat`, `HeatSupported` → `ModeHeatSupported`
- `cap_dry` → `cap_mode_dry`, `DrySupported` → `ModeDrySupported`
- `cap_fan_mode` → `cap_mode_fan`, `FanSupported` → `ModeFanSupported`
- `cap_auto_fan` → `cap_fan_auto`, `AutoFanSupported` → `FanAutoSupported`
- `OutdoorTempSupported` → `OutdoorTemperatureSupported`
- `TempCool` / `TempHeat` / `TempAuto` → `SetTemperatureCoolMinMax` / `SetTemperatureHeatMinMax` / `SetTemperatureAutoMinMax`
### Energy values
- `Power` (W) and `Energy` (kWh) published both inside `MiElHVAC` and in a standard Tasmota `ENERGY{}` sub-object (`Power`/`Total`) for Home Assistant auto-discovery.
- Added Web UI rows via `FUNC_WEB_SENSOR` showing instantaneous power (W) and cumulative total (kWh) on the Tasmota main page.
### Runtime i-See detection
New `sc_has_isee` flag in `miel_hvac_softc`, set in `miel_hvac_input_settings` on the first widevane value indicating i-See state. Once set, stays set for the session.
**Practical note:** after boot, the flag starts `false`. Units with i-See that has never been activated since Tasmota started will show `AirDirection` as hidden until i-See is first used (via IR remote or `HVACSetAirDirection`). Safe default — prevents showing misleading values on units without i-See.
* MiElHVAC Full support of AirDirection control
* MiELHVAC fix globals, VLA, duplicate JSON fields and float formatting
## Summary
- **Remove file-scope globals** (`temp_type`, `remotetemp_active`,
`remotetemp_auto_clear_time`, `remotetemp_last_call_time`,
`remotetemp_half`): moved into `miel_hvac_softc` as `sc_temp_type`
and `sc_remotetemp_*`. All references updated. Eliminates potential
conflict with other drivers sharing the same translation unit.
- **Fix VLA in `miel_hvac_send`**: `char hex_d[(len + 1) * 2]` replaced
with fixed-size `(MIEL_HVAC_DATABUFLEN + 1) * 2`. Variable-length
arrays on the stack are disallowed by `-Wvla` and unreliable on
embedded targets.
- **Make `temp_type` explicit**: helper functions `miel_hvac_deg2temp`,
`miel_hvac_temp2deg`, `miel_hvac_roomtemp2deg` now take `bool
temp_type` as a parameter. Detection of the extended encoding moved
from render functions to `miel_hvac_input_data()`, removing a hidden
side effect inside JSON serialisation code.
- **Remove duplicate JSON fields**: `Purifier`, `NightMode`, `EconoCool`
were emitted twice (from both the settings block and the options
block). Options block now only emits `OptionsHex`.
- **Fix `RemoteTemperatureSensorAutoClearTime` JSON type**: was
serialised as a string (`"10000"`), now a proper JSON number (`10000`).
- **Bool consistency**: assignments `= 1` / `= 0` on `bool` fields
replaced with `= true` / `= false`.
* MiElHVAC auto-enable i-See widevane when setting AirDirection
### Problem
`HVACSetAirDirection` (`indirect` / `direct` / `even`) sets the i-See airflow
direction via a `0x41 0x08` runstate packet (flag `0x2000`). The unit only
honors that value once i-See airflow control is enabled, which requires
`widevane=0x80` sent in a `0x01` settings packet.
Until now the user had to manually run `HVACSetWideVane isee` **before**
`HVACSetAirDirection`, otherwise the direction change was silently ignored.
### Change
`HVACSetAirDirection` now sets the widevane to
`MIEL_HVAC_SETTINGS_WIDEVANE_ISEE` (`0x80`) in the same call.
The dispatcher always sends the settings (`0x01`) packet before the runstate
(`0x41`) packet, so the unit receives the i-See enable first and the direction
value on the following tick — no manual ordering required.
### Notes
- `off` is unchanged (still expressed as `widevane=0x8c` via `0x01`).
- The existing capability guard (`cap_vane_v` + `sc_has_isee`) is unchanged.
* Fix GPIO name table offset from duplicate MODBUSRELAY entry
kSensorNames had the four D_MODBUSRELAY_* tokens twice while the
UserSelectablePins enum lists GPIO_MODBUSRELAY_* once, shifting every
following name by four - the GPIO dropdown showed MiEl HVAC MB Tx/Rx/DE
and CC1101 CS as 'MbsRelay ...'. Drop the duplicate row.
* MiEL HVAC accept fan_only as alias for fan mode
Home Assistant's climate integration uses 'fan_only'; map it to the same
value as 'fan' so HVACSetMode/HVACSetHAMode accept it. Reporting is
unchanged (miel_hvac_map_byval still returns 'fan').
Replace the on/off-only main-page presence with a full climate panel and
a live read-only state table, styled to a fixed dark palette scoped
under #hvacp so it is independent of the Tasmota theme.
- FUNC_WEB_ADD_MAIN_BUTTON renders the panel once: mode segment
(Auto/Heat/Cool/Dry/Fan/Off), target-temperature stepper + slider, and
Fan speed / Vane vertical / Vane horizontal / Air direction selects;
which controls appear is gated by the 0x7B 0xC9 Base Capabilities
(cap_mode_*, cap_vane_v, cap_vane_swing, cap_fan_auto, cap_run_state)
and miel_hvac_get_fan_count()
- controls call la('&<key>=<value>'); FUNC_WEB_GET_ARG turns each key
into the matching HVACSet* command (mode via HVACSetHAMode so a mode
button also powers the unit), reusing the existing validated setters
- FUNC_WEB_SENSOR refreshes the panel in place every web_refresh via the
<img onerror> exec trick: state-table cells, mode-button highlight and
every select value, skipped while a change is still in flight
(update_settings/runstate_pending) or the control is focused
- state table: Room/Set temp, Mode, Fan, Vane V/H, Air Direction,
Operation, Compressor, Power/Energy; locale decimal separator applied
(cherry picked from commit 6e013484f168f3748f6d388f18ef69f8313fab97)
Co-authored-by: Theo Arends <11044339+arendst@users.noreply.github.com>
* Fix GPIO name list falling out of sync without USE_MODBUS_RELAY
The enum in kGpioNiceList guards the four MODBUSRELAY entries with
#ifdef USE_MODBUS_RELAY, the matching name list does not. In any build
without that define - which is the default - the two run four entries
apart, and every GPIO role added after that point inherits a name that
belongs to something else.
Nothing visible went wrong so far because no role after the Modbus block
is currently offered in the selection list. The next one added would have
shown up as "MbsRelay TX".
* Add support for TFA Marbella pool thermometer
Receives the TFA Dostmann Marbella (30.3066.01) floating pool thermometer
on 868 MHz with a CC1101 and reports water temperature, sensor id, rolling
counter and battery state as sensor telemetry.
Packet layout, checksum algorithm and temperature formula are taken from
the rtl_433 project (src/devices/tfa_marbella.c and the reflected reverse
Galois digest in src/bit_util.c), Copyright (C) 2021 Benjamin Larsson,
GPL-2.0-or-later.
Wiring is the shared SPI bus plus two pins that belong to the module:
SCK / MISO / MOSI -> SPI CLK / SPI MISO / SPI MOSI
CSN -> CC1101 CS
GDO0 -> CC1101 GDO0
CC1101 CS is added as a role of its own, like every other SPI device has
one (LORA_CS, RC522_CS, MCP2515_CS and so on). The three bus lines stay
generic because they are shared - a display and this receiver hang on the
same wires, and only chip select tells them apart. The name string for it
already existed in the language files, unused.
Built on RadioLib, which already ships with Tasmota and drives the CC1101
on both ESP8266 and ESP32. The older lib/lib_rf/cc1101 declares
"platforms": "espressif8266" for a reason: on ESP32 it collides with the
CC1101 class RadioLib itself declares.
Three settings are not obvious and were established against hardware:
- The bit rate is 9.5238 kBit/s, not 9.6. rtl_433 documents a symbol
width of 105 us, and the 0.8 percent difference accumulates to about
0.7 bit over an 88 bit frame - enough that the first bytes arrive
intact and the last ones come out shifted.
- The sync word is 0xAAD2, although the frame itself begins with
AA 2D D4. The radio matches in hardware and strips what it matched,
so the two values describe the same transition from different sides.
- disableAddressFiltering() is required. RadioLib leaves address
checking on, and the chip then discards every frame whose first byte
does not match the node address - which for this sensor is always
0xAA. Without it reception runs and nothing ever arrives.
SPI is set up by the driver rather than left to RadioLib, whose ArduinoHal
calls SPI.begin() without arguments - on ESP32 that picks the default bus
pins and ignores the GPIO template, the same reason the LoRa drivers do it
themselves.
Reception follows the split the LoRa drivers use: the interrupt handler
sets a flag and nothing else, the packet is read in the main loop. The
flag is cleared after reception is restarted, not before, because
startReceive() puts GDO0 back to its idle level and that is an edge in
itself - clearing first makes the driver re-trigger on its own restart and
read the same stale buffer thousands of times per second. Both edges are
used: the pin does not reliably return to idle after a packet.
The driver binds to the first sensor with a valid checksum and ignores
every other one afterwards, so a neighbour's identical sensor cannot feed
readings into the same value. Marbella <id> rebinds, Marbella 0 relearns;
anything that is not a valid 24 bit id is refused rather than silently
turned into 0, which would look like a successful bind while clearing the
binding.
After TFA_MARBELLA_TIMEOUT seconds without a packet the reading disappears
from both the JSON payload and the web interface. A stale value looks
exactly like a measurement that simply does not change, which is the one
failure mode this driver must not produce.
No plausibility range is applied to the temperature. A range check of
0..50 degrees, as sometimes used in place of a checksum, would wrongly
discard a real reading of 51 degrees.
Verified against the sensor: frames decode with matching checksums, a
disturbed frame is rejected by the checksum, and the rolling counter
advances without gaps over consecutive receptions.
* Add SetOption166 to publish TFA Marbella readings on reception
Off by default, so readings go out with the regular TelePeriod telemetry
like those of every other sensor - a driver should not quietly behave
differently from the rest.
Worth turning on for this sensor: it transmits about once a minute, so
with the default TelePeriod of 300 seconds four out of five readings never
leave the device, and the one that does can be almost five minutes old.
With the option set, every reading becomes an MQTT message - useful for
rules, and more traffic on a busy broker, which is why it is a choice
rather than the default.
The bit is taken from the spare range of the SetOption146..177 block. If
the number collides with something merged in the meantime, it can be moved
without touching the driver.
* Add TFA Marbella build configuration
USE_TFA_MARBELLA is off by default and adds 12k6 of code on ESP8266 and
5k7 on ESP32, most of it RadioLib where it is not already in the build.
It sits with the other SPI peripherals because it needs USE_SPI: without
that, the CC1101 CS role cannot be assigned at all and the driver would be
silently dead.
USE_TFA_MARBELLA and USE_KEELOQ cannot be combined: both drive the same
CC1101, one transmitting on 433 MHz and one receiving on 868 MHz.
Whichever configured the chip last would win silently, so the combination
fails at compile time instead.
* Show the age of the last TFA Marbella reading in the web UI
The reading is still dropped from JSON after TFA_MARBELLA_TIMEOUT, so a
stale value never reaches MQTT. The web interface, however, showed nothing
at all in that case - and an empty page cannot be told apart from a driver
that is not running, a radio that never starts, or a sensor whose battery
died. Those need different answers.
The web interface now always shows a line: the age of the last packet
("12 sec ago"), or "no signal, last 4 Min ago" once the reading expired,
or "no signal yet" when nothing was ever received.
The age is shown rather than a timestamp: the sensor sends once a minute,
so what matters is whether the last packet is seconds or hours old - and a
clock time would be wrong anyway while the device has no NTP yet.
* Use SetOption147 for TFA Marbella and correct the radio parameters
Review feedback: drop the newly added SetOption166 and share the existing
SetOption147 instead. Readings are now published as they arrive, which is
what the sensor needs - it transmits once a minute while TelePeriod defaults
to 300 seconds - and SetOption147 suppresses that, the same handle the serial
bridge, IR, LoRaWAN and WizMote drivers already use. flag6 bit 20 goes back
to being spare.
Two radio parameters were wrong. Both came from the rtl_433 device
description and were never verified against a real sensor:
- Bit rate 9.4 kBit/s, not the 9523.8 Bd that rtl_433's 105 us per symbol
implies. Measured from three SDR recordings by decoding each at every
sampling phase and letting the sensor's own checksum decide: all three
peak at 9400 Bd, and none yields a single valid frame at 9523.8 Bd. Over
the 112 bits of a transmission a 1.3 % rate error drifts past a full bit,
so the tail of every frame arrives shifted - which is why 9.6 kBit/s
delivered nothing at all and 9.5238 only worked occasionally.
- Deviation 34.5 kHz, not 30, from the same recordings.
New setting: "Marbella <MHz>" tunes the receiver and is stored in
Settings->marbella_frequency (int16, 100 Hz steps, taken from the filler
after marbella_serial). The CC1101 derives its frequency from a 26 MHz
crystal, and one module measured here sat 27 kHz below what it was told.
That fits inside the 135 kHz receive filter but spends the margin the signal
needs, and it is a property of the board rather than of the driver - so it
is a setting instead of a constant, and nobody needs a private build for it.
A decimal point tells a frequency from a sensor id; ids now have to be the
full six hex digits, so "Marbella 868" is refused rather than silently
binding to sensor 000868.
Also documented at the sync word why 0xAAD2 is the right value: the sensor
sends an alternating preamble, then a single 0xD2, then the frame - so the
16 bits before AA 2D D4 read AA D2, which is exactly what the radio matches.
That was previously noted as unexplained.
Claude-Session: https://claude.ai/code/session_01EmAyStmKbyqFx9X7eTH7Qg
* Update tasmota_types.h
Use union
---------
Co-authored-by: Theo Arends <11044339+arendst@users.noreply.github.com>
* MIELHVAC New features and bug fixes
## Changelog
### Bug Fixes
**Memory leak in `miel_hvac_pre_init()`** — `goto del` jumped past the `free(sc)` label, leaking the allocated struct on serial init failure. Replaced with explicit `delete`/`free`/`return`.
**`remotetemp_clear` semantics inverted** — variable was `true` on boot causing a CLR frame to be sent before any sensor registered. Renamed to `remotetemp_active`, initialised `false`.
**`HVACSetPurify` used wrong map** — `airdirection_map` was passed instead of `purifier_map`.
**`0x08` Set Run State flags wrong byte order** — flags are little-endian on the wire (ref: muart-group/muart-group.github.io#17). Removed `htons()` from all `0x08` flag assignments.
**`widevane_isee` false positives** — values like `0x84`/`0x85`/`0x8c` (ISEE bit + position nibble) incorrectly reported `AirDirection:"even"`. Reverted to exact-match for `0x80`, `0x28`, `0xaa`.
---
### New Features
**`0x42` HVAC Options polling** — added `miel_hvac_data_hvac_options` struct and `MIEL_HVAC_REQUEST_HVAC_OPTIONS` request. The unit is polled for Purifier, NightMode and EconoCool state. Requires short request form (len=1). Results stored in `sc_hvac_options` and published in SENSOR and HVACSETTINGS when `cap_run_state=true`.
**Run State commands (`0x41 0x08`)** — new commands `HVACSetPurify`, `HVACSetNightMode`, `HVACSetEconoCool` and `HVACSetAirDirection` sent via `0x08` on units that report `cap_run_state=true`. Optimistic update to `sc_hvac_options` applied before confirmation.
**EconoCool** — `0x08` byte 14, flag `0x10`, COOL mode only. Command `HVACSetEconoCool on|off`.
**Base Capabilities (`0x5B 0xC9`)** — queried once after connecting. Parsed into `miel_hvac_capabilities`. Results published flat inside `MiElHVAC` as `*Supported` fields. Temperature ranges published as °C. Raw packet in `CapabilitiesHex`.
**Capabilities-aware command validation** — commands blocked when unit reports feature unavailable: modes (heat/dry/fan), temperature range from capabilities, fan speeds, run state commands return `NotSupported` or `ControlNotSupported`.
**`0x42` polling skip** — skipped on units with `cap_run_state=false`, eliminating recurring timeouts.
**`miel_hvac_append_settings_json()`** — shared helper replacing ~80 lines of duplicated JSON code between SENSOR and HVACSETTINGS topics.
---
### JSON Changes
- `Power` (W) and `Energy` (kWh) published inside `MiElHVAC` and also in a separate `ENERGY{}` object outside `MiElHVAC` for Home Assistant auto-discovery
- `Purifier`, `NightMode`, `EconoCool` shown in SENSOR and HVACSETTINGS when `cap_run_state=true`; otherwise omitted
- `AirDirection` always shows current state from `0x62 0x02` regardless of control support
- `*Supported` capability fields published flat inside `MiElHVAC`: `HeatSupported`, `DrySupported`, `FanSupported`, `VaneVSupported`, `SwingSupported`, `AutoFanSupported`, `OutdoorTempSupported`, `AirDirectionSupported`, `PurifierSupported`, `NightModeSupported`, `EconoCoolSupported`
---
### Tested on
MSZ-LN25VG2W — `cap_run_state=false`, temp ranges 16–31 °C cool/auto, 10–31 °C heat, 5 fan speeds.
* Extend AirDirection capability, added web ui energy sensor, cleanup
### AirDirection — respect i-See sensor presence
`AirDirection` now requires three conditions instead of just the vertical vane capability:
- `cap_vane_v` — unit has a vertical vane
- `sc_has_isee` — i-See sensor observed at runtime (widevane ever seen with bit `0x80`, or value `0x28`/`0xaa`)
- `cap_run_state` — unit supports `0x08` for control
Resulting behaviour:
| `cap_vane_v` | `sc_has_isee` | `cap_run_state` | `AirDirection` visible | `AirDirectionSupported` |
|:-:|:-:|:-:|:-:|:-:|
| ❌ | — | — | hidden | `not_supported` |
| ✅ | ❌ | — | hidden | `not_supported` |
| ✅ | ✅ | ❌ | shown | `control_not_supported` |
| ✅ | ✅ | ✅ | shown | `on` |
Units with vertical + horizontal vanes but no i-See sensor now correctly report `AirDirectionSupported:"not_supported"` and omit `AirDirection` from both `MiElHVAC` and `HVACSettings` JSON.
### Capability field renames
For naming consistency across the `*Supported` fields:
- `cap_heat` → `cap_mode_heat`, `HeatSupported` → `ModeHeatSupported`
- `cap_dry` → `cap_mode_dry`, `DrySupported` → `ModeDrySupported`
- `cap_fan_mode` → `cap_mode_fan`, `FanSupported` → `ModeFanSupported`
- `cap_auto_fan` → `cap_fan_auto`, `AutoFanSupported` → `FanAutoSupported`
- `OutdoorTempSupported` → `OutdoorTemperatureSupported`
- `TempCool` / `TempHeat` / `TempAuto` → `SetTemperatureCoolMinMax` / `SetTemperatureHeatMinMax` / `SetTemperatureAutoMinMax`
### Energy values
- `Power` (W) and `Energy` (kWh) published both inside `MiElHVAC` and in a standard Tasmota `ENERGY{}` sub-object (`Power`/`Total`) for Home Assistant auto-discovery.
- Added Web UI rows via `FUNC_WEB_SENSOR` showing instantaneous power (W) and cumulative total (kWh) on the Tasmota main page.
### Runtime i-See detection
New `sc_has_isee` flag in `miel_hvac_softc`, set in `miel_hvac_input_settings` on the first widevane value indicating i-See state. Once set, stays set for the session.
**Practical note:** after boot, the flag starts `false`. Units with i-See that has never been activated since Tasmota started will show `AirDirection` as hidden until i-See is first used (via IR remote or `HVACSetAirDirection`). Safe default — prevents showing misleading values on units without i-See.
* MiElHVAC Full support of AirDirection control
* MiELHVAC fix globals, VLA, duplicate JSON fields and float formatting
## Summary
- **Remove file-scope globals** (`temp_type`, `remotetemp_active`,
`remotetemp_auto_clear_time`, `remotetemp_last_call_time`,
`remotetemp_half`): moved into `miel_hvac_softc` as `sc_temp_type`
and `sc_remotetemp_*`. All references updated. Eliminates potential
conflict with other drivers sharing the same translation unit.
- **Fix VLA in `miel_hvac_send`**: `char hex_d[(len + 1) * 2]` replaced
with fixed-size `(MIEL_HVAC_DATABUFLEN + 1) * 2`. Variable-length
arrays on the stack are disallowed by `-Wvla` and unreliable on
embedded targets.
- **Make `temp_type` explicit**: helper functions `miel_hvac_deg2temp`,
`miel_hvac_temp2deg`, `miel_hvac_roomtemp2deg` now take `bool
temp_type` as a parameter. Detection of the extended encoding moved
from render functions to `miel_hvac_input_data()`, removing a hidden
side effect inside JSON serialisation code.
- **Remove duplicate JSON fields**: `Purifier`, `NightMode`, `EconoCool`
were emitted twice (from both the settings block and the options
block). Options block now only emits `OptionsHex`.
- **Fix `RemoteTemperatureSensorAutoClearTime` JSON type**: was
serialised as a string (`"10000"`), now a proper JSON number (`10000`).
- **Bool consistency**: assignments `= 1` / `= 0` on `bool` fields
replaced with `= true` / `= false`.
* MiElHVAC auto-enable i-See widevane when setting AirDirection
### Problem
`HVACSetAirDirection` (`indirect` / `direct` / `even`) sets the i-See airflow
direction via a `0x41 0x08` runstate packet (flag `0x2000`). The unit only
honors that value once i-See airflow control is enabled, which requires
`widevane=0x80` sent in a `0x01` settings packet.
Until now the user had to manually run `HVACSetWideVane isee` **before**
`HVACSetAirDirection`, otherwise the direction change was silently ignored.
### Change
`HVACSetAirDirection` now sets the widevane to
`MIEL_HVAC_SETTINGS_WIDEVANE_ISEE` (`0x80`) in the same call.
The dispatcher always sends the settings (`0x01`) packet before the runstate
(`0x41`) packet, so the unit receives the i-See enable first and the direction
value on the following tick — no manual ordering required.
### Notes
- `off` is unchanged (still expressed as `widevane=0x8c` via `0x01`).
- The existing capability guard (`cap_vane_v` + `sc_has_isee`) is unchanged.
* Add Modbus RTU slave to MiEL HVAC driver (xdrv_44)
Optional second RS485 serial port that exposes every MiEL HVAC state as
Modbus read registers and maps every driver function to write
registers/coils, so the unit can be driven directly from a PLC.
- ESP32 only, opt-in via USE_MIEL_HVAC_MODBUS_SLAVE
- new GPIOs: MiEl HVAC MB Tx / Rx / DE (RS485 direction, optional)
- persisted config (address, baudrate, serial config) reclaimed from
Settings free_eb0 + SBitfield1 spare bit; no settings version bump
- function codes 0x01-0x06, 0x0F, 0x10 with CRC-16, RTU inter-frame
gap framing in FUNC_LOOP, and Modbus exception replies
- input registers (FC04) mirror settings, room/outdoor temp, timers,
status/energy, stage, capabilities; holding registers/coils drive
power, mode, temp, fan, vane, widevane, airdirection, prohibit,
purifier, night mode, econocool, remote temp and clear time
- writes reuse new miel_hvac_apply_* typed setters (same capability
gating as the console commands, which are unchanged)
- commands HVACModbus / HVACModbusAddress / HVACModbusBaudrate /
HVACModbusConfig (all live), plus a "Modbus" block in SENSOR JSON
* Update tasmota_template.h
Fix duplicate GPIOs
* Update tasmota_types.h
Use union for parameters
---------
Co-authored-by: Theo Arends <11044339+arendst@users.noreply.github.com>
* [EQ3-TRV] Next part of code refactoring
- Update command description
- Correct and optimize filter and identification logic (Alias, Name, MAC address)
- Optimize logic for mode in result
- Correct and optimize logic for hassmode in result
- Add `Mode` to UI incl. enhancement of language files
- Tighten types (use bool/uint8_t)
- Harden some input validations
- Many other simplifications and improvements
* ValvePos, device identification
- Rename DutyCycle to ValvePos
- Simplify identification logic
- Make BLE remark smaller