Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions homeassistant/components/cover/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
from homeassistant.components.cover import (
CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT,
SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION,
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION)
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION,
ATTR_POSITION)
from homeassistant.exceptions import TemplateError
from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
STATE_CLOSED, STATE_UNKNOWN)
Expand All @@ -29,6 +31,8 @@

CONF_TILT_COMMAND_TOPIC = 'tilt_command_topic'
CONF_TILT_STATUS_TOPIC = 'tilt_status_topic'
CONF_POSITION_TOPIC = 'set_position_topic'
CONF_SET_POSITION_TEMPLATE = 'set_position_template'

CONF_PAYLOAD_OPEN = 'payload_open'
CONF_PAYLOAD_CLOSE = 'payload_close'
Expand All @@ -55,10 +59,17 @@
DEFAULT_TILT_OPTIMISTIC = False
DEFAULT_TILT_INVERT_STATE = False

OPEN_CLOSE_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP)
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
SUPPORT_SET_TILT_POSITION)

PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_COMMAND_TOPIC, default=None): valid_publish_topic,
vol.Optional(CONF_POSITION_TOPIC, default=None): valid_publish_topic,
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
Expand Down Expand Up @@ -89,6 +100,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
value_template.hass = hass

async_add_devices([MqttCover(
hass,
config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC),
Expand All @@ -109,18 +121,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_TILT_MAX),
config.get(CONF_TILT_STATE_OPTIMISTIC),
config.get(CONF_TILT_INVERT_STATE),
config.get(CONF_POSITION_TOPIC),
config.get(CONF_SET_POSITION_TEMPLATE),
)])


class MqttCover(CoverDevice):
"""Representation of a cover that can be controlled using MQTT."""

def __init__(self, name, state_topic, command_topic, tilt_command_topic,
def __init__(self, hass, name, state_topic, command_topic,
tilt_command_topic,
tilt_status_topic, qos, retain, state_open, state_closed,
payload_open, payload_close, payload_stop,
optimistic, value_template, tilt_open_position,
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
tilt_invert):
tilt_invert, position_topic, set_position_template):
"""Initialize the cover."""
self._position = None
self._state = None
Expand All @@ -145,6 +160,10 @@ def __init__(self, name, state_topic, command_topic, tilt_command_topic,
self._tilt_max = tilt_max
self._tilt_optimistic = tilt_optimistic
self._tilt_invert = tilt_invert
self._position_topic = position_topic
self._set_position_template = set_position_template
if set_position_template is not None:
self._set_position_template.hass = hass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put this logic in async_setup_platform so we don't need to pass hass in? Let's make it look the same as value_template.


@asyncio.coroutine
def async_added_to_hass(self):
Expand Down Expand Up @@ -233,7 +252,12 @@ def current_cover_tilt_position(self):
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
supported_features = 0
if self._command_topic is not None:
supported_features = OPEN_CLOSE_FEATURES

if self._position_topic is not None:
supported_features |= SUPPORT_SET_POSITION

if self.current_cover_position is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should now be removed since we're using self._position_topic to define SUPPORT_SET_POSITION. It probably shouldn't have been set before.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean remove the block starting at line 262?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right.

supported_features |= SUPPORT_SET_POSITION
Expand Down Expand Up @@ -315,6 +339,21 @@ def async_set_cover_tilt_position(self, **kwargs):
mqtt.async_publish(self.hass, self._tilt_command_topic,
level, self._qos, self._retain)

@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
if self._set_position_template is not None:
try:
position = self._set_position_template.async_render()

@emlove emlove May 31, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this to async_render(**kwargs). This will make the requested position from the service available to the template. Should also be noted in the docs.

except TemplateError as ex:
_LOGGER.error(ex)
self._state = None

mqtt.async_publish(self.hass, self._position_topic,
position, self._qos, self._retain)

def find_percentage_in_range(self, position):
"""Find the 0-100% value within the specified range."""
# the range of motion as defined by the min max values
Expand Down
154 changes: 138 additions & 16 deletions tests/components/cover/test_mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ def test_current_cover_position(self):
'cover.test').attributes
self.assertFalse('current_position' in state_attributes_dict)
self.assertFalse('current_tilt_position' in state_attributes_dict)
self.assertFalse(4 & self.hass.states.get(
'cover.test').attributes['supported_features'] == 4)

fire_mqtt_message(self.hass, 'state-topic', '0')
self.hass.block_till_done()
Expand All @@ -240,6 +242,126 @@ def test_current_cover_position(self):
'cover.test').attributes['current_position']
self.assertEqual(50, current_cover_position)

def test_set_cover_position(self):
"""Test setting cover position."""
self.assertTrue(setup_component(self.hass, cover.DOMAIN, {
cover.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'state-topic',
'command_topic': 'command-topic',
'set_position_topic': 'position-topic',
'payload_open': 'OPEN',
'payload_close': 'CLOSE',
'payload_stop': 'STOP'
}
}))

state_attributes_dict = self.hass.states.get(
'cover.test').attributes
self.assertFalse('current_position' in state_attributes_dict)
self.assertFalse('current_tilt_position' in state_attributes_dict)

self.assertTrue(4 & self.hass.states.get(
'cover.test').attributes['supported_features'] == 4)

