-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Add HomeKit support for fans #14351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add HomeKit support for fans #14351
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
824a34c
Initial commit
schmittx aae3162
Add support for rotation speed
schmittx eefeb14
Clean up rotation speed
schmittx 92afdfc
Clean up optional characteristics
schmittx 439c1d5
Update direction characteristic
schmittx 2c866c7
Update per comments
schmittx d62095b
Add tests
schmittx 8e8acc1
Hound fixes
schmittx e4641d3
Update tests
schmittx ff6c8a2
Style changes
cdce8p f1af5d3
Fix test
cdce8p 669daa8
Change speed assignment
cdce8p 208720e
pytest.fixture yield
cdce8p 5ef2ce5
Readded minStep
cdce8p b782083
Removed speed settings
cdce8p 14197ad
Small change
cdce8p File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| """Class to hold all light accessories.""" | ||
| import logging | ||
|
|
||
| from pyhap.const import CATEGORY_FAN | ||
|
|
||
| from homeassistant.components.fan import ( | ||
| ATTR_DIRECTION, ATTR_OSCILLATING, | ||
| DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, | ||
| SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) | ||
| from homeassistant.const import ( | ||
| ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, | ||
| SERVICE_TURN_OFF, SERVICE_TURN_ON) | ||
|
|
||
| from . import TYPES | ||
| from .accessories import HomeAccessory | ||
| from .const import ( | ||
| CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2) | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @TYPES.register('Fan') | ||
| class Fan(HomeAccessory): | ||
| """Generate a Fan accessory for a fan entity. | ||
|
|
||
| Currently supports: state, speed, oscillate, direction. | ||
| """ | ||
|
|
||
| def __init__(self, *args): | ||
| """Initialize a new Light accessory object.""" | ||
| super().__init__(*args, category=CATEGORY_FAN) | ||
| self._flag = {CHAR_ACTIVE: False, | ||
| CHAR_ROTATION_DIRECTION: False, | ||
| CHAR_SWING_MODE: False} | ||
| self._state = 0 | ||
|
|
||
| self.chars = [] | ||
| features = self.hass.states.get(self.entity_id) \ | ||
| .attributes.get(ATTR_SUPPORTED_FEATURES) | ||
| if features & SUPPORT_DIRECTION: | ||
| self.chars.append(CHAR_ROTATION_DIRECTION) | ||
| if features & SUPPORT_OSCILLATE: | ||
| self.chars.append(CHAR_SWING_MODE) | ||
|
|
||
| serv_fan = self.add_preload_service(SERV_FANV2, self.chars) | ||
| self.char_active = serv_fan.configure_char( | ||
| CHAR_ACTIVE, value=0, setter_callback=self.set_state) | ||
|
|
||
| if CHAR_ROTATION_DIRECTION in self.chars: | ||
| self.char_direction = serv_fan.configure_char( | ||
| CHAR_ROTATION_DIRECTION, value=0, | ||
| setter_callback=self.set_direction) | ||
|
|
||
| if CHAR_SWING_MODE in self.chars: | ||
| self.char_swing = serv_fan.configure_char( | ||
| CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating) | ||
|
|
||
| def set_state(self, value): | ||
| """Set state if call came from HomeKit.""" | ||
| if self._state == value: | ||
| return | ||
|
|
||
| _LOGGER.debug('%s: Set state to %d', self.entity_id, value) | ||
| self._flag[CHAR_ACTIVE] = True | ||
| service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF | ||
| params = {ATTR_ENTITY_ID: self.entity_id} | ||
| self.hass.services.call(DOMAIN, service, params) | ||
|
|
||
| def set_direction(self, value): | ||
| """Set state if call came from HomeKit.""" | ||
| _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) | ||
| self._flag[CHAR_ROTATION_DIRECTION] = True | ||
| direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD | ||
| params = {ATTR_ENTITY_ID: self.entity_id, | ||
| ATTR_DIRECTION: direction} | ||
| self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) | ||
|
|
||
| def set_oscillating(self, value): | ||
| """Set state if call came from HomeKit.""" | ||
| _LOGGER.debug('%s: Set oscillating to %d', self.entity_id, value) | ||
| self._flag[CHAR_SWING_MODE] = True | ||
| oscillating = True if value == 1 else False | ||
| params = {ATTR_ENTITY_ID: self.entity_id, | ||
| ATTR_OSCILLATING: oscillating} | ||
| self.hass.services.call(DOMAIN, SERVICE_OSCILLATE, params) | ||
|
|
||
| def update_state(self, new_state): | ||
| """Update fan after state change.""" | ||
| # Handle State | ||
| state = new_state.state | ||
| if state in (STATE_ON, STATE_OFF): | ||
| self._state = 1 if state == STATE_ON else 0 | ||
| if not self._flag[CHAR_ACTIVE] and \ | ||
| self.char_active.value != self._state: | ||
| self.char_active.set_value(self._state) | ||
| self._flag[CHAR_ACTIVE] = False | ||
|
|
||
| # Handle Direction | ||
| if CHAR_ROTATION_DIRECTION in self.chars: | ||
| direction = new_state.attributes.get(ATTR_DIRECTION) | ||
| if not self._flag[CHAR_ROTATION_DIRECTION] and \ | ||
| direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): | ||
| hk_direction = 1 if direction == DIRECTION_REVERSE else 0 | ||
| if self.char_direction.value != hk_direction: | ||
| self.char_direction.set_value(hk_direction) | ||
| self._flag[CHAR_ROTATION_DIRECTION] = False | ||
|
|
||
| # Handle Oscillating | ||
| if CHAR_SWING_MODE in self.chars: | ||
| oscillating = new_state.attributes.get(ATTR_OSCILLATING) | ||
| if not self._flag[CHAR_SWING_MODE] and \ | ||
| oscillating in (True, False): | ||
| hk_oscillating = 1 if oscillating else 0 | ||
| if self.char_swing.value != hk_oscillating: | ||
| self.char_swing.set_value(hk_oscillating) | ||
| self._flag[CHAR_SWING_MODE] = False | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| """Test different accessory types: Fans.""" | ||
| from collections import namedtuple | ||
|
|
||
| import pytest | ||
|
|
||
| from homeassistant.components.fan import ( | ||
| ATTR_DIRECTION, ATTR_OSCILLATING, | ||
| DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, | ||
| SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) | ||
| from homeassistant.const import ( | ||
| ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, | ||
| STATE_ON, STATE_OFF, STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) | ||
|
|
||
| from tests.common import async_mock_service | ||
| from tests.components.homekit.test_accessories import patch_debounce | ||
|
|
||
|
|
||
| @pytest.fixture(scope='module') | ||
| def cls(): | ||
| """Patch debounce decorator during import of type_fans.""" | ||
| patcher = patch_debounce() | ||
| patcher.start() | ||
| _import = __import__('homeassistant.components.homekit.type_fans', | ||
| fromlist=['Fan']) | ||
| patcher_tuple = namedtuple('Cls', ['fan']) | ||
| yield patcher_tuple(fan=_import.Fan) | ||
| patcher.stop() | ||
|
|
||
|
|
||
| async def test_fan_basic(hass, cls): | ||
| """Test fan with char state.""" | ||
| entity_id = 'fan.demo' | ||
|
|
||
| hass.states.async_set(entity_id, STATE_ON, | ||
| {ATTR_SUPPORTED_FEATURES: 0}) | ||
| await hass.async_block_till_done() | ||
| acc = cls.fan(hass, 'Fan', entity_id, 2, None) | ||
|
|
||
| assert acc.aid == 2 | ||
| assert acc.category == 3 # Fan | ||
| assert acc.char_active.value == 0 | ||
|
|
||
| await hass.async_add_job(acc.run) | ||
| await hass.async_block_till_done() | ||
| assert acc.char_active.value == 1 | ||
|
|
||
| hass.states.async_set(entity_id, STATE_OFF, | ||
| {ATTR_SUPPORTED_FEATURES: 0}) | ||
| await hass.async_block_till_done() | ||
| assert acc.char_active.value == 0 | ||
|
|
||
| hass.states.async_set(entity_id, STATE_UNKNOWN) | ||
| await hass.async_block_till_done() | ||
| assert acc.char_active.value == 0 | ||
|
|
||
| hass.states.async_remove(entity_id) | ||
| await hass.async_block_till_done() | ||
| assert acc.char_active.value == 0 | ||
|
|
||
| # Set from HomeKit | ||
| call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) | ||
| call_turn_off = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) | ||
|
|
||
| await hass.async_add_job(acc.char_active.client_update_value, 1) | ||
| await hass.async_block_till_done() | ||
| assert call_turn_on | ||
| assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id | ||
|
|
||
| hass.states.async_set(entity_id, STATE_ON) | ||
| await hass.async_block_till_done() | ||
|
|
||
| await hass.async_add_job(acc.char_active.client_update_value, 0) | ||
| await hass.async_block_till_done() | ||
| assert call_turn_off | ||
| assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id | ||
|
|
||
|
|
||
| async def test_fan_direction(hass, cls): | ||
| """Test fan with direction.""" | ||
| entity_id = 'fan.demo' | ||
|
|
||
| hass.states.async_set(entity_id, STATE_ON, { | ||
| ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, | ||
| ATTR_DIRECTION: DIRECTION_FORWARD}) | ||
| await hass.async_block_till_done() | ||
| acc = cls.fan(hass, 'Fan', entity_id, 2, None) | ||
|
|
||
| assert acc.char_direction.value == 0 | ||
|
|
||
| await hass.async_add_job(acc.run) | ||
| await hass.async_block_till_done() | ||
| assert acc.char_direction.value == 0 | ||
|
|
||
| hass.states.async_set(entity_id, STATE_ON, | ||
| {ATTR_DIRECTION: DIRECTION_REVERSE}) | ||
| await hass.async_block_till_done() | ||
| assert acc.char_direction.value == 1 | ||
|
|
||
| # Set from HomeKit | ||
| call_set_direction = async_mock_service(hass, DOMAIN, | ||
| SERVICE_SET_DIRECTION) | ||
|
|
||
| await hass.async_add_job(acc.char_direction.client_update_value, 0) | ||
| await hass.async_block_till_done() | ||
| assert call_set_direction[0] | ||
| assert call_set_direction[0].data[ATTR_ENTITY_ID] == entity_id | ||
| assert call_set_direction[0].data[ATTR_DIRECTION] == DIRECTION_FORWARD | ||
|
|
||
| await hass.async_add_job(acc.char_direction.client_update_value, 1) | ||
| await hass.async_block_till_done() | ||
| assert call_set_direction[1] | ||
| assert call_set_direction[1].data[ATTR_ENTITY_ID] == entity_id | ||
| assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE | ||
|
|
||
|
|
||
| async def test_fan_oscillate(hass, cls): | ||
| """Test fan with oscillate.""" | ||
| entity_id = 'fan.demo' | ||
|
|
||
| hass.states.async_set(entity_id, STATE_ON, { | ||
| ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}) | ||
| await hass.async_block_till_done() | ||
| acc = cls.fan(hass, 'Fan', entity_id, 2, None) | ||
|
|
||
| assert acc.char_swing.value == 0 | ||
|
|
||
| await hass.async_add_job(acc.run) | ||
| await hass.async_block_till_done() | ||
| assert acc.char_swing.value == 0 | ||
|
|
||
| hass.states.async_set(entity_id, STATE_ON, | ||
| {ATTR_OSCILLATING: True}) | ||
| await hass.async_block_till_done() | ||
| assert acc.char_swing.value == 1 | ||
|
|
||
| # Set from HomeKit | ||
| call_oscillate = async_mock_service(hass, DOMAIN, SERVICE_OSCILLATE) | ||
|
|
||
| await hass.async_add_job(acc.char_swing.client_update_value, 0) | ||
| await hass.async_block_till_done() | ||
| assert call_oscillate[0] | ||
| assert call_oscillate[0].data[ATTR_ENTITY_ID] == entity_id | ||
| assert call_oscillate[0].data[ATTR_OSCILLATING] is False | ||
|
|
||
| await hass.async_add_job(acc.char_swing.client_update_value, 1) | ||
| await hass.async_block_till_done() | ||
| assert call_oscillate[1] | ||
| assert call_oscillate[1].data[ATTR_ENTITY_ID] == entity_id | ||
| assert call_oscillate[1].data[ATTR_OSCILLATING] is True |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This currently doesn't work if set from the frontend. See home-assistant/frontend#1158