From 22fbd2294336ad9f4365a2a0d6b2183f3ef0e16d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jul 2023 00:31:01 +0200 Subject: [PATCH] Add more complete test coverage to gardena bluetooth (#96874) * Add tests for switch * Add tests for number * Add tests for 0 sensor * Enable coverage for gardena bluetooth --- .coveragerc | 7 -- .../components/gardena_bluetooth/switch.py | 2 +- .../components/gardena_bluetooth/conftest.py | 15 ++- .../snapshots/test_number.ambr | 102 ++++++++++++++++++ .../snapshots/test_sensor.ambr | 13 +++ .../snapshots/test_switch.ambr | 25 +++++ .../gardena_bluetooth/test_number.py | 93 +++++++++++++++- .../gardena_bluetooth/test_sensor.py | 1 + .../gardena_bluetooth/test_switch.py | 84 +++++++++++++++ 9 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 tests/components/gardena_bluetooth/snapshots/test_switch.ambr create mode 100644 tests/components/gardena_bluetooth/test_switch.py diff --git a/.coveragerc b/.coveragerc index b9ff3c4ea0b..4a5c843f357 100644 --- a/.coveragerc +++ b/.coveragerc @@ -406,13 +406,6 @@ omit = homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/sensor.py - homeassistant/components/gardena_bluetooth/__init__.py - homeassistant/components/gardena_bluetooth/binary_sensor.py - homeassistant/components/gardena_bluetooth/const.py - homeassistant/components/gardena_bluetooth/coordinator.py - homeassistant/components/gardena_bluetooth/number.py - homeassistant/components/gardena_bluetooth/sensor.py - homeassistant/components/gardena_bluetooth/switch.py homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/geocaching/__init__.py diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py index adb23c74c1d..bc83e3ed5a9 100644 --- a/homeassistant/components/gardena_bluetooth/switch.py +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -35,7 +35,7 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity): characteristics = { Valve.state.uuid, Valve.manual_watering_time.uuid, - Valve.manual_watering_time.uuid, + Valve.remaining_open_time.uuid, } def __init__( diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index a4d7170e945..a1d31c45807 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -63,7 +63,10 @@ def mock_client(enable_bluetooth: None, mock_read_char_raw: dict[str, Any]) -> N def _read_char_raw(uuid: str, default: Any = SENTINEL): try: - return mock_read_char_raw[uuid] + val = mock_read_char_raw[uuid] + if isinstance(val, Exception): + raise val + return val except KeyError: if default is SENTINEL: raise CharacteristicNotFound from KeyError @@ -85,3 +88,13 @@ def mock_client(enable_bluetooth: None, mock_read_char_raw: dict[str, Any]) -> N "2023-01-01", tz_offset=1 ): yield client + + +@pytest.fixture(autouse=True) +def enable_all_entities(): + """Make sure all entities are enabled.""" + with patch( + "homeassistant.components.gardena_bluetooth.coordinator.GardenaBluetoothEntity.entity_registry_enabled_default", + new=Mock(return_value=True), + ): + yield diff --git a/tests/components/gardena_bluetooth/snapshots/test_number.ambr b/tests/components/gardena_bluetooth/snapshots/test_number.ambr index a12cce06019..0c464f7cbc1 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_number.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_number.ambr @@ -1,4 +1,72 @@ # serializer version: 1 +# name: test_bluetooth_error_unavailable + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_bluetooth_error_unavailable.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Manual watering time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_manual_watering_time', + 'last_changed': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_bluetooth_error_unavailable.2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_bluetooth_error_unavailable.3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Manual watering time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_manual_watering_time', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -33,6 +101,40 @@ 'state': '10.0', }) # --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw2-number.mock_title_open_for] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr index 883f377c3a5..5a23b6d7f50 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr @@ -25,6 +25,19 @@ 'state': '2023-01-01T01:00:10+00:00', }) # --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Mock Title Valve closing', + }), + 'context': , + 'entity_id': 'sensor.mock_title_valve_closing', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/gardena_bluetooth/snapshots/test_switch.ambr b/tests/components/gardena_bluetooth/snapshots/test_switch.ambr new file mode 100644 index 00000000000..37dae0bff75 --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_switch.ambr @@ -0,0 +1,25 @@ +# serializer version: 1 +# name: test_setup + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Open', + }), + 'context': , + 'entity_id': 'switch.mock_title_open', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Open', + }), + 'context': , + 'entity_id': 'switch.mock_title_open', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_number.py b/tests/components/gardena_bluetooth/test_number.py index f1955905cce..588b73aadbb 100644 --- a/tests/components/gardena_bluetooth/test_number.py +++ b/tests/components/gardena_bluetooth/test_number.py @@ -1,11 +1,27 @@ """Test Gardena Bluetooth sensor.""" +from typing import Any +from unittest.mock import Mock, call + from gardena_bluetooth.const import Valve +from gardena_bluetooth.exceptions import ( + CharacteristicNoAccess, + GardenaBluetoothException, +) +from gardena_bluetooth.parse import Characteristic import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + Platform, +) from homeassistant.core import HomeAssistant from . import setup_entry @@ -29,6 +45,8 @@ from tests.common import MockConfigEntry [ Valve.remaining_open_time.encode(100), Valve.remaining_open_time.encode(10), + CharacteristicNoAccess("Test for no access"), + GardenaBluetoothException("Test for errors on bluetooth"), ], "number.mock_title_remaining_open_time", ), @@ -58,3 +76,76 @@ async def test_setup( mock_read_char_raw[uuid] = char_raw await coordinator.async_refresh() assert hass.states.get(entity_id) == snapshot + + +@pytest.mark.parametrize( + ("char", "value", "expected", "entity_id"), + [ + ( + Valve.manual_watering_time, + 100, + 100, + "number.mock_title_manual_watering_time", + ), + ( + Valve.remaining_open_time, + 100, + 100 * 60, + "number.mock_title_open_for", + ), + ], +) +async def test_config( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + mock_client: Mock, + char: Characteristic, + value: Any, + expected: Any, + entity_id: str, +) -> None: + """Test setup creates expected entities.""" + + mock_read_char_raw[char.uuid] = char.encode(value) + await setup_entry(hass, mock_entry, [Platform.NUMBER]) + assert hass.states.get(entity_id) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value}, + blocking=True, + ) + + assert mock_client.write_char.mock_calls == [ + call(char, expected), + ] + + +async def test_bluetooth_error_unavailable( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], +) -> None: + """Verify that a connectivity error makes all entities unavailable.""" + + mock_read_char_raw[ + Valve.manual_watering_time.uuid + ] = Valve.manual_watering_time.encode(0) + mock_read_char_raw[ + Valve.remaining_open_time.uuid + ] = Valve.remaining_open_time.encode(0) + + coordinator = await setup_entry(hass, mock_entry, [Platform.NUMBER]) + assert hass.states.get("number.mock_title_remaining_open_time") == snapshot + assert hass.states.get("number.mock_title_manual_watering_time") == snapshot + + mock_read_char_raw[Valve.manual_watering_time.uuid] = GardenaBluetoothException( + "Test for errors on bluetooth" + ) + + await coordinator.async_refresh() + assert hass.states.get("number.mock_title_remaining_open_time") == snapshot + assert hass.states.get("number.mock_title_manual_watering_time") == snapshot diff --git a/tests/components/gardena_bluetooth/test_sensor.py b/tests/components/gardena_bluetooth/test_sensor.py index d7cdc205f50..e9fd452e6a2 100644 --- a/tests/components/gardena_bluetooth/test_sensor.py +++ b/tests/components/gardena_bluetooth/test_sensor.py @@ -26,6 +26,7 @@ from tests.common import MockConfigEntry [ Valve.remaining_open_time.encode(100), Valve.remaining_open_time.encode(10), + Valve.remaining_open_time.encode(0), ], "sensor.mock_title_valve_closing", ), diff --git a/tests/components/gardena_bluetooth/test_switch.py b/tests/components/gardena_bluetooth/test_switch.py new file mode 100644 index 00000000000..c2571b7a588 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_switch.py @@ -0,0 +1,84 @@ +"""Test Gardena Bluetooth sensor.""" + + +from unittest.mock import Mock, call + +from gardena_bluetooth.const import Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + 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], +) -> None: + """Test setup creates expected entities.""" + + entity_id = "switch.mock_title_open" + coordinator = await setup_entry(hass, mock_entry, [Platform.SWITCH]) + assert hass.states.get(entity_id) == snapshot + + mock_switch_chars[Valve.state.uuid] = b"\x01" + await coordinator.async_refresh() + 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 = "switch.mock_title_open" + await setup_entry(hass, mock_entry, [Platform.SWITCH]) + assert hass.states.get(entity_id) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {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), + ]