diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 96045947814..650e479d027 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", "cover", "light", "lock", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "lock", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py new file mode 100644 index 00000000000..55955042804 --- /dev/null +++ b/homeassistant/components/freedompro/fan.py @@ -0,0 +1,124 @@ +"""Support for Freedompro fan.""" +import json + +from pyfreedompro import put_state + +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity +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 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro fan.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + FreedomproFan(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] == "fan" + ) + + +class FreedomproFan(CoordinatorEntity, FanEntity): + """Representation of an Freedompro fan.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro fan.""" + 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._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": device["type"], + "manufacturer": "Freedompro", + } + self._attr_is_on = False + self._attr_percentage = 0 + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._attr_is_on + + @property + def percentage(self): + """Return the current speed percentage.""" + return self._attr_percentage + + @property + def supported_features(self): + """Flag supported features.""" + if "rotationSpeed" in self._characteristics: + return SUPPORT_SET_SPEED + return 0 + + @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"] + self._attr_is_on = state["on"] + if "rotationSpeed" in state: + self._attr_percentage = state["rotationSpeed"] + 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_turn_on( + self, speed=None, percentage=None, preset_mode=None, **kwargs + ): + """Async function to turn on the fan.""" + payload = {"on": True} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Async function to turn off the fan.""" + payload = {"on": False} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_set_percentage(self, percentage: int): + """Set the speed percentage of the fan.""" + rotation_speed = {"rotationSpeed": percentage} + payload = json.dumps(rotation_speed) + 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_fan.py b/tests/components/freedompro/test_fan.py new file mode 100644 index 00000000000..6bf4bbe1e04 --- /dev/null +++ b/tests/components/freedompro/test_fan.py @@ -0,0 +1,159 @@ +"""Tests for the Freedompro fan.""" +from datetime import timedelta +from unittest.mock import ANY, patch + +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON +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 + +uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS" + + +async def test_fan_get_state(hass, init_integration): + """Test states of the fan.""" + 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 == "bedroom" + assert device.model == "fan" + + entity_id = "fan.bedroom" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_PERCENTAGE] == 0 + assert state.attributes.get("friendly_name") == "bedroom" + + 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"]["on"] = True + state_response["state"]["rotationSpeed"] = 50 + 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") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 50 + + +async def test_fan_set_off(hass, init_integration): + """Test turn off the fan.""" + init_integration + registry = er.async_get(hass) + + entity_id = "fan.bedroom" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes.get("friendly_name") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.fan.put_state") as mock_put_state: + assert await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": false}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.state == STATE_ON + + +async def test_fan_set_on(hass, init_integration): + """Test turn on the fan.""" + init_integration + registry = er.async_get(hass) + + entity_id = "fan.bedroom" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes.get("friendly_name") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.fan.put_state") as mock_put_state: + assert await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": true}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.state == STATE_ON + + +async def test_fan_set_percent(hass, init_integration): + """Test turn on the fan.""" + init_integration + registry = er.async_get(hass) + + entity_id = "fan.bedroom" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes.get("friendly_name") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.fan.put_state") as mock_put_state: + assert await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: [entity_id], ATTR_PERCENTAGE: 40}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"rotationSpeed": 40}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.state == STATE_ON