diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 328558d0aba..1067b9279a4 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -146,12 +146,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Fetch data from Yale.""" try: arm_status = self.yale.get_armed_status() - data = self.yale.get_all() - cycle = data["CYCLE"] - status = data["STATUS"] - online = data["ONLINE"] - panel_info = data["PANEL INFO"] - + data = self.yale.get_information() except AuthenticationError as error: raise ConfigEntryAuthFailed from error except YALE_BASE_ERRORS as error: @@ -159,8 +154,8 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): return { "arm_status": arm_status, - "cycle": cycle, - "status": status, - "online": online, - "panel_info": panel_info, + "cycle": data.cycle, + "status": data.status, + "online": data.online, + "panel_info": data.panel_info, } diff --git a/homeassistant/components/yale_smart_alarm/diagnostics.py b/homeassistant/components/yale_smart_alarm/diagnostics.py index 82d2ca9a915..eb7b2be9fb4 100644 --- a/homeassistant/components/yale_smart_alarm/diagnostics.py +++ b/homeassistant/components/yale_smart_alarm/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data @@ -29,4 +30,4 @@ async def async_get_config_entry_diagnostics( assert coordinator.yale get_all_data = await hass.async_add_executor_job(coordinator.yale.get_all) - return async_redact_data(get_all_data, TO_REDACT) + return async_redact_data(asdict(get_all_data), TO_REDACT) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index 3b4d0a19039..386e546afbf 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any from homeassistant.components.lock import LockEntity from homeassistant.const import ATTR_CODE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import YaleConfigEntry @@ -61,17 +61,22 @@ class YaleDoorlock(YaleEntity, LockEntity): """Set lock.""" if TYPE_CHECKING: assert self.coordinator.yale, "Connection to API is missing" + if command == "unlocked" and not code: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_code", + ) try: get_lock = await self.hass.async_add_executor_job( self.coordinator.yale.lock_api.get, self.lock_name ) - if command == "locked": + if get_lock and command == "locked": lock_state = await self.hass.async_add_executor_job( self.coordinator.yale.lock_api.close_lock, get_lock, ) - if command == "unlocked": + if code and get_lock and command == "unlocked": lock_state = await self.hass.async_add_executor_job( self.coordinator.yale.lock_api.open_lock, get_lock, code ) diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index ed494505bae..92dd774d1d9 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", "iot_class": "cloud_polling", "loggers": ["yalesmartalarmclient"], - "requirements": ["yalesmartalarmclient==0.3.9"] + "requirements": ["yalesmartalarmclient==0.4.0"] } diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json index ce89c9e69ea..63260c03e7f 100644 --- a/homeassistant/components/yale_smart_alarm/strings.json +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -67,6 +67,9 @@ "set_lock": { "message": "Could not set lock for {name}: {error}" }, + "no_code": { + "message": "Can not unlock without code" + }, "could_not_change_lock": { "message": "Could not set lock, check system ready for lock" }, diff --git a/requirements_all.txt b/requirements_all.txt index e0b7d069b18..9d3825d4f4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2955,7 +2955,7 @@ xmltodict==0.13.0 xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.9 +yalesmartalarmclient==0.4.0 # homeassistant.components.august # homeassistant.components.yalexs_ble diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 137cdf14b86..828cafcf37b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2335,7 +2335,7 @@ xknxproject==3.7.1 xmltodict==0.13.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.9 +yalesmartalarmclient==0.4.0 # homeassistant.components.august # homeassistant.components.yalexs_ble diff --git a/tests/components/yale_smart_alarm/conftest.py b/tests/components/yale_smart_alarm/conftest.py index 9583df5faa6..6ac6dfc6871 100644 --- a/tests/components/yale_smart_alarm/conftest.py +++ b/tests/components/yale_smart_alarm/conftest.py @@ -7,6 +7,7 @@ from typing import Any from unittest.mock import Mock, patch import pytest +from yalesmartalarmclient import YaleSmartAlarmData from yalesmartalarmclient.const import YALE_STATE_ARM_FULL from homeassistant.components.yale_smart_alarm.const import DOMAIN, PLATFORMS @@ -33,7 +34,10 @@ async def patch_platform_constant() -> list[Platform]: @pytest.fixture async def load_config_entry( - hass: HomeAssistant, load_json: dict[str, Any], load_platforms: list[Platform] + hass: HomeAssistant, + get_data: YaleSmartAlarmData, + get_all_data: YaleSmartAlarmData, + load_platforms: list[Platform], ) -> tuple[MockConfigEntry, Mock]: """Set up the Yale Smart Living integration in Home Assistant.""" with patch("homeassistant.components.yale_smart_alarm.PLATFORMS", load_platforms): @@ -56,7 +60,8 @@ async def load_config_entry( client = mock_client_class.return_value client.auth = Mock() client.lock_api = Mock() - client.get_all.return_value = load_json + client.get_all.return_value = get_all_data + client.get_information.return_value = get_data client.get_armed_status.return_value = YALE_STATE_ARM_FULL await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -64,10 +69,50 @@ async def load_config_entry( return (config_entry, client) -@pytest.fixture(name="load_json", scope="package") -def load_json_from_fixture() -> dict[str, Any]: +@pytest.fixture(name="loaded_fixture", scope="package") +def get_fixture_data() -> dict[str, Any]: """Load fixture with json data and return.""" data_fixture = load_fixture("get_all.json", "yale_smart_alarm") json_data: dict[str, Any] = json.loads(data_fixture) return json_data + + +@pytest.fixture(name="get_data", scope="package") +def get_update_data(loaded_fixture: dict[str, Any]) -> YaleSmartAlarmData: + """Load update data and return.""" + + status = loaded_fixture["STATUS"] + cycle = loaded_fixture["CYCLE"] + online = loaded_fixture["ONLINE"] + panel_info = loaded_fixture["PANEL INFO"] + return YaleSmartAlarmData( + status=status, + cycle=cycle, + online=online, + panel_info=panel_info, + ) + + +@pytest.fixture(name="get_all_data", scope="package") +def get_diag_data(loaded_fixture: dict[str, Any]) -> YaleSmartAlarmData: + """Load all data and return.""" + + devices = loaded_fixture["DEVICES"] + mode = loaded_fixture["MODE"] + status = loaded_fixture["STATUS"] + cycle = loaded_fixture["CYCLE"] + online = loaded_fixture["ONLINE"] + history = loaded_fixture["HISTORY"] + panel_info = loaded_fixture["PANEL INFO"] + auth_check = loaded_fixture["AUTH CHECK"] + return YaleSmartAlarmData( + devices=devices, + mode=mode, + status=status, + cycle=cycle, + online=online, + history=history, + panel_info=panel_info, + auth_check=auth_check, + ) diff --git a/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr index a5dfe4b50dd..d4bbd42aaeb 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'AUTH CHECK': dict({ + 'auth_check': dict({ 'agent': False, 'dealer_group': 'yale', 'dealer_id': '605', @@ -16,7 +16,7 @@ 'user_id': '**REDACTED**', 'xml_version': '2', }), - 'CYCLE': dict({ + 'cycle': dict({ 'alarm_event_latest': None, 'capture_latest': None, 'device_status': list([ @@ -650,7 +650,7 @@ 'utc_event_time': None, }), }), - 'DEVICES': list([ + 'devices': list([ dict({ 'address': '**REDACTED**', 'area': '1', @@ -1249,7 +1249,7 @@ 'type_no': '40', }), ]), - 'HISTORY': list([ + 'history': list([ dict({ 'area': 1, 'cid': '18180701000', @@ -1391,14 +1391,14 @@ 'zone': 1, }), ]), - 'MODE': list([ + 'mode': list([ dict({ 'area': '1', 'mode': 'disarm', }), ]), - 'ONLINE': 'online', - 'PANEL INFO': dict({ + 'online': 'online', + 'panel_info': dict({ 'SMS_Balance': '50', 'contact': '', 'dealer_name': 'Poland', @@ -1416,7 +1416,7 @@ 'zb_version': '4.1.2.6.2', 'zw_version': '', }), - 'STATUS': dict({ + 'status': dict({ 'acfail': 'main.normal', 'battery': 'main.normal', 'gsm_rssi': '0', diff --git a/tests/components/yale_smart_alarm/test_coordinator.py b/tests/components/yale_smart_alarm/test_coordinator.py index 6f1125fcf65..41362f2318a 100644 --- a/tests/components/yale_smart_alarm/test_coordinator.py +++ b/tests/components/yale_smart_alarm/test_coordinator.py @@ -3,12 +3,15 @@ from __future__ import annotations from datetime import timedelta -from typing import Any from unittest.mock import Mock, patch import pytest -from yalesmartalarmclient.const import YALE_STATE_ARM_FULL -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError +from yalesmartalarmclient import ( + YALE_STATE_ARM_FULL, + AuthenticationError, + UnknownError, + YaleSmartAlarmData, +) from homeassistant.components.yale_smart_alarm.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -32,7 +35,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed ) async def test_coordinator_setup_errors( hass: HomeAssistant, - load_json: dict[str, Any], + get_data: YaleSmartAlarmData, p_error: Exception, ) -> None: """Test the Yale Smart Living coordinator with errors.""" @@ -64,7 +67,7 @@ async def test_coordinator_setup_errors( async def test_coordinator_setup_and_update_errors( hass: HomeAssistant, load_config_entry: tuple[MockConfigEntry, Mock], - load_json: dict[str, Any], + get_data: YaleSmartAlarmData, ) -> None: """Test the Yale Smart Living coordinator with errors.""" @@ -74,51 +77,51 @@ async def test_coordinator_setup_and_update_errors( assert state.state == STATE_ALARM_ARMED_AWAY client.reset_mock() - client.get_all.side_effect = ConnectionError("Could not connect") + client.get_information.side_effect = ConnectionError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) await hass.async_block_till_done(wait_background_tasks=True) - client.get_all.assert_called_once() + client.get_information.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE client.reset_mock() - client.get_all.side_effect = ConnectionError("Could not connect") + client.get_information.side_effect = ConnectionError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) await hass.async_block_till_done(wait_background_tasks=True) - client.get_all.assert_called_once() + client.get_information.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE client.reset_mock() - client.get_all.side_effect = TimeoutError("Could not connect") + client.get_information.side_effect = TimeoutError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=3)) await hass.async_block_till_done(wait_background_tasks=True) - client.get_all.assert_called_once() + client.get_information.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE client.reset_mock() - client.get_all.side_effect = UnknownError("info") + client.get_information.side_effect = UnknownError("info") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) await hass.async_block_till_done(wait_background_tasks=True) - client.get_all.assert_called_once() + client.get_information.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE client.reset_mock() - client.get_all.side_effect = None - client.get_all.return_value = load_json + client.get_information.side_effect = None + client.get_information.return_value = get_data client.get_armed_status.return_value = YALE_STATE_ARM_FULL async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) await hass.async_block_till_done(wait_background_tasks=True) - client.get_all.assert_called_once() + client.get_information.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_ALARM_ARMED_AWAY client.reset_mock() - client.get_all.side_effect = AuthenticationError("Can not authenticate") + client.get_information.side_effect = AuthenticationError("Can not authenticate") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) await hass.async_block_till_done(wait_background_tasks=True) - client.get_all.assert_called_once() + client.get_information.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/yale_smart_alarm/test_lock.py b/tests/components/yale_smart_alarm/test_lock.py index 09ce8529084..7c67703924b 100644 --- a/tests/components/yale_smart_alarm/test_lock.py +++ b/tests/components/yale_smart_alarm/test_lock.py @@ -3,13 +3,11 @@ from __future__ import annotations from copy import deepcopy -from typing import Any from unittest.mock import Mock import pytest from syrupy.assertion import SnapshotAssertion -from yalesmartalarmclient.exceptions import UnknownError -from yalesmartalarmclient.lock import YaleDoorManAPI +from yalesmartalarmclient import UnknownError, YaleDoorManAPI, YaleSmartAlarmData from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( @@ -20,7 +18,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform @@ -47,7 +45,7 @@ async def test_lock( ) async def test_lock_service_calls( hass: HomeAssistant, - load_json: dict[str, Any], + get_data: YaleSmartAlarmData, load_config_entry: tuple[MockConfigEntry, Mock], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, @@ -56,8 +54,8 @@ async def test_lock_service_calls( client = load_config_entry[1] - data = deepcopy(load_json) - data["data"] = data.pop("DEVICES") + data = deepcopy(get_data.cycle) + data["data"] = data.pop("device_status") client.auth.get_authenticated = Mock(return_value=data) client.auth.post_authenticated = Mock(return_value={"code": "000"}) @@ -66,6 +64,14 @@ async def test_lock_service_calls( state = hass.states.get("lock.device1") assert state.state == "locked" + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.device1"}, + blocking=True, + ) + await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, @@ -93,7 +99,7 @@ async def test_lock_service_calls( ) async def test_lock_service_call_fails( hass: HomeAssistant, - load_json: dict[str, Any], + get_data: YaleSmartAlarmData, load_config_entry: tuple[MockConfigEntry, Mock], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, @@ -102,8 +108,8 @@ async def test_lock_service_call_fails( client = load_config_entry[1] - data = deepcopy(load_json) - data["data"] = data.pop("DEVICES") + data = deepcopy(get_data.cycle) + data["data"] = data.pop("device_status") client.auth.get_authenticated = Mock(return_value=data) client.auth.post_authenticated = Mock(side_effect=UnknownError("test_side_effect")) @@ -145,7 +151,7 @@ async def test_lock_service_call_fails( ) async def test_lock_service_call_fails_with_incorrect_status( hass: HomeAssistant, - load_json: dict[str, Any], + get_data: YaleSmartAlarmData, load_config_entry: tuple[MockConfigEntry, Mock], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, @@ -154,8 +160,8 @@ async def test_lock_service_call_fails_with_incorrect_status( client = load_config_entry[1] - data = deepcopy(load_json) - data["data"] = data.pop("DEVICES") + data = deepcopy(get_data.cycle) + data["data"] = data.pop("device_status") client.auth.get_authenticated = Mock(return_value=data) client.auth.post_authenticated = Mock(return_value={"code": "FFF"}) diff --git a/tests/components/yale_smart_alarm/test_sensor.py b/tests/components/yale_smart_alarm/test_sensor.py index d91ddc0e6ce..848d31cedc3 100644 --- a/tests/components/yale_smart_alarm/test_sensor.py +++ b/tests/components/yale_smart_alarm/test_sensor.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import Any from unittest.mock import Mock +from yalesmartalarmclient import YaleSmartAlarmData + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -13,7 +14,7 @@ from tests.common import MockConfigEntry async def test_coordinator_setup_and_update_errors( hass: HomeAssistant, load_config_entry: tuple[MockConfigEntry, Mock], - load_json: dict[str, Any], + get_data: YaleSmartAlarmData, ) -> None: """Test the Yale Smart Living coordinator with errors."""