diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index c8ca1a819e0..cec71f91750 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -24,6 +24,7 @@ from pysmartthings import ( SmartThingsSinkError, Status, ) +from pysmartthings.models import HealthStatus from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -79,6 +80,7 @@ class FullDevice: device: Device status: dict[str, ComponentStatus] + online: bool type SmartThingsConfigEntry = ConfigEntry[SmartThingsData] @@ -192,7 +194,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry) devices = await client.get_devices() for device in devices: status = process_status(await client.get_device_status(device.device_id)) - device_status[device.device_id] = FullDevice(device=device, status=status) + online = await client.get_device_health(device.device_id) + device_status[device.device_id] = FullDevice( + device=device, status=status, online=online.state == HealthStatus.ONLINE + ) except SmartThingsAuthenticationFailedError as err: raise ConfigEntryAuthFailed from err diff --git a/homeassistant/components/smartthings/entity.py b/homeassistant/components/smartthings/entity.py index 5544297a4c6..b25838ad8c9 100644 --- a/homeassistant/components/smartthings/entity.py +++ b/homeassistant/components/smartthings/entity.py @@ -10,8 +10,10 @@ from pysmartthings import ( Command, ComponentStatus, DeviceEvent, + DeviceHealthEvent, SmartThings, ) +from pysmartthings.models import HealthStatus from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity @@ -48,6 +50,7 @@ class SmartThingsEntity(Entity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.device.device_id)}, ) + self._attr_available = device.online async def async_added_to_hass(self) -> None: """Subscribe to updates.""" @@ -61,8 +64,17 @@ class SmartThingsEntity(Entity): self._update_handler, ) ) + self.async_on_remove( + self.client.add_device_availability_event_listener( + self.device.device.device_id, self._availability_handler + ) + ) self._update_attr() + def _availability_handler(self, event: DeviceHealthEvent) -> None: + self._attr_available = event.status != HealthStatus.OFFLINE + self.async_write_ha_state() + def _update_handler(self, event: DeviceEvent) -> None: self._internal_state[event.capability][event.attribute].value = event.value self._internal_state[event.capability][event.attribute].data = event.data diff --git a/homeassistant/components/smartthings/quality_scale.yaml b/homeassistant/components/smartthings/quality_scale.yaml index be8a9039617..384ce2ea0b6 100644 --- a/homeassistant/components/smartthings/quality_scale.yaml +++ b/homeassistant/components/smartthings/quality_scale.yaml @@ -37,7 +37,7 @@ rules: docs-installation-parameters: status: exempt comment: No parameters needed during installation - entity-unavailable: todo + entity-unavailable: done integration-owner: done log-when-unavailable: todo parallel-updates: todo diff --git a/tests/components/smartthings/__init__.py b/tests/components/smartthings/__init__.py index fce344b57a7..f316db7bef8 100644 --- a/tests/components/smartthings/__init__.py +++ b/tests/components/smartthings/__init__.py @@ -3,7 +3,8 @@ from typing import Any from unittest.mock import AsyncMock -from pysmartthings import Attribute, Capability, DeviceEvent +from pysmartthings import Attribute, Capability, DeviceEvent, DeviceHealthEvent +from pysmartthings.models import HealthStatus from syrupy import SnapshotAssertion from homeassistant.components.smartthings.const import MAIN @@ -78,3 +79,14 @@ async def trigger_update( if call[0][0] == device_id and call[0][2] == capability: call[0][3](event) await hass.async_block_till_done() + + +async def trigger_health_update( + hass: HomeAssistant, mock: AsyncMock, device_id: str, status: HealthStatus +) -> None: + """Trigger a health update.""" + event = DeviceHealthEvent("abc", "abc", status) + for call in mock.add_device_availability_event_listener.call_args_list: + if call[0][0] == device_id: + call[0][1](event) + await hass.async_block_till_done() diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index aa29a610620..e556ee5698f 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -5,6 +5,7 @@ import time from unittest.mock import AsyncMock, patch from pysmartthings import ( + DeviceHealth, DeviceResponse, DeviceStatus, LocationResponse, @@ -12,6 +13,7 @@ from pysmartthings import ( SceneResponse, Subscription, ) +from pysmartthings.models import HealthStatus import pytest from homeassistant.components.application_credentials import ( @@ -86,6 +88,9 @@ def mock_smartthings() -> Generator[AsyncMock]: client.create_subscription.return_value = Subscription.from_json( load_fixture("subscription.json", DOMAIN) ) + client.get_device_health.return_value = DeviceHealth.from_json( + load_fixture("device_health.json", DOMAIN) + ) yield client @@ -170,6 +175,13 @@ def devices(mock_smartthings: AsyncMock, device_fixture: str) -> Generator[Async return mock_smartthings +@pytest.fixture +def unavailable_device(devices: AsyncMock) -> AsyncMock: + """Mock an unavailable device.""" + devices.get_device_health.return_value.state = HealthStatus.OFFLINE + return devices + + @pytest.fixture def mock_config_entry(expires_at: int) -> MockConfigEntry: """Mock a config entry.""" diff --git a/tests/components/smartthings/fixtures/device_health.json b/tests/components/smartthings/fixtures/device_health.json new file mode 100644 index 00000000000..7ae42d6206e --- /dev/null +++ b/tests/components/smartthings/fixtures/device_health.json @@ -0,0 +1,5 @@ +{ + "deviceId": "612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3", + "state": "ONLINE", + "lastUpdatedDate": "2025-04-28T11:43:31.600Z" +} diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 9f9d8d66317..22ca94df81a 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -11,12 +12,17 @@ from homeassistant.components.automation import automations_with_entity from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.script import scripts_with_entity from homeassistant.components.smartthings import DOMAIN, MAIN -from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -60,6 +66,47 @@ async def test_state_update( assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_ON +@pytest.mark.parametrize("device_fixture", ["da_ref_normal_000001"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_OFF + + await trigger_health_update( + hass, devices, "7db87911-7dce-1cf2-7119-b953432a2f09", HealthStatus.OFFLINE + ) + + assert ( + hass.states.get("binary_sensor.refrigerator_cooler_door").state + == STATE_UNAVAILABLE + ) + + await trigger_health_update( + hass, devices, "7db87911-7dce-1cf2-7119-b953432a2f09", HealthStatus.ONLINE + ) + + assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_OFF + + +@pytest.mark.parametrize("device_fixture", ["da_ref_normal_000001"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert ( + hass.states.get("binary_sensor.refrigerator_cooler_door").state + == STATE_UNAVAILABLE + ) + + @pytest.mark.parametrize( ("device_fixture", "unique_id", "suggested_object_id", "issue_string", "entity_id"), [ diff --git a/tests/components/smartthings/test_button.py b/tests/components/smartthings/test_button.py index 4a348d079ca..5c5f98912e2 100644 --- a/tests/components/smartthings/test_button.py +++ b/tests/components/smartthings/test_button.py @@ -4,16 +4,22 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from pysmartthings import Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.smartthings import MAIN -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities +from . import setup_integration, snapshot_smartthings_entities, trigger_health_update from tests.common import MockConfigEntry @@ -54,3 +60,38 @@ async def test_press( Command.STOP, MAIN, ) + + +@pytest.mark.parametrize("device_fixture", ["da_ks_microwave_0101x"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("button.microwave_stop").state == STATE_UNKNOWN + + await trigger_health_update( + hass, devices, "2bad3237-4886-e699-1b90-4a51a3d55c8a", HealthStatus.OFFLINE + ) + + assert hass.states.get("button.microwave_stop").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "2bad3237-4886-e699-1b90-4a51a3d55c8a", HealthStatus.ONLINE + ) + + assert hass.states.get("button.microwave_stop").state == STATE_UNKNOWN + + +@pytest.mark.parametrize("device_fixture", ["da_ks_microwave_0101x"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("button.microwave_stop").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 75b864598bd..138601ec08b 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -4,6 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, call from pysmartthings import Attribute, Capability, Command, Status +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -36,6 +37,8 @@ from homeassistant.const import ( ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant @@ -45,6 +48,7 @@ from . import ( set_attribute_value, setup_integration, snapshot_smartthings_entities, + trigger_health_update, trigger_update, ) @@ -857,3 +861,38 @@ async def test_thermostat_state_attributes_update( ) assert hass.states.get("climate.asd").attributes[state_attribute] == expected_value + + +@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("climate.ac_office_granit").state == STATE_OFF + + await trigger_health_update( + hass, devices, "96a5ef74-5832-a84b-f1f7-ca799957065d", HealthStatus.OFFLINE + ) + + assert hass.states.get("climate.ac_office_granit").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "96a5ef74-5832-a84b-f1f7-ca799957065d", HealthStatus.ONLINE + ) + + assert hass.states.get("climate.ac_office_granit").state == STATE_OFF + + +@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("climate.ac_office_granit").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 37f12b44880..559c6821204 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command, Status +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -20,12 +21,18 @@ from homeassistant.const import ( SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -190,3 +197,38 @@ async def test_position_update( ) assert hass.states.get("cover.curtain_1a").attributes[ATTR_CURRENT_POSITION] == 50 + + +@pytest.mark.parametrize("device_fixture", ["c2c_shade"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("cover.curtain_1a").state == STATE_OPEN + + await trigger_health_update( + hass, devices, "571af102-15db-4030-b76b-245a691f74a5", HealthStatus.OFFLINE + ) + + assert hass.states.get("cover.curtain_1a").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "571af102-15db-4030-b76b-245a691f74a5", HealthStatus.ONLINE + ) + + assert hass.states.get("cover.curtain_1a").state == STATE_OPEN + + +@pytest.mark.parametrize("device_fixture", ["c2c_shade"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("cover.curtain_1a").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_event.py b/tests/components/smartthings/test_event.py index 34a96e9c6b4..b9a6fc8be86 100644 --- a/tests/components/smartthings/test_event.py +++ b/tests/components/smartthings/test_event.py @@ -4,15 +4,21 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from pysmartthings import Attribute, Capability +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion from homeassistant.components.event import ATTR_EVENT_TYPES -from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -97,3 +103,48 @@ async def test_supported_button_values_update( assert hass.states.get("event.livingroom_smart_switch_button1").attributes[ ATTR_EVENT_TYPES ] == ["pushed", "held", "down_hold", "pushed_2x"] + + +@pytest.mark.parametrize("device_fixture", ["heatit_zpushwall"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("event.livingroom_smart_switch_button1").state == STATE_UNKNOWN + ) + + await trigger_health_update( + hass, devices, "5e5b97f3-3094-44e6-abc0-f61283412d6a", HealthStatus.OFFLINE + ) + + assert ( + hass.states.get("event.livingroom_smart_switch_button1").state + == STATE_UNAVAILABLE + ) + + await trigger_health_update( + hass, devices, "5e5b97f3-3094-44e6-abc0-f61283412d6a", HealthStatus.ONLINE + ) + + assert ( + hass.states.get("event.livingroom_smart_switch_button1").state == STATE_UNKNOWN + ) + + +@pytest.mark.parametrize("device_fixture", ["heatit_zpushwall"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert ( + hass.states.get("event.livingroom_smart_switch_button1").state + == STATE_UNAVAILABLE + ) diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 58287355381..04196417690 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -18,12 +19,14 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities +from . import setup_integration, snapshot_smartthings_entities, trigger_health_update from tests.common import MockConfigEntry @@ -166,3 +169,38 @@ async def test_set_preset_mode( MAIN, argument="turbo", ) + + +@pytest.mark.parametrize("device_fixture", ["fake_fan"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("fan.fake_fan").state == STATE_OFF + + await trigger_health_update( + hass, devices, "f1af21a2-d5a1-437c-b10a-b34a87394b71", HealthStatus.OFFLINE + ) + + assert hass.states.get("fan.fake_fan").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "f1af21a2-d5a1-437c-b10a-b34a87394b71", HealthStatus.ONLINE + ) + + assert hass.states.get("fan.fake_fan").state == STATE_OFF + + +@pytest.mark.parametrize("device_fixture", ["fake_fan"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("fan.fake_fan").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 56eadde748b..46f8f3ae7a3 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -4,6 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, call from pysmartthings import Attribute, Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -28,6 +29,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant, State @@ -37,6 +39,7 @@ from . import ( set_attribute_value, setup_integration, snapshot_smartthings_entities, + trigger_health_update, trigger_update, ) @@ -413,3 +416,38 @@ async def test_color_mode_after_startup( hass.states.get("light.standing_light").attributes[ATTR_COLOR_MODE] is ColorMode.COLOR_TEMP ) + + +@pytest.mark.parametrize("device_fixture", ["hue_rgbw_color_bulb"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("light.standing_light").state == STATE_OFF + + await trigger_health_update( + hass, devices, "cb958955-b015-498c-9e62-fc0c51abd054", HealthStatus.OFFLINE + ) + + assert hass.states.get("light.standing_light").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "cb958955-b015-498c-9e62-fc0c51abd054", HealthStatus.ONLINE + ) + + assert hass.states.get("light.standing_light").state == STATE_OFF + + +@pytest.mark.parametrize("device_fixture", ["hue_rgbw_color_bulb"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("light.standing_light").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 28191eceb9a..48e83f479fa 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -3,16 +3,28 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState from homeassistant.components.smartthings.const import MAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_LOCK, + SERVICE_UNLOCK, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -83,3 +95,38 @@ async def test_state_update( ) assert hass.states.get("lock.basement_door_lock").state == LockState.UNLOCKED + + +@pytest.mark.parametrize("device_fixture", ["yale_push_button_deadbolt_lock"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("lock.basement_door_lock").state == LockState.LOCKED + + await trigger_health_update( + hass, devices, "a9f587c5-5d8b-4273-8907-e7f609af5158", HealthStatus.OFFLINE + ) + + assert hass.states.get("lock.basement_door_lock").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "a9f587c5-5d8b-4273-8907-e7f609af5158", HealthStatus.ONLINE + ) + + assert hass.states.get("lock.basement_door_lock").state == LockState.LOCKED + + +@pytest.mark.parametrize("device_fixture", ["yale_push_button_deadbolt_lock"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("lock.basement_door_lock").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_media_player.py b/tests/components/smartthings/test_media_player.py index b7cecfe8408..e3f3652c0ed 100644 --- a/tests/components/smartthings/test_media_player.py +++ b/tests/components/smartthings/test_media_player.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command, Status +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -34,12 +35,18 @@ from homeassistant.const import ( SERVICE_VOLUME_UP, STATE_OFF, STATE_PLAYING, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -430,3 +437,38 @@ async def test_state_update( ) assert hass.states.get("media_player.soundbar").state == STATE_OFF + + +@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("media_player.soundbar").state == STATE_PLAYING + + await trigger_health_update( + hass, devices, "afcf3b91-0000-1111-2222-ddff2a0a6577", HealthStatus.OFFLINE + ) + + assert hass.states.get("media_player.soundbar").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "afcf3b91-0000-1111-2222-ddff2a0a6577", HealthStatus.ONLINE + ) + + assert hass.states.get("media_player.soundbar").state == STATE_PLAYING + + +@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("media_player.soundbar").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_number.py b/tests/components/smartthings/test_number.py index 578b94e050f..fa485776c37 100644 --- a/tests/components/smartthings/test_number.py +++ b/tests/components/smartthings/test_number.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -12,11 +13,16 @@ from homeassistant.components.number import ( SERVICE_SET_VALUE, ) from homeassistant.components.smartthings import MAIN -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -79,3 +85,38 @@ async def test_state_update( ) assert hass.states.get("number.washer_rinse_cycles").state == "3" + + +@pytest.mark.parametrize("device_fixture", ["da_wm_wm_000001"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("number.washer_rinse_cycles").state == "2" + + await trigger_health_update( + hass, devices, "f984b91d-f250-9d42-3436-33f09a422a47", HealthStatus.OFFLINE + ) + + assert hass.states.get("number.washer_rinse_cycles").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "f984b91d-f250-9d42-3436-33f09a422a47", HealthStatus.ONLINE + ) + + assert hass.states.get("number.washer_rinse_cycles").state == "2" + + +@pytest.mark.parametrize("device_fixture", ["da_wm_wm_000001"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("number.washer_rinse_cycles").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_select.py b/tests/components/smartthings/test_select.py index 2c5c55239f2..ce3bea08ca2 100644 --- a/tests/components/smartthings/test_select.py +++ b/tests/components/smartthings/test_select.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -12,7 +13,7 @@ from homeassistant.components.select import ( SERVICE_SELECT_OPTION, ) from homeassistant.components.smartthings import MAIN -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er @@ -21,6 +22,7 @@ from . import ( set_attribute_value, setup_integration, snapshot_smartthings_entities, + trigger_health_update, trigger_update, ) @@ -119,3 +121,38 @@ async def test_select_option_without_remote_control( blocking=True, ) devices.execute_device_command.assert_not_called() + + +@pytest.mark.parametrize("device_fixture", ["da_wm_wd_000001"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("select.dryer").state == "stop" + + await trigger_health_update( + hass, devices, "02f7256e-8353-5bdd-547f-bd5b1647e01b", HealthStatus.OFFLINE + ) + + assert hass.states.get("select.dryer").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "02f7256e-8353-5bdd-547f-bd5b1647e01b", HealthStatus.ONLINE + ) + + assert hass.states.get("select.dryer").state == "stop" + + +@pytest.mark.parametrize("device_fixture", ["da_wm_wd_000001"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("select.dryer").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index e90c177bd6d..ecdcd700cab 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -11,12 +12,17 @@ from homeassistant.components.automation import automations_with_entity from homeassistant.components.script import scripts_with_entity from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.smartthings.const import DOMAIN, MAIN -from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -296,3 +302,44 @@ async def test_create_issue( # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 + + +@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.ac_office_granit_temperature").state == "25" + + await trigger_health_update( + hass, devices, "96a5ef74-5832-a84b-f1f7-ca799957065d", HealthStatus.OFFLINE + ) + + assert ( + hass.states.get("sensor.ac_office_granit_temperature").state + == STATE_UNAVAILABLE + ) + + await trigger_health_update( + hass, devices, "96a5ef74-5832-a84b-f1f7-ca799957065d", HealthStatus.ONLINE + ) + + assert hass.states.get("sensor.ac_office_granit_temperature").state == "25" + + +@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert ( + hass.states.get("sensor.ac_office_granit_temperature").state + == STATE_UNAVAILABLE + ) diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index a47ecde7e0d..0f759d8e6b5 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -17,13 +18,19 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -377,3 +384,38 @@ async def test_create_issue( # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 + + +@pytest.mark.parametrize("device_fixture", ["c2c_arlo_pro_3_switch"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("switch.2nd_floor_hallway").state == STATE_ON + + await trigger_health_update( + hass, devices, "10e06a70-ee7d-4832-85e9-a0a06a7a05bd", HealthStatus.OFFLINE + ) + + assert hass.states.get("switch.2nd_floor_hallway").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "10e06a70-ee7d-4832-85e9-a0a06a7a05bd", HealthStatus.ONLINE + ) + + assert hass.states.get("switch.2nd_floor_hallway").state == STATE_ON + + +@pytest.mark.parametrize("device_fixture", ["c2c_arlo_pro_3_switch"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("switch.2nd_floor_hallway").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_update.py b/tests/components/smartthings/test_update.py index 8c3d9e1a968..e4b360e0398 100644 --- a/tests/components/smartthings/test_update.py +++ b/tests/components/smartthings/test_update.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -12,11 +13,22 @@ from homeassistant.components.update import ( DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -140,3 +152,38 @@ async def test_state_update_available( ) assert hass.states.get("update.dimmer_debian_firmware").state == STATE_ON + + +@pytest.mark.parametrize("device_fixture", ["centralite"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("update.dimmer_debian_firmware").state == STATE_OFF + + await trigger_health_update( + hass, devices, "d0268a69-abfb-4c92-a646-61cec2e510ad", HealthStatus.OFFLINE + ) + + assert hass.states.get("update.dimmer_debian_firmware").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "d0268a69-abfb-4c92-a646-61cec2e510ad", HealthStatus.ONLINE + ) + + assert hass.states.get("update.dimmer_debian_firmware").state == STATE_OFF + + +@pytest.mark.parametrize("device_fixture", ["centralite"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("update.dimmer_debian_firmware").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_valve.py b/tests/components/smartthings/test_valve.py index f0ba34c8264..9d2cef65035 100644 --- a/tests/components/smartthings/test_valve.py +++ b/tests/components/smartthings/test_valve.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command +from pysmartthings.models import HealthStatus import pytest from syrupy import SnapshotAssertion @@ -12,12 +13,18 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration, snapshot_smartthings_entities, trigger_update +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) from tests.common import MockConfigEntry @@ -85,3 +92,38 @@ async def test_state_update( ) assert hass.states.get("valve.volvo").state == ValveState.OPEN + + +@pytest.mark.parametrize("device_fixture", ["virtual_valve"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("valve.volvo").state == ValveState.CLOSED + + await trigger_health_update( + hass, devices, "612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3", HealthStatus.OFFLINE + ) + + assert hass.states.get("valve.volvo").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3", HealthStatus.ONLINE + ) + + assert hass.states.get("valve.volvo").state == ValveState.CLOSED + + +@pytest.mark.parametrize("device_fixture", ["virtual_valve"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("valve.volvo").state == STATE_UNAVAILABLE