From f0bf7b0def5b8d781b1b27dd07994b82448b7d92 Mon Sep 17 00:00:00 2001 From: Dan Nixon Date: Tue, 2 Jan 2018 02:32:29 +0000 Subject: [PATCH] More support for availability reporting on MQTT components (#11336) * Abstract MQTT availability from individual components - Moved availability topic and payloads to MQTT base schema. - Updated components that already report availability: - Switch - Binary sensor - Cover * Add availability reporting to additional MQTT components - Light - JSON light - Template light - Lock - Fan - HVAC - Sensor - Vacuum - Alarm control panel * Annotate MQTT platform coroutines --- .../components/alarm_control_panel/mqtt.py | 24 ++++--- .../components/binary_sensor/mqtt.py | 49 +++----------- homeassistant/components/camera/mqtt.py | 6 +- homeassistant/components/climate/mqtt.py | 22 +++++-- homeassistant/components/cover/mqtt.py | 34 +++------- homeassistant/components/fan/mqtt.py | 21 ++++-- homeassistant/components/light/mqtt.py | 21 ++++-- homeassistant/components/light/mqtt_json.py | 23 ++++--- .../components/light/mqtt_template.py | 23 ++++--- homeassistant/components/lock/mqtt.py | 21 ++++-- homeassistant/components/mqtt/__init__.py | 51 +++++++++++++++ homeassistant/components/sensor/mqtt.py | 24 ++++--- homeassistant/components/sensor/mqtt_room.py | 6 +- homeassistant/components/switch/mqtt.py | 49 +++----------- homeassistant/components/vacuum/mqtt.py | 23 +++++-- .../alarm_control_panel/test_mqtt.py | 33 +++++++++- tests/components/climate/test_mqtt.py | 26 +++++++- tests/components/fan/test_mqtt.py | 64 +++++++++++++++++++ tests/components/light/test_mqtt.py | 33 +++++++++- tests/components/light/test_mqtt_json.py | 32 +++++++++- tests/components/light/test_mqtt_template.py | 33 +++++++++- tests/components/lock/test_mqtt.py | 35 +++++++++- tests/components/sensor/test_mqtt.py | 30 ++++++++- tests/components/switch/test_mqtt.py | 10 +-- tests/components/vacuum/test_mqtt.py | 30 ++++++++- 25 files changed, 530 insertions(+), 193 deletions(-) create mode 100644 tests/components/fan/test_mqtt.py diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index fca935388c1..a4559160e3b 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -17,7 +17,9 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, CONF_NAME, CONF_CODE) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, + MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -54,15 +56,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_PAYLOAD_DISARM), config.get(CONF_PAYLOAD_ARM_HOME), config.get(CONF_PAYLOAD_ARM_AWAY), - config.get(CONF_CODE))]) + config.get(CONF_CODE), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE))]) -class MqttAlarm(alarm.AlarmControlPanel): +class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" def __init__(self, name, state_topic, command_topic, qos, payload_disarm, - payload_arm_home, payload_arm_away, code): + payload_arm_home, payload_arm_away, code, availability_topic, + payload_available, payload_not_available): """Init the MQTT Alarm Control Panel.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -73,11 +81,11 @@ class MqttAlarm(alarm.AlarmControlPanel): self._payload_arm_away = payload_arm_away self._code = code + @asyncio.coroutine def async_added_to_hass(self): - """Subscribe mqtt events. + """Subscribe mqtt events.""" + yield from super().async_added_to_hass() - This method must be run in the event loop and returns a coroutine. - """ @callback def message_received(topic, payload, qos): """Run when new MQTT message has been received.""" @@ -89,7 +97,7 @@ class MqttAlarm(alarm.AlarmControlPanel): self._state = payload self.async_schedule_update_ha_state() - return mqtt.async_subscribe( + yield from mqtt.async_subscribe( self.hass, self._state_topic, message_received, self._qos) @property diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index c5fba72bde0..983c879338d 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -17,19 +17,15 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_QOS, valid_subscribe_topic) + CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_PAYLOAD_AVAILABLE = 'payload_available' -CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' - DEFAULT_NAME = 'MQTT Binary sensor' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_ON = 'ON' -DEFAULT_PAYLOAD_AVAILABLE = 'online' -DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline' DEPENDENCIES = ['mqtt'] @@ -38,12 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_PAYLOAD_AVAILABLE, - default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, - vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, - default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -70,31 +61,29 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): )]) -class MqttBinarySensor(BinarySensorDevice): +class MqttBinarySensor(MqttAvailability, BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, name, state_topic, availability_topic, device_class, qos, payload_on, payload_off, payload_available, payload_not_available, value_template): """Initialize the MQTT binary sensor.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._state = None self._state_topic = state_topic - self._availability_topic = availability_topic - self._available = True if availability_topic is None else False self._device_class = device_class self._payload_on = payload_on self._payload_off = payload_off - self._payload_available = payload_available - self._payload_not_available = payload_not_available self._qos = qos self._template = value_template + @asyncio.coroutine def async_added_to_hass(self): - """Subscribe mqtt events. + """Subscribe mqtt events.""" + yield from super().async_added_to_hass() - This method must be run in the event loop and returns a coroutine. - """ @callback def state_message_received(topic, payload, qos): """Handle a new received MQTT state message.""" @@ -111,21 +100,6 @@ class MqttBinarySensor(BinarySensorDevice): yield from mqtt.async_subscribe( self.hass, self._state_topic, state_message_received, self._qos) - @callback - def availability_message_received(topic, payload, qos): - """Handle a new received MQTT availability message.""" - if payload == self._payload_available: - self._available = True - elif payload == self._payload_not_available: - self._available = False - - self.async_schedule_update_ha_state() - - if self._availability_topic is not None: - yield from mqtt.async_subscribe( - self.hass, self._availability_topic, - availability_message_received, self._qos) - @property def should_poll(self): """Return the polling state.""" @@ -136,11 +110,6 @@ class MqttBinarySensor(BinarySensorDevice): """Return the name of the binary sensor.""" return self._name - @property - def available(self) -> bool: - """Return if the binary sensor is available.""" - return self._available - @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py index 8d72ec35a28..b7a7510e0eb 100755 --- a/homeassistant/components/camera/mqtt.py +++ b/homeassistant/components/camera/mqtt.py @@ -60,11 +60,9 @@ class MqttCamera(Camera): """Return the name of this camera.""" return self._name + @asyncio.coroutine def async_added_to_hass(self): - """Subscribe MQTT events. - - This method must be run in the event loop and returns a coroutine. - """ + """Subscribe MQTT events.""" @callback def message_received(topic, payload, qos): """Handle new MQTT messages.""" diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index d571ebd39e4..ae71e5a48dc 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -20,8 +20,9 @@ from homeassistant.components.climate import ( SUPPORT_AUX_HEAT) from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) -from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN, - MQTT_BASE_PLATFORM_SCHEMA) +from homeassistant.components.mqtt import ( + CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability) import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) @@ -93,7 +94,7 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -134,19 +135,25 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): STATE_OFF, STATE_OFF, False, config.get(CONF_SEND_IF_OFF), config.get(CONF_PAYLOAD_ON), - config.get(CONF_PAYLOAD_OFF)) + config.get(CONF_PAYLOAD_OFF), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE)) ]) -class MqttClimate(ClimateDevice): +class MqttClimate(MqttAvailability, ClimateDevice): """Representation of a demo climate device.""" def __init__(self, hass, name, topic, qos, retain, mode_list, fan_mode_list, swing_mode_list, target_temperature, away, hold, current_fan_mode, current_swing_mode, current_operation, aux, send_if_off, payload_on, - payload_off): + payload_off, availability_topic, payload_available, + payload_not_available): """Initialize the climate device.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self.hass = hass self._name = name self._topic = topic @@ -169,8 +176,11 @@ class MqttClimate(ClimateDevice): self._payload_on = payload_on self._payload_off = payload_off + @asyncio.coroutine def async_added_to_hass(self): """Handle being added to home assistant.""" + yield from super().async_added_to_hass() + @callback def handle_current_temp_received(topic, payload, qos): """Handle current temperature coming via MQTT.""" diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 0a49679b9c4..9b75f03c232 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -21,8 +21,9 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, - CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + valid_publish_topic, valid_subscribe_topic, MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,8 +38,6 @@ CONF_SET_POSITION_TEMPLATE = 'set_position_template' CONF_PAYLOAD_OPEN = 'payload_open' CONF_PAYLOAD_CLOSE = 'payload_close' CONF_PAYLOAD_STOP = 'payload_stop' -CONF_PAYLOAD_AVAILABLE = 'payload_available' -CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_STATE_OPEN = 'state_open' CONF_STATE_CLOSED = 'state_closed' CONF_TILT_CLOSED_POSITION = 'tilt_closed_value' @@ -52,8 +51,6 @@ DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' DEFAULT_PAYLOAD_CLOSE = 'CLOSE' DEFAULT_PAYLOAD_STOP = 'STOP' -DEFAULT_PAYLOAD_AVAILABLE = 'online' -DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline' DEFAULT_OPTIMISTIC = False DEFAULT_RETAIN = False DEFAULT_TILT_CLOSED_POSITION = 0 @@ -73,16 +70,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_AVAILABILITY_TOPIC, default=None): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string, vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, - vol.Optional(CONF_PAYLOAD_AVAILABLE, - default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, - vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, - default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -98,7 +90,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ default=DEFAULT_TILT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_INVERT_STATE, default=DEFAULT_TILT_INVERT_STATE): cv.boolean, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -143,7 +135,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): )]) -class MqttCover(CoverDevice): +class MqttCover(MqttAvailability, CoverDevice): """Representation of a cover that can be controlled using MQTT.""" def __init__(self, name, state_topic, command_topic, availability_topic, @@ -154,21 +146,19 @@ class MqttCover(CoverDevice): tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, tilt_invert, position_topic, set_position_template): """Initialize the cover.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._position = None self._state = None self._name = name self._state_topic = state_topic self._command_topic = command_topic - self._availability_topic = availability_topic - self._available = True if availability_topic is None else False self._tilt_command_topic = tilt_command_topic self._tilt_status_topic = tilt_status_topic self._qos = qos self._payload_open = payload_open self._payload_close = payload_close self._payload_stop = payload_stop - self._payload_available = payload_available - self._payload_not_available = payload_not_available self._state_open = state_open self._state_closed = state_closed self._retain = retain @@ -186,10 +176,9 @@ class MqttCover(CoverDevice): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe MQTT events. + """Subscribe MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def tilt_updated(topic, payload, qos): """Handle tilt updates.""" @@ -266,11 +255,6 @@ class MqttCover(CoverDevice): """Return the name of the cover.""" return self._name - @property - def available(self) -> bool: - """Return if cover is available.""" - return self._available - @property def is_closed(self): """Return if the cover is closed.""" diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index eed6cf898c1..1ecbb12bcb4 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -15,7 +15,9 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, @@ -72,7 +74,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -111,15 +113,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): }, config.get(CONF_SPEED_LIST), config.get(CONF_OPTIMISTIC), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), )]) -class MqttFan(FanEntity): +class MqttFan(MqttAvailability, FanEntity): """A MQTT fan component.""" def __init__(self, name, topic, templates, qos, retain, payload, - speed_list, optimistic): + speed_list, optimistic, availability_topic, payload_available, + payload_not_available): """Initialize the MQTT fan.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._topic = topic self._qos = qos @@ -143,10 +151,9 @@ class MqttFan(FanEntity): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ templates = {} for key, tpl in list(self._templates.items()): if tpl is None: diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 0348af664a5..f97e37127b1 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -21,7 +21,9 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC) + CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -95,7 +97,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In(VALUES_ON_COMMAND_TYPE), -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -148,16 +150,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_BRIGHTNESS_SCALE), config.get(CONF_WHITE_VALUE_SCALE), config.get(CONF_ON_COMMAND_TYPE), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), )]) -class MqttLight(Light): +class MqttLight(MqttAvailability, Light): """Representation of a MQTT light.""" def __init__(self, name, effect_list, topic, templates, qos, retain, payload, optimistic, brightness_scale, - white_value_scale, on_command_type): + white_value_scale, on_command_type, availability_topic, + payload_available, payload_not_available): """Initialize MQTT light.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._effect_list = effect_list self._topic = topic @@ -208,10 +216,9 @@ class MqttLight(Light): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ templates = {} for key, tpl in list(self._templates.items()): if tpl is None: diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index e3e3f7dafde..3646de977cf 100755 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -21,7 +21,9 @@ from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -66,7 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -97,17 +99,23 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): CONF_FLASH_TIME_SHORT, CONF_FLASH_TIME_LONG ) - } + }, + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE) )]) -class MqttJson(Light): +class MqttJson(MqttAvailability, Light): """Representation of a MQTT JSON light.""" def __init__(self, name, effect_list, topic, qos, retain, optimistic, brightness, color_temp, effect, rgb, white_value, xy, - flash_times): + flash_times, availability_topic, payload_available, + payload_not_available): """Initialize MQTT JSON light.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._effect_list = effect_list self._topic = topic @@ -157,10 +165,9 @@ class MqttJson(Light): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index 6dabedbd444..de0f6d934c6 100755 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -17,7 +17,9 @@ from homeassistant.components.light import ( SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -60,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -95,16 +97,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): }, config.get(CONF_OPTIMISTIC), config.get(CONF_QOS), - config.get(CONF_RETAIN) + config.get(CONF_RETAIN), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), )]) -class MqttTemplate(Light): +class MqttTemplate(MqttAvailability, Light): """Representation of a MQTT Template light.""" def __init__(self, hass, name, effect_list, topics, templates, optimistic, - qos, retain): + qos, retain, availability_topic, payload_available, + payload_not_available): """Initialize a MQTT Template light.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._effect_list = effect_list self._topics = topics @@ -145,10 +153,9 @@ class MqttTemplate(Light): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index b2533145a20..e73e35a9900 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.lock import LockDevice from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) import homeassistant.components.mqtt as mqtt @@ -36,7 +38,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -56,15 +58,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_PAYLOAD_UNLOCK), config.get(CONF_OPTIMISTIC), value_template, + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE) )]) -class MqttLock(LockDevice): +class MqttLock(MqttAvailability, LockDevice): """Representation of a lock that can be toggled using MQTT.""" def __init__(self, name, state_topic, command_topic, qos, retain, - payload_lock, payload_unlock, optimistic, value_template): + payload_lock, payload_unlock, optimistic, value_template, + availability_topic, payload_available, payload_not_available): """Initialize the lock.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._state = False self._name = name self._state_topic = state_topic @@ -78,10 +86,9 @@ class MqttLock(LockDevice): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def message_received(topic, payload, qos): """Handle new MQTT messages.""" diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3a6abec0ddf..def89603b28 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -23,6 +23,7 @@ from homeassistant.loader import bind_hass from homeassistant.helpers import template, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity from homeassistant.util.async import ( run_coroutine_threadsafe, run_callback_threadsafe) from homeassistant.const import ( @@ -59,6 +60,8 @@ CONF_WILL_MESSAGE = 'will_message' CONF_STATE_TOPIC = 'state_topic' CONF_COMMAND_TOPIC = 'command_topic' CONF_AVAILABILITY_TOPIC = 'availability_topic' +CONF_PAYLOAD_AVAILABLE = 'payload_available' +CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_QOS = 'qos' CONF_RETAIN = 'retain' @@ -73,6 +76,8 @@ DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_DISCOVERY = False DEFAULT_DISCOVERY_PREFIX = 'homeassistant' DEFAULT_TLS_PROTOCOL = 'auto' +DEFAULT_PAYLOAD_AVAILABLE = 'online' +DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline' ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' @@ -145,6 +150,14 @@ SCHEMA_BASE = { vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, } +MQTT_AVAILABILITY_SCHEMA = vol.Schema({ + vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_PAYLOAD_AVAILABLE, + default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, + default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, +}) + MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) # Sensor type platforms subscribe to MQTT events @@ -653,3 +666,41 @@ def _match_topic(subscription, topic): reg = re.compile(reg_ex) return reg.match(topic) is not None + + +class MqttAvailability(Entity): + """Mixin used for platforms that report availability.""" + + def __init__(self, availability_topic, qos, payload_available, + payload_not_available): + """Initialize the availability mixin.""" + self._availability_topic = availability_topic + self._availability_qos = qos + self._available = availability_topic is None + self._payload_available = payload_available + self._payload_not_available = payload_not_available + + def async_added_to_hass(self): + """Subscribe mqtt events. + + This method must be run in the event loop and returns a coroutine. + """ + @callback + def availability_message_received(topic, payload, qos): + """Handle a new received MQTT availability message.""" + if payload == self._payload_available: + self._available = True + elif payload == self._payload_not_available: + self._available = False + + self.async_schedule_update_ha_state() + + if self._availability_topic is not None: + yield from async_subscribe( + self.hass, self._availability_topic, + availability_message_received, self. _availability_qos) + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index bf7de94b5d7..f82c87c9ef5 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -11,7 +11,9 @@ from datetime import timedelta import voluptuous as vol from homeassistant.core import callback -from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS +from homeassistant.components.mqtt import ( + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT) @@ -34,7 +36,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -55,15 +57,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), value_template, + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), )]) -class MqttSensor(Entity): +class MqttSensor(MqttAvailability, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, - force_update, expire_after, value_template): + force_update, expire_after, value_template, + availability_topic, payload_available, payload_not_available): """Initialize the sensor.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -74,11 +82,11 @@ class MqttSensor(Entity): self._expire_after = expire_after self._expiration_trigger = None + @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method must be run in the event loop and returns a coroutine. - """ @callback def message_received(topic, payload, qos): """Handle new MQTT messages.""" @@ -102,7 +110,7 @@ class MqttSensor(Entity): self._state = payload self.async_schedule_update_ha_state() - return mqtt.async_subscribe( + yield from mqtt.async_subscribe( self.hass, self._state_topic, message_received, self._qos) @callback diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index 40c6ce7458c..2c0f8eb4d5a 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -80,11 +80,9 @@ class MQTTRoomSensor(Entity): self._distance = None self._updated = None + @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. - - This method must be run in the event loop and returns a coroutine. - """ + """Subscribe to MQTT events.""" @callback def update_state(device_id, room, distance): """Update the sensor state.""" diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 21820b4a015..a4aea1ded9f 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -11,8 +11,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_QOS, - CONF_RETAIN) + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, @@ -24,26 +25,17 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_PAYLOAD_AVAILABLE = 'payload_available' -CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' - DEFAULT_NAME = 'MQTT Switch' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False -DEFAULT_PAYLOAD_AVAILABLE = 'ON' -DEFAULT_PAYLOAD_NOT_AVAILABLE = 'OFF' PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 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_PAYLOAD_AVAILABLE, - default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, - vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, - default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -72,34 +64,31 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): )]) -class MqttSwitch(SwitchDevice): +class MqttSwitch(MqttAvailability, SwitchDevice): """Representation of a switch that can be toggled using MQTT.""" def __init__(self, name, state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, payload_available, payload_not_available, value_template): """Initialize the MQTT switch.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._state = False self._name = name self._state_topic = state_topic self._command_topic = command_topic - self._availability_topic = availability_topic - self._available = True if availability_topic is None else False self._qos = qos self._retain = retain self._payload_on = payload_on self._payload_off = payload_off self._optimistic = optimistic self._template = value_template - self._payload_available = payload_available - self._payload_not_available = payload_not_available @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def state_message_received(topic, payload, qos): """Handle new MQTT state messages.""" @@ -113,16 +102,6 @@ class MqttSwitch(SwitchDevice): self.async_schedule_update_ha_state() - @callback - def availability_message_received(topic, payload, qos): - """Handle new MQTT availability messages.""" - if payload == self._payload_available: - self._available = True - elif payload == self._payload_not_available: - self._available = False - - self.async_schedule_update_ha_state() - if self._state_topic is None: # Force into optimistic mode. self._optimistic = True @@ -131,11 +110,6 @@ class MqttSwitch(SwitchDevice): self.hass, self._state_topic, state_message_received, self._qos) - if self._availability_topic is not None: - yield from mqtt.async_subscribe( - self.hass, self._availability_topic, - availability_message_received, self._qos) - @property def should_poll(self): """Return the polling state.""" @@ -146,11 +120,6 @@ class MqttSwitch(SwitchDevice): """Return the name of the switch.""" return self._name - @property - def available(self) -> bool: - """Return if switch is available.""" - return self._available - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 9929ae46e09..54aea793a22 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -11,6 +11,7 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv +from homeassistant.components.mqtt import MqttAvailability from homeassistant.components.vacuum import ( DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, @@ -135,7 +136,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -187,6 +188,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) + availability_topic = config.get(mqtt.CONF_AVAILABILITY_TOPIC) + payload_available = config.get(mqtt.CONF_PAYLOAD_AVAILABLE) + payload_not_available = config.get(mqtt.CONF_PAYLOAD_NOT_AVAILABLE) + async_add_devices([ MqttVacuum( name, supported_features, qos, retain, command_topic, @@ -196,12 +201,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): charging_topic, charging_template, cleaning_topic, cleaning_template, docked_topic, docked_template, fan_speed_topic, fan_speed_template, set_fan_speed_topic, fan_speed_list, - send_command_topic + send_command_topic, availability_topic, payload_available, + payload_not_available ), ]) -class MqttVacuum(VacuumDevice): +class MqttVacuum(MqttAvailability, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" # pylint: disable=no-self-use @@ -213,8 +219,12 @@ class MqttVacuum(VacuumDevice): charging_topic, charging_template, cleaning_topic, cleaning_template, docked_topic, docked_template, fan_speed_topic, fan_speed_template, set_fan_speed_topic, fan_speed_list, - send_command_topic): + send_command_topic, availability_topic, payload_available, + payload_not_available): """Initialize the vacuum.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) + self._name = name self._supported_features = supported_features self._qos = qos @@ -257,10 +267,9 @@ class MqttVacuum(VacuumDevice): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe MQTT events. + """Subscribe MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def message_received(topic, payload, qos): """Handle new MQTT message.""" diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index 368a43e6113..200978ea1a0 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -4,7 +4,8 @@ import unittest from homeassistant.setup 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) + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, + STATE_UNKNOWN) from homeassistant.components import alarm_control_panel from tests.common import ( @@ -190,3 +191,33 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): alarm_control_panel.alarm_disarm(self.hass, 'abcd') self.hass.block_till_done() self.assertEqual(call_count, self.mock_publish.call_count) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + 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', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + }) + + state = self.hass.states.get('alarm_control_panel.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('alarm_control_panel.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('alarm_control_panel.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 43f90eeee20..4c179fa8042 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -7,7 +7,7 @@ from homeassistant.util.unit_system import ( ) from homeassistant.setup import setup_component from homeassistant.components import climate -from homeassistant.const import STATE_OFF +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, @@ -432,3 +432,27 @@ class TestMQTTClimate(unittest.TestCase): self.mock_publish.mock_calls[-2][1]) state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['availability_topic'] = 'availability-topic' + config['climate']['payload_available'] = 'good' + config['climate']['payload_not_available'] = 'nogood' + + assert setup_component(self.hass, climate.DOMAIN, config) + + state = self.hass.states.get('climate.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('climate.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('climate.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py new file mode 100644 index 00000000000..3846887f21c --- /dev/null +++ b/tests/components/fan/test_mqtt.py @@ -0,0 +1,64 @@ +"""Test MQTT fans.""" +import unittest + +from homeassistant.setup import setup_component +from homeassistant.components import fan +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE + +from tests.common import ( + mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + + +class TestMqttFan(unittest.TestCase): + """Test the MQTT fan platform.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """"Stop everything that was started.""" + self.hass.stop() + + def test_custom_availability_payload(self): + """Test the availability payload.""" + assert setup_component(self.hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'availability_topic': 'availability_topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + }) + + state = self.hass.states.get('fan.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability_topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('fan.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + fire_mqtt_message(self.hass, 'availability_topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('fan.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '1') + self.hass.block_till_done() + + state = self.hass.states.get('fan.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability_topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('fan.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index b074c5d84d8..d6dabaf9a4f 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -142,7 +142,8 @@ import unittest from unittest import mock from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, @@ -794,3 +795,33 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.mock_calls[-4][1]) self.assertEqual(('test_light/bright', 50, 0, False), self.mock_publish.mock_calls[-2][1]) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test_light/set', + 'brightness_command_topic': 'test_light/bright', + 'rgb_command_topic': "test_light/rgb", + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 10bb3f030e9..6bf24f595ac 100755 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -82,7 +82,8 @@ import unittest from homeassistant.setup import setup_component from homeassistant.const import ( - STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES) + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, + ATTR_SUPPORTED_FEATURES) import homeassistant.components.light as light from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, @@ -472,3 +473,32 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) self.assertEqual(255, state.attributes.get('white_value')) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 4cda6fc64de..fddb75880cc 100755 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -29,7 +29,8 @@ If your light doesn't support RGB feature, omit `(red|green|blue)_template`. import unittest from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, @@ -463,3 +464,33 @@ class TestLightMQTTTemplate(unittest.TestCase): # effect should not have changed state = self.hass.states.get('light.test') self.assertEqual('rainbow', state.attributes.get('effect')) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index c66ed5f2b26..667908e13fa 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -2,8 +2,8 @@ import unittest from homeassistant.setup import setup_component -from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED, - ATTR_ASSUMED_STATE) +from homeassistant.const import ( + STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.lock as lock from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) @@ -111,3 +111,34 @@ class TestLockMQTT(unittest.TestCase): state = self.hass.states.get('lock.test') self.assertEqual(STATE_UNLOCKED, state.state) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, lock.DOMAIN, { + lock.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'payload_lock': 'LOCK', + 'payload_unlock': 'UNLOCK', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('lock.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('lock.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('lock.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 42136966e13..4f9161a5b7f 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -7,7 +7,7 @@ from unittest.mock import patch import homeassistant.core as ha from homeassistant.setup import setup_component import homeassistant.components.sensor as sensor -from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util from tests.common import mock_mqtt_component, fire_mqtt_message @@ -185,6 +185,34 @@ class TestSensorMQTT(unittest.TestCase): self.hass.block_till_done() self.assertEqual(2, len(events)) + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('sensor.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + def _send_time_changed(self, now): """Send a time changed event.""" self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 21ab1dd31f2..a3118f8ebf0 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -167,22 +167,22 @@ class TestSwitchMQTT(unittest.TestCase): 'availability_topic': 'availability_topic', 'payload_on': 1, 'payload_off': 0, - 'payload_available': 'online', - 'payload_not_available': 'offline' + 'payload_available': 'good', + 'payload_not_available': 'nogood' } }) state = self.hass.states.get('switch.test') self.assertEqual(STATE_UNAVAILABLE, state.state) - fire_mqtt_message(self.hass, 'availability_topic', 'online') + fire_mqtt_message(self.hass, 'availability_topic', 'good') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - fire_mqtt_message(self.hass, 'availability_topic', 'offline') + fire_mqtt_message(self.hass, 'availability_topic', 'nogood') self.hass.block_till_done() state = self.hass.states.get('switch.test') @@ -194,7 +194,7 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_UNAVAILABLE, state.state) - fire_mqtt_message(self.hass, 'availability_topic', 'online') + fire_mqtt_message(self.hass, 'availability_topic', 'good') self.hass.block_till_done() state = self.hass.states.get('switch.test') diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index f4c63d63708..f81a5c849ec 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -6,7 +6,8 @@ from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, ATTR_FAN_SPEED, mqtt) from homeassistant.components.mqtt import CONF_COMMAND_TOPIC -from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME +from homeassistant.const import ( + CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, CONF_NAME) from homeassistant.setup import setup_component from tests.common import ( fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) @@ -197,3 +198,30 @@ class TestVacuumMQTT(unittest.TestCase): state = self.hass.states.get('vacuum.mqtttest') self.assertEqual(STATE_OFF, state.state) self.assertEqual("Stopped", state.attributes.get(ATTR_STATUS)) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.default_config.update({ + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + }) + + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { + vacuum.DOMAIN: self.default_config, + })) + + state = self.hass.states.get('vacuum.mqtttest') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('vacuum.mqtttest') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('vacuum.mqtttest') + self.assertEqual(STATE_UNAVAILABLE, state.state)