Add binary sensor platform to FlowIt (#184296)

This commit is contained in:
Alberto Geniola
2026-10-05 16:11:55 +02:00
committed by GitHub
parent aed25e8135
commit f92d304993
5 changed files with 621 additions and 0 deletions
@@ -14,6 +14,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.FAN,
Platform.SENSOR,
Platform.SWITCH,
@@ -0,0 +1,101 @@
"""Binary sensor platform for Flow-it."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import override
from flow_it_api.models import MachineStatusResponse
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import FlowItConfigEntry
from .entity import FlowItVmcEntity
@dataclass(frozen=True, kw_only=True)
class FlowItVmcBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Flow-it binary sensor entity."""
value_fn: Callable[[MachineStatusResponse], bool | None]
BINARY_SENSORS: tuple[FlowItVmcBinarySensorEntityDescription, ...] = (
FlowItVmcBinarySensorEntityDescription(
key="bypass_on",
translation_key="bypass_on",
value_fn=lambda data: data.data.mode.bypassOn,
),
FlowItVmcBinarySensorEntityDescription(
key="condensation",
translation_key="condensation",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.data.alert.condensation,
),
FlowItVmcBinarySensorEntityDescription(
key="ice",
translation_key="ice",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.data.alert.ice,
),
FlowItVmcBinarySensorEntityDescription(
key="service",
translation_key="service",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.data.alert.service,
),
FlowItVmcBinarySensorEntityDescription(
key="update_reboot",
translation_key="update_reboot",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.data.alert.update_reboot,
),
FlowItVmcBinarySensorEntityDescription(
key="warmup",
translation_key="warmup",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda data: data.data.alert.warmup,
),
FlowItVmcBinarySensorEntityDescription(
key="worries",
translation_key="worries",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.data.alert.worries,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: FlowItConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Flow-it binary sensors."""
data = config_entry.runtime_data
async_add_entities(
FlowItVmcBinarySensor(data.coordinator, data.vmc, description)
for description in BINARY_SENSORS
)
class FlowItVmcBinarySensor(FlowItVmcEntity, BinarySensorEntity):
"""Flow-it binary sensor entity."""
entity_description: FlowItVmcBinarySensorEntityDescription
@override
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self.entity_description.value_fn(self.coordinator.data.state)
@@ -37,6 +37,29 @@
}
},
"entity": {
"binary_sensor": {
"bypass_on": {
"name": "Bypass active"
},
"condensation": {
"name": "Condensation alert"
},
"ice": {
"name": "Ice alert"
},
"service": {
"name": "Service required"
},
"update_reboot": {
"name": "Reboot pending"
},
"warmup": {
"name": "Warmup mode"
},
"worries": {
"name": "General issue"
}
},
"sensor": {
"g4_filter_status": {
"name": "G4 filter status",
@@ -0,0 +1,355 @@
# serializer version: 1
# name: test_binary_sensor_setup[binary_sensor.001122334455_bypass_active-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.001122334455_bypass_active',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Bypass active',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Bypass active',
'platform': 'flow_it',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'bypass_on',
'unique_id': '001122334455_bypass_on',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_bypass_active-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '001122334455 Bypass active',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.001122334455_bypass_active',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_condensation_alert-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.001122334455_condensation_alert',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Condensation alert',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Condensation alert',
'platform': 'flow_it',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'condensation',
'unique_id': '001122334455_condensation',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_condensation_alert-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '001122334455 Condensation alert',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.001122334455_condensation_alert',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_general_issue-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.001122334455_general_issue',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'General issue',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'General issue',
'platform': 'flow_it',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'worries',
'unique_id': '001122334455_worries',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_general_issue-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '001122334455 General issue',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.001122334455_general_issue',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_ice_alert-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.001122334455_ice_alert',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Ice alert',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Ice alert',
'platform': 'flow_it',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ice',
'unique_id': '001122334455_ice',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_ice_alert-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '001122334455 Ice alert',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.001122334455_ice_alert',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_reboot_pending-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.001122334455_reboot_pending',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reboot pending',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reboot pending',
'platform': 'flow_it',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'update_reboot',
'unique_id': '001122334455_update_reboot',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_reboot_pending-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '001122334455 Reboot pending',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.001122334455_reboot_pending',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_service_required-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.001122334455_service_required',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Service required',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Service required',
'platform': 'flow_it',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'service',
'unique_id': '001122334455_service',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_service_required-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '001122334455 Service required',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.001122334455_service_required',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_warmup_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.001122334455_warmup_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Warmup mode',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Warmup mode',
'platform': 'flow_it',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'warmup',
'unique_id': '001122334455_warmup',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.001122334455_warmup_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '001122334455 Warmup mode',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.001122334455_warmup_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
@@ -0,0 +1,141 @@
"""Test Flow-it binary sensor platform."""
from collections.abc import Generator
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
BYPASS_ENTITY_ID = "binary_sensor.001122334455_bypass_active"
CONDENSATION_ENTITY_ID = "binary_sensor.001122334455_condensation_alert"
ICE_ENTITY_ID = "binary_sensor.001122334455_ice_alert"
REBOOT_PENDING_ENTITY_ID = "binary_sensor.001122334455_reboot_pending"
SERVICE_ENTITY_ID = "binary_sensor.001122334455_service_required"
WARMUP_ENTITY_ID = "binary_sensor.001122334455_warmup_mode"
WORRIES_ENTITY_ID = "binary_sensor.001122334455_general_issue"
@pytest.fixture(autouse=True)
def binary_sensor_only() -> Generator[None]:
"""Only setup binary sensor platform."""
with patch("homeassistant.components.flow_it.PLATFORMS", [Platform.BINARY_SENSOR]):
yield
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_binary_sensor_setup(
hass: HomeAssistant,
mock_flow_it: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test binary sensor platform setup and entity registry."""
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_binary_sensor_warmup_disabled_by_default(
hass: HomeAssistant,
mock_flow_it: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test warmup binary sensor is disabled by default."""
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entry = entity_registry.async_get(WARMUP_ENTITY_ID)
assert entry
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
assert hass.states.get(WARMUP_ENTITY_ID) is None
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_binary_sensor_none_values(
hass: HomeAssistant,
mock_flow_it: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test binary sensor state when values are None."""
mock_flow_it.return_value.state.data.alert.ice = None
mock_flow_it.return_value.state.data.alert.condensation = None
mock_flow_it.return_value.state.data.alert.service = None
mock_flow_it.return_value.state.data.alert.worries = None
mock_flow_it.return_value.state.data.alert.update_reboot = None
mock_flow_it.return_value.state.data.alert.warmup = None
mock_flow_it.return_value.state.data.mode.bypassOn = None
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
for entity_id in (
ICE_ENTITY_ID,
CONDENSATION_ENTITY_ID,
SERVICE_ENTITY_ID,
WORRIES_ENTITY_ID,
REBOOT_PENDING_ENTITY_ID,
BYPASS_ENTITY_ID,
WARMUP_ENTITY_ID,
):
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNKNOWN
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("entity_id", "field", "is_mode"),
[
pytest.param(BYPASS_ENTITY_ID, "bypassOn", True, id="bypass_on"),
pytest.param(CONDENSATION_ENTITY_ID, "condensation", False, id="condensation"),
pytest.param(ICE_ENTITY_ID, "ice", False, id="ice"),
pytest.param(
REBOOT_PENDING_ENTITY_ID, "update_reboot", False, id="update_reboot"
),
pytest.param(SERVICE_ENTITY_ID, "service", False, id="service"),
pytest.param(WARMUP_ENTITY_ID, "warmup", False, id="warmup"),
pytest.param(WORRIES_ENTITY_ID, "worries", False, id="worries"),
],
)
async def test_binary_sensor_update(
hass: HomeAssistant,
mock_flow_it: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
entity_id: str,
field: str,
is_mode: bool,
) -> None:
"""Test binary sensor state updates via periodic coordinator refresh."""
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
target = (
mock_flow_it.return_value.state.data.mode
if is_mode
else mock_flow_it.return_value.state.data.alert
)
setattr(target, field, True)
freezer.tick(timedelta(seconds=60))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_ON