mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add discovery support to mqtt climate component. (#15085)
* Add discovery support to mqtt climate component. * - Fix flake8 error (./homeassistant/components/climate/mqtt.py:130:1: D202 No blank lines allowed after function docstring) - Fix test error (since climate component was expected not to work - changed it to "lock" component, which also does not have MQTT discovery support yet) * Fix old assert statement to reflect new lock component usage * Change invalid MQTT discovery component type from 'lock' to 'timer', since contrary to the documentation the lock component is properly supported when using MQTT discovery. * Make configuration of invalid MQTT config component a single point of entry to prevent missing the assertion later in the code when changing. * Add new testcases to cover not-yet-covered code paths in https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/climate/mqtt.py
This commit is contained in:
parent
d3ceb9080c
commit
73034c933e
@ -129,6 +129,9 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up the MQTT climate devices."""
|
"""Set up the MQTT climate devices."""
|
||||||
|
if discovery_info is not None:
|
||||||
|
config = PLATFORM_SCHEMA(discovery_info)
|
||||||
|
|
||||||
template_keys = (
|
template_keys = (
|
||||||
CONF_POWER_STATE_TEMPLATE,
|
CONF_POWER_STATE_TEMPLATE,
|
||||||
CONF_MODE_STATE_TEMPLATE,
|
CONF_MODE_STATE_TEMPLATE,
|
||||||
|
@ -21,7 +21,7 @@ TOPIC_MATCHER = re.compile(
|
|||||||
|
|
||||||
SUPPORTED_COMPONENTS = [
|
SUPPORTED_COMPONENTS = [
|
||||||
'binary_sensor', 'camera', 'cover', 'fan',
|
'binary_sensor', 'camera', 'cover', 'fan',
|
||||||
'light', 'sensor', 'switch', 'lock']
|
'light', 'sensor', 'switch', 'lock', 'climate']
|
||||||
|
|
||||||
ALLOWED_PLATFORMS = {
|
ALLOWED_PLATFORMS = {
|
||||||
'binary_sensor': ['mqtt'],
|
'binary_sensor': ['mqtt'],
|
||||||
@ -32,6 +32,7 @@ ALLOWED_PLATFORMS = {
|
|||||||
'lock': ['mqtt'],
|
'lock': ['mqtt'],
|
||||||
'sensor': ['mqtt'],
|
'sensor': ['mqtt'],
|
||||||
'switch': ['mqtt'],
|
'switch': ['mqtt'],
|
||||||
|
'climate': ['mqtt'],
|
||||||
}
|
}
|
||||||
|
|
||||||
ALREADY_DISCOVERED = 'mqtt_discovered_components'
|
ALREADY_DISCOVERED = 'mqtt_discovered_components'
|
||||||
|
@ -137,6 +137,37 @@ class TestMQTTClimate(unittest.TestCase):
|
|||||||
self.assertEqual("cool", state.attributes.get('operation_mode'))
|
self.assertEqual("cool", state.attributes.get('operation_mode'))
|
||||||
self.assertEqual("cool", state.state)
|
self.assertEqual("cool", state.state)
|
||||||
|
|
||||||
|
def test_set_operation_with_power_command(self):
|
||||||
|
"""Test setting of new operation mode with power command enabled."""
|
||||||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||||
|
config['climate']['power_command_topic'] = 'power-command'
|
||||||
|
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||||
|
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
self.assertEqual("off", state.attributes.get('operation_mode'))
|
||||||
|
self.assertEqual("off", state.state)
|
||||||
|
climate.set_operation_mode(self.hass, "on", ENTITY_CLIMATE)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
self.assertEqual("on", state.attributes.get('operation_mode'))
|
||||||
|
self.assertEqual("on", state.state)
|
||||||
|
self.mock_publish.async_publish.assert_has_calls([
|
||||||
|
unittest.mock.call('power-command', 'ON', 0, False),
|
||||||
|
unittest.mock.call('mode-topic', 'on', 0, False)
|
||||||
|
])
|
||||||
|
self.mock_publish.async_publish.reset_mock()
|
||||||
|
|
||||||
|
climate.set_operation_mode(self.hass, "off", ENTITY_CLIMATE)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
self.assertEqual("off", state.attributes.get('operation_mode'))
|
||||||
|
self.assertEqual("off", state.state)
|
||||||
|
self.mock_publish.async_publish.assert_has_calls([
|
||||||
|
unittest.mock.call('power-command', 'OFF', 0, False),
|
||||||
|
unittest.mock.call('mode-topic', 'off', 0, False)
|
||||||
|
])
|
||||||
|
self.mock_publish.async_publish.reset_mock()
|
||||||
|
|
||||||
def test_set_fan_mode_bad_attr(self):
|
def test_set_fan_mode_bad_attr(self):
|
||||||
"""Test setting fan mode without required attribute."""
|
"""Test setting fan mode without required attribute."""
|
||||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||||
@ -241,6 +272,8 @@ class TestMQTTClimate(unittest.TestCase):
|
|||||||
self.assertEqual(21, state.attributes.get('temperature'))
|
self.assertEqual(21, state.attributes.get('temperature'))
|
||||||
climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE)
|
climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
self.assertEqual('heat', state.attributes.get('operation_mode'))
|
||||||
self.mock_publish.async_publish.assert_called_once_with(
|
self.mock_publish.async_publish.assert_called_once_with(
|
||||||
'mode-topic', 'heat', 0, False)
|
'mode-topic', 'heat', 0, False)
|
||||||
self.mock_publish.async_publish.reset_mock()
|
self.mock_publish.async_publish.reset_mock()
|
||||||
@ -252,6 +285,21 @@ class TestMQTTClimate(unittest.TestCase):
|
|||||||
self.mock_publish.async_publish.assert_called_once_with(
|
self.mock_publish.async_publish.assert_called_once_with(
|
||||||
'temperature-topic', 47, 0, False)
|
'temperature-topic', 47, 0, False)
|
||||||
|
|
||||||
|
# also test directly supplying the operation mode to set_temperature
|
||||||
|
self.mock_publish.async_publish.reset_mock()
|
||||||
|
climate.set_temperature(self.hass, temperature=21,
|
||||||
|
operation_mode="cool",
|
||||||
|
entity_id=ENTITY_CLIMATE)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
self.assertEqual('cool', state.attributes.get('operation_mode'))
|
||||||
|
self.assertEqual(21, state.attributes.get('temperature'))
|
||||||
|
self.mock_publish.async_publish.assert_has_calls([
|
||||||
|
unittest.mock.call('mode-topic', 'cool', 0, False),
|
||||||
|
unittest.mock.call('temperature-topic', 21, 0, False)
|
||||||
|
])
|
||||||
|
self.mock_publish.async_publish.reset_mock()
|
||||||
|
|
||||||
def test_set_target_temperature_pessimistic(self):
|
def test_set_target_temperature_pessimistic(self):
|
||||||
"""Test setting the target temperature."""
|
"""Test setting the target temperature."""
|
||||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||||
@ -508,13 +556,28 @@ class TestMQTTClimate(unittest.TestCase):
|
|||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
self.assertEqual("on", state.attributes.get('swing_mode'))
|
self.assertEqual("on", state.attributes.get('swing_mode'))
|
||||||
|
|
||||||
# Temperature
|
# Temperature - with valid value
|
||||||
self.assertEqual(21, state.attributes.get('temperature'))
|
self.assertEqual(21, state.attributes.get('temperature'))
|
||||||
fire_mqtt_message(self.hass, 'temperature-state', '"1031"')
|
fire_mqtt_message(self.hass, 'temperature-state', '"1031"')
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
self.assertEqual(1031, state.attributes.get('temperature'))
|
self.assertEqual(1031, state.attributes.get('temperature'))
|
||||||
|
|
||||||
|
# Temperature - with invalid value
|
||||||
|
with self.assertLogs(level='ERROR') as log:
|
||||||
|
fire_mqtt_message(self.hass, 'temperature-state', '"-INVALID-"')
|
||||||
|
self.hass.block_till_done()
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
# make sure, the invalid value gets logged...
|
||||||
|
self.assertEqual(len(log.output), 1)
|
||||||
|
self.assertEqual(len(log.records), 1)
|
||||||
|
self.assertIn(
|
||||||
|
"Could not parse temperature from -INVALID-",
|
||||||
|
log.output[0]
|
||||||
|
)
|
||||||
|
# ... but the actual value stays unchanged.
|
||||||
|
self.assertEqual(1031, state.attributes.get('temperature'))
|
||||||
|
|
||||||
# Away Mode
|
# Away Mode
|
||||||
self.assertEqual('off', state.attributes.get('away_mode'))
|
self.assertEqual('off', state.attributes.get('away_mode'))
|
||||||
fire_mqtt_message(self.hass, 'away-state', '"ON"')
|
fire_mqtt_message(self.hass, 'away-state', '"ON"')
|
||||||
@ -522,6 +585,17 @@ class TestMQTTClimate(unittest.TestCase):
|
|||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
self.assertEqual('on', state.attributes.get('away_mode'))
|
self.assertEqual('on', state.attributes.get('away_mode'))
|
||||||
|
|
||||||
|
# Away Mode with JSON values
|
||||||
|
fire_mqtt_message(self.hass, 'away-state', 'false')
|
||||||
|
self.hass.block_till_done()
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
self.assertEqual('off', state.attributes.get('away_mode'))
|
||||||
|
|
||||||
|
fire_mqtt_message(self.hass, 'away-state', 'true')
|
||||||
|
self.hass.block_till_done()
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
self.assertEqual('on', state.attributes.get('away_mode'))
|
||||||
|
|
||||||
# Hold Mode
|
# Hold Mode
|
||||||
self.assertEqual(None, state.attributes.get('hold_mode'))
|
self.assertEqual(None, state.attributes.get('hold_mode'))
|
||||||
fire_mqtt_message(self.hass, 'hold-state', """
|
fire_mqtt_message(self.hass, 'hold-state', """
|
||||||
@ -538,6 +612,12 @@ class TestMQTTClimate(unittest.TestCase):
|
|||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
self.assertEqual('on', state.attributes.get('aux_heat'))
|
self.assertEqual('on', state.attributes.get('aux_heat'))
|
||||||
|
|
||||||
|
# anything other than 'switchmeon' should turn Aux mode off
|
||||||
|
fire_mqtt_message(self.hass, 'aux-state', 'somerandomstring')
|
||||||
|
self.hass.block_till_done()
|
||||||
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
|
self.assertEqual('off', state.attributes.get('aux_heat'))
|
||||||
|
|
||||||
# Current temperature
|
# Current temperature
|
||||||
fire_mqtt_message(self.hass, 'current-temperature', '"74656"')
|
fire_mqtt_message(self.hass, 'current-temperature', '"74656"')
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
|
@ -52,12 +52,21 @@ def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog):
|
def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog):
|
||||||
"""Test for a valid component."""
|
"""Test for a valid component."""
|
||||||
|
invalid_component = "timer"
|
||||||
|
|
||||||
mock_load_platform.return_value = mock_coro()
|
mock_load_platform.return_value = mock_coro()
|
||||||
yield from async_start(hass, 'homeassistant', {})
|
yield from async_start(hass, 'homeassistant', {})
|
||||||
|
|
||||||
async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', '{}')
|
async_fire_mqtt_message(hass, 'homeassistant/{}/bla/config'.format(
|
||||||
|
invalid_component
|
||||||
|
), '{}')
|
||||||
|
|
||||||
yield from hass.async_block_till_done()
|
yield from hass.async_block_till_done()
|
||||||
assert 'Component climate is not supported' in caplog.text
|
|
||||||
|
assert 'Component {} is not supported'.format(
|
||||||
|
invalid_component
|
||||||
|
) in caplog.text
|
||||||
|
|
||||||
assert not mock_load_platform.called
|
assert not mock_load_platform.called
|
||||||
|
|
||||||
|
|
||||||
@ -94,6 +103,27 @@ def test_discover_fan(hass, mqtt_mock, caplog):
|
|||||||
assert ('fan', 'bla') in hass.data[ALREADY_DISCOVERED]
|
assert ('fan', 'bla') in hass.data[ALREADY_DISCOVERED]
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_discover_climate(hass, mqtt_mock, caplog):
|
||||||
|
"""Test discovering an MQTT climate component."""
|
||||||
|
yield from async_start(hass, 'homeassistant', {})
|
||||||
|
|
||||||
|
data = (
|
||||||
|
'{ "name": "ClimateTest",'
|
||||||
|
' "current_temperature_topic": "climate/bla/current_temp",'
|
||||||
|
' "temperature_command_topic": "climate/bla/target_temp" }'
|
||||||
|
)
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data)
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('climate.ClimateTest')
|
||||||
|
|
||||||
|
assert state is not None
|
||||||
|
assert state.name == 'ClimateTest'
|
||||||
|
assert ('climate', 'bla') in hass.data[ALREADY_DISCOVERED]
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_discovery_incl_nodeid(hass, mqtt_mock, caplog):
|
def test_discovery_incl_nodeid(hass, mqtt_mock, caplog):
|
||||||
"""Test sending in correct JSON with optional node_id included."""
|
"""Test sending in correct JSON with optional node_id included."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user