diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/fan/zwave.py new file mode 100644 index 00000000000..fe01ae5f3a4 --- /dev/null +++ b/homeassistant/components/fan/zwave.py @@ -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 diff --git a/homeassistant/components/zwave/workaround.py b/homeassistant/components/zwave/workaround.py index 9d2433b7118..0a882a093c6 100644 --- a/homeassistant/components/zwave/workaround.py +++ b/homeassistant/components/zwave/workaround.py @@ -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 diff --git a/tests/components/fan/test_zwave.py b/tests/components/fan/test_zwave.py new file mode 100644 index 00000000000..b7d7e497c03 --- /dev/null +++ b/tests/components/fan/test_zwave.py @@ -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 diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py index 9629744bc16..a260d160bb5 100644 --- a/tests/components/light/test_zwave.py +++ b/tests/components/light/test_zwave.py @@ -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) diff --git a/tests/components/zwave/test_workaround.py b/tests/components/zwave/test_workaround.py index de901ad4dc1..ebc21692e85 100644 --- a/tests/components/zwave/test_workaround.py +++ b/tests/components/zwave/test_workaround.py @@ -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=' ')