fire_mqtt_message(self.hass, 'state-topic', '22')
self.hass.block_till_done()
state_attributes_dict = self.hass.states.get(
'cover.test').attributes
self.assertTrue('current_position' in state_attributes_dict)
self.assertFalse('current_tilt_position' in state_attributes_dict)
current_cover_position = self.hass.states.get(
'cover.test').attributes['current_position']
self.assertEqual(22, current_cover_position)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line contains whitespace

def test_set_position_templated(self):
"""Test setting cover position via template."""
self.assertTrue(setup_component(self.hass, cover.DOMAIN, {
cover.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'state-topic',
'command_topic': 'command-topic',
'set_position_topic': 'position-topic',
'set_position_template': '{{100-62}}',
'payload_open': 'OPEN',
'payload_close': 'CLOSE',
'payload_stop': 'STOP'
}
}))

cover.set_cover_position(self.hass, 100, 'cover.test')
self.hass.block_till_done()

self.assertEqual(('position-topic', '38', 0, False),
self.mock_publish.mock_calls[-2][1])

def test_set_position_untemplated(self):
"""Test setting cover position via template."""
self.assertTrue(setup_component(self.hass, cover.DOMAIN, {
cover.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'state-topic',
'command_topic': 'command-topic',
'set_position_topic': 'position-topic',
'payload_open': 'OPEN',
'payload_close': 'CLOSE',
'payload_stop': 'STOP'
}
}))

cover.set_cover_position(self.hass, 62, 'cover.test')
self.hass.block_till_done()

self.assertEqual(('position-topic', 62, 0, False),
self.mock_publish.mock_calls[-2][1])


def test_no_command_topic(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too many blank lines (2)

"""Test with no command topic."""
self.assertTrue(setup_component(self.hass, cover.DOMAIN, {
cover.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'qos': 0,
'payload_open': 'OPEN',
'payload_close': 'CLOSE',
'payload_stop': 'STOP',
'tilt_command_topic': 'tilt-command',
'tilt_status_topic': 'tilt-status'
}
}))

state_attributes_dict = self.hass.states.get(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local variable 'state_attributes_dict' is assigned to but never used

'cover.test').attributes

self.assertEqual(240, self.hass.states.get(
'cover.test').attributes['supported_features'])

def test_with_command_topic_and_tilt(self):
"""Test with command topic and tilt config."""
self.assertTrue(setup_component(self.hass, cover.DOMAIN, {
cover.DOMAIN: {
'command_topic': 'test',
'platform': 'mqtt',
'name': 'test',
'qos': 0,
'payload_open': 'OPEN',
'payload_close': 'CLOSE',
'payload_stop': 'STOP',
'tilt_command_topic': 'tilt-command',
'tilt_status_topic': 'tilt-status'
}
}))

state_attributes_dict = self.hass.states.get(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local variable 'state_attributes_dict' is assigned to but never used

'cover.test').attributes

self.assertEqual(251, self.hass.states.get(
'cover.test').attributes['supported_features'])

def test_tilt_defaults(self):
"""Test the defaults."""
self.assertTrue(setup_component(self.hass, cover.DOMAIN, {
Expand Down Expand Up @@ -455,71 +577,71 @@ def test_tilt_position_altered_range(self):
def test_find_percentage_in_range_defaults(self):
"""Test find percentage in range with default range."""
mqtt_cover = MqttCover(
'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
None, 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None,
100, 0, 0, 100, False, False)
100, 0, 0, 100, False, False, None, None)

self.assertEqual(44, mqtt_cover.find_percentage_in_range(44))

def test_find_percentage_in_range_altered(self):
"""Test find percentage in range with altered range."""
mqtt_cover = MqttCover(
'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
None, 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None,
180, 80, 80, 180, False, False)
180, 80, 80, 180, False, False, None, None)

self.assertEqual(40, mqtt_cover.find_percentage_in_range(120))

def test_find_percentage_in_range_defaults_inverted(self):
"""Test find percentage in range with default range but inverted."""
mqtt_cover = MqttCover(
'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
None, 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None,
100, 0, 0, 100, False, True)
100, 0, 0, 100, False, True, None, None)

self.assertEqual(56, mqtt_cover.find_percentage_in_range(44))

def test_find_percentage_in_range_altered_inverted(self):
"""Test find percentage in range with altered range and inverted."""
mqtt_cover = MqttCover(
'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
None, 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None,
180, 80, 80, 180, False, True)
180, 80, 80, 180, False, True, None, None)

self.assertEqual(60, mqtt_cover.find_percentage_in_range(120))

def test_find_in_range_defaults(self):
"""Test find in range with default range."""
mqtt_cover = MqttCover(
'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
None, 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None,
100, 0, 0, 100, False, False)
100, 0, 0, 100, False, False, None, None)

self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44))

def test_find_in_range_altered(self):
"""Test find in range with altered range."""
mqtt_cover = MqttCover(
'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
None, 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None,
180, 80, 80, 180, False, False)
180, 80, 80, 180, False, False, None, None)

self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40))

def test_find_in_range_defaults_inverted(self):
"""Test find in range with default range but inverted."""
mqtt_cover = MqttCover(
'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
None, 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None,
100, 0, 0, 100, False, True)
100, 0, 0, 100, False, True, None, None)

self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56))

def test_find_in_range_altered_inverted(self):
"""Test find in range with altered range and inverted."""
mqtt_cover = MqttCover(
'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
None, 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None,
180, 80, 80, 180, False, True)
180, 80, 80, 180, False, True, None, None)

self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60))