diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index c2b3ae6732b..ed5b1c14ba3 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -26,6 +26,7 @@ PLATFORMS: list[Platform] = [ Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, + Platform.VALVE, ] LOGGER = logging.getLogger(__name__) TIMEOUT = 20.0 diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py index a57130c3acf..d010665e427 100644 --- a/homeassistant/components/gardena_bluetooth/switch.py +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -50,6 +50,7 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity): self._attr_unique_id = f"{coordinator.address}-{Valve.state.uuid}" self._attr_translation_key = "state" self._attr_is_on = None + self._attr_entity_registry_enabled_default = False def _handle_coordinator_update(self) -> None: self._attr_is_on = self.coordinator.get_cached(Valve.state) diff --git a/homeassistant/components/gardena_bluetooth/valve.py b/homeassistant/components/gardena_bluetooth/valve.py new file mode 100644 index 00000000000..3faf758f7e9 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/valve.py @@ -0,0 +1,74 @@ +"""Support for switch entities.""" + +from __future__ import annotations + +from typing import Any + +from gardena_bluetooth.const import Valve + +from homeassistant.components.valve import ValveEntity, ValveEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import Coordinator, GardenaBluetoothEntity + +FALLBACK_WATERING_TIME_IN_SECONDS = 60 * 60 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up switch based on a config entry.""" + coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] + entities = [] + if GardenaBluetoothValve.characteristics.issubset(coordinator.characteristics): + entities.append(GardenaBluetoothValve(coordinator)) + + async_add_entities(entities) + + +class GardenaBluetoothValve(GardenaBluetoothEntity, ValveEntity): + """Representation of a valve switch.""" + + _attr_name = None + _attr_is_closed: bool | None = None + _attr_reports_position = False + _attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + + characteristics = { + Valve.state.uuid, + Valve.manual_watering_time.uuid, + Valve.remaining_open_time.uuid, + } + + def __init__( + self, + coordinator: Coordinator, + ) -> None: + """Initialize the switch.""" + super().__init__( + coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid} + ) + self._attr_unique_id = f"{coordinator.address}-{Valve.state.uuid}" + + def _handle_coordinator_update(self) -> None: + self._attr_is_closed = not self.coordinator.get_cached(Valve.state) + super()._handle_coordinator_update() + + async def async_open_valve(self, **kwargs: Any) -> None: + """Turn the entity on.""" + value = ( + self.coordinator.get_cached(Valve.manual_watering_time) + or FALLBACK_WATERING_TIME_IN_SECONDS + ) + await self.coordinator.write(Valve.remaining_open_time, value) + self._attr_is_closed = False + self.async_write_ha_state() + + async def async_close_valve(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.coordinator.write(Valve.remaining_open_time, 0) + self._attr_is_closed = True + self.async_write_ha_state() diff --git a/tests/components/gardena_bluetooth/snapshots/test_valve.ambr b/tests/components/gardena_bluetooth/snapshots/test_valve.ambr new file mode 100644 index 00000000000..c030332e75b --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_valve.ambr @@ -0,0 +1,29 @@ +# serializer version: 1 +# name: test_setup + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.mock_title', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- +# name: test_setup.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.mock_title', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_valve.py b/tests/components/gardena_bluetooth/test_valve.py new file mode 100644 index 00000000000..411778658f4 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_valve.py @@ -0,0 +1,85 @@ +"""Test Gardena Bluetooth valve.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import Mock, call + +from gardena_bluetooth.const import Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, + Platform, +) +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_switch_chars(mock_read_char_raw): + """Mock data on device.""" + mock_read_char_raw[Valve.state.uuid] = b"\x00" + mock_read_char_raw[Valve.remaining_open_time.uuid] = ( + Valve.remaining_open_time.encode(0) + ) + mock_read_char_raw[Valve.manual_watering_time.uuid] = ( + Valve.manual_watering_time.encode(1000) + ) + return mock_read_char_raw + + +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_switch_chars: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], +) -> None: + """Test setup creates expected entities.""" + + entity_id = "valve.mock_title" + await setup_entry(hass, mock_entry, [Platform.VALVE]) + assert hass.states.get(entity_id) == snapshot + + mock_switch_chars[Valve.state.uuid] = b"\x01" + await scan_step() + assert hass.states.get(entity_id) == snapshot + + +async def test_switching( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_switch_chars: dict[str, bytes], +) -> None: + """Test switching makes correct calls.""" + + entity_id = "valve.mock_title" + await setup_entry(hass, mock_entry, [Platform.VALVE]) + assert hass.states.get(entity_id) + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_OPEN_VALVE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_client.write_char.mock_calls == [ + call(Valve.remaining_open_time, 1000), + call(Valve.remaining_open_time, 0), + ]