Skip to content

feat: expose valve/subdevice run modes (duration/volume) in Home Assistant - #20

Merged
mplogas merged 16 commits into
mainfrom
feat-valve-run-modes
Jun 4, 2026
Merged

mplogas merged 16 commits into
mainfrom
feat-valve-run-modes

Conversation

@mplogas

@mplogas mplogas commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

Exposes the Ecowitt subdevice "run for a duration / volume" capability as first-class Home Assistant entities, so users trigger device-self-closing runs instead of orchestrating open → HA-timer → close. The command path already existed (SubdeviceApiCommand carries Duration/Unit) but was never exposed to HA or tested on hardware; this builds the control surface and verifies the encoding against real devices.

Safety motivation: with HA timers, a lost close command leaves a valve stuck open (flooding). With device-side duration, the single start command carries its own deadline — the worst case flips from "stuck open" to "didn't start."

Entities (capability-driven, per controllable subdevice)

On top of the existing always-on switch (unchanged):

  • Run Mode select — only when 2+ modes apply
  • Run Duration (min) / Run Volume (L) numbers — entity_category: config
  • Start Run button

Capability resolver: Duration for all controllable subdevices (WFC01/WFC02/AC1100); Volume for WFC01 (flow built in) and WFC02 only when flowmeter == 1; never for AC1100. Single-mode devices skip the select.

