diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 62f500d9952..b8c8627c038 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -58,6 +58,114 @@ CONFIG_ENTRY_IS_SETUP = 'mqtt_config_entry_is_setup' MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}' MQTT_DISCOVERY_NEW = 'mqtt_discovery_new_{}_{}' +TOPIC_BASE = '~' + +ABBREVIATIONS = { + 'aux_cmd_t': 'aux_command_topic', + 'aux_stat_tpl': 'aux_state_template', + 'aux_stat_t': 'aux_state_topic', + 'avty_t': 'availability_topic', + 'away_mode_cmd_t': 'away_mode_command_topic', + 'away_mode_stat_tpl': 'away_mode_state_template', + 'away_mode_stat_t': 'away_mode_state_topic', + 'bri_cmd_t': 'brightness_command_topic', + 'bri_scl': 'brightness_scale', + 'bri_stat_t': 'brightness_state_topic', + 'bri_val_tpl': 'brightness_value_template', + 'clr_temp_cmd_t': 'color_temp_command_topic', + 'clr_temp_stat_t': 'color_temp_state_topic', + 'clr_temp_val_tpl': 'color_temp_value_template', + 'cmd_t': 'command_topic', + 'curr_temp_t': 'current_temperature_topic', + 'dev_cla': 'device_class', + 'fx_cmd_t': 'effect_command_topic', + 'fx_list': 'effect_list', + 'fx_stat_t': 'effect_state_topic', + 'fx_val_tpl': 'effect_value_template', + 'exp_aft': 'expire_after', + 'fan_mode_cmd_t': 'fan_mode_command_topic', + 'fan_mode_stat_tpl': 'fan_mode_state_template', + 'fan_mode_stat_t': 'fan_mode_state_topic', + 'frc_upd': 'force_update', + 'hold_cmd_t': 'hold_command_topic', + 'hold_stat_tpl': 'hold_state_template', + 'hold_stat_t': 'hold_state_topic', + 'ic': 'icon', + 'init': 'initial', + 'json_attr': 'json_attributes', + 'max_temp': 'max_temp', + 'min_temp': 'min_temp', + 'mode_cmd_t': 'mode_command_topic', + 'mode_stat_tpl': 'mode_state_template', + 'mode_stat_t': 'mode_state_topic', + 'name': 'name', + 'on_cmd_type': 'on_command_type', + 'opt': 'optimistic', + 'osc_cmd_t': 'oscillation_command_topic', + 'osc_stat_t': 'oscillation_state_topic', + 'osc_val_tpl': 'oscillation_value_template', + 'pl_arm_away': 'payload_arm_away', + 'pl_arm_home': 'payload_arm_home', + 'pl_avail': 'payload_available', + 'pl_cls': 'payload_close', + 'pl_disarm': 'payload_disarm', + 'pl_hi_spd': 'payload_high_speed', + 'pl_lock': 'payload_lock', + 'pl_lo_spd': 'payload_low_speed', + 'pl_med_spd': 'payload_medium_speed', + 'pl_not_avail': 'payload_not_available', + 'pl_off': 'payload_off', + 'pl_on': 'payload_on', + 'pl_open': 'payload_open', + 'pl_osc_off': 'payload_oscillation_off', + 'pl_osc_on': 'payload_oscillation_on', + 'pl_stop': 'payload_stop', + 'pl_unlk': 'payload_unlock', + 'pow_cmd_t': 'power_command_topic', + 'ret': 'retain', + 'rgb_cmd_tpl': 'rgb_command_template', + 'rgb_cmd_t': 'rgb_command_topic', + 'rgb_stat_t': 'rgb_state_topic', + 'rgb_val_tpl': 'rgb_value_template', + 'send_if_off': 'send_if_off', + 'set_pos_tpl': 'set_position_template', + 'set_pos_t': 'set_position_topic', + 'spd_cmd_t': 'speed_command_topic', + 'spd_stat_t': 'speed_state_topic', + 'spd_val_tpl': 'speed_value_template', + 'spds': 'speeds', + 'stat_clsd': 'state_closed', + 'stat_off': 'state_off', + 'stat_on': 'state_on', + 'stat_open': 'state_open', + 'stat_t': 'state_topic', + 'stat_val_tpl': 'state_value_template', + 'swing_mode_cmd_t': 'swing_mode_command_topic', + 'swing_mode_stat_tpl': 'swing_mode_state_template', + 'swing_mode_stat_t': 'swing_mode_state_topic', + 'temp_cmd_t': 'temperature_command_topic', + 'temp_stat_tpl': 'temperature_state_template', + 'temp_stat_t': 'temperature_state_topic', + 'tilt_clsd_val': 'tilt_closed_value', + 'tilt_cmd_t': 'tilt_command_topic', + 'tilt_inv_stat': 'tilt_invert_state', + 'tilt_max': 'tilt_max', + 'tilt_min': 'tilt_min', + 'tilt_opnd_val': 'tilt_opened_value', + 'tilt_status_opt': 'tilt_status_optimistic', + 'tilt_status_t': 'tilt_status_topic', + 't': 'topic', + 'uniq_id': 'unique_id', + 'unit_of_meas': 'unit_of_measurement', + 'val_tpl': 'value_template', + 'whit_val_cmd_t': 'white_value_command_topic', + 'whit_val_stat_t': 'white_value_state_topic', + 'whit_val_tpl': 'white_value_template', + 'xy_cmd_t': 'xy_command_topic', + 'xy_stat_t': 'xy_state_topic', + 'xy_val_tpl': 'xy_value_template', +} + async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, config_entry=None) -> bool: @@ -75,6 +183,29 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, _LOGGER.warning("Component %s is not supported", component) return + if payload: + try: + payload = json.loads(payload) + except ValueError: + _LOGGER.warning("Unable to parse JSON %s: '%s'", + object_id, payload) + return + + payload = dict(payload) + + for key in list(payload.keys()): + abbreviated_key = key + key = ABBREVIATIONS.get(key, key) + payload[key] = payload.pop(abbreviated_key) + + if TOPIC_BASE in payload: + base = payload[TOPIC_BASE] + for key, value in payload.items(): + if value[0] == TOPIC_BASE and key.endswith('_topic'): + payload[key] = "{}{}".format(base, value[1:]) + if value[-1] == TOPIC_BASE and key.endswith('_topic'): + payload[key] = "{}{}".format(value[:-1], base) + # If present, the node_id will be included in the discovered object id discovery_id = '_'.join((node_id, object_id)) if node_id else object_id @@ -91,14 +222,6 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), payload) elif payload: # Add component - try: - payload = json.loads(payload) - except ValueError: - _LOGGER.warning("Unable to parse JSON %s: '%s'", - object_id, payload) - return - - payload = dict(payload) platform = payload.get(CONF_PLATFORM, 'mqtt') if platform not in ALLOWED_PLATFORMS.get(component, []): _LOGGER.warning("Platform %s (component %s) is not allowed", diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 36b022de7a6..dd3ab2e6f7a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start, \ ALREADY_DISCOVERED +from homeassistant.const import STATE_ON, STATE_OFF from tests.common import async_fire_mqtt_message, mock_coro, MockConfigEntry @@ -208,3 +209,36 @@ def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert state_duplicate is None assert 'Component has already been discovered: ' \ 'binary_sensor bla' in caplog.text + + +@asyncio.coroutine +def test_discovery_expansion(hass, mqtt_mock, caplog): + """Test expansion of abbreviated discovery payload.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic" }' + ) + + async_fire_mqtt_message( + hass, 'homeassistant/switch/bla/config', data) + yield from hass.async_block_till_done() + + state = hass.states.get('switch.DiscoveryExpansionTest1') + assert state is not None + assert state.name == 'DiscoveryExpansionTest1' + assert ('switch', 'bla') in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, 'test_topic/some/base/topic', + 'ON') + yield from hass.async_block_till_done() + yield from hass.async_block_till_done() + + state = hass.states.get('switch.DiscoveryExpansionTest1') + assert state.state == STATE_ON