Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MQTT WS command to remove device #31989

Merged
merged 3 commits into from Feb 25, 2020

Conversation

emontnemery
Copy link
Contributor

Proposed change

Add MQTT WS command to remove a device from device registry.

When removing the device:

  • Any entities belonging to the device are removed from entity registry
  • Any MQTT device trigger is forgotten
  • Retained discovery topics are cleared
    • As a result of clearing the discovery topics, states are also removed

Possible extensions (in this or separate follow-up PRs):

  • It's already possible to remove a discovered MQTT entity from the state machine by sending an empty discovery topic. Maybe an empty discovery topic should also trigger removal from the entity registry, and if there are no more entities or MQTT device triggers, remove the device as well?
  • Remove the state from the state machine when an entity is removed from the entity registry

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • The code has been formatted using Black (black --fast homeassistant tests)
  • Tests have been added to verify that the new code works.

The integration reached or maintains the following Integration Quality Scale:

  • No score or internal
  • 🥈 Silver
  • 🥇 Gold
  • 🏆 Platinum

@probot-home-assistant
Copy link

Hey there @home-assistant/core, mind taking a look at this pull request as its been labeled with a integration (mqtt) you are listed as a codeowner for? Thanks!

@@ -1247,6 +1265,18 @@ def device_info(self):
return device_info_from_config(self._device_config)


@websocket_api.websocket_command(
{vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's nothing specific to MQTT in the implementation, should this maybe be moved elsewhere?

async def async_removed_from_registry(self) -> None:
"""Clear retained discovery topic in broker."""
async_publish(
self.hass, self._discovery_topic, "", retain=True,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will also cause the state to be removed from the state machine.

@@ -534,6 +540,9 @@ def async_on_remove(self, func: CALLBACK_TYPE) -> None:
async def _async_registry_updated(self, event):
"""Handle entity registry update."""
data = event.data
if data["action"] == "remove" and data["entity_id"] == self.entity_id:
await self.async_removed_from_registry()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could call self.async_remove() here as well to remove the state from the state machine, but that should probably be done in a separate PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So can you document/explain to me how delete changes cascade throughout the MQTT system?

These two things can happen:

  • A user indicates that an MQTT device needs to be removed
  • An empty discovery configuration is pushed to a topic that we previously got a configuration from.

These two cases need to be handled and we should document this in the MQTT integration somewhere how this works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creation / Discovery of MQTT entities / devices by MQTT:

Handled by https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mqtt/discovery.py
When receiving a discovery message, a state is created in the Hass state machine, or updated if it already exists.
(In case of an MQTT device trigger, no state is created).
If unique_id is set in the config data, an entity registry entry will be created, or updated if it already exists.
If device dict is set in the config data, a device registry will be created, or updated if it already exists.

Discovered MQTT enitities will listen for discovery updates - which must be sent on the same MQTT topic - with help of the MqttDiscoveryUpdated mixin:
https://github.com/home-assistant/home-assistant/blob/e170a11547b716de8cfe46a99473f4dbfcb7f62b/homeassistant/components/mqtt/__init__.py#L1159-L1160

Update of MQTT entities / devices by MQTT:

Handled by the MqttDiscoveryUpdated mixin:
https://github.com/home-assistant/home-assistant/blob/e170a11547b716de8cfe46a99473f4dbfcb7f62b/homeassistant/components/mqtt/__init__.py#L1187-L1190

Removal of MQTT entities / devices by MQTT:

If an empty discovery message is received, this will result in:

Removal of MQTT entities / devices by WS:

A WS helper exists to fully remove an MQTT device, which will result in:

self._discovery_hash = (
discovery_data[ATTR_DISCOVERY_HASH] if discovery_data else None
)
self._discovery_topic = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why keep two variables around if you can just store discovery_data ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, cleaned up.

{vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str}
)
@websocket_api.async_response
async def websocket_remove_device(hass, connection, msg):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also remove the retained discovery config from MQTT.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""Delete device."""
device_id = msg["device_id"]
dev_registry = await get_dev_reg(hass)
dev_registry.async_remove_device(device_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can raise a KeyError. We should also only delete devices that are part of MQTT.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine if it throws, this will result in an error response to the WS call?
Added check to only delete devices that are part of MQTT:
https://github.com/home-assistant/home-assistant/blob/e170a11547b716de8cfe46a99473f4dbfcb7f62b/homeassistant/components/mqtt/__init__.py#L1277-L1279

@@ -98,15 +98,14 @@

async def async_discover(discovery_payload):
"""Discover and add an MQTT alarm control panel."""
discovery_data = discovery_payload.__discovery_data__
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there double underscores before/after the name ? Those are reserved for system values.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add _discovery_data_. But why not make it _discovery_data_?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, OK, changed to discovery_data.

if config_entry.domain == DOMAIN:
dev_registry.async_remove_device(device_id)
connection.send_message(websocket_api.result_message(msg["id"]))
break
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not sending an error if you can't find the device .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but isn't it enough to not send success?
Trying to remove a non existing device is tested in this test case:
https://github.com/home-assistant/home-assistant/blob/e170a11547b716de8cfe46a99473f4dbfcb7f62b/tests/components/mqtt/test_init.py#L881

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. This will cause the test to never finish if you're awaiting a response.

@codecov
Copy link

codecov bot commented Feb 20, 2020

Codecov Report

Merging #31989 into dev will increase coverage by 0.00%.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##              dev   #31989   +/-   ##
=======================================
  Coverage   94.69%   94.70%           
=======================================
  Files         766      766           
  Lines       55567    55613   +46     
=======================================
+ Hits        52619    52667   +48     
+ Misses       2948     2946    -2     
Impacted Files Coverage Δ
homeassistant/components/template/cover.py 96.34% <0.00%> (-1.37%) ⬇️
homeassistant/bootstrap.py 75.40% <0.00%> (ø) ⬆️
homeassistant/components/vizio/config_flow.py 100.00% <0.00%> (ø) ⬆️
...omeassistant/components/homematicip_cloud/cover.py 100.00% <0.00%> (ø) ⬆️
...meassistant/components/homematicip_cloud/sensor.py 100.00% <0.00%> (ø) ⬆️
...sistant/components/input_number/reproduce_state.py 100.00% <0.00%> (ø) ⬆️
homeassistant/__init__.py 89.02% <0.00%> (+0.56%) ⬆️
homeassistant/components/uk_transport/sensor.py 94.20% <0.00%> (+0.72%) ⬆️
setup.py 88.02% <0.00%> (+1.79%) ⬆️
...meassistant/components/websocket_api/connection.py 97.22% <0.00%> (+2.77%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 32cd58e...1d6c5e1. Read the comment docs.

Dev automation moved this from Needs review to Reviewer approved Feb 25, 2020
@balloob balloob merged commit 7e387f9 into home-assistant:dev Feb 25, 2020
Dev automation moved this from Reviewer approved to Done Feb 25, 2020
@lock lock bot locked and limited conversation to collaborators Feb 28, 2020
@emontnemery emontnemery deleted the mqtt_remove_device branch March 19, 2020 16:00
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
Dev
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

3 participants