diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 829c3748d2f..9d97851b5b4 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -40,6 +40,7 @@ CONF_TILT_OPEN_POSITION = 'tilt_opened_value' CONF_TILT_MIN = 'tilt_min' CONF_TILT_MAX = 'tilt_max' CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' +CONF_TILT_INVERT_STATE = "tilt_invert_state" DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' @@ -52,6 +53,7 @@ DEFAULT_TILT_OPEN_POSITION = 100 DEFAULT_TILT_MIN = 0 DEFAULT_TILT_MAX = 100 DEFAULT_TILT_OPTIMISTIC = False +DEFAULT_TILT_INVERT_STATE = False TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT | SUPPORT_SET_TILT_POSITION) @@ -74,6 +76,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, vol.Optional(CONF_TILT_STATE_OPTIMISTIC, default=DEFAULT_TILT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_TILT_INVERT_STATE, + default=DEFAULT_TILT_INVERT_STATE): cv.boolean, }) @@ -104,6 +108,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_TILT_MIN), config.get(CONF_TILT_MAX), config.get(CONF_TILT_STATE_OPTIMISTIC), + config.get(CONF_TILT_INVERT_STATE), )]) @@ -114,7 +119,8 @@ class MqttCover(CoverDevice): 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_closed_position, tilt_min, tilt_max, tilt_optimistic, + tilt_invert): """Initialize the cover.""" self._position = None self._state = None @@ -138,6 +144,7 @@ class MqttCover(CoverDevice): self._tilt_min = tilt_min self._tilt_max = tilt_max self._tilt_optimistic = tilt_optimistic + self._tilt_invert = tilt_invert @asyncio.coroutine def async_added_to_hass(self): @@ -150,8 +157,8 @@ class MqttCover(CoverDevice): """Handle tilt updates.""" if (payload.isnumeric() and self._tilt_min <= int(payload) <= self._tilt_max): - tilt_range = self._tilt_max - self._tilt_min - level = round(float(payload) / tilt_range * 100.0) + + level = self.find_percentage_in_range(float(payload)) self._tilt_value = level self.hass.async_add_job(self.async_update_ha_state()) @@ -278,7 +285,8 @@ class MqttCover(CoverDevice): def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" mqtt.async_publish(self.hass, self._tilt_command_topic, - self._tilt_open_position, self._qos, self._retain) + self._tilt_open_position, self._qos, + self._retain) if self._tilt_optimistic: self._tilt_value = self._tilt_open_position self.hass.async_add_job(self.async_update_ha_state()) @@ -287,7 +295,8 @@ class MqttCover(CoverDevice): def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" mqtt.async_publish(self.hass, self._tilt_command_topic, - self._tilt_closed_position, self._qos, self._retain) + self._tilt_closed_position, self._qos, + self._retain) if self._tilt_optimistic: self._tilt_value = self._tilt_closed_position self.hass.async_add_job(self.async_update_ha_state()) @@ -301,9 +310,36 @@ class MqttCover(CoverDevice): position = float(kwargs[ATTR_TILT_POSITION]) # The position needs to be between min and max - tilt_range = self._tilt_max - self._tilt_min - percentage = position / 100.0 - level = round(tilt_range * percentage) + level = self.find_in_range_from_percent(position) mqtt.async_publish(self.hass, self._tilt_command_topic, level, 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 + tilt_range = self._tilt_max - self._tilt_min + # offset to be zero based + offset_position = position - self._tilt_min + # the percentage value within the range + position_percentage = float(offset_position) / tilt_range * 100.0 + if self._tilt_invert: + return 100 - position_percentage + else: + return position_percentage + + def find_in_range_from_percent(self, percentage): + """Find the adjusted value for 0-100% within the specified range.""" + # if the range is 80-180 and the percentage is 90 + # this method would determine the value to send on the topic + # by offsetting the max and min, getting the percentage value and + # returning the offset + offset = self._tilt_min + tilt_range = self._tilt_max - self._tilt_min + + position = round(tilt_range * (percentage / 100.0)) + position += offset + + if self._tilt_invert: + position = self._tilt_max - position + offset + return position diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index b2dcf8e175d..e685a51f56c 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -4,6 +4,7 @@ import unittest from homeassistant.setup import setup_component from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN import homeassistant.components.cover as cover +from homeassistant.components.cover.mqtt import MqttCover from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) @@ -450,3 +451,75 @@ class TestCoverMQTT(unittest.TestCase): self.assertEqual(('tilt-command-topic', 25, 0, False), self.mock_publish.mock_calls[-2][1]) + + 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, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, False) + + 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, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, False) + + 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, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, True) + + 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, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, True) + + 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, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, False) + + 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, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, False) + + 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, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, True) + + 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, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, True) + + self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60))