mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +00:00
Remove discovered MQTT Switch device when discovery topic is cleared (#16605)
* Remove discovered device when discovery topic is cleared * Move entity removal away from mqtt discovery * Move discovery update to mixin class * Add testcase * Review comments
This commit is contained in:
parent
a5cb4e6c2b
commit
5ee4718e24
@ -92,6 +92,7 @@ ATTR_PAYLOAD = 'payload'
|
|||||||
ATTR_PAYLOAD_TEMPLATE = 'payload_template'
|
ATTR_PAYLOAD_TEMPLATE = 'payload_template'
|
||||||
ATTR_QOS = CONF_QOS
|
ATTR_QOS = CONF_QOS
|
||||||
ATTR_RETAIN = CONF_RETAIN
|
ATTR_RETAIN = CONF_RETAIN
|
||||||
|
ATTR_DISCOVERY_HASH = 'discovery_hash'
|
||||||
|
|
||||||
MAX_RECONNECT_WAIT = 300 # seconds
|
MAX_RECONNECT_WAIT = 300 # seconds
|
||||||
|
|
||||||
@ -833,3 +834,36 @@ class MqttAvailability(Entity):
|
|||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return if the device is available."""
|
"""Return if the device is available."""
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
|
|
||||||
|
class MqttDiscoveryUpdate(Entity):
|
||||||
|
"""Mixin used to handle updated discovery message."""
|
||||||
|
|
||||||
|
def __init__(self, discovery_hash) -> None:
|
||||||
|
"""Initialize the discovery update mixin."""
|
||||||
|
self._discovery_hash = discovery_hash
|
||||||
|
self._remove_signal = None
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Subscribe to discovery updates."""
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.components.mqtt.discovery import (
|
||||||
|
ALREADY_DISCOVERED, MQTT_DISCOVERY_UPDATED)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def discovery_callback(payload):
|
||||||
|
"""Handle discovery update."""
|
||||||
|
_LOGGER.info("Got update for entity with hash: %s '%s'",
|
||||||
|
self._discovery_hash, payload)
|
||||||
|
if not payload:
|
||||||
|
# Empty payload: Remove component
|
||||||
|
_LOGGER.info("Removing component: %s", self.entity_id)
|
||||||
|
self.hass.async_create_task(self.async_remove())
|
||||||
|
del self.hass.data[ALREADY_DISCOVERED][self._discovery_hash]
|
||||||
|
self._remove_signal()
|
||||||
|
|
||||||
|
if self._discovery_hash:
|
||||||
|
self._remove_signal = async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
MQTT_DISCOVERY_UPDATED.format(self._discovery_hash),
|
||||||
|
discovery_callback)
|
||||||
|
@ -9,9 +9,10 @@ import logging
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from homeassistant.components import mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.components.mqtt import CONF_STATE_TOPIC
|
from homeassistant.components.mqtt import CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.const import CONF_PLATFORM
|
||||||
from homeassistant.helpers.discovery import async_load_platform
|
from homeassistant.helpers.discovery import async_load_platform
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ ALLOWED_PLATFORMS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ALREADY_DISCOVERED = 'mqtt_discovered_components'
|
ALREADY_DISCOVERED = 'mqtt_discovered_components'
|
||||||
|
MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}'
|
||||||
|
|
||||||
|
|
||||||
async def async_start(hass, discovery_topic, hass_config):
|
async def async_start(hass, discovery_topic, hass_config):
|
||||||
@ -51,47 +53,53 @@ async def async_start(hass, discovery_topic, hass_config):
|
|||||||
|
|
||||||
_prefix_topic, component, node_id, object_id = match.groups()
|
_prefix_topic, component, node_id, object_id = match.groups()
|
||||||
|
|
||||||
try:
|
|
||||||
payload = json.loads(payload)
|
|
||||||
except ValueError:
|
|
||||||
_LOGGER.warning("Unable to parse JSON %s: %s", object_id, payload)
|
|
||||||
return
|
|
||||||
|
|
||||||
if component not in SUPPORTED_COMPONENTS:
|
if component not in SUPPORTED_COMPONENTS:
|
||||||
_LOGGER.warning("Component %s is not supported", component)
|
_LOGGER.warning("Component %s is not supported", component)
|
||||||
return
|
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",
|
|
||||||
platform, component)
|
|
||||||
return
|
|
||||||
|
|
||||||
payload[CONF_PLATFORM] = platform
|
|
||||||
if CONF_STATE_TOPIC not in payload:
|
|
||||||
payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format(
|
|
||||||
discovery_topic, component, '%s/' % node_id if node_id else '',
|
|
||||||
object_id)
|
|
||||||
|
|
||||||
if ALREADY_DISCOVERED not in hass.data:
|
|
||||||
hass.data[ALREADY_DISCOVERED] = set()
|
|
||||||
|
|
||||||
# If present, the node_id will be included in the discovered object id
|
# 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
|
discovery_id = '_'.join((node_id, object_id)) if node_id else object_id
|
||||||
|
|
||||||
|
if ALREADY_DISCOVERED not in hass.data:
|
||||||
|
hass.data[ALREADY_DISCOVERED] = {}
|
||||||
|
|
||||||
discovery_hash = (component, discovery_id)
|
discovery_hash = (component, discovery_id)
|
||||||
|
|
||||||
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
|
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
|
||||||
_LOGGER.info("Component has already been discovered: %s %s",
|
_LOGGER.info(
|
||||||
component, discovery_id)
|
"Component has already been discovered: %s %s, sending update",
|
||||||
return
|
component, discovery_id)
|
||||||
|
async_dispatcher_send(
|
||||||
|
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
|
||||||
|
|
||||||
hass.data[ALREADY_DISCOVERED].add(discovery_hash)
|
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",
|
||||||
|
platform, component)
|
||||||
|
return
|
||||||
|
|
||||||
_LOGGER.info("Found new component: %s %s", component, discovery_id)
|
payload[CONF_PLATFORM] = platform
|
||||||
|
if CONF_STATE_TOPIC not in payload:
|
||||||
|
payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format(
|
||||||
|
discovery_topic, component,
|
||||||
|
'%s/' % node_id if node_id else '', object_id)
|
||||||
|
|
||||||
await async_load_platform(
|
hass.data[ALREADY_DISCOVERED][discovery_hash] = None
|
||||||
hass, component, platform, payload, hass_config)
|
payload[ATTR_DISCOVERY_HASH] = discovery_hash
|
||||||
|
|
||||||
|
_LOGGER.info("Found new component: %s %s", component, discovery_id)
|
||||||
|
|
||||||
|
await async_load_platform(
|
||||||
|
hass, component, platform, payload, hass_config)
|
||||||
|
|
||||||
await mqtt.async_subscribe(
|
await mqtt.async_subscribe(
|
||||||
hass, discovery_topic + '/#', async_device_message_received, 0)
|
hass, discovery_topic + '/#', async_device_message_received, 0)
|
||||||
|
@ -11,9 +11,10 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.mqtt import (
|
from homeassistant.components.mqtt import (
|
||||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC,
|
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
|
CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
|
||||||
MqttAvailability)
|
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability,
|
||||||
|
MqttDiscoveryUpdate)
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF,
|
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF,
|
||||||
@ -56,7 +57,11 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
|
|
||||||
async_add_entities([MqttSwitch(
|
discovery_hash = None
|
||||||
|
if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info:
|
||||||
|
discovery_hash = discovery_info[ATTR_DISCOVERY_HASH]
|
||||||
|
|
||||||
|
newswitch = MqttSwitch(
|
||||||
config.get(CONF_NAME),
|
config.get(CONF_NAME),
|
||||||
config.get(CONF_ICON),
|
config.get(CONF_ICON),
|
||||||
config.get(CONF_STATE_TOPIC),
|
config.get(CONF_STATE_TOPIC),
|
||||||
@ -73,10 +78,13 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||||
config.get(CONF_UNIQUE_ID),
|
config.get(CONF_UNIQUE_ID),
|
||||||
value_template,
|
value_template,
|
||||||
)])
|
discovery_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities([newswitch])
|
||||||
|
|
||||||
|
|
||||||
class MqttSwitch(MqttAvailability, SwitchDevice):
|
class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, SwitchDevice):
|
||||||
"""Representation of a switch that can be toggled using MQTT."""
|
"""Representation of a switch that can be toggled using MQTT."""
|
||||||
|
|
||||||
def __init__(self, name, icon,
|
def __init__(self, name, icon,
|
||||||
@ -84,10 +92,11 @@ class MqttSwitch(MqttAvailability, SwitchDevice):
|
|||||||
qos, retain, payload_on, payload_off, state_on,
|
qos, retain, payload_on, payload_off, state_on,
|
||||||
state_off, optimistic, payload_available,
|
state_off, optimistic, payload_available,
|
||||||
payload_not_available, unique_id: Optional[str],
|
payload_not_available, unique_id: Optional[str],
|
||||||
value_template):
|
value_template, discovery_hash):
|
||||||
"""Initialize the MQTT switch."""
|
"""Initialize the MQTT switch."""
|
||||||
super().__init__(availability_topic, qos, payload_available,
|
MqttAvailability.__init__(self, availability_topic, qos,
|
||||||
payload_not_available)
|
payload_available, payload_not_available)
|
||||||
|
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||||
self._state = False
|
self._state = False
|
||||||
self._name = name
|
self._name = name
|
||||||
self._icon = icon
|
self._icon = icon
|
||||||
@ -102,10 +111,12 @@ class MqttSwitch(MqttAvailability, SwitchDevice):
|
|||||||
self._optimistic = optimistic
|
self._optimistic = optimistic
|
||||||
self._template = value_template
|
self._template = value_template
|
||||||
self._unique_id = unique_id
|
self._unique_id = unique_id
|
||||||
|
self._discovery_hash = discovery_hash
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Subscribe to MQTT events."""
|
"""Subscribe to MQTT events."""
|
||||||
await super().async_added_to_hass()
|
await MqttAvailability.async_added_to_hass(self)
|
||||||
|
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def state_message_received(topic, payload, qos):
|
def state_message_received(topic, payload, qos):
|
||||||
|
@ -181,3 +181,31 @@ def test_non_duplicate_discovery(hass, mqtt_mock, caplog):
|
|||||||
assert state_duplicate is None
|
assert state_duplicate is None
|
||||||
assert 'Component has already been discovered: ' \
|
assert 'Component has already been discovered: ' \
|
||||||
'binary_sensor bla' in caplog.text
|
'binary_sensor bla' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_discovery_removal(hass, mqtt_mock, caplog):
|
||||||
|
"""Test expansion of abbreviated discovery payload."""
|
||||||
|
yield from async_start(hass, 'homeassistant', {})
|
||||||
|
|
||||||
|
data = (
|
||||||
|
'{ "name": "Beer",'
|
||||||
|
' "status_topic": "test_topic",'
|
||||||
|
' "command_topic": "test_topic" }'
|
||||||
|
)
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config',
|
||||||
|
data)
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('switch.beer')
|
||||||
|
assert state is not None
|
||||||
|
assert state.name == 'Beer'
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config',
|
||||||
|
'')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('switch.beer')
|
||||||
|
assert state is None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user