diff --git a/homeassistant/components/drop_connect/__init__.py b/homeassistant/components/drop_connect/__init__.py index 45978a48d9a..63aad855829 100644 --- a/homeassistant/components/drop_connect/__init__.py +++ b/homeassistant/components/drop_connect/__init__.py @@ -15,7 +15,7 @@ from .coordinator import DROPDeviceDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/drop_connect/binary_sensor.py b/homeassistant/components/drop_connect/binary_sensor.py new file mode 100644 index 00000000000..4c392eb8ce1 --- /dev/null +++ b/homeassistant/components/drop_connect/binary_sensor.py @@ -0,0 +1,138 @@ +"""Support for DROP binary sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + CONF_DEVICE_TYPE, + DEV_HUB, + DEV_LEAK_DETECTOR, + DEV_PROTECTION_VALVE, + DEV_PUMP_CONTROLLER, + DEV_RO_FILTER, + DEV_SALT_SENSOR, + DEV_SOFTENER, + DOMAIN, +) +from .coordinator import DROPDeviceDataUpdateCoordinator +from .entity import DROPEntity + +_LOGGER = logging.getLogger(__name__) + +LEAK_ICON = "mdi:pipe-leak" +NOTIFICATION_ICON = "mdi:bell-ring" +PUMP_ICON = "mdi:water-pump" +SALT_ICON = "mdi:shaker" +WATER_ICON = "mdi:water" + +# Binary sensor type constants +LEAK_DETECTED = "leak" +PENDING_NOTIFICATION = "pending_notification" +PUMP_STATUS = "pump" +RESERVE_IN_USE = "reserve_in_use" +SALT_LOW = "salt" + + +@dataclass(kw_only=True, frozen=True) +class DROPBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes DROP binary sensor entity.""" + + value_fn: Callable[[DROPDeviceDataUpdateCoordinator], bool | None] + + +BINARY_SENSORS: list[DROPBinarySensorEntityDescription] = [ + DROPBinarySensorEntityDescription( + key=LEAK_DETECTED, + translation_key=LEAK_DETECTED, + icon=LEAK_ICON, + device_class=BinarySensorDeviceClass.MOISTURE, + value_fn=lambda device: device.drop_api.leak_detected(), + ), + DROPBinarySensorEntityDescription( + key=PENDING_NOTIFICATION, + translation_key=PENDING_NOTIFICATION, + icon=NOTIFICATION_ICON, + value_fn=lambda device: device.drop_api.notification_pending(), + ), + DROPBinarySensorEntityDescription( + key=SALT_LOW, + translation_key=SALT_LOW, + icon=SALT_ICON, + value_fn=lambda device: device.drop_api.salt_low(), + ), + DROPBinarySensorEntityDescription( + key=RESERVE_IN_USE, + translation_key=RESERVE_IN_USE, + icon=WATER_ICON, + value_fn=lambda device: device.drop_api.reserve_in_use(), + ), + DROPBinarySensorEntityDescription( + key=PUMP_STATUS, + translation_key=PUMP_STATUS, + icon=PUMP_ICON, + value_fn=lambda device: device.drop_api.pump_status(), + ), +] + +# Defines which binary sensors are used by each device type +DEVICE_BINARY_SENSORS: dict[str, list[str]] = { + DEV_HUB: [LEAK_DETECTED, PENDING_NOTIFICATION], + DEV_LEAK_DETECTOR: [LEAK_DETECTED], + DEV_PROTECTION_VALVE: [LEAK_DETECTED], + DEV_PUMP_CONTROLLER: [LEAK_DETECTED, PUMP_STATUS], + DEV_RO_FILTER: [LEAK_DETECTED], + DEV_SALT_SENSOR: [SALT_LOW], + DEV_SOFTENER: [RESERVE_IN_USE], +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the DROP binary sensors from config entry.""" + _LOGGER.debug( + "Set up binary sensor for device type %s with entry_id is %s", + config_entry.data[CONF_DEVICE_TYPE], + config_entry.entry_id, + ) + + if config_entry.data[CONF_DEVICE_TYPE] in DEVICE_BINARY_SENSORS: + async_add_entities( + DROPBinarySensor(hass.data[DOMAIN][config_entry.entry_id], sensor) + for sensor in BINARY_SENSORS + if sensor.key in DEVICE_BINARY_SENSORS[config_entry.data[CONF_DEVICE_TYPE]] + ) + + +class DROPBinarySensor(DROPEntity, BinarySensorEntity): + """Representation of a DROP binary sensor.""" + + entity_description: DROPBinarySensorEntityDescription + + def __init__( + self, + coordinator: DROPDeviceDataUpdateCoordinator, + entity_description: DROPBinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(entity_description.key, coordinator) + self.entity_description = entity_description + + @property + def is_on(self) -> bool: + """Return the state of the binary sensor.""" + return self.entity_description.value_fn(self.coordinator) == 1 diff --git a/homeassistant/components/drop_connect/strings.json b/homeassistant/components/drop_connect/strings.json index 0674515412f..2f11cf29cf8 100644 --- a/homeassistant/components/drop_connect/strings.json +++ b/homeassistant/components/drop_connect/strings.json @@ -25,6 +25,13 @@ "cart1": { "name": "Cartridge 1 life remaining" }, "cart2": { "name": "Cartridge 2 life remaining" }, "cart3": { "name": "Cartridge 3 life remaining" } + }, + "binary_sensor": { + "leak": { "name": "Leak detected" }, + "pending_notification": { "name": "Notification unread" }, + "reserve_in_use": { "name": "Reserve capacity in use" }, + "salt": { "name": "Salt low" }, + "pump": { "name": "Pump status" } } } } diff --git a/tests/components/drop_connect/test_binary_sensor.py b/tests/components/drop_connect/test_binary_sensor.py new file mode 100644 index 00000000000..ca94faeec5e --- /dev/null +++ b/tests/components/drop_connect/test_binary_sensor.py @@ -0,0 +1,192 @@ +"""Test DROP binary sensor entities.""" + +from homeassistant.components.drop_connect.const import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .common import ( + TEST_DATA_HUB, + TEST_DATA_HUB_RESET, + TEST_DATA_HUB_TOPIC, + TEST_DATA_LEAK, + TEST_DATA_LEAK_RESET, + TEST_DATA_LEAK_TOPIC, + TEST_DATA_PROTECTION_VALVE, + TEST_DATA_PROTECTION_VALVE_RESET, + TEST_DATA_PROTECTION_VALVE_TOPIC, + TEST_DATA_PUMP_CONTROLLER, + TEST_DATA_PUMP_CONTROLLER_RESET, + TEST_DATA_PUMP_CONTROLLER_TOPIC, + TEST_DATA_RO_FILTER, + TEST_DATA_RO_FILTER_RESET, + TEST_DATA_RO_FILTER_TOPIC, + TEST_DATA_SALT, + TEST_DATA_SALT_RESET, + TEST_DATA_SALT_TOPIC, + TEST_DATA_SOFTENER, + TEST_DATA_SOFTENER_RESET, + TEST_DATA_SOFTENER_TOPIC, +) + +from tests.common import async_fire_mqtt_message +from tests.typing import MqttMockHAClient + + +async def test_binary_sensors_hub( + hass: HomeAssistant, config_entry_hub, mqtt_mock: MqttMockHAClient +) -> None: + """Test DROP binary sensors for hubs.""" + config_entry_hub.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + pending_notifications_sensor_name = ( + "binary_sensor.hub_drop_1_c0ffee_notification_unread" + ) + hass.states.async_set(pending_notifications_sensor_name, STATE_UNKNOWN) + leak_sensor_name = "binary_sensor.hub_drop_1_c0ffee_leak_detected" + hass.states.async_set(leak_sensor_name, STATE_UNKNOWN) + + async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, TEST_DATA_HUB_RESET) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, TEST_DATA_HUB) + await hass.async_block_till_done() + + pending_notifications = hass.states.get(pending_notifications_sensor_name) + assert pending_notifications.state == STATE_ON + + leak = hass.states.get(leak_sensor_name) + assert leak.state == STATE_OFF + + +async def test_binary_sensors_salt( + hass: HomeAssistant, config_entry_salt, mqtt_mock: MqttMockHAClient +) -> None: + """Test DROP binary sensors for salt sensors.""" + config_entry_salt.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + salt_sensor_name = "binary_sensor.salt_sensor_salt_low" + hass.states.async_set(salt_sensor_name, STATE_UNKNOWN) + + async_fire_mqtt_message(hass, TEST_DATA_SALT_TOPIC, TEST_DATA_SALT_RESET) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, TEST_DATA_SALT_TOPIC, TEST_DATA_SALT) + await hass.async_block_till_done() + + salt = hass.states.get(salt_sensor_name) + assert salt.state == STATE_ON + + +async def test_binary_sensors_leak( + hass: HomeAssistant, config_entry_leak, mqtt_mock: MqttMockHAClient +) -> None: + """Test DROP binary sensors for leak detectors.""" + config_entry_leak.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + leak_sensor_name = "binary_sensor.leak_detector_leak_detected" + hass.states.async_set(leak_sensor_name, STATE_UNKNOWN) + + async_fire_mqtt_message(hass, TEST_DATA_LEAK_TOPIC, TEST_DATA_LEAK_RESET) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, TEST_DATA_LEAK_TOPIC, TEST_DATA_LEAK) + await hass.async_block_till_done() + + leak = hass.states.get(leak_sensor_name) + assert leak.state == STATE_ON + + +async def test_binary_sensors_softener( + hass: HomeAssistant, config_entry_softener, mqtt_mock: MqttMockHAClient +) -> None: + """Test DROP binary sensors for softeners.""" + config_entry_softener.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + reserve_in_use_sensor_name = "binary_sensor.softener_reserve_capacity_in_use" + hass.states.async_set(reserve_in_use_sensor_name, STATE_UNKNOWN) + + async_fire_mqtt_message(hass, TEST_DATA_SOFTENER_TOPIC, TEST_DATA_SOFTENER_RESET) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, TEST_DATA_SOFTENER_TOPIC, TEST_DATA_SOFTENER) + await hass.async_block_till_done() + + reserve_in_use = hass.states.get(reserve_in_use_sensor_name) + assert reserve_in_use.state == STATE_ON + + +async def test_binary_sensors_protection_valve( + hass: HomeAssistant, config_entry_protection_valve, mqtt_mock: MqttMockHAClient +) -> None: + """Test DROP binary sensors for protection valves.""" + config_entry_protection_valve.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + leak_sensor_name = "binary_sensor.protection_valve_leak_detected" + hass.states.async_set(leak_sensor_name, STATE_UNKNOWN) + + async_fire_mqtt_message( + hass, TEST_DATA_PROTECTION_VALVE_TOPIC, TEST_DATA_PROTECTION_VALVE_RESET + ) + await hass.async_block_till_done() + async_fire_mqtt_message( + hass, TEST_DATA_PROTECTION_VALVE_TOPIC, TEST_DATA_PROTECTION_VALVE + ) + await hass.async_block_till_done() + + leak = hass.states.get(leak_sensor_name) + assert leak.state == STATE_ON + + +async def test_binary_sensors_pump_controller( + hass: HomeAssistant, config_entry_pump_controller, mqtt_mock: MqttMockHAClient +) -> None: + """Test DROP binary sensors for pump controllers.""" + config_entry_pump_controller.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + leak_sensor_name = "binary_sensor.pump_controller_leak_detected" + hass.states.async_set(leak_sensor_name, STATE_UNKNOWN) + pump_sensor_name = "binary_sensor.pump_controller_pump_status" + hass.states.async_set(pump_sensor_name, STATE_UNKNOWN) + + async_fire_mqtt_message( + hass, TEST_DATA_PUMP_CONTROLLER_TOPIC, TEST_DATA_PUMP_CONTROLLER_RESET + ) + await hass.async_block_till_done() + async_fire_mqtt_message( + hass, TEST_DATA_PUMP_CONTROLLER_TOPIC, TEST_DATA_PUMP_CONTROLLER + ) + await hass.async_block_till_done() + + leak = hass.states.get(leak_sensor_name) + assert leak.state == STATE_ON + pump = hass.states.get(pump_sensor_name) + assert pump.state == STATE_ON + + +async def test_binary_sensors_ro_filter( + hass: HomeAssistant, config_entry_ro_filter, mqtt_mock: MqttMockHAClient +) -> None: + """Test DROP binary sensors for RO filters.""" + config_entry_ro_filter.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + leak_sensor_name = "binary_sensor.ro_filter_leak_detected" + hass.states.async_set(leak_sensor_name, STATE_UNKNOWN) + + async_fire_mqtt_message(hass, TEST_DATA_RO_FILTER_TOPIC, TEST_DATA_RO_FILTER_RESET) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, TEST_DATA_RO_FILTER_TOPIC, TEST_DATA_RO_FILTER) + await hass.async_block_till_done() + + leak = hass.states.get(leak_sensor_name) + assert leak.state == STATE_ON