diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 5397daeb784..2878717d91b 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -129,6 +129,9 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT climate devices.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + template_keys = ( CONF_POWER_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d5a3b4a2efb..3916714b8d1 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -21,7 +21,7 @@ TOPIC_MATCHER = re.compile( SUPPORTED_COMPONENTS = [ 'binary_sensor', 'camera', 'cover', 'fan', - 'light', 'sensor', 'switch', 'lock'] + 'light', 'sensor', 'switch', 'lock', 'climate'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], @@ -32,6 +32,7 @@ ALLOWED_PLATFORMS = { 'lock': ['mqtt'], 'sensor': ['mqtt'], 'switch': ['mqtt'], + 'climate': ['mqtt'], } ALREADY_DISCOVERED = 'mqtt_discovered_components' diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 255d482d584..5db77331cd4 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -137,6 +137,37 @@ class TestMQTTClimate(unittest.TestCase): self.assertEqual("cool", state.attributes.get('operation_mode')) 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): """Test setting fan mode without required attribute.""" assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) @@ -241,6 +272,8 @@ class TestMQTTClimate(unittest.TestCase): self.assertEqual(21, state.attributes.get('temperature')) climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) 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( 'mode-topic', 'heat', 0, False) self.mock_publish.async_publish.reset_mock() @@ -252,6 +285,21 @@ class TestMQTTClimate(unittest.TestCase): self.mock_publish.async_publish.assert_called_once_with( '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): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -508,13 +556,28 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("on", state.attributes.get('swing_mode')) - # Temperature + # Temperature - with valid value self.assertEqual(21, state.attributes.get('temperature')) fire_mqtt_message(self.hass, 'temperature-state', '"1031"') self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) 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 self.assertEqual('off', state.attributes.get('away_mode')) fire_mqtt_message(self.hass, 'away-state', '"ON"') @@ -522,6 +585,17 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) 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 self.assertEqual(None, state.attributes.get('hold_mode')) fire_mqtt_message(self.hass, 'hold-state', """ @@ -538,6 +612,12 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) 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 fire_mqtt_message(self.hass, 'current-temperature', '"74656"') self.hass.block_till_done() diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 1dd29909ffd..ed6c77f676c 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -52,12 +52,21 @@ def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): @asyncio.coroutine def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): """Test for a valid component.""" + invalid_component = "timer" + mock_load_platform.return_value = mock_coro() 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() - 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 @@ -94,6 +103,27 @@ def test_discover_fan(hass, mqtt_mock, caplog): 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 def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): """Test sending in correct JSON with optional node_id included."""