diff --git a/homeassistant/components/weheat/__init__.py b/homeassistant/components/weheat/__init__.py index d924d6ceaab45e..a043a3a68455fe 100644 --- a/homeassistant/components/weheat/__init__.py +++ b/homeassistant/components/weheat/__init__.py @@ -17,7 +17,7 @@ from .const import API_URL, LOGGER from .coordinator import WeheatDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] type WeheatConfigEntry = ConfigEntry[list[WeheatDataUpdateCoordinator]] diff --git a/homeassistant/components/weheat/binary_sensor.py b/homeassistant/components/weheat/binary_sensor.py new file mode 100644 index 00000000000000..ea939227e77e40 --- /dev/null +++ b/homeassistant/components/weheat/binary_sensor.py @@ -0,0 +1,100 @@ +"""Binary sensor platform for Weheat integration.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from weheat.abstractions.heat_pump import HeatPump + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import WeheatConfigEntry +from .coordinator import WeheatDataUpdateCoordinator +from .entity import WeheatEntity + + +@dataclass(frozen=True, kw_only=True) +class WeHeatBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Weheat binary sensor entity.""" + + value_fn: Callable[[HeatPump], StateType] + + +BINARY_SENSORS = [ + WeHeatBinarySensorEntityDescription( + translation_key="indoor_unit_water_pump_state", + key="indoor_unit_water_pump_state", + device_class=BinarySensorDeviceClass.RUNNING, + value_fn=lambda status: status.indoor_unit_water_pump_state, + ), + WeHeatBinarySensorEntityDescription( + translation_key="indoor_unit_auxiliary_pump_state", + key="indoor_unit_auxiliary_pump_state", + device_class=BinarySensorDeviceClass.RUNNING, + value_fn=lambda status: status.indoor_unit_auxiliary_pump_state, + ), + WeHeatBinarySensorEntityDescription( + translation_key="indoor_unit_dhw_valve_or_pump_state", + key="indoor_unit_dhw_valve_or_pump_state", + device_class=BinarySensorDeviceClass.RUNNING, + value_fn=lambda status: status.indoor_unit_dhw_valve_or_pump_state, + ), + WeHeatBinarySensorEntityDescription( + translation_key="indoor_unit_gas_boiler_state", + key="indoor_unit_gas_boiler_state", + value_fn=lambda status: status.indoor_unit_gas_boiler_state, + ), + WeHeatBinarySensorEntityDescription( + translation_key="indoor_unit_electric_heater_state", + key="indoor_unit_electric_heater_state", + device_class=BinarySensorDeviceClass.RUNNING, + value_fn=lambda status: status.indoor_unit_electric_heater_state, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: WeheatConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensors for weheat heat pump.""" + entities = [ + WeheatHeatPumpBinarySensor(coordinator, entity_description) + for entity_description in BINARY_SENSORS + for coordinator in entry.runtime_data + if entity_description.value_fn(coordinator.data) is not None + ] + + async_add_entities(entities) + + +class WeheatHeatPumpBinarySensor(WeheatEntity, BinarySensorEntity): + """Defines a Weheat heat pump binary sensor.""" + + coordinator: WeheatDataUpdateCoordinator + entity_description: WeHeatBinarySensorEntityDescription + + def __init__( + self, + coordinator: WeheatDataUpdateCoordinator, + entity_description: WeHeatBinarySensorEntityDescription, + ) -> None: + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + + self.entity_description = entity_description + + self._attr_unique_id = f"{coordinator.heatpump_id}_{entity_description.key}" + + @property + def is_on(self) -> bool | None: + """Return True if the binary sensor is on.""" + value = self.entity_description.value_fn(self.coordinator.data) + return bool(value) if value is not None else None diff --git a/homeassistant/components/weheat/icons.json b/homeassistant/components/weheat/icons.json index 7efd13b0dfbca6..e7f54b478c6838 100644 --- a/homeassistant/components/weheat/icons.json +++ b/homeassistant/components/weheat/icons.json @@ -1,5 +1,22 @@ { "entity": { + "binary_sensor": { + "indoor_unit_water_pump_state": { + "default": "mdi:pump" + }, + "indoor_unit_auxiliary_pump_state": { + "default": "mdi:pump" + }, + "indoor_unit_dhw_valve_or_pump_state": { + "default": "mdi:pump" + }, + "indoor_unit_gas_boiler_state": { + "default": "mdi:toggle-switch" + }, + "indoor_unit_electric_heater_state": { + "default": "mdi:heating-coil" + } + }, "sensor": { "power_output": { "default": "mdi:heat-wave" diff --git a/homeassistant/components/weheat/strings.json b/homeassistant/components/weheat/strings.json index c993a6beefed07..2a208c2f8ca7c7 100644 --- a/homeassistant/components/weheat/strings.json +++ b/homeassistant/components/weheat/strings.json @@ -32,6 +32,23 @@ } }, "entity": { + "binary_sensor": { + "indoor_unit_water_pump_state": { + "name": "Indoor unit water pump" + }, + "indoor_unit_auxiliary_pump_state": { + "name": "Indoor unit auxilary water pump" + }, + "indoor_unit_dhw_valve_or_pump_state": { + "name": "Indoor unit DHW valve or water pump" + }, + "indoor_unit_gas_boiler_state": { + "name": "Indoor unit gas boiler heating allowed" + }, + "indoor_unit_electric_heater_state": { + "name": "Indoor unit electric heater" + } + }, "sensor": { "power_output": { "name": "Output power" diff --git a/tests/components/weheat/conftest.py b/tests/components/weheat/conftest.py index 7169a3b56c8b08..1bbe91fc573c90 100644 --- a/tests/components/weheat/conftest.py +++ b/tests/components/weheat/conftest.py @@ -124,6 +124,11 @@ def mock_weheat_heat_pump_instance() -> MagicMock: mock_heat_pump_instance.energy_output = 56789 mock_heat_pump_instance.compressor_rpm = 4500 mock_heat_pump_instance.compressor_percentage = 100 + mock_heat_pump_instance.indoor_unit_water_pump_state = False + mock_heat_pump_instance.indoor_unit_auxiliary_pump_state = False + mock_heat_pump_instance.indoor_unit_dhw_valve_or_pump_state = None + mock_heat_pump_instance.indoor_unit_gas_boiler_state = False + mock_heat_pump_instance.indoor_unit_electric_heater_state = True return mock_heat_pump_instance diff --git a/tests/components/weheat/snapshots/test_binary_sensor.ambr b/tests/components/weheat/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000000..08d609ca610c60 --- /dev/null +++ b/tests/components/weheat/snapshots/test_binary_sensor.ambr @@ -0,0 +1,188 @@ +# serializer version: 1 +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_auxilary_water_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_model_indoor_unit_auxilary_water_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Indoor unit auxilary water pump', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_unit_auxiliary_pump_state', + 'unique_id': '0000-1111-2222-3333_indoor_unit_auxiliary_pump_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_auxilary_water_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Model Indoor unit auxilary water pump', + }), + 'context': , + 'entity_id': 'binary_sensor.test_model_indoor_unit_auxilary_water_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_electric_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_model_indoor_unit_electric_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Indoor unit electric heater', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_unit_electric_heater_state', + 'unique_id': '0000-1111-2222-3333_indoor_unit_electric_heater_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_electric_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Model Indoor unit electric heater', + }), + 'context': , + 'entity_id': 'binary_sensor.test_model_indoor_unit_electric_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_gas_boiler_heating_allowed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_model_indoor_unit_gas_boiler_heating_allowed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indoor unit gas boiler heating allowed', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_unit_gas_boiler_state', + 'unique_id': '0000-1111-2222-3333_indoor_unit_gas_boiler_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_gas_boiler_heating_allowed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Model Indoor unit gas boiler heating allowed', + }), + 'context': , + 'entity_id': 'binary_sensor.test_model_indoor_unit_gas_boiler_heating_allowed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_water_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_model_indoor_unit_water_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Indoor unit water pump', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_unit_water_pump_state', + 'unique_id': '0000-1111-2222-3333_indoor_unit_water_pump_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_water_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Model Indoor unit water pump', + }), + 'context': , + 'entity_id': 'binary_sensor.test_model_indoor_unit_water_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/weheat/test_binary_sensor.py b/tests/components/weheat/test_binary_sensor.py new file mode 100644 index 00000000000000..e75cb282e2460e --- /dev/null +++ b/tests/components/weheat/test_binary_sensor.py @@ -0,0 +1,52 @@ +"""Tests for the weheat sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion +from weheat.abstractions.discovery import HeatPumpDiscovery + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_weheat_discover: AsyncMock, + mock_weheat_heat_pump: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.weheat.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_create_binary_entities( + hass: HomeAssistant, + mock_weheat_discover: AsyncMock, + mock_weheat_heat_pump: AsyncMock, + mock_heat_pump_info: HeatPumpDiscovery.HeatPumpInfo, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test creating entities.""" + mock_weheat_discover.return_value = [mock_heat_pump_info] + + with patch("homeassistant.components.weheat.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4