Skip to content

Commit

Permalink
Add valve entity to gardena (home-assistant#120160)
Browse files Browse the repository at this point in the history
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
  • Loading branch information
elupus and frenck authored Jun 22, 2024
1 parent ed0e0ee commit b5a7fb1
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 0 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/gardena_bluetooth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
Platform.VALVE,
]
LOGGER = logging.getLogger(__name__)
TIMEOUT = 20.0
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/gardena_bluetooth/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def __init__(
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)
Expand Down
74 changes: 74 additions & 0 deletions homeassistant/components/gardena_bluetooth/valve.py
Original file line number Diff line number Diff line change
@@ -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()
29 changes: 29 additions & 0 deletions tests/components/gardena_bluetooth/snapshots/test_valve.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# serializer version: 1
# name: test_setup
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title',
'supported_features': <ValveEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'valve.mock_title',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---
# name: test_setup.1
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title',
'supported_features': <ValveEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'valve.mock_title',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'open',
})
# ---
85 changes: 85 additions & 0 deletions tests/components/gardena_bluetooth/test_valve.py
Original file line number Diff line number Diff line change
@@ -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),
]

0 comments on commit b5a7fb1

Please sign in to comment.