Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion homeassistant/components/rabbitair/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator

PLATFORMS: list[Platform] = [Platform.FAN]
PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: RabbitAirConfigEntry) -> bool:
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/rabbitair/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
}
}
}
},
"sensor": {
"air_quality": {
"default": "mdi:air-filter"
}
}
}
}
64 changes: 64 additions & 0 deletions homeassistant/components/rabbitair/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Support for Rabbit Air sensors."""

from rabbitair import Quality

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType

from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator
from .entity import RabbitAirBaseEntity


def _quality_value(quality: Quality | None) -> StateType:
"""Return the air quality state."""
return None if quality is None else quality.name.lower()


AIR_QUALITY_OPTIONS = [quality.name.lower() for quality in Quality]

AIR_QUALITY_DESCRIPTION = SensorEntityDescription(
key="air_quality",
translation_key="air_quality",
device_class=SensorDeviceClass.ENUM,
options=AIR_QUALITY_OPTIONS,
)


async def async_setup_entry(
hass: HomeAssistant,
entry: RabbitAirConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Rabbit Air sensors."""
if entry.runtime_data.data.quality is not None:
async_add_entities([RabbitAirAirQualitySensor(entry.runtime_data, entry)])
Comment on lines +39 to +40
Copy link
Copy Markdown
Contributor Author

@MagikalUnicorn MagikalUnicorn Jun 4, 2026

Choose a reason for hiding this comment

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

@joostlek - I’m not sure we can definitively know which current and future Rabbit Air models support quality, though my assumption is that they all will do. Given that, would you prefer that we always create the air quality sensor and let it report unknown when the device returns quality=None, or keep the current setup-time check and only create the entity when the first fetched state includes a quality value?



class RabbitAirAirQualitySensor(RabbitAirBaseEntity, SensorEntity):
"""Rabbit Air air quality sensor."""

entity_description = AIR_QUALITY_DESCRIPTION
_attr_has_entity_name = True

def __init__(
self,
coordinator: RabbitAirDataUpdateCoordinator,
entry: RabbitAirConfigEntry,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator, entry)
del self._attr_name
Comment thread
MagikalUnicorn marked this conversation as resolved.
Outdated
self._attr_unique_id = f"{entry.unique_id}_{self.entity_description.key}"
self._attr_native_value = _quality_value(coordinator.data.quality)

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._attr_native_value = _quality_value(self.coordinator.data.quality)
super()._handle_coordinator_update()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead I'd implement the native_value property

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated. The air quality sensor now implements native_value as a property based on the coordinator data instead of storing _attr_native_value and updating it manually.

12 changes: 12 additions & 0 deletions homeassistant/components/rabbitair/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@
}
}
}
},
"sensor": {
"air_quality": {
"name": "Air quality",
"state": {
"high": "[%key:common::state::high%]",
"highest": "Highest",
"low": "[%key:common::state::low%]",
"lowest": "Lowest",
"medium": "[%key:common::state::medium%]"
}
}
}
}
}
4 changes: 3 additions & 1 deletion tests/components/rabbitair/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from unittest.mock import MagicMock, Mock, patch

import pytest
from rabbitair import Mode, Model, Speed
from rabbitair import Mode, Model, Quality, Speed

from homeassistant import config_entries
from homeassistant.components.rabbitair.const import DOMAIN
Expand Down Expand Up @@ -62,6 +62,7 @@ def get_mock_state(
main_firmware: str | None = TEST_HARDWARE,
power: bool | None = True,
mode: Mode | None = Mode.Auto,
quality: Quality | None = Quality.High,
speed: Speed | None = Speed.Low,
wifi_firmware: str | None = TEST_FIRMWARE,
) -> Mock:
Expand All @@ -71,6 +72,7 @@ def get_mock_state(
mock_state.main_firmware = main_firmware
mock_state.power = power
mock_state.mode = mode
mock_state.quality = quality
mock_state.speed = speed
mock_state.wifi_firmware = wifi_firmware
return mock_state
Expand Down
55 changes: 55 additions & 0 deletions tests/components/rabbitair/test_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Test the Rabbit Air sensor platform."""

from unittest.mock import patch

import pytest
from rabbitair import Quality

from homeassistant.components.rabbitair.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MAC
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er

from .test_config_flow import (
TEST_HOST,
TEST_MAC,
TEST_TITLE,
TEST_TOKEN,
TEST_UNIQUE_ID,
get_mock_state,
)

from tests.common import MockConfigEntry


@pytest.mark.usefixtures("mock_async_zeroconf")
async def test_air_quality_sensor(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
Comment thread
MagikalUnicorn marked this conversation as resolved.
Outdated
"""Test the air quality sensor."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: TEST_HOST,
CONF_ACCESS_TOKEN: TEST_TOKEN,
CONF_MAC: TEST_MAC,
},
title=TEST_TITLE,
unique_id=TEST_UNIQUE_ID,
)
Comment on lines +30 to +39
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we make this a test fixture?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated. The repeated MockConfigEntry setup is now in a mock_config_entry fixture.

entry.add_to_hass(hass)

with patch(
"rabbitair.UdpClient.get_state",
return_value=get_mock_state(quality=Quality.High),
):
Comment on lines +50 to +53
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we patch it where we use it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated. The tests now patch homeassistant.components.rabbitair.coordinator.Client.get_state, where the integration uses the client.

assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

state = hass.states.get("sensor.rabbit_air_air_quality")
assert state
assert state.state == "high"

registry_entry = entity_registry.async_get("sensor.rabbit_air_air_quality")
assert registry_entry
assert registry_entry.unique_id == f"{TEST_UNIQUE_ID}_air_quality"
Loading