diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index 29e897916b9..5072eb0deb9 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -37,10 +37,10 @@ IDS_TO_DEVICES = { class FakeHarmonyClient: """FakeHarmonyClient to mock away network calls.""" - def __init__( + def initialize( self, ip_address: str = "", callbacks: ClientCallbackType = MagicMock() ): - """Initialize FakeHarmonyClient class.""" + """Initialize FakeHarmonyClient class to capture callbacks.""" self._activity_name = "Watch TV" self.close = AsyncMock() self.send_commands = AsyncMock() @@ -49,6 +49,8 @@ class FakeHarmonyClient: self._callbacks = callbacks self.fw_version = "123.456" + return self + async def connect(self): """Connect and call the appropriate callbacks.""" self._callbacks.connect(None) @@ -130,13 +132,28 @@ class FakeHarmonyClient: ) return config + def mock_reconnection(self): + """Simulate reconnection to the hub.""" + self._callbacks.connect(None) + + def mock_disconnection(self): + """Simulate disconnection to the hub.""" + self._callbacks.disconnect(None) + @pytest.fixture() -def mock_hc(): - """Create a mock HarmonyClient.""" +def harmony_client(): + """Create the FakeHarmonyClient instance.""" + return FakeHarmonyClient() + + +@pytest.fixture() +def mock_hc(harmony_client): + """Patch the real HarmonyClient with initialization side effect.""" + with patch( "homeassistant.components.harmony.data.HarmonyClient", - side_effect=FakeHarmonyClient, + side_effect=harmony_client.initialize, ) as fake: yield fake diff --git a/tests/components/harmony/test_connection_changes.py b/tests/components/harmony/test_connection_changes.py deleted file mode 100644 index 15d46298855..00000000000 --- a/tests/components/harmony/test_connection_changes.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Test the Logitech Harmony Hub entities with connection state changes.""" - -from datetime import timedelta - -from homeassistant.components.harmony.const import DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.util import utcnow - -from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME - -from tests.common import MockConfigEntry, async_fire_time_changed - - -async def test_connection_state_changes(mock_hc, hass, mock_write_config): - """Ensure connection changes are reflected in the switch states.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - data = hass.data[DOMAIN][entry.entry_id] - - # mocks start with current activity == Watch TV - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - data._disconnected() - await hass.async_block_till_done() - - # Entities do not immediately show as unavailable - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - future_time = utcnow() + timedelta(seconds=10) - async_fire_time_changed(hass, future_time) - await hass.async_block_till_done() - assert hass.states.is_state(ENTITY_REMOTE, STATE_UNAVAILABLE) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_UNAVAILABLE) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_UNAVAILABLE) - - data._connected() - await hass.async_block_till_done() - - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - data._disconnected() - data._connected() - future_time = utcnow() + timedelta(seconds=10) - async_fire_time_changed(hass, future_time) - - await hass.async_block_till_done() - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) diff --git a/tests/components/harmony/test_commands.py b/tests/components/harmony/test_remote.py similarity index 62% rename from tests/components/harmony/test_commands.py rename to tests/components/harmony/test_remote.py index 62056a08e1d..8c4d67e1117 100644 --- a/tests/components/harmony/test_commands.py +++ b/tests/components/harmony/test_remote.py @@ -1,4 +1,6 @@ -"""Test sending commands to the Harmony Hub remote.""" +"""Test the Logitech Harmony Hub remote.""" + +from datetime import timedelta from aioharmony.const import SendCommandDevice @@ -9,6 +11,7 @@ from homeassistant.components.harmony.const import ( ) from homeassistant.components.harmony.remote import ATTR_CHANNEL, ATTR_DELAY_SECS from homeassistant.components.remote import ( + ATTR_ACTIVITY, ATTR_COMMAND, ATTR_DEVICE, ATTR_NUM_REPEATS, @@ -16,18 +19,136 @@ from homeassistant.components.remote import ( DEFAULT_HOLD_SECS, DOMAIN as REMOTE_DOMAIN, SERVICE_SEND_COMMAND, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.util import utcnow -from .conftest import TV_DEVICE_ID, TV_DEVICE_NAME -from .const import ENTITY_REMOTE, HUB_NAME +from .conftest import ACTIVITIES_TO_IDS, TV_DEVICE_ID, TV_DEVICE_NAME +from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed PLAY_COMMAND = "Play" STOP_COMMAND = "Stop" +async def test_connection_state_changes( + harmony_client, mock_hc, hass, mock_write_config +): + """Ensure connection changes are reflected in the remote state.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + + harmony_client.mock_disconnection() + await hass.async_block_till_done() + + # Entities do not immediately show as unavailable + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + + future_time = utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future_time) + await hass.async_block_till_done() + assert hass.states.is_state(ENTITY_REMOTE, STATE_UNAVAILABLE) + + harmony_client.mock_reconnection() + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + + harmony_client.mock_disconnection() + harmony_client.mock_reconnection() + future_time = utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future_time) + + await hass.async_block_till_done() + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + + +async def test_remote_toggles(mock_hc, hass, mock_write_config): + """Ensure calls to the remote also updates the switches.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn off remote + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_REMOTE}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn on remote, restoring the last activity + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # send new activity command, with activity name + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: "Play Music"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON) + + # send new activity command, with activity id + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: ACTIVITIES_TO_IDS["Watch TV"]}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + async def test_async_send_command(mock_hc, hass, mock_write_config): """Ensure calls to send remote commands properly propagate to devices.""" entry = MockConfigEntry( diff --git a/tests/components/harmony/test_activity_changes.py b/tests/components/harmony/test_switch.py similarity index 64% rename from tests/components/harmony/test_activity_changes.py rename to tests/components/harmony/test_switch.py index dbbc6beef5b..1940c54e112 100644 --- a/tests/components/harmony/test_activity_changes.py +++ b/tests/components/harmony/test_switch.py @@ -1,6 +1,8 @@ """Test the Logitech Harmony Hub activity switches.""" + +from datetime import timedelta + from homeassistant.components.harmony.const import DOMAIN -from homeassistant.components.remote import ATTR_ACTIVITY, DOMAIN as REMOTE_DOMAIN from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -12,12 +14,58 @@ from homeassistant.const import ( CONF_NAME, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) +from homeassistant.util import utcnow -from .conftest import ACTIVITIES_TO_IDS from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_connection_state_changes( + harmony_client, mock_hc, hass, mock_write_config +): + """Ensure connection changes are reflected in the switch states.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + harmony_client.mock_disconnection() + await hass.async_block_till_done() + + # Entities do not immediately show as unavailable + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + future_time = utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future_time) + await hass.async_block_till_done() + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_UNAVAILABLE) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_UNAVAILABLE) + + harmony_client.mock_reconnection() + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + harmony_client.mock_disconnection() + harmony_client.mock_reconnection() + future_time = utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future_time) + + await hass.async_block_till_done() + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) async def test_switch_toggles(mock_hc, hass, mock_write_config): @@ -54,74 +102,6 @@ async def test_switch_toggles(mock_hc, hass, mock_write_config): assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) -async def test_remote_toggles(mock_hc, hass, mock_write_config): - """Ensure calls to the remote also updates the switches.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - # mocks start with current activity == Watch TV - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - # turn off remote - await hass.services.async_call( - REMOTE_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_REMOTE}, - blocking=True, - ) - await hass.async_block_till_done() - - assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - # turn on remote, restoring the last activity - await hass.services.async_call( - REMOTE_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_REMOTE}, - blocking=True, - ) - await hass.async_block_till_done() - - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - # send new activity command, with activity name - await hass.services.async_call( - REMOTE_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: "Play Music"}, - blocking=True, - ) - await hass.async_block_till_done() - - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON) - - # send new activity command, with activity id - await hass.services.async_call( - REMOTE_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: ACTIVITIES_TO_IDS["Watch TV"]}, - blocking=True, - ) - await hass.async_block_till_done() - - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - async def _toggle_switch_and_wait(hass, service_name, entity): await hass.services.async_call( SWITCH_DOMAIN,