Add cover entity to Fluss#169840
Closed
Marcello17 wants to merge 9 commits intohome-assistant:devfrom
Closed
Conversation
Contributor
|
Hey there @fluss, mind taking a look at this pull request as it has been labeled with an integration ( Code owner commandsCode owners of
|
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new cover platform to the Fluss+ integration, exposing devices that report an openCloseStatus as garage-door-style covers while keeping the existing button entity for devices without a position sensor.
Changes:
- Introduces a Fluss cover entity with open/close services and state derived from
openCloseStatus. - Refactors coordinator data into a typed
FlussDevicedataclass and expands per-device status fetching to include position status. - Adds snapshot + behavioral test coverage for the new cover platform, including mixed device dispatch.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
homeassistant/components/fluss/__init__.py |
Forwards the new cover platform alongside the existing button platform. |
homeassistant/components/fluss/coordinator.py |
Introduces FlussDevice dataclass and fetches/merges per-device status payloads. |
homeassistant/components/fluss/entity.py |
Updates base entity to use typed FlussDevice and adds helper for sensor capability detection. |
homeassistant/components/fluss/button.py |
Filters out cover-capable devices and updates to use typed coordinator data. |
homeassistant/components/fluss/cover.py |
Adds the new cover entity implementation (state parsing + open/close commands). |
homeassistant/components/fluss/strings.json |
Adds translated exception messages for open/close failures. |
tests/components/fluss/__init__.py |
Updates platform-forwarding expectation to include cover. |
tests/components/fluss/test_cover.py |
Adds tests for cover registration, state parsing, services, errors, and mixed dispatch. |
tests/components/fluss/snapshots/test_cover.ambr |
Adds entity/state snapshots for the cover platform. |
Comment on lines
+37
to
+56
| class FlussCover(FlussEntity, CoverEntity): | ||
| """Representation of a Fluss+ cover (garage door / gate).""" | ||
|
|
||
| _attr_device_class = CoverDeviceClass.GARAGE | ||
| _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | ||
| _attr_name = None | ||
|
|
||
| @property | ||
| def is_closed(self) -> bool | None: | ||
| """Return whether the cover is closed.""" | ||
| status = self.device.open_close_status | ||
| if isinstance(status, bool): | ||
| return not status | ||
| if isinstance(status, str): | ||
| normalized = status.lower() | ||
| if normalized == "closed": | ||
| return True | ||
| if normalized == "open": | ||
| return False | ||
| return None |
|
|
||
| def has_open_close_sensor(device: FlussDevice) -> bool: | ||
| """Return whether a device reports an open/close position sensor.""" | ||
| return device.open_close_status is not None |
Comment on lines
+53
to
+59
| async def _async_get_status(self, device_id: str) -> dict[str, Any]: | ||
| """Return per-device status; defaults to offline on API error.""" | ||
| try: | ||
| status = await self.api.async_get_device_status(device_id) | ||
| response = await self.api.async_get_device_status(device_id) | ||
| except FlussApiClientError: | ||
| return False | ||
| return status["status"]["internetConnected"] | ||
| return {"internetConnected": False} | ||
| return response["status"] |
Comment on lines
+96
to
+101
| internet_connected = status.get("internetConnected", False) | ||
| if "openCloseStatus" in status: | ||
| self._cover_capable.add(device_id) | ||
| is_closed = status["openCloseStatus"] == "Close" | ||
| else: | ||
| is_closed = None |
Comment on lines
+80
to
+95
| @pytest.mark.parametrize( | ||
| ("status_value", "expected_state"), | ||
| [("Close", STATE_CLOSED), ("Open", STATE_OPEN)], | ||
| ) | ||
| async def test_cover_state( | ||
| hass: HomeAssistant, | ||
| mock_api_client: AsyncMock, | ||
| mock_config_entry: MockConfigEntry, | ||
| status_value: str, | ||
| expected_state: str, | ||
| ) -> None: | ||
| """The API contract is exactly "Open" or "Close" — verify both map correctly.""" | ||
| mock_api_client.async_get_device_status.side_effect = _status_side_effect( | ||
| {DEVICE_ID_1: {"openCloseStatus": status_value}} | ||
| ) | ||
| await _setup_cover_only(hass, mock_config_entry) |
Comment on lines
+72
to
+77
| except FlussApiClientError as err: | ||
| raise HomeAssistantError( | ||
| translation_domain=DOMAIN, | ||
| translation_key="command_failed", | ||
| translation_placeholders={"error": str(err)}, | ||
| ) from err |
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
homeassistant/components/fluss/button.py:48
- Use a translated
HomeAssistantError(translation_domain/key) for button press failures to match the new translated cover command errors. The current plain string error message creates inconsistent UX and makes it harder to localize errors across the integration.
try:
await self.coordinator.api.async_trigger_device(self.device_id)
except FlussApiClientError as err:
raise HomeAssistantError(f"Failed to trigger device: {err}") from err
Comment on lines
+96
to
+100
| internet_connected = status.get("internetConnected", False) | ||
| if "openCloseStatus" in status: | ||
| self._cover_capable.add(device_id) | ||
| is_closed = status["openCloseStatus"] == "Closed" | ||
| else: |
| internet_connected = status.get("internetConnected", False) | ||
| if "openCloseStatus" in status: | ||
| self._cover_capable.add(device_id) | ||
| is_closed = status["openCloseStatus"] == "Closed" |
Comment on lines
+68
to
+77
| async def async_open_cover(self, **kwargs: Any) -> None: | ||
| """Open the cover.""" | ||
| try: | ||
| await self.coordinator.api.async_open_device(self.device_id) | ||
| except FlussApiClientError as err: | ||
| raise HomeAssistantError( | ||
| translation_domain=DOMAIN, | ||
| translation_key="command_failed", | ||
| translation_placeholders={"error": str(err)}, | ||
| ) from err |
Comment on lines
+83
to
+92
| [("Closed", STATE_CLOSED), ("Open", STATE_OPEN)], | ||
| ) | ||
| async def test_cover_state( | ||
| hass: HomeAssistant, | ||
| mock_api_client: AsyncMock, | ||
| mock_config_entry: MockConfigEntry, | ||
| status_value: str, | ||
| expected_state: str, | ||
| ) -> None: | ||
| """The API contract is exactly "Open" or "Closed" — verify both map correctly.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Breaking change
Proposed change
Adds a
coverplatform to the Fluss+ integration so devices with aposition sensor are exposed as a garage door / gate cover instead of a
button. Builds on the per-device status fetch from #168154.
The dispatch is decided once per device at platform setup, based on
whether the API returns
openCloseStatus. Devices that report it become aCoverEntity(CoverDeviceClass.GARAGE,OPEN | CLOSE) and gainexplicit open/close commands plus open/closed state. Devices that don't
report it keep the existing button — the two are mutually exclusive per
device, so no user with an existing button setup loses anything.
Open and close go through the library's
async_open_device/async_close_deviceand end withcoordinator.async_request_refresh(),so state in the UI reflects the new position immediately rather than
waiting for the next 30 minute coordinator cycle. Library failures
translate to
HomeAssistantErrorviaopen_failed/close_failedtranslation keys.
Cover and button share the same coordinator, so we still make exactly one
status call per device per refresh — the existing
_async_get_connectivityhelper is generalised to return the fullstatus payload, keeping connectivity behaviour and feeding
openCloseStatusto the cover from the same data.
While here, the coordinator is moved to a typed
FlussDevicedataclass(
dict[str, FlussDevice]) instead ofdict[str, dict[str, Any]], matchingthe pattern in newer Platinum integrations like
peblar,airgradient,airos,airobot, andapcupsd. snake_case fields throughout; camelCaseconversion happens at the coordinator boundary.
openCloseStatusis parsed defensively for both the documented booleanshape and the example-payload string shape —
"Closed"/"Open"(case-insensitive) and
True/Falseboth map correctly. Verifiedlocally against a real Fluss+ device.
100% test coverage on every fluss file (31 tests).
ruff,hassfest,and
mypyclean.Type of change
Additional information
once this lands.
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: