diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 311f1794866a..96045947814b 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "light", "lock", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "light", "lock", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py new file mode 100644 index 000000000000..439887c9626c --- /dev/null +++ b/homeassistant/components/freedompro/cover.py @@ -0,0 +1,117 @@ +"""Support for Freedompro cover.""" +import json + +from pyfreedompro import put_state + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DEVICE_CLASS_WINDOW, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +DEVICE_CLASS_MAP = { + "windowCovering": DEVICE_CLASS_BLIND, + "gate": DEVICE_CLASS_GATE, + "garageDoor": DEVICE_CLASS_GARAGE, + "door": DEVICE_CLASS_DOOR, + "window": DEVICE_CLASS_WINDOW, +} + +SUPPORTED_SENSORS = {"windowCovering", "gate", "garageDoor", "door", "window"} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro cover.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] in SUPPORTED_SENSORS + ) + + +class Device(CoordinatorEntity, CoverEntity): + """Representation of an Freedompro cover.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro cover.""" + super().__init__(coordinator) + self._session = aiohttp_client.async_get_clientsession(hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": device["type"], + "manufacturer": "Freedompro", + } + self._attr_current_cover_position = 0 + self._attr_is_closed = True + self._attr_supported_features = ( + SUPPORT_CLOSE | SUPPORT_OPEN | SUPPORT_SET_POSITION + ) + self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self.unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + if "position" in state: + self._attr_current_cover_position = state["position"] + if self._attr_current_cover_position == 0: + self._attr_is_closed = True + else: + self._attr_is_closed = False + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self.async_set_cover_position(position=100) + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + await self.async_set_cover_position(position=0) + + async def async_set_cover_position(self, **kwargs): + """Async function to set position to cover.""" + payload = {} + payload["position"] = kwargs[ATTR_POSITION] + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() diff --git a/tests/components/freedompro/test_cover.py b/tests/components/freedompro/test_cover.py new file mode 100644 index 000000000000..d0338dec82c3 --- /dev/null +++ b/tests/components/freedompro/test_cover.py @@ -0,0 +1,200 @@ +"""Tests for the Freedompro cover.""" +from datetime import timedelta +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.cover import ATTR_POSITION, DOMAIN as COVER_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + STATE_CLOSED, + STATE_OPEN, +) +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "cover.blind", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "blind", + "windowCovering", + ) + ], +) +async def test_cover_get_state( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test states of the cover.""" + init_integration + registry = er.async_get(hass) + registry_device = dr.async_get(hass) + + device = registry_device.async_get_device({("freedompro", uid)}) + assert device is not None + assert device.identifiers == {("freedompro", uid)} + assert device.manufacturer == "Freedompro" + assert device.name == name + assert device.model == model + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_CLOSED + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + state_response["state"]["position"] = 100 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_OPEN + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "cover.blind", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "blind", + "windowCovering", + ) + ], +) +async def test_cover_set_position( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test set position of the cover.""" + init_integration + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.cover.put_state") as mock_put_state: + assert await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: [entity_id], ATTR_POSITION: 33}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 33}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OPEN + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "cover.blind", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "blind", + "windowCovering", + ) + ], +) +async def test_cover_close( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test close cover.""" + init_integration + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.cover.put_state") as mock_put_state: + assert await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 0}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OPEN + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "cover.blind", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "blind", + "windowCovering", + ) + ], +) +async def test_cover_open( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test open cover.""" + init_integration + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.cover.put_state") as mock_put_state: + assert await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 100}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OPEN