From 8cca2bb344c82f4dd9d61dfb0afb9def1862d264 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Mon, 4 Apr 2016 00:22:04 -0400 Subject: [PATCH 01/17] Config validation for MQTT --- homeassistant/components/mqtt/__init__.py | 39 +++++++++++++++-------- tests/common.py | 3 +- tests/components/mqtt/test_init.py | 11 ++++--- tests/components/mqtt/test_server.py | 18 +++++++---- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 927b75024ab..5bae39233ad 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -14,7 +14,6 @@ import voluptuous as vol from homeassistant.bootstrap import prepare_setup_platform from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError -import homeassistant.util as util from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.const import ( @@ -58,6 +57,25 @@ ATTR_RETAIN = 'retain' MAX_RECONNECT_WAIT = 300 # seconds +_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): + vol.All(vol.Coerce(int), vol.Range(min=15)), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.IsFile, + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): + [PROTOCOL_31, PROTOCOL_311], + vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA, + }), +}) + # Service call validation schema def mqtt_topic(value): @@ -136,8 +154,8 @@ def setup(hass, config): # pylint: disable=too-many-locals conf = config.get(DOMAIN, {}) - client_id = util.convert(conf.get(CONF_CLIENT_ID), str) - keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE) + client_id = conf.get(CONF_CLIENT_ID) + keepalive = conf.get(CONF_KEEPALIVE) broker_config = _setup_server(hass, config) @@ -151,16 +169,11 @@ def setup(hass, config): if CONF_BROKER in conf: broker = conf[CONF_BROKER] - port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) - username = util.convert(conf.get(CONF_USERNAME), str) - password = util.convert(conf.get(CONF_PASSWORD), str) - certificate = util.convert(conf.get(CONF_CERTIFICATE), str) - protocol = util.convert(conf.get(CONF_PROTOCOL), str, DEFAULT_PROTOCOL) - - if protocol not in (PROTOCOL_31, PROTOCOL_311): - _LOGGER.error('Invalid protocol specified: %s. Allowed values: %s, %s', - protocol, PROTOCOL_31, PROTOCOL_311) - return False + port = conf[CONF_PORT] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + certificate = conf.get(CONF_CERTIFICATE) + protocol = conf[CONF_PROTOCOL] # For cloudmqtt.com, secured connection, auto fill in certificate if certificate is None and 19999 < port < 30000 and \ diff --git a/tests/common.py b/tests/common.py index 8f0c5543c1c..1c8a2454c12 100644 --- a/tests/common.py +++ b/tests/common.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest import mock from homeassistant import core as ha, loader +from homeassistant.bootstrap import _setup_component from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, @@ -123,7 +124,7 @@ def mock_http_component(hass): @mock.patch('homeassistant.components.mqtt.MQTT') def mock_mqtt_component(hass, mock_mqtt): """Mock the MQTT component.""" - mqtt.setup(hass, { + _setup_component(hass, mqtt.DOMAIN, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'mock-broker', } diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index d57504562c1..f342284cdec 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4,6 +4,7 @@ import unittest from unittest import mock import socket +from homeassistant.bootstrap import _setup_component import homeassistant.components.mqtt as mqtt from homeassistant.const import ( EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, EVENT_HOMEASSISTANT_START, @@ -48,9 +49,11 @@ class TestMQTT(unittest.TestCase): """Test for setup failure if connection to broker is missing.""" with mock.patch('homeassistant.components.mqtt.MQTT', side_effect=socket.error()): - self.assertFalse(mqtt.setup(self.hass, {mqtt.DOMAIN: { - mqtt.CONF_BROKER: 'test-broker', - }})) + assert not _setup_component(self.hass, mqtt.DOMAIN, { + mqtt.DOMAIN: { + mqtt.CONF_BROKER: 'test-broker', + } + }) def test_publish_calls_service(self): """Test the publishing of call to services.""" @@ -211,7 +214,7 @@ class TestMQTTCallbacks(unittest.TestCase): # mock_mqtt_component(self.hass) with mock.patch('paho.mqtt.client.Client'): - mqtt.setup(self.hass, { + _setup_component(self.hass, mqtt.DOMAIN, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'mock-broker', } diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index d8710035916..1d0cd5d062e 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -1,6 +1,7 @@ """The tests for the MQTT component embedded server.""" from unittest.mock import MagicMock, patch +from homeassistant.bootstrap import _setup_component import homeassistant.components.mqtt as mqtt from tests.common import get_test_home_assistant @@ -27,15 +28,20 @@ class TestMQTT: password = 'super_secret' self.hass.config.api = MagicMock(api_password=password) - assert mqtt.setup(self.hass, {}) + assert _setup_component(self.hass, mqtt.DOMAIN, {}) assert mock_mqtt.called assert mock_mqtt.mock_calls[0][1][5] == 'homeassistant' assert mock_mqtt.mock_calls[0][1][6] == password - mock_mqtt.reset_mock() - + @patch('homeassistant.components.mqtt.MQTT') + @patch('asyncio.gather') + @patch('asyncio.new_event_loop') + def test_creating_config_no_http_pass(self, mock_new_loop, mock_gather, + mock_mqtt): + """Test if the MQTT server gets started and subscribe/publish msg.""" + self.hass.config.components.append('http') self.hass.config.api = MagicMock(api_password=None) - assert mqtt.setup(self.hass, {}) + assert _setup_component(self.hass, mqtt.DOMAIN, {}) assert mock_mqtt.called assert mock_mqtt.mock_calls[0][1][5] is None assert mock_mqtt.mock_calls[0][1][6] is None @@ -50,6 +56,6 @@ class TestMQTT: mock_gather.side_effect = BrokerException self.hass.config.api = MagicMock(api_password=None) - assert not mqtt.setup(self.hass, { - 'mqtt': {'embedded': {}} + assert not _setup_component(self.hass, mqtt.DOMAIN, { + mqtt.DOMAIN: {mqtt.CONF_EMBEDDED: {}} }) From 58ea589f99daa80e974eaa68e7616473d6b2b7be Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Mon, 4 Apr 2016 22:56:25 -0400 Subject: [PATCH 02/17] Fixes for mqtt config validation tests. --- tests/common.py | 1 - tests/components/mqtt/test_init.py | 5 +++-- tests/components/mqtt/test_server.py | 10 +++------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/common.py b/tests/common.py index 1c8a2454c12..18f827961d0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -129,7 +129,6 @@ def mock_mqtt_component(hass, mock_mqtt): mqtt.CONF_BROKER: 'mock-broker', } }) - hass.config.components.append(mqtt.DOMAIN) return mock_mqtt diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index f342284cdec..9c168b71a32 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -49,6 +49,7 @@ class TestMQTT(unittest.TestCase): """Test for setup failure if connection to broker is missing.""" with mock.patch('homeassistant.components.mqtt.MQTT', side_effect=socket.error()): + self.hass.config.components = [] assert not _setup_component(self.hass, mqtt.DOMAIN, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'test-broker', @@ -214,12 +215,12 @@ class TestMQTTCallbacks(unittest.TestCase): # mock_mqtt_component(self.hass) with mock.patch('paho.mqtt.client.Client'): - _setup_component(self.hass, mqtt.DOMAIN, { + self.hass.config.components = [] + assert _setup_component(self.hass, mqtt.DOMAIN, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'mock-broker', } }) - self.hass.config.components.append(mqtt.DOMAIN) def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 1d0cd5d062e..ab84c3c29f0 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -33,13 +33,9 @@ class TestMQTT: assert mock_mqtt.mock_calls[0][1][5] == 'homeassistant' assert mock_mqtt.mock_calls[0][1][6] == password - @patch('homeassistant.components.mqtt.MQTT') - @patch('asyncio.gather') - @patch('asyncio.new_event_loop') - def test_creating_config_no_http_pass(self, mock_new_loop, mock_gather, - mock_mqtt): - """Test if the MQTT server gets started and subscribe/publish msg.""" - self.hass.config.components.append('http') + mock_mqtt.reset_mock() + + self.hass.config.components = ['http'] self.hass.config.api = MagicMock(api_password=None) assert _setup_component(self.hass, mqtt.DOMAIN, {}) assert mock_mqtt.called From 2c119091dc40838e69da44516a59a6fedc36fa28 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 5 Apr 2016 00:21:31 -0400 Subject: [PATCH 03/17] Add MQTT schema validation functions for platform schemas. --- homeassistant/components/mqtt/__init__.py | 36 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 5bae39233ad..89112378c67 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -17,7 +17,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + CONF_PLATFORM, CONF_SCAN_INTERVAL) _LOGGER = logging.getLogger(__name__) @@ -39,6 +40,7 @@ CONF_USERNAME = 'username' CONF_PASSWORD = 'password' CONF_CERTIFICATE = 'certificate' CONF_PROTOCOL = 'protocol' +CONF_QOS = 'qos' PROTOCOL_31 = '3.1' PROTOCOL_311 = '3.1.1' @@ -52,11 +54,12 @@ DEFAULT_PROTOCOL = PROTOCOL_311 ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' ATTR_PAYLOAD_TEMPLATE = 'payload_template' -ATTR_QOS = 'qos' +ATTR_QOS = CONF_QOS ATTR_RETAIN = 'retain' MAX_RECONNECT_WAIT = 300 # seconds +_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) _HBMQTT_CONFIG_SCHEMA = vol.Schema(dict) CONFIG_SCHEMA = vol.Schema({ @@ -76,20 +79,37 @@ CONFIG_SCHEMA = vol.Schema({ }), }) +MQTT_BASE_PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): DOMAIN, + vol.Optional(CONF_SCAN_INTERVAL): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, +}) + # Service call validation schema -def mqtt_topic(value): - """Validate that we can publish using this MQTT topic.""" - if isinstance(value, str) and all(c not in value for c in '#+\0'): +def _mqtt_topic(value, invalid_chars='\0'): + """Validate MQTT topic.""" + if isinstance(value, str) and all(c not in value for c in invalid_chars): return vol.Length(min=1, max=65535)(value) raise vol.Invalid('Invalid MQTT topic name') + +def valid_publish_topic(value): + """Validate that we can publish using this MQTT topic.""" + return _mqtt_topic(value, invalid_chars='#+\0') + + +def valid_subscribe_topic(value): + """Validate that we can subscribe using this MQTT topic.""" + return _mqtt_topic(value) + + MQTT_PUBLISH_SCHEMA = vol.Schema({ - vol.Required(ATTR_TOPIC): mqtt_topic, + vol.Required(ATTR_TOPIC): valid_publish_topic, vol.Exclusive(ATTR_PAYLOAD, 'payload'): object, vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, 'payload'): cv.string, - vol.Required(ATTR_QOS, default=DEFAULT_QOS): - vol.All(vol.Coerce(int), vol.In([0, 1, 2])), + vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, required=True) From 0bd4e15fcb6fb652e19b90d7fbc47b2be37d1172 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 5 Apr 2016 00:22:58 -0400 Subject: [PATCH 04/17] Config validation for MQTT alarm_control_panel platform. --- .../alarm_control_panel/__init__.py | 1 + .../components/alarm_control_panel/mqtt.py | 56 ++++++----- .../alarm_control_panel/test_mqtt.py | 92 +++++++++++-------- 3 files changed, 88 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index f70da3d54ec..e3bde441211 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 0e86e0df875..687de5de474 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -6,42 +6,54 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.mqtt as mqtt from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, + CONF_NAME) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "MQTT Alarm" -DEFAULT_QOS = 0 -DEFAULT_PAYLOAD_DISARM = "DISARM" -DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME" -DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY" - DEPENDENCIES = ['mqtt'] +CONF_STATE_TOPIC = 'state_topic' +CONF_COMMAND_TOPIC = 'command_topic' +CONF_PAYLOAD_DISARM = 'payload_disarm' +CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' +CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' +CONF_CODE = 'code' + +DEFAULT_NAME = "MQTT Alarm" +DEFAULT_DISARM = "DISARM" +DEFAULT_ARM_HOME = "ARM_HOME" +DEFAULT_ARM_AWAY = "ARM_AWAY" + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, + vol.Optional(CONF_CODE): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MQTT platform.""" - if config.get('state_topic') is None: - _LOGGER.error("Missing required variable: state_topic") - return False - - if config.get('command_topic') is None: - _LOGGER.error("Missing required variable: command_topic") - return False - add_devices([MqttAlarm( hass, - config.get('name', DEFAULT_NAME), - config.get('state_topic'), - config.get('command_topic'), - config.get('qos', DEFAULT_QOS), - config.get('payload_disarm', DEFAULT_PAYLOAD_DISARM), - config.get('payload_arm_home', DEFAULT_PAYLOAD_ARM_HOME), - config.get('payload_arm_away', DEFAULT_PAYLOAD_ARM_AWAY), + config[CONF_NAME], + config[CONF_STATE_TOPIC], + config[CONF_COMMAND_TOPIC], + config[mqtt.CONF_QOS], + config[CONF_PAYLOAD_DISARM], + config[CONF_PAYLOAD_ARM_HOME], + config[CONF_PAYLOAD_ARM_AWAY], config.get('code'))]) diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index c44a2f3a120..9941c500a75 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -1,7 +1,7 @@ """The tests the MQTT alarm control panel component.""" import unittest -from unittest.mock import patch +from homeassistant.bootstrap import _setup_component from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) @@ -25,37 +25,37 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): """Stop down stuff we started.""" self.hass.stop() - @patch('homeassistant.components.alarm_control_panel.mqtt._LOGGER.error') - def test_fail_setup_without_state_topic(self, mock_error): + def test_fail_setup_without_state_topic(self): """Test for failing with no state topic.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert not _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'command_topic': 'alarm/command' - }})) + } + }) - self.assertEqual(1, mock_error.call_count) - - @patch('homeassistant.components.alarm_control_panel.mqtt._LOGGER.error') - def test_fail_setup_without_command_topic(self, mock_error): + def test_fail_setup_without_command_topic(self): """Test failing with no command topic.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert not _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'state_topic': 'alarm/state' - }})) - - self.assertEqual(1, mock_error.call_count) + } + }) def test_update_state_via_state_topic(self): """Test updating with via state topic.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'alarm/state', 'command_topic': 'alarm/command', - }})) + } + }) entity_id = 'alarm_control_panel.test' @@ -71,13 +71,15 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): def test_ignore_update_state_if_unknown_via_state_topic(self): """Test ignoring updates via state topic.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'alarm/state', 'command_topic': 'alarm/command', - }})) + } + }) entity_id = 'alarm_control_panel.test' @@ -90,13 +92,15 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): def test_arm_home_publishes_mqtt(self): """Test publishing of MQTT messages while armed.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'alarm/state', 'command_topic': 'alarm/command', - }})) + } + }) alarm_control_panel.alarm_arm_home(self.hass) self.hass.pool.block_till_done() @@ -105,14 +109,16 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): def test_arm_home_not_publishes_mqtt_with_invalid_code(self): """Test not publishing of MQTT messages with invalid code.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'alarm/state', 'command_topic': 'alarm/command', 'code': '1234' - }})) + } + }) call_count = self.mock_publish.call_count alarm_control_panel.alarm_arm_home(self.hass, 'abcd') @@ -121,13 +127,15 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): def test_arm_away_publishes_mqtt(self): """Test publishing of MQTT messages while armed.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'alarm/state', 'command_topic': 'alarm/command', - }})) + } + }) alarm_control_panel.alarm_arm_away(self.hass) self.hass.pool.block_till_done() @@ -136,14 +144,16 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): def test_arm_away_not_publishes_mqtt_with_invalid_code(self): """Test not publishing of MQTT messages with invalid code.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'alarm/state', 'command_topic': 'alarm/command', 'code': '1234' - }})) + } + }) call_count = self.mock_publish.call_count alarm_control_panel.alarm_arm_away(self.hass, 'abcd') @@ -152,13 +162,15 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): def test_disarm_publishes_mqtt(self): """Test publishing of MQTT messages while disarmed.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'alarm/state', 'command_topic': 'alarm/command', - }})) + } + }) alarm_control_panel.alarm_disarm(self.hass) self.hass.pool.block_till_done() @@ -167,14 +179,16 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): def test_disarm_not_publishes_mqtt_with_invalid_code(self): """Test not publishing of MQTT messages with invalid code.""" - self.assertTrue(alarm_control_panel.setup(self.hass, { - 'alarm_control_panel': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'alarm/state', 'command_topic': 'alarm/command', 'code': '1234' - }})) + } + }) call_count = self.mock_publish.call_count alarm_control_panel.alarm_disarm(self.hass, 'abcd') From 287f0f4f688a823255bcc36b97dc435a230273a8 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 5 Apr 2016 00:53:58 -0400 Subject: [PATCH 05/17] Config validation for MQTT binary_sensor platform. --- .../components/binary_sensor/mqtt.py | 47 +++++++++++-------- tests/components/binary_sensor/test_mqtt.py | 22 +++++---- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 88cbceccc45..15b17066ce5 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -6,43 +6,52 @@ https://home-assistant.io/components/binary_sensor.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.mqtt as mqtt from homeassistant.components.binary_sensor import (BinarySensorDevice, SENSOR_CLASSES) -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mqtt'] + +CONF_STATE_TOPIC = 'state_topic' +CONF_SENSOR_CLASS = 'sensor_class' +CONF_PAYLOAD_ON = 'payload_on' +CONF_PAYLOAD_OFF = 'payload_off' + DEFAULT_NAME = 'MQTT Binary sensor' -DEFAULT_QOS = 0 DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' -DEPENDENCIES = ['mqtt'] +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SENSOR_CLASS, default=None): + vol.Any(vol.In(SENSOR_CLASSES), vol.SetTo(None)), + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Add MQTT binary sensor.""" - if config.get('state_topic') is None: - _LOGGER.error('Missing required variable: state_topic') - return False - - sensor_class = config.get('sensor_class') - if sensor_class not in SENSOR_CLASSES: - _LOGGER.warning('Unknown sensor class: %s', sensor_class) - sensor_class = None - add_devices([MqttBinarySensor( hass, - config.get('name', DEFAULT_NAME), - config.get('state_topic', None), - sensor_class, - config.get('qos', DEFAULT_QOS), - config.get('payload_on', DEFAULT_PAYLOAD_ON), - config.get('payload_off', DEFAULT_PAYLOAD_OFF), - config.get(CONF_VALUE_TEMPLATE))]) + config[CONF_NAME], + config[CONF_STATE_TOPIC], + config[CONF_SENSOR_CLASS], + config[mqtt.CONF_QOS], + config[CONF_PAYLOAD_ON], + config[CONF_PAYLOAD_OFF], + config.get(CONF_VALUE_TEMPLATE) + )]) # pylint: disable=too-many-arguments, too-many-instance-attributes diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 75124be10f7..833fef14fc3 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -1,6 +1,7 @@ """The tests for the MQTT binary sensor platform.""" import unittest +from homeassistant.bootstrap import _setup_component import homeassistant.components.binary_sensor as binary_sensor from tests.common import mock_mqtt_component, fire_mqtt_message from homeassistant.const import (STATE_OFF, STATE_ON) @@ -22,15 +23,16 @@ class TestSensorMQTT(unittest.TestCase): def test_setting_sensor_value_via_mqtt_message(self): """Test the setting of the value via MQTT.""" - self.assertTrue(binary_sensor.setup(self.hass, { - 'binary_sensor': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'test-topic', 'payload_on': 'ON', 'payload_off': 'OFF', } - })) + }) state = self.hass.states.get('binary_sensor.test') self.assertEqual(STATE_OFF, state.state) @@ -47,28 +49,30 @@ class TestSensorMQTT(unittest.TestCase): def test_valid_sensor_class(self): """Test the setting of a valid sensor class.""" - self.assertTrue(binary_sensor.setup(self.hass, { - 'binary_sensor': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'sensor_class': 'motion', 'state_topic': 'test-topic', } - })) + }) state = self.hass.states.get('binary_sensor.test') self.assertEqual('motion', state.attributes.get('sensor_class')) def test_invalid_sensor_class(self): """Test the setting of an invalid sensor class.""" - self.assertTrue(binary_sensor.setup(self.hass, { - 'binary_sensor': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'sensor_class': 'abc123', 'state_topic': 'test-topic', } - })) + }) state = self.hass.states.get('binary_sensor.test') self.assertIsNone(state.attributes.get('sensor_class')) From 88da42fe62f91f51a954f1308bbfbb7e181f7815 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 5 Apr 2016 01:41:11 -0400 Subject: [PATCH 06/17] Config validation for MQTT device_tracker platform. --- .../components/device_tracker/mqtt.py | 20 +++++++++---------- tests/components/device_tracker/test_mqtt.py | 7 +++++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index d754156f217..609d8cc713a 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -6,28 +6,26 @@ https://home-assistant.io/components/device_tracker.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.mqtt as mqtt -from homeassistant import util +import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['mqtt'] -CONF_QOS = 'qos' CONF_DEVICES = 'devices' -DEFAULT_QOS = 0 - _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, +}) + def setup_scanner(hass, config, see): """Setup the MQTT tracker.""" - devices = config.get(CONF_DEVICES) - qos = util.convert(config.get(CONF_QOS), int, DEFAULT_QOS) - - if not isinstance(devices, dict): - _LOGGER.error('Expected %s to be a dict, found %s', CONF_DEVICES, - devices) - return False + devices = config[CONF_DEVICES] + qos = config[mqtt.CONF_QOS] dev_id_lookup = {} diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index 7b6024c60f1..139316a35bf 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -2,6 +2,7 @@ import unittest import os +from homeassistant.bootstrap import _setup_component from homeassistant.components import device_tracker from homeassistant.const import CONF_PLATFORM @@ -31,11 +32,13 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): topic = '/location/paulus' location = 'work' - self.assertTrue(device_tracker.setup(self.hass, { + self.hass.config.components = ['mqtt', 'zone'] + assert _setup_component(self.hass, device_tracker.DOMAIN, { device_tracker.DOMAIN: { CONF_PLATFORM: 'mqtt', 'devices': {dev_id: topic} - }})) + } + }) fire_mqtt_message(self.hass, topic, location) self.hass.pool.block_till_done() self.assertEqual(location, self.hass.states.get(enttiy_id).state) From 29a8403741c5a2f8740e82dcb47d2c46323395fe Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 5 Apr 2016 20:55:20 -0400 Subject: [PATCH 07/17] Config validation for MQTT light platform. --- homeassistant/components/light/mqtt.py | 77 +++++++++++++++++++------- tests/components/light/test_mqtt.py | 50 ++++++++++------- 2 files changed, 87 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index f55cc310192..47d770e4cd2 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -7,46 +7,85 @@ https://home-assistant.io/components/light.mqtt/ import logging from functools import partial +import voluptuous as vol + import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_RGB_COLOR, Light) +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import render_with_possible_json_value -from homeassistant.util import convert _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mqtt'] + +CONF_STATE_TOPIC = 'state_topic' +CONF_COMMAND_TOPIC = 'command_topic' +CONF_STATE_VALUE_TEMPLATE = 'state_value_template' +CONF_BRIGHTNESS_STATE_TOPIC = 'brightness_state_topic' +CONF_BRIGHTNESS_COMMAND_TOPIC = 'brightness_command_topic' +CONF_BRIGHTNESS_VALUE_TEMPLATE = 'brightness_value_template' +CONF_RGB_STATE_TOPIC = 'rgb_state_topic' +CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic' +CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template' +CONF_PAYLOAD_ON = 'payload_on' +CONF_PAYLOAD_OFF = 'payload_off' +CONF_OPTIMISTIC = 'optimistic' +CONF_BRIGHTNESS_SCALE = 'brightness_scale' + DEFAULT_NAME = 'MQTT Light' -DEFAULT_QOS = 0 DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False DEFAULT_BRIGHTNESS_SCALE = 255 -DEPENDENCIES = ['mqtt'] +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE): + vol.All(vol.Coerce(int), vol.Range(min=1)), +}) def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Add MQTT Light.""" - if config.get('command_topic') is None: - _LOGGER.error("Missing required variable: command_topic") - return False - add_devices_callback([MqttLight( hass, - convert(config.get('name'), str, DEFAULT_NAME), - {key: convert(config.get(key), str) for key in - (typ + topic - for typ in ('', 'brightness_', 'rgb_') - for topic in ('state_topic', 'command_topic'))}, - {key: convert(config.get(key + '_value_template'), str) - for key in ('state', 'brightness', 'rgb')}, - convert(config.get('qos'), int, DEFAULT_QOS), + config[CONF_NAME], { - 'on': convert(config.get('payload_on'), str, DEFAULT_PAYLOAD_ON), - 'off': convert(config.get('payload_off'), str, DEFAULT_PAYLOAD_OFF) + key: config.get(key) for key in ( + CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, + CONF_BRIGHTNESS_STATE_TOPIC, + CONF_BRIGHTNESS_COMMAND_TOPIC, + CONF_RGB_STATE_TOPIC, + CONF_RGB_COMMAND_TOPIC, + ) }, - convert(config.get('optimistic'), bool, DEFAULT_OPTIMISTIC), - convert(config.get('brightness_scale'), int, DEFAULT_BRIGHTNESS_SCALE) + { + 'state': config.get(CONF_STATE_VALUE_TEMPLATE), + 'brightness': config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE), + 'rgb': config.get(CONF_RGB_VALUE_TEMPLATE) + }, + config[mqtt.CONF_QOS], + { + 'on': config[CONF_PAYLOAD_ON], + 'off': config[CONF_PAYLOAD_OFF], + }, + config[CONF_OPTIMISTIC], + config[CONF_BRIGHTNESS_SCALE], )]) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index d9343908f46..b830c2d0dd3 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -58,6 +58,7 @@ light: """ import unittest +from homeassistant.bootstrap import _setup_component from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE import homeassistant.components.light as light from tests.common import ( @@ -78,24 +79,26 @@ class TestLightMQTT(unittest.TestCase): def test_fail_setup_if_no_command_topic(self): """Test if command fails with command topic.""" - self.assertTrue(light.setup(self.hass, { - 'light': { + self.hass.config.components = ['mqtt'] + assert not _setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { 'platform': 'mqtt', 'name': 'test', } - })) + }) self.assertIsNone(self.hass.states.get('light.test')) def test_no_color_or_brightness_if_no_topics(self): """Test if there is no color and brightness if no topic.""" - self.assertTrue(light.setup(self.hass, { - 'light': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'test_light_rgb/status', 'command_topic': 'test_light_rgb/set', } - })) + }) state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) @@ -112,8 +115,9 @@ class TestLightMQTT(unittest.TestCase): def test_controlling_state_via_topic(self): """Test the controlling of the state via topic.""" - self.assertTrue(light.setup(self.hass, { - 'light': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'test_light_rgb/status', @@ -126,7 +130,7 @@ class TestLightMQTT(unittest.TestCase): 'payload_on': 1, 'payload_off': 0 } - })) + }) state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) @@ -172,8 +176,9 @@ class TestLightMQTT(unittest.TestCase): def test_controlling_scale(self): """Test the controlling scale.""" - self.assertTrue(light.setup(self.hass, { - 'light': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'test_scale/status', @@ -185,7 +190,7 @@ class TestLightMQTT(unittest.TestCase): 'payload_on': 'on', 'payload_off': 'off' } - })) + }) state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) @@ -218,8 +223,9 @@ class TestLightMQTT(unittest.TestCase): def test_controlling_state_via_topic_with_templates(self): """Test the setting og the state with a template.""" - self.assertTrue(light.setup(self.hass, { - 'light': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'test_light_rgb/status', @@ -230,7 +236,7 @@ class TestLightMQTT(unittest.TestCase): 'brightness_value_template': '{{ value_json.hello }}', 'rgb_value_template': '{{ value_json.hello | join(",") }}', } - })) + }) state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) @@ -252,8 +258,9 @@ class TestLightMQTT(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): """Test the sending of command in optimistic mode.""" - self.assertTrue(light.setup(self.hass, { - 'light': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'command_topic': 'test_light_rgb/set', @@ -263,7 +270,7 @@ class TestLightMQTT(unittest.TestCase): 'payload_on': 'on', 'payload_off': 'off' } - })) + }) state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) @@ -310,15 +317,16 @@ class TestLightMQTT(unittest.TestCase): def test_show_brightness_if_only_command_topic(self): """Test the brightness if only a command topic is present.""" - self.assertTrue(light.setup(self.hass, { - 'light': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'brightness_command_topic': 'test_light_rgb/brightness/set', 'command_topic': 'test_light_rgb/set', 'state_topic': 'test_light_rgb/status', } - })) + }) state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) From 4e864b5caa29dcceaac243fa83e780cb0ae4759c Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 5 Apr 2016 23:20:06 -0400 Subject: [PATCH 08/17] Rename _mqtt_topic to valid_subscribe_topic. --- homeassistant/components/mqtt/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 89112378c67..4b5756fdccb 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -88,8 +88,8 @@ MQTT_BASE_PLATFORM_SCHEMA = vol.Schema({ # Service call validation schema -def _mqtt_topic(value, invalid_chars='\0'): - """Validate MQTT topic.""" +def valid_subscribe_topic(value, invalid_chars='\0'): + """Validate that we can subscribe using this MQTT topic.""" if isinstance(value, str) and all(c not in value for c in invalid_chars): return vol.Length(min=1, max=65535)(value) raise vol.Invalid('Invalid MQTT topic name') @@ -97,13 +97,7 @@ def _mqtt_topic(value, invalid_chars='\0'): def valid_publish_topic(value): """Validate that we can publish using this MQTT topic.""" - return _mqtt_topic(value, invalid_chars='#+\0') - - -def valid_subscribe_topic(value): - """Validate that we can subscribe using this MQTT topic.""" - return _mqtt_topic(value) - + return valid_subscribe_topic(value, invalid_chars='#+\0') MQTT_PUBLISH_SCHEMA = vol.Schema({ vol.Required(ATTR_TOPIC): valid_publish_topic, From c8df06bb9f1bddec6022b56d99e8768a5bb98a00 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 5 Apr 2016 23:34:04 -0400 Subject: [PATCH 09/17] Config validation for MQTT lock platform. --- homeassistant/components/lock/mqtt.py | 48 ++++++++++++++++++--------- tests/components/lock/test_mqtt.py | 22 +++++++----- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index a68a3303de6..4cea06543e2 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -6,40 +6,58 @@ https://home-assistant.io/components/lock.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.mqtt as mqtt from homeassistant.components.lock import LockDevice -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mqtt'] + +CONF_STATE_TOPIC = 'state_topic' +CONF_COMMAND_TOPIC = 'command_topic' +CONF_RETAIN = 'retain' +CONF_PAYLOAD_LOCK = 'payload_lock' +CONF_PAYLOAD_UNLOCK = 'payload_unlock' +CONF_OPTIMISTIC = 'optimistic' + DEFAULT_NAME = "MQTT Lock" DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" -DEFAULT_QOS = 0 DEFAULT_OPTIMISTIC = False DEFAULT_RETAIN = False -DEPENDENCIES = ['mqtt'] +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): + cv.string, + vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): + cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the MQTT lock.""" - if config.get('command_topic') is None: - _LOGGER.error("Missing required variable: command_topic") - return False - add_devices_callback([MqttLock( hass, - config.get('name', DEFAULT_NAME), - config.get('state_topic'), - config.get('command_topic'), - config.get('qos', DEFAULT_QOS), - config.get('retain', DEFAULT_RETAIN), - config.get('payload_lock', DEFAULT_PAYLOAD_LOCK), - config.get('payload_unlock', DEFAULT_PAYLOAD_UNLOCK), - config.get('optimistic', DEFAULT_OPTIMISTIC), + config[CONF_NAME], + config.get(CONF_STATE_TOPIC), + config[CONF_COMMAND_TOPIC], + config[mqtt.CONF_QOS], + config[CONF_RETAIN], + config[CONF_PAYLOAD_LOCK], + config[CONF_PAYLOAD_UNLOCK], + config[CONF_OPTIMISTIC], config.get(CONF_VALUE_TEMPLATE))]) diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index 810d5c5449f..006d241bcad 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -1,6 +1,7 @@ """The tests for the MQTT lock platform.""" import unittest +from homeassistant.bootstrap import _setup_component from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED, ATTR_ASSUMED_STATE) import homeassistant.components.lock as lock @@ -22,8 +23,9 @@ class TestLockMQTT(unittest.TestCase): def test_controlling_state_via_topic(self): """Test the controlling state via topic.""" - self.assertTrue(lock.setup(self.hass, { - 'lock': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, lock.DOMAIN, { + lock.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', @@ -31,7 +33,7 @@ class TestLockMQTT(unittest.TestCase): 'payload_lock': 'LOCK', 'payload_unlock': 'UNLOCK' } - })) + }) state = self.hass.states.get('lock.test') self.assertEqual(STATE_UNLOCKED, state.state) @@ -51,8 +53,9 @@ class TestLockMQTT(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): """Test the sending MQTT commands in optimistic mode.""" - self.assertTrue(lock.setup(self.hass, { - 'lock': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, lock.DOMAIN, { + lock.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'command_topic': 'command-topic', @@ -60,7 +63,7 @@ class TestLockMQTT(unittest.TestCase): 'payload_unlock': 'UNLOCK', 'qos': 2 } - })) + }) state = self.hass.states.get('lock.test') self.assertEqual(STATE_UNLOCKED, state.state) @@ -84,8 +87,9 @@ class TestLockMQTT(unittest.TestCase): def test_controlling_state_via_topic_and_json_message(self): """Test the controlling state via topic and JSON message.""" - self.assertTrue(lock.setup(self.hass, { - 'lock': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, lock.DOMAIN, { + lock.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', @@ -94,7 +98,7 @@ class TestLockMQTT(unittest.TestCase): 'payload_unlock': 'UNLOCK', 'value_template': '{{ value_json.val }}' } - })) + }) state = self.hass.states.get('lock.test') self.assertEqual(STATE_UNLOCKED, state.state) From 33838545065df5bc1a6640f7b7ebc2ae875b165a Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 5 Apr 2016 23:39:13 -0400 Subject: [PATCH 10/17] Move CONF_OPTIMISTIC to homeassistant.const. --- homeassistant/components/light/mqtt.py | 3 +-- homeassistant/components/lock/mqtt.py | 3 +-- homeassistant/components/mysensors.py | 4 ++-- homeassistant/const.py | 1 + 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 47d770e4cd2..65d7d925723 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_RGB_COLOR, Light) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import render_with_possible_json_value @@ -31,7 +31,6 @@ CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic' CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template' CONF_PAYLOAD_ON = 'payload_on' CONF_PAYLOAD_OFF = 'payload_off' -CONF_OPTIMISTIC = 'optimistic' CONF_BRIGHTNESS_SCALE = 'brightness_scale' DEFAULT_NAME = 'MQTT Light' diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index 4cea06543e2..c6e81716adf 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -10,7 +10,7 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.lock import LockDevice -from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -23,7 +23,6 @@ CONF_COMMAND_TOPIC = 'command_topic' CONF_RETAIN = 'retain' CONF_PAYLOAD_LOCK = 'payload_lock' CONF_PAYLOAD_UNLOCK = 'payload_unlock' -CONF_OPTIMISTIC = 'optimistic' DEFAULT_NAME = "MQTT Lock" DEFAULT_PAYLOAD_LOCK = "LOCK" diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 2d3c8eb1f53..570a59151ce 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -9,7 +9,8 @@ import logging import homeassistant.bootstrap as bootstrap from homeassistant.const import ( ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED, TEMP_CELCIUS) + EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED, TEMP_CELCIUS, + CONF_OPTIMISTIC) from homeassistant.helpers import validate_config CONF_GATEWAYS = 'gateways' @@ -19,7 +20,6 @@ CONF_PERSISTENCE = 'persistence' CONF_PERSISTENCE_FILE = 'persistence_file' CONF_VERSION = 'version' CONF_BAUD_RATE = 'baud_rate' -CONF_OPTIMISTIC = 'optimistic' DEFAULT_VERSION = '1.4' DEFAULT_BAUD_RATE = 115200 diff --git a/homeassistant/const.py b/homeassistant/const.py index 17dcc9e8602..69874c575a5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -29,6 +29,7 @@ CONF_PASSWORD = "password" CONF_API_KEY = "api_key" CONF_ACCESS_TOKEN = "access_token" CONF_FILENAME = "filename" +CONF_OPTIMISTIC = 'optimistic' CONF_SCAN_INTERVAL = "scan_interval" CONF_VALUE_TEMPLATE = "value_template" From eb3f812e38c27f586a48238eddb45aa5cdc68710 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 6 Apr 2016 20:32:35 -0400 Subject: [PATCH 11/17] Config validation for MQTT sensor platform. --- homeassistant/components/sensor/mqtt.py | 35 ++++++++++++++++--------- tests/components/sensor/test_mqtt.py | 15 ++++++----- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index b0f8e8e0887..4b23eeb3d82 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -6,33 +6,42 @@ https://home-assistant.io/components/sensor.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.mqtt as mqtt -from homeassistant.const import CONF_VALUE_TEMPLATE, STATE_UNKNOWN +from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "MQTT Sensor" -DEFAULT_QOS = 0 - DEPENDENCIES = ['mqtt'] +CONF_STATE_TOPIC = 'state_topic' +CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' + +DEFAULT_NAME = "MQTT Sensor" + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup MQTT Sensor.""" - if config.get('state_topic') is None: - _LOGGER.error("Missing required variable: state_topic") - return False - add_devices_callback([MqttSensor( hass, - config.get('name', DEFAULT_NAME), - config.get('state_topic'), - config.get('qos', DEFAULT_QOS), - config.get('unit_of_measurement'), - config.get(CONF_VALUE_TEMPLATE))]) + config[CONF_NAME], + config[CONF_STATE_TOPIC], + config[mqtt.CONF_QOS], + config.get(CONF_UNIT_OF_MEASUREMENT), + config.get(CONF_VALUE_TEMPLATE), + )]) # pylint: disable=too-many-arguments, too-many-instance-attributes diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 280542bb305..1c8aa8996db 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -1,6 +1,7 @@ """The tests for the MQTT sensor platform.""" import unittest +from homeassistant.bootstrap import _setup_component import homeassistant.components.sensor as sensor from tests.common import mock_mqtt_component, fire_mqtt_message @@ -21,14 +22,15 @@ class TestSensorMQTT(unittest.TestCase): def test_setting_sensor_value_via_mqtt_message(self): """Test the setting of the value via MQTT.""" - self.assertTrue(sensor.setup(self.hass, { - 'sensor': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'test-topic', 'unit_of_measurement': 'fav unit' } - })) + }) fire_mqtt_message(self.hass, 'test-topic', '100') self.hass.pool.block_till_done() @@ -40,15 +42,16 @@ class TestSensorMQTT(unittest.TestCase): def test_setting_sensor_value_via_mqtt_json_message(self): """Test the setting of the value via MQTT with JSON playload.""" - self.assertTrue(sensor.setup(self.hass, { - 'sensor': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'test-topic', 'unit_of_measurement': 'fav unit', 'value_template': '{{ value_json.val }}' } - })) + }) fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') self.hass.pool.block_till_done() From deecec5e4e7c4585c544b2fc89ed1e841d675ed7 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 6 Apr 2016 20:47:47 -0400 Subject: [PATCH 12/17] Config validation for MQTT switch platform. --- homeassistant/components/switch/mqtt.py | 46 ++++++++++++++++--------- tests/components/switch/test_mqtt.py | 22 +++++++----- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index ebb9bfa4433..92711f83d62 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -6,41 +6,55 @@ https://home-assistant.io/components/switch.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.mqtt as mqtt from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template -from homeassistant.util import convert _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mqtt'] + +CONF_STATE_TOPIC = 'state_topic' +CONF_COMMAND_TOPIC = 'command_topic' +CONF_RETAIN = 'retain' +CONF_PAYLOAD_ON = 'payload_on' +CONF_PAYLOAD_OFF = 'payload_off' + DEFAULT_NAME = "MQTT Switch" -DEFAULT_QOS = 0 DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_OPTIMISTIC = False DEFAULT_RETAIN = False -DEPENDENCIES = ['mqtt'] +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Add MQTT switch.""" - if config.get('command_topic') is None: - _LOGGER.error("Missing required variable: command_topic") - return False - add_devices_callback([MqttSwitch( hass, - convert(config.get('name'), str, DEFAULT_NAME), - config.get('state_topic'), - config.get('command_topic'), - convert(config.get('qos'), int, DEFAULT_QOS), - convert(config.get('retain'), bool, DEFAULT_RETAIN), - convert(config.get('payload_on'), str, DEFAULT_PAYLOAD_ON), - convert(config.get('payload_off'), str, DEFAULT_PAYLOAD_OFF), - convert(config.get('optimistic'), bool, DEFAULT_OPTIMISTIC), + config[CONF_NAME], + config.get(CONF_STATE_TOPIC), + config[CONF_COMMAND_TOPIC], + config[mqtt.CONF_QOS], + config[CONF_RETAIN], + config[CONF_PAYLOAD_ON], + config[CONF_PAYLOAD_OFF], + config[CONF_OPTIMISTIC], config.get(CONF_VALUE_TEMPLATE))]) diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index d2a1e7a5835..61c14be70d1 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -1,6 +1,7 @@ """The tests for the MQTT switch platform.""" import unittest +from homeassistant.bootstrap import _setup_component from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE import homeassistant.components.switch as switch from tests.common import ( @@ -21,8 +22,9 @@ class TestSensorMQTT(unittest.TestCase): def test_controlling_state_via_topic(self): """Test the controlling state via topic.""" - self.assertTrue(switch.setup(self.hass, { - 'switch': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', @@ -30,7 +32,7 @@ class TestSensorMQTT(unittest.TestCase): 'payload_on': 1, 'payload_off': 0 } - })) + }) state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) @@ -50,8 +52,9 @@ class TestSensorMQTT(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): """Test the sending MQTT commands in optimistic mode.""" - self.assertTrue(switch.setup(self.hass, { - 'switch': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'command_topic': 'command-topic', @@ -59,7 +62,7 @@ class TestSensorMQTT(unittest.TestCase): 'payload_off': 'beer off', 'qos': '2' } - })) + }) state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) @@ -83,8 +86,9 @@ class TestSensorMQTT(unittest.TestCase): def test_controlling_state_via_topic_and_json_message(self): """Test the controlling state via topic and JSON message.""" - self.assertTrue(switch.setup(self.hass, { - 'switch': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', @@ -93,7 +97,7 @@ class TestSensorMQTT(unittest.TestCase): 'payload_off': 'beer off', 'value_template': '{{ value_json.val }}' } - })) + }) state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) From fca08b095a0af61f7e2d91b1a499d926e3c4ead0 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 6 Apr 2016 21:00:14 -0400 Subject: [PATCH 13/17] Config validation for MQTT rollershutter platform. --- .../components/rollershutter/mqtt.py | 43 +++++++++++++------ tests/components/rollershutter/test_mqtt.py | 36 +++++++++------- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/rollershutter/mqtt.py index 45ca9f6d631..c7e098d41bd 100644 --- a/homeassistant/components/rollershutter/mqtt.py +++ b/homeassistant/components/rollershutter/mqtt.py @@ -6,38 +6,53 @@ https://home-assistant.io/components/rollershutter.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.mqtt as mqtt from homeassistant.components.rollershutter import RollershutterDevice -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] +CONF_STATE_TOPIC = 'state_topic' +CONF_COMMAND_TOPIC = 'command_topic' +CONF_PAYLOAD_UP = 'payload_up' +CONF_PAYLOAD_DOWN = 'payload_down' +CONF_PAYLOAD_STOP = 'payload_stop' + DEFAULT_NAME = "MQTT Rollershutter" -DEFAULT_QOS = 0 DEFAULT_PAYLOAD_UP = "UP" DEFAULT_PAYLOAD_DOWN = "DOWN" DEFAULT_PAYLOAD_STOP = "STOP" +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_PAYLOAD_UP, default=DEFAULT_PAYLOAD_UP): cv.string, + vol.Optional(CONF_PAYLOAD_DOWN, default=DEFAULT_PAYLOAD_DOWN): cv.string, + vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Add MQTT Rollershutter.""" - if config.get('command_topic') is None: - _LOGGER.error("Missing required variable: command_topic") - return False - add_devices_callback([MqttRollershutter( hass, - config.get('name', DEFAULT_NAME), - config.get('state_topic'), - config.get('command_topic'), - config.get('qos', DEFAULT_QOS), - config.get('payload_up', DEFAULT_PAYLOAD_UP), - config.get('payload_down', DEFAULT_PAYLOAD_DOWN), - config.get('payload_stop', DEFAULT_PAYLOAD_STOP), - config.get(CONF_VALUE_TEMPLATE))]) + config[CONF_NAME], + config.get(CONF_STATE_TOPIC), + config[CONF_COMMAND_TOPIC], + config[mqtt.CONF_QOS], + config[CONF_PAYLOAD_UP], + config[CONF_PAYLOAD_DOWN], + config[CONF_PAYLOAD_STOP], + config.get(CONF_VALUE_TEMPLATE) + )]) # pylint: disable=too-many-arguments, too-many-instance-attributes diff --git a/tests/components/rollershutter/test_mqtt.py b/tests/components/rollershutter/test_mqtt.py index b871a8d06e2..4345864f83f 100644 --- a/tests/components/rollershutter/test_mqtt.py +++ b/tests/components/rollershutter/test_mqtt.py @@ -1,6 +1,7 @@ """The tests for the MQTT roller shutter platform.""" import unittest +from homeassistant.bootstrap import _setup_component from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN import homeassistant.components.rollershutter as rollershutter from tests.common import mock_mqtt_component, fire_mqtt_message @@ -22,8 +23,9 @@ class TestRollershutterMQTT(unittest.TestCase): def test_controlling_state_via_topic(self): """Test the controlling state via topic.""" - self.assertTrue(rollershutter.setup(self.hass, { - 'rollershutter': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, rollershutter.DOMAIN, { + rollershutter.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', @@ -33,7 +35,7 @@ class TestRollershutterMQTT(unittest.TestCase): 'payload_down': 'DOWN', 'payload_stop': 'STOP' } - })) + }) state = self.hass.states.get('rollershutter.test') self.assertEqual(STATE_UNKNOWN, state.state) @@ -58,15 +60,16 @@ class TestRollershutterMQTT(unittest.TestCase): def test_send_move_up_command(self): """Test the sending of move_up.""" - self.assertTrue(rollershutter.setup(self.hass, { - 'rollershutter': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, rollershutter.DOMAIN, { + rollershutter.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', 'command_topic': 'command-topic', 'qos': 2 } - })) + }) state = self.hass.states.get('rollershutter.test') self.assertEqual(STATE_UNKNOWN, state.state) @@ -81,15 +84,16 @@ class TestRollershutterMQTT(unittest.TestCase): def test_send_move_down_command(self): """Test the sending of move_down.""" - self.assertTrue(rollershutter.setup(self.hass, { - 'rollershutter': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, rollershutter.DOMAIN, { + rollershutter.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', 'command_topic': 'command-topic', 'qos': 2 } - })) + }) state = self.hass.states.get('rollershutter.test') self.assertEqual(STATE_UNKNOWN, state.state) @@ -104,15 +108,16 @@ class TestRollershutterMQTT(unittest.TestCase): def test_send_stop_command(self): """Test the sending of stop.""" - self.assertTrue(rollershutter.setup(self.hass, { - 'rollershutter': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, rollershutter.DOMAIN, { + rollershutter.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', 'command_topic': 'command-topic', 'qos': 2 } - })) + }) state = self.hass.states.get('rollershutter.test') self.assertEqual(STATE_UNKNOWN, state.state) @@ -127,8 +132,9 @@ class TestRollershutterMQTT(unittest.TestCase): def test_state_attributes_current_position(self): """Test the current position.""" - self.assertTrue(rollershutter.setup(self.hass, { - 'rollershutter': { + self.hass.config.components = ['mqtt'] + assert _setup_component(self.hass, rollershutter.DOMAIN, { + rollershutter.DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', @@ -137,7 +143,7 @@ class TestRollershutterMQTT(unittest.TestCase): 'payload_down': 'DOWN', 'payload_stop': 'STOP' } - })) + }) state_attributes_dict = self.hass.states.get( 'rollershutter.test').attributes From 0ef0d4bac7588b84b775ab587d860f63b04cb8b2 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 6 Apr 2016 21:12:51 -0400 Subject: [PATCH 14/17] Config validation for automation MQTT trigger --- homeassistant/components/automation/mqtt.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index db63d81e54b..db0c1be7c2a 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -4,26 +4,29 @@ Offer MQTT listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#mqtt-trigger """ -import logging +import voluptuous as vol import homeassistant.components.mqtt as mqtt +from homeassistant.const import CONF_PLATFORM +import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['mqtt'] CONF_TOPIC = 'topic' CONF_PAYLOAD = 'payload' +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): mqtt.DOMAIN, + vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PAYLOAD): cv.string, +}) + def trigger(hass, config, action): """Listen for state changes based on configuration.""" - topic = config.get(CONF_TOPIC) + topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) - if topic is None: - logging.getLogger(__name__).error( - "Missing configuration key %s", CONF_TOPIC) - return False - def mqtt_automation_listener(msg_topic, msg_payload, qos): """Listen for MQTT messages.""" if payload is None or payload == msg_payload: From a7016e4b32d62ee009153a3910a7f82e11c5b938 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 6 Apr 2016 21:35:46 -0400 Subject: [PATCH 15/17] Move CONF_STATE_TOPIC, CONF_COMMAND_TOPIC and CONF_RETAIN to mqtt component. --- .../components/alarm_control_panel/mqtt.py | 10 ++--- .../components/binary_sensor/mqtt.py | 8 ++-- .../components/device_tracker/mqtt.py | 3 +- homeassistant/components/light/mqtt.py | 29 ++++++------ homeassistant/components/lock/mqtt.py | 14 ++---- homeassistant/components/mqtt/__init__.py | 45 +++++++++++++------ .../components/rollershutter/mqtt.py | 11 ++--- homeassistant/components/sensor/mqtt.py | 8 ++-- homeassistant/components/switch/mqtt.py | 14 ++---- 9 files changed, 73 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 687de5de474..3bc7b860869 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -14,14 +14,14 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, CONF_NAME) +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_STATE_TOPIC = 'state_topic' -CONF_COMMAND_TOPIC = 'command_topic' CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' @@ -50,11 +50,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config[CONF_NAME], config[CONF_STATE_TOPIC], config[CONF_COMMAND_TOPIC], - config[mqtt.CONF_QOS], + config[CONF_QOS], config[CONF_PAYLOAD_DISARM], config[CONF_PAYLOAD_ARM_HOME], config[CONF_PAYLOAD_ARM_AWAY], - config.get('code'))]) + config.get(CONF_CODE))]) # pylint: disable=too-many-arguments, too-many-instance-attributes @@ -74,7 +74,7 @@ class MqttAlarm(alarm.AlarmControlPanel): self._payload_disarm = payload_disarm self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away - self._code = str(code) if code else None + self._code = code def message_received(topic, payload, qos): """A new MQTT message has been received.""" diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 15b17066ce5..a381305691a 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -12,6 +12,7 @@ import homeassistant.components.mqtt as mqtt from homeassistant.components.binary_sensor import (BinarySensorDevice, SENSOR_CLASSES) from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE +from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -19,7 +20,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_STATE_TOPIC = 'state_topic' CONF_SENSOR_CLASS = 'sensor_class' CONF_PAYLOAD_ON = 'payload_on' CONF_PAYLOAD_OFF = 'payload_off' @@ -28,14 +28,12 @@ DEFAULT_NAME = 'MQTT Binary sensor' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_SENSOR_CLASS, default=None): vol.Any(vol.In(SENSOR_CLASSES), vol.SetTo(None)), vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) @@ -47,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config[CONF_NAME], config[CONF_STATE_TOPIC], config[CONF_SENSOR_CLASS], - config[mqtt.CONF_QOS], + config[CONF_QOS], config[CONF_PAYLOAD_ON], config[CONF_PAYLOAD_OFF], config.get(CONF_VALUE_TEMPLATE) diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index 609d8cc713a..0998e227857 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -9,6 +9,7 @@ import logging import voluptuous as vol import homeassistant.components.mqtt as mqtt +from homeassistant.components.mqtt import CONF_QOS import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['mqtt'] @@ -25,7 +26,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ def setup_scanner(hass, config, see): """Setup the MQTT tracker.""" devices = config[CONF_DEVICES] - qos = config[mqtt.CONF_QOS] + qos = config[CONF_QOS] dev_id_lookup = {} diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 65d7d925723..2d0e7bb6df0 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -12,7 +12,9 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_RGB_COLOR, Light) -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import render_with_possible_json_value @@ -20,8 +22,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_STATE_TOPIC = 'state_topic' -CONF_COMMAND_TOPIC = 'command_topic' CONF_STATE_VALUE_TEMPLATE = 'state_value_template' CONF_BRIGHTNESS_STATE_TOPIC = 'brightness_state_topic' CONF_BRIGHTNESS_COMMAND_TOPIC = 'brightness_command_topic' @@ -39,10 +39,8 @@ DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False DEFAULT_BRIGHTNESS_SCALE = 255 -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -60,6 +58,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Add MQTT Light.""" + config.setdefault(CONF_STATE_VALUE_TEMPLATE, + config.get(CONF_VALUE_TEMPLATE)) add_devices_callback([MqttLight( hass, config[CONF_NAME], @@ -78,7 +78,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): 'brightness': config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE), 'rgb': config.get(CONF_RGB_VALUE_TEMPLATE) }, - config[mqtt.CONF_QOS], + config[CONF_QOS], + config[CONF_RETAIN], { 'on': config[CONF_PAYLOAD_ON], 'off': config[CONF_PAYLOAD_OFF], @@ -92,13 +93,14 @@ class MqttLight(Light): """MQTT light.""" # pylint: disable=too-many-arguments,too-many-instance-attributes - def __init__(self, hass, name, topic, templates, qos, payload, optimistic, - brightness_scale): + def __init__(self, hass, name, topic, templates, qos, retain, payload, + optimistic, brightness_scale): """Initialize MQTT light.""" self._hass = hass self._name = name self._topic = topic self._qos = qos + self._retain = retain self._payload = payload self._optimistic = optimistic or topic["state_topic"] is None self._optimistic_rgb = optimistic or topic["rgb_state_topic"] is None @@ -194,7 +196,8 @@ class MqttLight(Light): self._topic["rgb_command_topic"] is not None: mqtt.publish(self._hass, self._topic["rgb_command_topic"], - "{},{},{}".format(*kwargs[ATTR_RGB_COLOR]), self._qos) + "{},{},{}".format(*kwargs[ATTR_RGB_COLOR]), + self._qos, self._retain) if self._optimistic_rgb: self._rgb = kwargs[ATTR_RGB_COLOR] @@ -205,14 +208,14 @@ class MqttLight(Light): percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 device_brightness = int(percent_bright * self._brightness_scale) mqtt.publish(self._hass, self._topic["brightness_command_topic"], - device_brightness, self._qos) + device_brightness, self._qos, self._retain) if self._optimistic_brightness: self._brightness = kwargs[ATTR_BRIGHTNESS] should_update = True mqtt.publish(self._hass, self._topic["command_topic"], - self._payload["on"], self._qos) + self._payload["on"], self._qos, self._retain) if self._optimistic: # Optimistically assume that switch has changed state. @@ -225,7 +228,7 @@ class MqttLight(Light): def turn_off(self, **kwargs): """Turn the device off.""" mqtt.publish(self._hass, self._topic["command_topic"], - self._payload["off"], self._qos) + self._payload["off"], self._qos, self._retain) if self._optimistic: # Optimistically assume that switch has changed state. diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index c6e81716adf..b188de21edc 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -11,6 +11,8 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.lock import LockDevice from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -18,9 +20,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_STATE_TOPIC = 'state_topic' -CONF_COMMAND_TOPIC = 'command_topic' -CONF_RETAIN = 'retain' CONF_PAYLOAD_LOCK = 'payload_lock' CONF_PAYLOAD_UNLOCK = 'payload_unlock' @@ -28,19 +27,14 @@ DEFAULT_NAME = "MQTT Lock" DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_OPTIMISTIC = False -DEFAULT_RETAIN = False -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) @@ -52,7 +46,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): config[CONF_NAME], config.get(CONF_STATE_TOPIC), config[CONF_COMMAND_TOPIC], - config[mqtt.CONF_QOS], + config[CONF_QOS], config[CONF_RETAIN], config[CONF_PAYLOAD_LOCK], config[CONF_PAYLOAD_UNLOCK], diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 4b5756fdccb..e78496f1e06 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - CONF_PLATFORM, CONF_SCAN_INTERVAL) + CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_VALUE_TEMPLATE) _LOGGER = logging.getLogger(__name__) @@ -40,7 +40,11 @@ CONF_USERNAME = 'username' CONF_PASSWORD = 'password' CONF_CERTIFICATE = 'certificate' CONF_PROTOCOL = 'protocol' + +CONF_STATE_TOPIC = 'state_topic' +CONF_COMMAND_TOPIC = 'command_topic' CONF_QOS = 'qos' +CONF_RETAIN = 'retain' PROTOCOL_31 = '3.1' PROTOCOL_311 = '3.1.1' @@ -55,10 +59,22 @@ ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' ATTR_PAYLOAD_TEMPLATE = 'payload_template' ATTR_QOS = CONF_QOS -ATTR_RETAIN = 'retain' +ATTR_RETAIN = CONF_RETAIN MAX_RECONNECT_WAIT = 300 # seconds + +def valid_subscribe_topic(value, invalid_chars='\0'): + """Validate that we can subscribe using this MQTT topic.""" + if isinstance(value, str) and all(c not in value for c in invalid_chars): + return vol.Length(min=1, max=65535)(value) + raise vol.Invalid('Invalid MQTT topic name') + + +def valid_publish_topic(value): + """Validate that we can publish using this MQTT topic.""" + return valid_subscribe_topic(value, invalid_chars='#+\0') + _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) _HBMQTT_CONFIG_SCHEMA = vol.Schema(dict) @@ -86,19 +102,22 @@ MQTT_BASE_PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, }) +# Sensor type platforms subscribe to mqtt events +MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + +# Switch type platforms publish to mqtt and may subscribe +MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + # Service call validation schema -def valid_subscribe_topic(value, invalid_chars='\0'): - """Validate that we can subscribe using this MQTT topic.""" - if isinstance(value, str) and all(c not in value for c in invalid_chars): - return vol.Length(min=1, max=65535)(value) - raise vol.Invalid('Invalid MQTT topic name') - - -def valid_publish_topic(value): - """Validate that we can publish using this MQTT topic.""" - return valid_subscribe_topic(value, invalid_chars='#+\0') - MQTT_PUBLISH_SCHEMA = vol.Schema({ vol.Required(ATTR_TOPIC): valid_publish_topic, vol.Exclusive(ATTR_PAYLOAD, 'payload'): object, diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/rollershutter/mqtt.py index c7e098d41bd..6465c02dca2 100644 --- a/homeassistant/components/rollershutter/mqtt.py +++ b/homeassistant/components/rollershutter/mqtt.py @@ -11,6 +11,8 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS) from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -18,8 +20,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_STATE_TOPIC = 'state_topic' -CONF_COMMAND_TOPIC = 'command_topic' CONF_PAYLOAD_UP = 'payload_up' CONF_PAYLOAD_DOWN = 'payload_down' CONF_PAYLOAD_STOP = 'payload_stop' @@ -29,14 +29,11 @@ DEFAULT_PAYLOAD_UP = "UP" DEFAULT_PAYLOAD_DOWN = "DOWN" DEFAULT_PAYLOAD_STOP = "STOP" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_PAYLOAD_UP, default=DEFAULT_PAYLOAD_UP): cv.string, vol.Optional(CONF_PAYLOAD_DOWN, default=DEFAULT_PAYLOAD_DOWN): cv.string, vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) @@ -47,7 +44,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): config[CONF_NAME], config.get(CONF_STATE_TOPIC), config[CONF_COMMAND_TOPIC], - config[mqtt.CONF_QOS], + config[CONF_QOS], config[CONF_PAYLOAD_UP], config[CONF_PAYLOAD_DOWN], config[CONF_PAYLOAD_STOP], diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 4b23eeb3d82..eaa856b010e 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -10,6 +10,7 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN +from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers import template @@ -18,16 +19,13 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_STATE_TOPIC = 'state_topic' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' DEFAULT_NAME = "MQTT Sensor" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) @@ -38,7 +36,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): hass, config[CONF_NAME], config[CONF_STATE_TOPIC], - config[mqtt.CONF_QOS], + config[CONF_QOS], config.get(CONF_UNIT_OF_MEASUREMENT), config.get(CONF_VALUE_TEMPLATE), )]) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 92711f83d62..2a2b2aed547 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -11,6 +11,8 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.switch import SwitchDevice from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template @@ -18,9 +20,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_STATE_TOPIC = 'state_topic' -CONF_COMMAND_TOPIC = 'command_topic' -CONF_RETAIN = 'retain' CONF_PAYLOAD_ON = 'payload_on' CONF_PAYLOAD_OFF = 'payload_off' @@ -28,17 +27,12 @@ DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_OPTIMISTIC = False -DEFAULT_RETAIN = False -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) @@ -50,7 +44,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): config[CONF_NAME], config.get(CONF_STATE_TOPIC), config[CONF_COMMAND_TOPIC], - config[mqtt.CONF_QOS], + config[CONF_QOS], config[CONF_RETAIN], config[CONF_PAYLOAD_ON], config[CONF_PAYLOAD_OFF], From 94835235a483c124fbfcb3ef68e76151d978e8d9 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 6 Apr 2016 23:25:28 -0400 Subject: [PATCH 16/17] Config validation for mqtt_eventstream component. --- homeassistant/components/mqtt_eventstream.py | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index a807487c90f..d73beff725f 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -6,9 +6,11 @@ https://home-assistant.io/components/mqtt_eventstream/ """ import json +import voluptuous as vol + import homeassistant.loader as loader -from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN -from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH +from homeassistant.components.mqtt import ( + valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, EVENT_SERVICE_EXECUTED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) @@ -18,12 +20,23 @@ from homeassistant.remote import JSONEncoder DOMAIN = "mqtt_eventstream" DEPENDENCIES = ['mqtt'] +CONF_PUBLISH_TOPIC = 'publish_topic' +CONF_SUBSCRIBE_TOPIC = 'subscribe_topic' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_PUBLISH_TOPIC): valid_publish_topic, + vol.Optional(CONF_SUBSCRIBE_TOPIC): valid_subscribe_topic, + }), +}) + def setup(hass, config): - """Setup th MQTT eventstream component.""" + """Setup the MQTT eventstream component.""" mqtt = loader.get_component('mqtt') - pub_topic = config[DOMAIN].get('publish_topic', None) - sub_topic = config[DOMAIN].get('subscribe_topic', None) + conf = config.get(DOMAIN, {}) + pub_topic = conf.get(CONF_PUBLISH_TOPIC) + sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) def _event_publisher(event): """Handle events by publishing them on the MQTT queue.""" @@ -36,8 +49,8 @@ def setup(hass, config): # to the MQTT topic, or you will end up in an infinite loop. if event.event_type == EVENT_CALL_SERVICE: if ( - event.data.get('domain') == MQTT_DOMAIN and - event.data.get('service') == MQTT_SVC_PUBLISH and + event.data.get('domain') == mqtt.DOMAIN and + event.data.get('service') == mqtt.SERVICE_PUBLISH and event.data[ATTR_SERVICE_DATA].get('topic') == pub_topic ): return From 3bc06ac79e5d09465dc431f067c083ca3f772877 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Thu, 7 Apr 2016 00:42:56 -0400 Subject: [PATCH 17/17] We need to allow extra keys on the top-level component config. --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt_eventstream.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e78496f1e06..07e030a8883 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -93,7 +93,7 @@ CONFIG_SCHEMA = vol.Schema({ [PROTOCOL_31, PROTOCOL_311], vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA, }), -}) +}, extra=vol.ALLOW_EXTRA) MQTT_BASE_PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): DOMAIN, diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index d73beff725f..293b644da1f 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -28,7 +28,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_PUBLISH_TOPIC): valid_publish_topic, vol.Optional(CONF_SUBSCRIBE_TOPIC): valid_subscribe_topic, }), -}) +}, extra=vol.ALLOW_EXTRA) def setup(hass, config):