From fdb6de4d2332f05d6797068c2da797f9fcc87abf Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Sat, 27 Aug 2016 14:53:12 -0600 Subject: [PATCH] Fan demo (#2976) * Update attr to property and default state method * State prop is defined in parent class * Demo platform fan * PyDoc * Copy-pasta artifact * PyDoc * Linting * Raise error if turn_off and turn_on not implemented * Update demo platform * Initial unit test commit * Readability * Unneeded typing * Should inherit from fan entity * Turn off polling * Initial oscillating flag * Pass HASS into demo * Typing * Invoke set_speed instead of setting directly * Service update * Update demo tests * Forgot to block after service call. * linting * Test to make sure not implemented is thrown * Is On Method test * Update const to match string * Update services yaml * Toggle method * Toggle service * Typing * TYPE O * Attribute check * Type-o * Type-o * Put typing back * ToggleEntity * Linting * Linting * Oops * Stale prints * Demo support --- homeassistant/components/fan/__init__.py | 61 ++++++++++----- homeassistant/components/fan/demo.py | 75 ++++++++++++++++++ homeassistant/components/fan/insteon_hub.py | 5 -- homeassistant/components/fan/services.yaml | 12 ++- tests/components/fan/__init__.py | 38 ++++++++++ tests/components/fan/test_demo.py | 84 +++++++++++++++++++++ tests/components/fan/test_insteon_hub.py | 25 +++--- 7 files changed, 260 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/fan/demo.py create mode 100644 tests/components/fan/test_demo.py diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index d4af7562920..13244569dbb 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -11,10 +11,10 @@ import voluptuous as vol from homeassistant.components import group from homeassistant.config import load_yaml_config_file -from homeassistant.const import ( - STATE_OFF, SERVICE_TURN_ON, - SERVICE_TURN_OFF, ATTR_ENTITY_ID) -from homeassistant.helpers.entity import Entity +from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE, + SERVICE_TURN_OFF, ATTR_ENTITY_ID, + STATE_UNKNOWN) +from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv @@ -43,12 +43,12 @@ SPEED_HIGH = 'high' ATTR_SPEED = 'speed' ATTR_SPEED_LIST = 'speed_list' -ATTR_OSCILLATE = 'oscillate' +ATTR_OSCILLATING = 'oscillating' PROP_TO_ATTR = { 'speed': ATTR_SPEED, 'speed_list': ATTR_SPEED_LIST, - 'oscillate': ATTR_OSCILLATE, + 'oscillating': ATTR_OSCILLATING, 'supported_features': ATTR_SUPPORTED_FEATURES, } # type: dict @@ -68,16 +68,21 @@ FAN_TURN_OFF_SCHEMA = vol.Schema({ FAN_OSCILLATE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_OSCILLATE): cv.boolean + vol.Required(ATTR_OSCILLATING): cv.boolean }) # type: dict +FAN_TOGGLE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids +}) + _LOGGER = logging.getLogger(__name__) def is_on(hass, entity_id: str=None) -> bool: """Return if the fans are on based on the statemachine.""" entity_id = entity_id or ENTITY_ID_ALL_FANS - return not hass.states.is_state(entity_id, STATE_OFF) + state = hass.states.get(entity_id) + return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] # pylint: disable=too-many-arguments @@ -102,12 +107,21 @@ def turn_off(hass, entity_id: str=None) -> None: hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) +def toggle(hass, entity_id: str=None) -> None: + """Toggle all or specified fans.""" + data = { + ATTR_ENTITY_ID: entity_id + } + + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + def oscillate(hass, entity_id: str=None, should_oscillate: bool=True) -> None: """Set oscillation on all or specified fan.""" data = { key: value for key, value in [ (ATTR_ENTITY_ID, entity_id), - (ATTR_OSCILLATE, should_oscillate), + (ATTR_OSCILLATING, should_oscillate), ] if value is not None } @@ -180,38 +194,47 @@ def setup(hass, config: dict) -> None: return True -class FanEntity(Entity): +class FanEntity(ToggleEntity): """Representation of a fan.""" # pylint: disable=no-self-use, abstract-method - def set_speed(self: Entity, speed: str) -> None: + def set_speed(self: ToggleEntity, speed: str) -> None: """Set the speed of the fan.""" pass - def turn_on(self: Entity, speed: str=None) -> None: + def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: """Turn on the fan.""" - pass + raise NotImplementedError() - def turn_off(self: Entity) -> None: + def turn_off(self: ToggleEntity, **kwargs) -> None: """Turn off the fan.""" - pass + raise NotImplementedError() - def oscillate(self: Entity) -> None: + def oscillate(self: ToggleEntity, oscillating: bool) -> None: """Oscillate the fan.""" pass @property - def speed_list(self: Entity) -> list: + def is_on(self): + """Return true if the entity is on.""" + return self.state_attributes.get(ATTR_SPEED, STATE_UNKNOWN) \ + not in [SPEED_OFF, STATE_UNKNOWN] + + @property + def speed_list(self: ToggleEntity) -> list: """Get the list of available speeds.""" return [] @property - def state_attributes(self: Entity) -> dict: + def state_attributes(self: ToggleEntity) -> dict: """Return optional state attributes.""" data = {} # type: dict for prop, attr in PROP_TO_ATTR.items(): + if not hasattr(self, prop): + continue + value = getattr(self, prop) if value is not None: data[attr] = value @@ -219,6 +242,6 @@ class FanEntity(Entity): return data @property - def supported_features(self: Entity) -> int: + def supported_features(self: ToggleEntity) -> int: """Flag supported features.""" return 0 diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py new file mode 100644 index 00000000000..83508063fa9 --- /dev/null +++ b/homeassistant/components/fan/demo.py @@ -0,0 +1,75 @@ +""" +Demo garage door platform that has a fake fan. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" + +from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH, + FanEntity, SUPPORT_SET_SPEED, + SUPPORT_OSCILLATE) +from homeassistant.const import STATE_OFF + + +FAN_NAME = 'Living Room Fan' +FAN_ENTITY_ID = 'fan.living_room_fan' + +DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup demo garage door platform.""" + add_devices_callback([ + DemoFan(hass, FAN_NAME, STATE_OFF), + ]) + + +class DemoFan(FanEntity): + """A demonstration fan component.""" + + def __init__(self, hass, name: str, initial_state: str) -> None: + """Initialize the entity.""" + self.hass = hass + self.speed = initial_state + self.oscillating = False + self._name = name + + @property + def name(self) -> str: + """Get entity name.""" + return self._name + + @property + def should_poll(self): + """No polling needed for a demo fan.""" + return False + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return [STATE_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH] + + def turn_on(self, speed: str=SPEED_MED) -> None: + """Turn on the entity.""" + self.set_speed(speed) + + def turn_off(self) -> None: + """Turn off the entity.""" + self.oscillate(False) + self.set_speed(STATE_OFF) + + def set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + self.speed = speed + self.update_ha_state() + + def oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + self.oscillating = oscillating + self.update_ha_state() + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return DEMO_SUPPORT diff --git a/homeassistant/components/fan/insteon_hub.py b/homeassistant/components/fan/insteon_hub.py index 2f24bb0bc9b..4d65ee1f02b 100644 --- a/homeassistant/components/fan/insteon_hub.py +++ b/homeassistant/components/fan/insteon_hub.py @@ -64,8 +64,3 @@ class InsteonFanDevice(InsteonDevice, FanEntity): def speed_list(self) -> list: """Get the available speeds for the fan.""" return [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH] - - @property - def state(self) -> str: - """Get the current device state.""" - return self.speed diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 4f20e6036ce..e729e7f7e89 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -40,6 +40,14 @@ oscillate: description: Name(s) of the entities to oscillate example: 'fan.desk_fan' - oscillate: + oscillating: description: Flag to turn on/off oscillation - example: True \ No newline at end of file + example: True + +toggle: + description: Toggle the fan on/off + + fields: + entity_id: + description: Name(s) of the entities to toggle + exampl: 'fan.living_room' \ No newline at end of file diff --git a/tests/components/fan/__init__.py b/tests/components/fan/__init__.py index 251a118e32e..463e96a4319 100644 --- a/tests/components/fan/__init__.py +++ b/tests/components/fan/__init__.py @@ -1 +1,39 @@ """Test fan component plaforms.""" + +import unittest + +from homeassistant.components.fan import FanEntity + + +class BaseFan(FanEntity): + """Implementation of the abstract FanEntity.""" + + def __init__(self): + """Initialize the fan.""" + pass + + +class TestFanEntity(unittest.TestCase): + """Test coverage for base fan entity class.""" + + def setUp(self): + """Setup test data.""" + self.fan = BaseFan() + + def tearDown(self): + """Tear down unit test data.""" + self.fan = None + + def test_fanentity(self): + """Test fan entity methods.""" + self.assertIsNone(self.fan.state) + self.assertEqual(0, len(self.fan.speed_list)) + self.assertEqual(0, self.fan.supported_features) + self.assertEqual({}, self.fan.state_attributes) + # Test set_speed not required + self.fan.set_speed() + self.fan.oscillate() + with self.assertRaises(NotImplementedError): + self.fan.turn_on() + with self.assertRaises(NotImplementedError): + self.fan.turn_off() diff --git a/tests/components/fan/test_demo.py b/tests/components/fan/test_demo.py new file mode 100644 index 00000000000..db894ee54ed --- /dev/null +++ b/tests/components/fan/test_demo.py @@ -0,0 +1,84 @@ +"""Test cases around the demo fan platform.""" + +import unittest + +from homeassistant.components import fan +from homeassistant.components.fan.demo import FAN_ENTITY_ID +from homeassistant.const import STATE_OFF, STATE_ON + +from tests.common import get_test_home_assistant + + +class TestDemoFan(unittest.TestCase): + """Test the fan demo platform.""" + + def get_entity(self): + """Helper method to get the fan entity.""" + return self.hass.states.get(FAN_ENTITY_ID) + + def setUp(self): + """Initialize unit test data.""" + self.hass = get_test_home_assistant() + self.assertTrue(fan.setup(self.hass, {'fan': { + 'platform': 'demo', + }})) + self.hass.pool.block_till_done() + + def tearDown(self): + """Tear down unit test data.""" + self.hass.stop() + + def test_turn_on(self): + """Test turning on the device.""" + self.assertEqual(STATE_OFF, self.get_entity().state) + + fan.turn_on(self.hass, FAN_ENTITY_ID) + self.hass.pool.block_till_done() + self.assertNotEqual(STATE_OFF, self.get_entity().state) + + fan.turn_on(self.hass, FAN_ENTITY_ID, fan.SPEED_HIGH) + self.hass.pool.block_till_done() + self.assertEqual(STATE_ON, self.get_entity().state) + self.assertEqual(fan.SPEED_HIGH, + self.get_entity().attributes[fan.ATTR_SPEED]) + + def test_turn_off(self): + """Test turning off the device.""" + self.assertEqual(STATE_OFF, self.get_entity().state) + + fan.turn_on(self.hass, FAN_ENTITY_ID) + self.hass.pool.block_till_done() + self.assertNotEqual(STATE_OFF, self.get_entity().state) + + fan.turn_off(self.hass, FAN_ENTITY_ID) + self.hass.pool.block_till_done() + self.assertEqual(STATE_OFF, self.get_entity().state) + + def test_set_speed(self): + """Test setting the speed of the device.""" + self.assertEqual(STATE_OFF, self.get_entity().state) + + fan.set_speed(self.hass, FAN_ENTITY_ID, fan.SPEED_LOW) + self.hass.pool.block_till_done() + self.assertEqual(fan.SPEED_LOW, + self.get_entity().attributes.get('speed')) + + def test_oscillate(self): + """Test oscillating the fan.""" + self.assertFalse(self.get_entity().attributes.get('oscillating')) + + fan.oscillate(self.hass, FAN_ENTITY_ID, True) + self.hass.pool.block_till_done() + self.assertTrue(self.get_entity().attributes.get('oscillating')) + + fan.oscillate(self.hass, FAN_ENTITY_ID, False) + self.hass.pool.block_till_done() + self.assertFalse(self.get_entity().attributes.get('oscillating')) + + def test_is_on(self): + """Test is on service call.""" + self.assertFalse(fan.is_on(self.hass, FAN_ENTITY_ID)) + + fan.turn_on(self.hass, FAN_ENTITY_ID) + self.hass.pool.block_till_done() + self.assertTrue(fan.is_on(self.hass, FAN_ENTITY_ID)) diff --git a/tests/components/fan/test_insteon_hub.py b/tests/components/fan/test_insteon_hub.py index 270687c9fd3..dfdb4b7a9f0 100644 --- a/tests/components/fan/test_insteon_hub.py +++ b/tests/components/fan/test_insteon_hub.py @@ -1,8 +1,9 @@ """Tests for the insteon hub fan platform.""" import unittest -from homeassistant.const import STATE_OFF, STATE_UNKNOWN -from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH) +from homeassistant.const import (STATE_OFF, STATE_ON) +from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH, + ATTR_SPEED) from homeassistant.components.fan.insteon_hub import (InsteonFanDevice, SUPPORT_SET_SPEED) @@ -50,27 +51,23 @@ class TestInsteonHubFanDevice(unittest.TestCase): self._NODE.response = { 'status': 'succeeded' } - self.assertEqual(STATE_UNKNOWN, self._DEVICE.state) + self.assertEqual(STATE_OFF, self._DEVICE.state) self._DEVICE.turn_on() - self.assertNotEqual(STATE_OFF, self._DEVICE.state) + self.assertEqual(STATE_ON, self._DEVICE.state) self._DEVICE.turn_on(SPEED_MED) - self.assertEqual(SPEED_MED, self._DEVICE.state) - - self._NODE.response = { - } - self._DEVICE.turn_on(SPEED_HIGH) - - self.assertNotEqual(SPEED_HIGH, self._DEVICE.state) + self.assertEqual(STATE_ON, self._DEVICE.state) + self.assertEqual(SPEED_MED, self._DEVICE.state_attributes[ATTR_SPEED]) def test_turn_off(self): """Test turning off device.""" self._NODE.response = { 'status': 'succeeded' } - self.assertEqual(STATE_UNKNOWN, self._DEVICE.state) - self._DEVICE.turn_off() - + self.assertEqual(STATE_OFF, self._DEVICE.state) + self._DEVICE.turn_on() + self.assertEqual(STATE_ON, self._DEVICE.state) + self._DEVICE.turn_off() self.assertEqual(STATE_OFF, self._DEVICE.state)