diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 1196deb27b7..f250c3f565e 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -17,7 +17,7 @@ from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["cover", "sensor"] +PLATFORMS = ["cover", "sensor", "switch"] PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index 71d2193f904..f5eba403c75 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( STATE_OPEN, STATE_OPENING, ) +from homeassistant.components.switch import DEVICE_CLASS_SWITCH from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS DOMAIN = "blebox" @@ -26,6 +27,8 @@ BLEBOX_TO_HASS_DEVICE_CLASSES = { "shutter": DEVICE_CLASS_SHUTTER, "gatebox": DEVICE_CLASS_DOOR, "gate": DEVICE_CLASS_GATE, + "relay": DEVICE_CLASS_SWITCH, + "temperature": DEVICE_CLASS_TEMPERATURE, } BLEBOX_TO_HASS_COVER_STATES = { @@ -43,7 +46,6 @@ BLEBOX_TO_HASS_COVER_STATES = { } BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} -BLEBOX_DEV_CLASS_MAP = {"temperature": DEVICE_CLASS_TEMPERATURE} DEFAULT_HOST = "192.168.0.2" DEFAULT_PORT = 80 diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index b43b4f21da5..7a7aa0bac8d 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -3,7 +3,7 @@ from homeassistant.helpers.entity import Entity from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_DEV_CLASS_MAP, BLEBOX_TO_UNIT_MAP, DOMAIN, PRODUCT +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, BLEBOX_TO_UNIT_MAP, DOMAIN, PRODUCT async def async_setup_entry(hass, config_entry, async_add): @@ -30,4 +30,4 @@ class BleBoxSensorEntity(BleBoxEntity, Entity): @property def device_class(self): """Return the device class.""" - return BLEBOX_DEV_CLASS_MAP[self._feature.device_class] + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py new file mode 100644 index 00000000000..116ae1645f4 --- /dev/null +++ b/homeassistant/components/blebox/switch.py @@ -0,0 +1,35 @@ +"""BleBox switch implementation.""" +from homeassistant.components.switch import SwitchDevice + +from . import BleBoxEntity, create_blebox_entities +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, DOMAIN, PRODUCT + + +async def async_setup_entry(hass, config_entry, async_add): + """Set up a BleBox switch entity.""" + + product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] + create_blebox_entities(product, async_add, BleBoxSwitchEntity, "switches") + return True + + +class BleBoxSwitchEntity(BleBoxEntity, SwitchDevice): + """Representation of a BleBox switch feature.""" + + @property + def device_class(self): + """Return the device class.""" + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] + + @property + def is_on(self): + """Return whether switch is on.""" + return self._feature.is_on + + async def async_turn_on(self, **kwargs): + """Turn on the switch.""" + return await self._feature.async_turn_on() + + async def async_turn_off(self, **kwargs): + """Turn off the switch.""" + return await self._feature.async_turn_off() diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py index 69798d01fb8..569c16f813f 100644 --- a/tests/components/blebox/conftest.py +++ b/tests/components/blebox/conftest.py @@ -76,12 +76,19 @@ def feature(request): return request.getfixturevalue(request.param) -async def async_setup_entity(hass, config, entity_id): - """Return a configured entity with the given entity_id.""" +async def async_setup_entities(hass, config, entity_ids): + """Return configured entries with the given entity ids.""" + config_entry = mock_config() config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() entity_registry = await hass.helpers.entity_registry.async_get_registry() - return entity_registry.async_get(entity_id) + return [entity_registry.async_get(entity_id) for entity_id in entity_ids] + + +async def async_setup_entity(hass, config, entity_id): + """Return a configured entry with the given entity_id.""" + + return (await async_setup_entities(hass, config, [entity_id]))[0] diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py new file mode 100644 index 00000000000..c41273757f2 --- /dev/null +++ b/tests/components/blebox/test_switch.py @@ -0,0 +1,386 @@ +"""Blebox switch tests.""" + +import logging + +import blebox_uniapi +import pytest + +from homeassistant.components.switch import DEVICE_CLASS_SWITCH +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) + +from .conftest import ( + async_setup_entities, + async_setup_entity, + mock_feature, + mock_only_feature, + setup_product_mock, +) + +from tests.async_mock import AsyncMock, PropertyMock + + +@pytest.fixture(name="switchbox") +def switchbox_fixture(): + """Return a default switchBox switch entity mock.""" + feature = mock_feature( + "switches", + blebox_uniapi.switch.Switch, + unique_id="BleBox-switchBox-1afe34e750b8-0.relay", + full_name="switchBox-0.relay", + device_class="relay", + is_on=False, + ) + feature.async_update = AsyncMock() + product = feature.product + type(product).name = PropertyMock(return_value="My switch box") + type(product).model = PropertyMock(return_value="switchBox") + return (feature, "switch.switchbox_0_relay") + + +async def test_switchbox_init(switchbox, hass, config): + """Test switch default state.""" + + feature_mock, entity_id = switchbox + + feature_mock.async_update = AsyncMock() + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-switchBox-1afe34e750b8-0.relay" + + state = hass.states.get(entity_id) + assert state.name == "switchBox-0.relay" + + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + + assert state.state == STATE_OFF + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My switch box" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "switchBox" + assert device.sw_version == "1.23" + + +async def test_switchbox_update_when_off(switchbox, hass, config): + """Test switch updating when off.""" + + feature_mock, entity_id = switchbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +async def test_switchbox_update_when_on(switchbox, hass, config): + """Test switch updating when on.""" + + feature_mock, entity_id = switchbox + + def initial_update(): + feature_mock.is_on = True + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_switchbox_on(switchbox, hass, config): + """Test turning switch on.""" + + feature_mock, entity_id = switchbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + def turn_on(): + feature_mock.is_on = True + + feature_mock.async_turn_on = AsyncMock(side_effect=turn_on) + + await hass.services.async_call( + "switch", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_switchbox_off(switchbox, hass, config): + """Test turning switch off.""" + + feature_mock, entity_id = switchbox + + def initial_update(): + feature_mock.is_on = True + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + def turn_off(): + feature_mock.is_on = False + + feature_mock.async_turn_off = AsyncMock(side_effect=turn_off) + + await hass.services.async_call( + "switch", SERVICE_TURN_OFF, {"entity_id": entity_id}, blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +def relay_mock(relay_id=0): + """Return a default switchBoxD switch entity mock.""" + + return mock_only_feature( + blebox_uniapi.switch.Switch, + unique_id=f"BleBox-switchBoxD-1afe34e750b8-{relay_id}.relay", + full_name=f"switchBoxD-{relay_id}.relay", + device_class="relay", + is_on=None, + ) + + +@pytest.fixture(name="switchbox_d") +def switchbox_d_fixture(): + """Set up two mocked Switch features representing a switchBoxD.""" + + relay1 = relay_mock(0) + relay2 = relay_mock(1) + features = [relay1, relay2] + + product = setup_product_mock("switches", features) + + type(product).name = PropertyMock(return_value="My relays") + type(product).model = PropertyMock(return_value="switchBoxD") + type(product).brand = PropertyMock(return_value="BleBox") + type(product).firmware_version = PropertyMock(return_value="1.23") + type(product).unique_id = PropertyMock(return_value="abcd0123ef5678") + + type(relay1).product = product + type(relay2).product = product + + return (features, ["switch.switchboxd_0_relay", "switch.switchboxd_1_relay"]) + + +async def test_switchbox_d_init(switchbox_d, hass, config): + """Test switch default state.""" + + feature_mocks, entity_ids = switchbox_d + + feature_mocks[0].async_update = AsyncMock() + feature_mocks[1].async_update = AsyncMock() + entries = await async_setup_entities(hass, config, entity_ids) + + entry = entries[0] + assert entry.unique_id == "BleBox-switchBoxD-1afe34e750b8-0.relay" + + state = hass.states.get(entity_ids[0]) + assert state.name == "switchBoxD-0.relay" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN? + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My relays" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "switchBoxD" + assert device.sw_version == "1.23" + + entry = entries[1] + assert entry.unique_id == "BleBox-switchBoxD-1afe34e750b8-1.relay" + + state = hass.states.get(entity_ids[1]) + assert state.name == "switchBoxD-1.relay" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN? + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My relays" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "switchBoxD" + assert device.sw_version == "1.23" + + +async def test_switchbox_d_update_when_off(switchbox_d, hass, config): + """Test switch updating when off.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update0(): + feature_mocks[0].is_on = False + feature_mocks[1].is_on = False + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update0) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + + assert hass.states.get(entity_ids[0]).state == STATE_OFF + assert hass.states.get(entity_ids[1]).state == STATE_OFF + + +async def test_switchbox_d_update_when_second_off(switchbox_d, hass, config): + """Test switch updating when off.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update0(): + feature_mocks[0].is_on = True + feature_mocks[1].is_on = False + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update0) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + + assert hass.states.get(entity_ids[0]).state == STATE_ON + assert hass.states.get(entity_ids[1]).state == STATE_OFF + + +async def test_switchbox_d_turn_first_on(switchbox_d, hass, config): + """Test turning switch on.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update0(): + feature_mocks[0].is_on = False + feature_mocks[1].is_on = False + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update0) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + feature_mocks[0].async_update = AsyncMock() + + def turn_on0(): + feature_mocks[0].is_on = True + + feature_mocks[0].async_turn_on = AsyncMock(side_effect=turn_on0) + await hass.services.async_call( + "switch", SERVICE_TURN_ON, {"entity_id": entity_ids[0]}, blocking=True, + ) + + assert hass.states.get(entity_ids[0]).state == STATE_ON + assert hass.states.get(entity_ids[1]).state == STATE_OFF + + +async def test_switchbox_d_second_on(switchbox_d, hass, config): + """Test turning switch on.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update0(): + feature_mocks[0].is_on = False + feature_mocks[1].is_on = False + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update0) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + feature_mocks[0].async_update = AsyncMock() + + def turn_on1(): + feature_mocks[1].is_on = True + + feature_mocks[1].async_turn_on = AsyncMock(side_effect=turn_on1) + await hass.services.async_call( + "switch", SERVICE_TURN_ON, {"entity_id": entity_ids[1]}, blocking=True, + ) + + assert hass.states.get(entity_ids[0]).state == STATE_OFF + assert hass.states.get(entity_ids[1]).state == STATE_ON + + +async def test_switchbox_d_first_off(switchbox_d, hass, config): + """Test turning switch on.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update_any(): + feature_mocks[0].is_on = True + feature_mocks[1].is_on = True + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update_any) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + feature_mocks[0].async_update = AsyncMock() + + def turn_off0(): + feature_mocks[0].is_on = False + + feature_mocks[0].async_turn_off = AsyncMock(side_effect=turn_off0) + await hass.services.async_call( + "switch", SERVICE_TURN_OFF, {"entity_id": entity_ids[0]}, blocking=True, + ) + + assert hass.states.get(entity_ids[0]).state == STATE_OFF + assert hass.states.get(entity_ids[1]).state == STATE_ON + + +async def test_switchbox_d_second_off(switchbox_d, hass, config): + """Test turning switch on.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update_any(): + feature_mocks[0].is_on = True + feature_mocks[1].is_on = True + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update_any) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + feature_mocks[0].async_update = AsyncMock() + + def turn_off1(): + feature_mocks[1].is_on = False + + feature_mocks[1].async_turn_off = AsyncMock(side_effect=turn_off1) + await hass.services.async_call( + "switch", SERVICE_TURN_OFF, {"entity_id": entity_ids[1]}, blocking=True, + ) + assert hass.states.get(entity_ids[0]).state == STATE_ON + assert hass.states.get(entity_ids[1]).state == STATE_OFF + + +ALL_SWITCH_FIXTURES = ["switchbox", "switchbox_d"] + + +@pytest.mark.parametrize("feature", ALL_SWITCH_FIXTURES, indirect=["feature"]) +async def test_update_failure(feature, hass, config, caplog): + """Test that update failures are logged.""" + + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = feature + + if isinstance(feature_mock, list): + feature_mock[0].async_update = AsyncMock() + feature_mock[1].async_update = AsyncMock() + feature_mock = feature_mock[0] + entity_id = entity_id[0] + + feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) + await async_setup_entity(hass, config, entity_id) + + assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text