diff --git a/homeassistant/components/flow_it/__init__.py b/homeassistant/components/flow_it/__init__.py index c94bae3a8a45..d5befb6509e8 100644 --- a/homeassistant/components/flow_it/__init__.py +++ b/homeassistant/components/flow_it/__init__.py @@ -14,6 +14,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.FAN, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/flow_it/binary_sensor.py b/homeassistant/components/flow_it/binary_sensor.py new file mode 100644 index 000000000000..f69039a29e7d --- /dev/null +++ b/homeassistant/components/flow_it/binary_sensor.py @@ -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) diff --git a/homeassistant/components/flow_it/strings.json b/homeassistant/components/flow_it/strings.json index 5ca4994bab5b..f3075463121e 100644 --- a/homeassistant/components/flow_it/strings.json +++ b/homeassistant/components/flow_it/strings.json @@ -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", diff --git a/tests/components/flow_it/snapshots/test_binary_sensor.ambr b/tests/components/flow_it/snapshots/test_binary_sensor.ambr new file mode 100644 index 000000000000..dd68c28c79d4 --- /dev/null +++ b/tests/components/flow_it/snapshots/test_binary_sensor.ambr @@ -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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + '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({ + : '001122334455 Bypass active', + }), + 'context': , + 'entity_id': 'binary_sensor.001122334455_bypass_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.001122334455_condensation_alert', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Condensation alert', + 'options': dict({ + }), + 'original_device_class': , + '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({ + : 'problem', + : '001122334455 Condensation alert', + }), + 'context': , + 'entity_id': 'binary_sensor.001122334455_condensation_alert', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.001122334455_general_issue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'General issue', + 'options': dict({ + }), + 'original_device_class': , + '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({ + : 'problem', + : '001122334455 General issue', + }), + 'context': , + 'entity_id': 'binary_sensor.001122334455_general_issue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.001122334455_ice_alert', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Ice alert', + 'options': dict({ + }), + 'original_device_class': , + '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({ + : 'problem', + : '001122334455 Ice alert', + }), + 'context': , + 'entity_id': 'binary_sensor.001122334455_ice_alert', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.001122334455_reboot_pending', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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({ + : '001122334455 Reboot pending', + }), + 'context': , + 'entity_id': 'binary_sensor.001122334455_reboot_pending', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.001122334455_service_required', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Service required', + 'options': dict({ + }), + 'original_device_class': , + '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({ + : 'problem', + : '001122334455 Service required', + }), + 'context': , + 'entity_id': 'binary_sensor.001122334455_service_required', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.001122334455_warmup_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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({ + : '001122334455 Warmup mode', + }), + 'context': , + 'entity_id': 'binary_sensor.001122334455_warmup_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/flow_it/test_binary_sensor.py b/tests/components/flow_it/test_binary_sensor.py new file mode 100644 index 000000000000..cae92d4f66f8 --- /dev/null +++ b/tests/components/flow_it/test_binary_sensor.py @@ -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