Architecture

  • Staged config lives on the Subdevice in DeviceStore. A new SubdeviceRunConfig bus message carries partial updates + a Start flag; MqttService stays a pure translator (+/subdevices/+/cmd/# subscription routes the new topics); Dispatcher merges and, on Start, routes an assembled SubdeviceApiCommand through a shared GuardRunCommand into the existing SubdeviceCommandDispatch path.
  • Magic numbers isolated in the gateway adapter (SendSubdeviceCommand.ToGatewayRun): time → seconds val_type:0 (×60/×3600); volume → deciliters val_type:3 (×10). Verified on hardware (a 5 L command reads back val:50).
  • Guard scoped to parameterized runs (Unit.HasValue): a bare always-on Start (the HA switch) is left untouched; null/≤0/negative → default; over-max → clamp. Clamping before the multiply makes overflow impossible.

Bug fixes folded in

  • Case-insensitive direct-command deserialization (PropertyNameCaseInsensitive + JsonStringEnumConverter) so the documented lowercase payload actually binds; docs/mqtt.md corrected.

Smoke tested — all green

  • WFC02 (dev): duration + volume runs self-close; switch preserved; direct-MQTT -1 defaults to 3 min.
  • AC1100 (prod): duration-only entities, timed run self-closes.
  • WFC01 (prod): select + both numbers, duration and volume self-close.

The shared-protocol assumption for AC1100/WFC01 held — all three encode runs identically.

Known limitation (documented, v1)

Staged config may reset to defaults after a controller restart (retained replay arrives before the subdevice is re-polled into the store). Device-side runs are unaffected; the user re-sets the number. Proper fix is a follow-up.

Tests

210/210 (33 new across conversion, registry, capability resolver, flowmeter, discovery builders, guard, path-builder). Build clean.

Out of scope

Cycle mode (registry-extensible), live state reflection, per-subdevice configurable defaults.

mplogas added 14 commits June 4, 2026 23:01
… Subdevice.cs

Remove the continue after setting HasFlowMeter so the flowmeter field falls
through to SensorBuilder, preserving the "Flowmeter Available" binary_sensor
in Subdevice.Sensors. Add a regression assertion to Map_WFC02_ParsesFlowMeterCapability.
Add using Ecowitt.Controller.Model.Api to Subdevice.cs and shorten the
StagedRunConfig.Mode property declaration from the fully-qualified name.
… merge tests

Three quality issues from review:
- Log a warning when UpsertGateway returns false after staged run config merge
  (mirrors every other call site in Dispatcher.ConsumerHttp.cs)
- Warn and skip dispatch when the staged value for the active run mode is <= 0
  (spec: selected-mode input of 0 -> warning, no dispatch)
- Add DispatcherMergeTest covering partial merge, assembly, duration/volume
  dispatch, zero-value no-dispatch, unknown subdevice, and no-start paths
…very

RunModeRegistry.ApplicableModes is the single source of truth for which
models support run modes. The method already returns early when modes is
empty, so the outer model check in the caller was a DRY violation that
would require updating in two places when a new model is added.
Copilot AI review requested due to automatic review settings June 4, 2026 21:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class Home Assistant control surfaces for Ecowitt valve “parameterized runs” (run for a duration or volume), including MQTT discovery entities, staged per-subdevice configuration stored in the controller, and a guarded command translation path to gateway-native (val_type, val) encoding.

Changes:

  • Introduces run-mode capability modeling (RunModeRegistry) and per-subdevice staged run configuration (StagedRunConfig) with a new SubdeviceRunConfig message flow for partial updates + “Start”.
  • Expands MQTT command handling and HA discovery to publish/select/configure run mode + run parameters and to start runs via a button entity.
  • Refactors gateway command encoding to normalize time to seconds and volume to deciliters, with test coverage around conversion, registry behavior, guard logic, and merge/dispatch behavior.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Ecowitt.Controller/Service/Orchestrator/Dispatcher.cs Adds SubdeviceRunConfig consumption to orchestrator dispatcher.
src/Ecowitt.Controller/Service/Orchestrator/Dispatcher.ConsumerHttp.cs Implements staged run-config merge/start flow and shared command guard.
src/Ecowitt.Controller/Service/Mqtt/MqttService.Events.cs Adds /cmd/* topic routing for run-mode control; improves direct-command deserialization options.
src/Ecowitt.Controller/Service/Mqtt/MqttService.DiscoveryPublisher.cs Publishes HA discovery for run-mode select/number/button entities and retained staged values.
src/Ecowitt.Controller/Service/Mqtt/MqttService.cs Emits run-mode discovery per subdevice during HA discovery emission.
src/Ecowitt.Controller/Service/Mqtt/MqttService.Consumer.cs Subscribes to the full subdevice command subtree (+/cmd/#).
src/Ecowitt.Controller/Service/Mqtt/MqttPathBuilder.cs Adds helpers for run-mode MQTT topics and a cmd/# subtree filter.
src/Ecowitt.Controller/Service/Http/HttpPublishingService.cs Adds ToGatewayRun() mapping and uses it for start commands (seconds/deciliters).
src/Ecowitt.Controller/Program.cs Registers SlimMessageBus produce/consume for SubdeviceRunConfig.
src/Ecowitt.Controller/Model/Subdevice.cs Adds HasFlowMeter and StagedRunConfig to the subdevice model.
src/Ecowitt.Controller/Model/Message/Data/SubdeviceRunConfig.cs New message type for staged run config updates and start trigger.
src/Ecowitt.Controller/Model/Mapping/ApiDataExtension.cs Parses flowmeter field into HasFlowMeter (while still building a sensor).
src/Ecowitt.Controller/Model/Discovery/DiscoveryBuilder.cs Adds discovery config builders for number, select, and button.
src/Ecowitt.Controller/Model/Discovery/Config.cs Extends discovery config DTO with number/select/button fields.
src/Ecowitt.Controller/Model/Api/RunMode.cs New run-mode capability model + registry (duration/volume gating).
src/EcoWitt.Controller.Tests/SubdeviceMappingTest.cs Adds tests for flowmeter parsing and staged run-config defaults.
src/EcoWitt.Controller.Tests/SubdeviceCommandConversionTest.cs New tests for gateway run-value conversion mapping.
src/EcoWitt.Controller.Tests/RunModeTest.cs New tests for run-mode registry defaults and capability gating.
src/EcoWitt.Controller.Tests/MqttPathBuilderTest.cs Adds tests for new cmd-subtree and run-mode topic builders.
src/EcoWitt.Controller.Tests/DispatcherMergeTest.cs New tests for staged merge/start assembly and dispatch behavior.
src/EcoWitt.Controller.Tests/DispatcherGuardTest.cs New tests for guard defaulting/clamping and always-on behavior.
src/EcoWitt.Controller.Tests/DiscoveryBuilderTest.cs Adds tests for new discovery config builders.
docs/mqtt.md Updates direct MQTT command docs to match improved enum/string deserialization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings June 4, 2026 21:16
@mplogas
mplogas merged commit 5ed0024 into main Jun 4, 2026
1 check passed
@mplogas
mplogas deleted the feat-valve-run-modes branch June 4, 2026 21:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Comment on lines +67 to +71
var selId = DiscoveryBuilder.BuildIdentifier(subdevice.Nickname, "runmode");
var selCfg = DiscoveryBuilder.BuildSelectConfig(device, _origin, "Run Mode", selId,
commandTopic: $"{baseTopic}/cmd/mode",
options: modes.Select(m => m.HaLabel).ToList());
await PublishDiscoveryMessage("select", $"select/{nick}", selCfg);
Comment on lines +79 to +83
var numId = DiscoveryBuilder.BuildIdentifier(subdevice.Nickname, $"run{param}");
var numCfg = DiscoveryBuilder.BuildNumberConfig(device, _origin, $"Run {m.HaLabel}", numId,
commandTopic: $"{baseTopic}/cmd/set/{param}",
min: m.Min, max: m.Max, step: 1, unitOfMeasurement: unit, entityCategory: "config");
await PublishDiscoveryMessage("number", $"number/{nick}_{param}", numCfg);
Comment thread docs/mqtt.md
- **`<base>/+/subdevices/+/cmd/set/volume`** — run volume in liters (plain integer; flow-capable valves only).
- **`<base>/+/subdevices/+/cmd/start`** — start a run using the currently staged mode + value.

All subdevice command topics (the four above plus `cmd` and `cmd/homeassistant`) are covered by a single `<base>/+/subdevices/+/cmd/#` subscription. A run mode the device doesn't support (e.g. `Volume` on a flow-less valve) is rejected; a `Start` with a missing/invalid value falls back to a safe default (3 min / 5 L).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants