mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 10:47:10 +00:00
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:
parent
6899c7b6f7
commit
9e9705d6b2
86
homeassistant/components/fan/zwave.py
Normal file
86
homeassistant/components/fan/zwave.py
Normal 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
|
@ -3,22 +3,30 @@ from . import const
|
|||||||
|
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
FIBARO = 0x010f
|
FIBARO = 0x010f
|
||||||
|
GE = 0x0063
|
||||||
PHILIO = 0x013c
|
PHILIO = 0x013c
|
||||||
|
SOMFY = 0x0047
|
||||||
WENZHOU = 0x0118
|
WENZHOU = 0x0118
|
||||||
SOMFY = 0x47
|
VIZIA = 0x001D
|
||||||
|
|
||||||
# Product IDs
|
# Product IDs
|
||||||
|
GE_FAN_CONTROLLER_12730 = 0x3034
|
||||||
|
GE_FAN_CONTROLLER_14287 = 0x3131
|
||||||
|
JASCO_FAN_CONTROLLER_14314 = 0x3138
|
||||||
PHILIO_SLIM_SENSOR = 0x0002
|
PHILIO_SLIM_SENSOR = 0x0002
|
||||||
PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000d
|
PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000d
|
||||||
PHILIO_PAN07 = 0x0005
|
PHILIO_PAN07 = 0x0005
|
||||||
|
VIZIA_FAN_CONTROLLER_VRF01 = 0x0334
|
||||||
|
|
||||||
# Product Types
|
# Product Types
|
||||||
FGFS101_FLOOD_SENSOR_TYPE = 0x0b00
|
FGFS101_FLOOD_SENSOR_TYPE = 0x0b00
|
||||||
FGRM222_SHUTTER2 = 0x0301
|
FGRM222_SHUTTER2 = 0x0301
|
||||||
FGR222_SHUTTER2 = 0x0302
|
FGR222_SHUTTER2 = 0x0302
|
||||||
|
GE_DIMMER = 0x4944
|
||||||
PHILIO_SWITCH = 0x0001
|
PHILIO_SWITCH = 0x0001
|
||||||
PHILIO_SENSOR = 0x0002
|
PHILIO_SENSOR = 0x0002
|
||||||
SOMFY_ZRTSI = 0x5a52
|
SOMFY_ZRTSI = 0x5a52
|
||||||
|
VIZIA_DIMMER = 0x1001
|
||||||
|
|
||||||
# Mapping devices
|
# Mapping devices
|
||||||
PHILIO_SLIM_SENSOR_MOTION_MTII = (PHILIO, PHILIO_SENSOR, PHILIO_SLIM_SENSOR, 0)
|
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,
|
SOMFY_ZRTSI_CONTROLLER_MT: WORKAROUND_NO_POSITION,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Component mapping devices
|
# Component mapping devices
|
||||||
FIBARO_FGFS101_SENSOR_ALARM = (
|
FIBARO_FGFS101_SENSOR_ALARM = (
|
||||||
FIBARO, FGFS101_FLOOD_SENSOR_TYPE, const.COMMAND_CLASS_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, FGRM222_SHUTTER2, const.COMMAND_CLASS_SWITCH_BINARY)
|
||||||
FIBARO_FGR222_BINARY = (
|
FIBARO_FGR222_BINARY = (
|
||||||
FIBARO, FGR222_SHUTTER2, const.COMMAND_CLASS_SWITCH_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
|
# List of component workarounds by
|
||||||
# (manufacturer_id, product_type, command_class)
|
# (manufacturer_id, product_type, command_class)
|
||||||
@ -70,6 +89,15 @@ DEVICE_COMPONENT_MAPPING = {
|
|||||||
FIBARO_FGR222_BINARY: WORKAROUND_IGNORE,
|
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):
|
def get_device_component_mapping(value):
|
||||||
"""Get mapping of value to another component."""
|
"""Get mapping of value to another component."""
|
||||||
@ -77,8 +105,16 @@ def get_device_component_mapping(value):
|
|||||||
value.node.product_type.strip()):
|
value.node.product_type.strip()):
|
||||||
manufacturer_id = int(value.node.manufacturer_id, 16)
|
manufacturer_id = int(value.node.manufacturer_id, 16)
|
||||||
product_type = int(value.node.product_type, 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))
|
(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
|
return None
|
||||||
|
|
||||||
|
117
tests/components/fan/test_zwave.py
Normal file
117
tests/components/fan/test_zwave.py
Normal 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
|
@ -24,7 +24,7 @@ class MockLightValues(MockEntityValues):
|
|||||||
|
|
||||||
|
|
||||||
def test_get_device_detects_dimmer(mock_openzwave):
|
def test_get_device_detects_dimmer(mock_openzwave):
|
||||||
"""Test get_device returns a color light."""
|
"""Test get_device returns a normal dimmer."""
|
||||||
node = MockNode()
|
node = MockNode()
|
||||||
value = MockValue(data=0, node=node)
|
value = MockValue(data=0, node=node)
|
||||||
values = MockLightValues(primary=value)
|
values = MockLightValues(primary=value)
|
||||||
|
@ -18,6 +18,23 @@ def test_get_device_component_mapping():
|
|||||||
assert workaround.get_device_component_mapping(value) == 'binary_sensor'
|
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():
|
def test_get_device_no_mapping():
|
||||||
"""Test that no device mapping is returned."""
|
"""Test that no device mapping is returned."""
|
||||||
node = MockNode(manufacturer_id=' ')
|
node = MockNode(manufacturer_id=' ')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user