Support for GE Zwave fan controller (#7767)

* Support for GE Zwave fan controller

* Tests for zwave fan

* Add additional fan workarounds
This commit is contained in:
Adam Mills 2017-05-26 01:55:00 -04:00 committed by Paulus Schoutsen
parent 6899c7b6f7
commit 9e9705d6b2
5 changed files with 260 additions and 4 deletions

View File

@ -0,0 +1,86 @@
"""
Z-Wave platform that handles fans.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.zwave/
"""
import logging
import math
from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
SPEED_LIST = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
SUPPORTED_FEATURES = SUPPORT_SET_SPEED
# Value will first be divided to an integer
VALUE_TO_SPEED = {
0: SPEED_OFF,
1: SPEED_LOW,
2: SPEED_MEDIUM,
3: SPEED_HIGH,
}
SPEED_TO_VALUE = {
SPEED_OFF: 0,
SPEED_LOW: 1,
SPEED_MEDIUM: 50,
SPEED_HIGH: 99,
}
def get_device(values, **kwargs):
"""Create zwave entity device."""
return ZwaveFan(values)
class ZwaveFan(zwave.ZWaveDeviceEntity, FanEntity):
"""Representation of a Z-Wave fan."""
def __init__(self, values):
"""Initialize the Z-Wave fan device."""
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self.update_properties()
def update_properties(self):
"""Handle data changes for node values."""
value = math.ceil(self.values.primary.data * 3 / 100)
self._state = VALUE_TO_SPEED[value]
def set_speed(self, speed):
"""Set the speed of the fan."""
self.node.set_dimmer(
self.values.primary.value_id, SPEED_TO_VALUE[speed])
def turn_on(self, speed=None, **kwargs):
"""Turn the device on."""
if speed is None:
# Value 255 tells device to return to previous value
self.node.set_dimmer(self.values.primary.value_id, 255)
else:
self.set_speed(speed)
def turn_off(self, **kwargs):
"""Turn the device off."""
self.node.set_dimmer(self.values.primary.value_id, 0)
@property
def speed(self):
"""Return the current speed."""
return self._state
@property
def speed_list(self):
"""Get the list of available speeds."""
return SPEED_LIST
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORTED_FEATURES

View File

@ -3,22 +3,30 @@ from . import const
# Manufacturers
FIBARO = 0x010f
GE = 0x0063
PHILIO = 0x013c
SOMFY = 0x0047
WENZHOU = 0x0118
SOMFY = 0x47
VIZIA = 0x001D
# Product IDs
GE_FAN_CONTROLLER_12730 = 0x3034
GE_FAN_CONTROLLER_14287 = 0x3131
JASCO_FAN_CONTROLLER_14314 = 0x3138
PHILIO_SLIM_SENSOR = 0x0002
PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000d
PHILIO_PAN07 = 0x0005
VIZIA_FAN_CONTROLLER_VRF01 = 0x0334
# Product Types
FGFS101_FLOOD_SENSOR_TYPE = 0x0b00
FGRM222_SHUTTER2 = 0x0301
FGR222_SHUTTER2 = 0x0302
GE_DIMMER = 0x4944
PHILIO_SWITCH = 0x0001
PHILIO_SENSOR = 0x0002
SOMFY_ZRTSI = 0x5a52
VIZIA_DIMMER = 0x1001
# Mapping devices
PHILIO_SLIM_SENSOR_MOTION_MTII = (PHILIO, PHILIO_SENSOR, PHILIO_SLIM_SENSOR, 0)
@ -53,7 +61,6 @@ DEVICE_MAPPINGS_MT = {
SOMFY_ZRTSI_CONTROLLER_MT: WORKAROUND_NO_POSITION,
}
# Component mapping devices
FIBARO_FGFS101_SENSOR_ALARM = (
FIBARO, FGFS101_FLOOD_SENSOR_TYPE, const.COMMAND_CLASS_SENSOR_ALARM)
@ -61,6 +68,18 @@ FIBARO_FGRM222_BINARY = (
FIBARO, FGRM222_SHUTTER2, const.COMMAND_CLASS_SWITCH_BINARY)
FIBARO_FGR222_BINARY = (
FIBARO, FGR222_SHUTTER2, const.COMMAND_CLASS_SWITCH_BINARY)
GE_FAN_CONTROLLER_12730_MULTILEVEL = (
GE, GE_DIMMER, GE_FAN_CONTROLLER_12730,
const.COMMAND_CLASS_SWITCH_MULTILEVEL)
GE_FAN_CONTROLLER_14287_MULTILEVEL = (
GE, GE_DIMMER, GE_FAN_CONTROLLER_14287,
const.COMMAND_CLASS_SWITCH_MULTILEVEL)
JASCO_FAN_CONTROLLER_14314_MULTILEVEL = (
GE, GE_DIMMER, JASCO_FAN_CONTROLLER_14314,
const.COMMAND_CLASS_SWITCH_MULTILEVEL)
VIZIA_FAN_CONTROLLER_VRF01_MULTILEVEL = (
VIZIA, VIZIA_DIMMER, VIZIA_FAN_CONTROLLER_VRF01,
const.COMMAND_CLASS_SWITCH_MULTILEVEL)
# List of component workarounds by
# (manufacturer_id, product_type, command_class)
@ -70,6 +89,15 @@ DEVICE_COMPONENT_MAPPING = {
FIBARO_FGR222_BINARY: WORKAROUND_IGNORE,
}
# List of component workarounds by
# (manufacturer_id, product_type, product_id, command_class)
DEVICE_COMPONENT_MAPPING_MTI = {
GE_FAN_CONTROLLER_12730_MULTILEVEL: 'fan',
GE_FAN_CONTROLLER_14287_MULTILEVEL: 'fan',
JASCO_FAN_CONTROLLER_14314_MULTILEVEL: 'fan',
VIZIA_FAN_CONTROLLER_VRF01_MULTILEVEL: 'fan',
}
def get_device_component_mapping(value):
"""Get mapping of value to another component."""
@ -77,8 +105,16 @@ def get_device_component_mapping(value):
value.node.product_type.strip()):
manufacturer_id = int(value.node.manufacturer_id, 16)
product_type = int(value.node.product_type, 16)
return DEVICE_COMPONENT_MAPPING.get(
product_id = int(value.node.product_id, 16)
result = DEVICE_COMPONENT_MAPPING.get(
(manufacturer_id, product_type, value.command_class))
if result:
return result
result = DEVICE_COMPONENT_MAPPING_MTI.get(
(manufacturer_id, product_type, product_id, value.command_class))
if result:
return result
return None

View File

@ -0,0 +1,117 @@
"""Test Z-Wave fans."""
from homeassistant.components.fan import (
zwave, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED)
from tests.mock.zwave import (
MockNode, MockValue, MockEntityValues, value_changed)
def test_get_device_detects_fan(mock_openzwave):
"""Test get_device returns a zwave fan."""
node = MockNode()
value = MockValue(data=0, node=node)
values = MockEntityValues(primary=value)
device = zwave.get_device(node=node, values=values, node_config={})
assert isinstance(device, zwave.ZwaveFan)
assert device.supported_features == SUPPORT_SET_SPEED
assert device.speed_list == [
SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def test_fan_turn_on(mock_openzwave):
"""Test turning on a zwave fan."""
node = MockNode()
value = MockValue(data=0, node=node)
values = MockEntityValues(primary=value)
device = zwave.get_device(node=node, values=values, node_config={})
device.turn_on()
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
assert brightness == 255
node.reset_mock()
device.turn_on(speed=SPEED_OFF)
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
assert brightness == 0
node.reset_mock()
device.turn_on(speed=SPEED_LOW)
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
assert brightness == 1
node.reset_mock()
device.turn_on(speed=SPEED_MEDIUM)
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
assert brightness == 50
node.reset_mock()
device.turn_on(speed=SPEED_HIGH)
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
assert brightness == 99
def test_fan_turn_off(mock_openzwave):
"""Test turning off a dimmable zwave fan."""
node = MockNode()
value = MockValue(data=46, node=node)
values = MockEntityValues(primary=value)
device = zwave.get_device(node=node, values=values, node_config={})
device.turn_off()
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
assert brightness == 0
def test_fan_value_changed(mock_openzwave):
"""Test value changed for zwave fan."""
node = MockNode()
value = MockValue(data=0, node=node)
values = MockEntityValues(primary=value)
device = zwave.get_device(node=node, values=values, node_config={})
assert not device.is_on
value.data = 10
value_changed(value)
assert device.is_on
assert device.speed == SPEED_LOW
value.data = 50
value_changed(value)
assert device.is_on
assert device.speed == SPEED_MEDIUM
value.data = 90
value_changed(value)
assert device.is_on
assert device.speed == SPEED_HIGH

View File

@ -24,7 +24,7 @@ class MockLightValues(MockEntityValues):
def test_get_device_detects_dimmer(mock_openzwave):
"""Test get_device returns a color light."""
"""Test get_device returns a normal dimmer."""
node = MockNode()
value = MockValue(data=0, node=node)
values = MockLightValues(primary=value)

View File

@ -18,6 +18,23 @@ def test_get_device_component_mapping():
assert workaround.get_device_component_mapping(value) == 'binary_sensor'
def test_get_device_component_mapping_mti():
"""Test that component is returned."""
# GE Fan controller
node = MockNode(manufacturer_id='0063', product_type='4944',
product_id='3034')
value = MockValue(data=0, node=node,
command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL)
assert workaround.get_device_component_mapping(value) == 'fan'
# GE Dimmer
node = MockNode(manufacturer_id='0063', product_type='4944',
product_id='3031')
value = MockValue(data=0, node=node,
command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL)
assert workaround.get_device_component_mapping(value) is None
def test_get_device_no_mapping():
"""Test that no device mapping is returned."""
node = MockNode(manufacturer_id=' ')