From 0b09376360eef91f95c227f4d2b958010729662d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 May 2022 09:05:49 -0700 Subject: [PATCH] Mobile app to notify when sensor is disabled (#71561) * Mobile app to notify when sensor is disabled * Add entity status to get_config * Allow overriding enabled/disabled --- homeassistant/components/mobile_app/const.py | 2 +- homeassistant/components/mobile_app/entity.py | 4 +- .../components/mobile_app/webhook.py | 47 +++++++++++-- tests/components/mobile_app/test_sensor.py | 69 ++++++++++++++++++- tests/components/mobile_app/test_webhook.py | 53 ++++++++++++-- 5 files changed, 162 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index e6a26430f11..efc105a80ea 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -67,7 +67,7 @@ ERR_INVALID_FORMAT = "invalid_format" ATTR_SENSOR_ATTRIBUTES = "attributes" ATTR_SENSOR_DEVICE_CLASS = "device_class" -ATTR_SENSOR_DEFAULT_DISABLED = "default_disabled" +ATTR_SENSOR_DISABLED = "disabled" ATTR_SENSOR_ENTITY_CATEGORY = "entity_category" ATTR_SENSOR_ICON = "icon" ATTR_SENSOR_NAME = "name" diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index b38774d56d6..d4c4374b8d9 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -9,8 +9,8 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_SENSOR_ATTRIBUTES, - ATTR_SENSOR_DEFAULT_DISABLED, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_DISABLED, ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_STATE, @@ -64,7 +64,7 @@ class MobileAppEntity(RestoreEntity): @property def entity_registry_enabled_default(self) -> bool: """Return if entity should be enabled by default.""" - return not self._config.get(ATTR_SENSOR_DEFAULT_DISABLED) + return not self._config.get(ATTR_SENSOR_DISABLED) @property def device_class(self): diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index cdf6fc874d9..97d386691d1 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -64,8 +64,8 @@ from .const import ( ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, - ATTR_SENSOR_DEFAULT_DISABLED, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_DISABLED, ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, @@ -439,6 +439,11 @@ def _gen_unique_id(webhook_id, sensor_unique_id): return f"{webhook_id}_{sensor_unique_id}" +def _extract_sensor_unique_id(webhook_id, unique_id): + """Return a unique sensor ID.""" + return unique_id[len(webhook_id) + 1 :] + + @WEBHOOK_COMMANDS.register("register_sensor") @validate_schema( vol.All( @@ -457,7 +462,7 @@ def _gen_unique_id(webhook_id, sensor_unique_id): vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES), - vol.Optional(ATTR_SENSOR_DEFAULT_DISABLED): bool, + vol.Optional(ATTR_SENSOR_DISABLED): bool, }, _validate_state_class_sensor, ) @@ -490,6 +495,15 @@ async def webhook_register_sensor(hass, config_entry, data): ) != entry.original_name: changes["original_name"] = new_name + if ( + should_be_disabled := data.get(ATTR_SENSOR_DISABLED) + ) is None or should_be_disabled == entry.disabled: + pass + elif should_be_disabled: + changes["disabled_by"] = er.RegistryEntryDisabler.INTEGRATION + else: + changes["disabled_by"] = None + for ent_reg_key, data_key in ( ("device_class", ATTR_SENSOR_DEVICE_CLASS), ("unit_of_measurement", ATTR_SENSOR_UOM), @@ -551,6 +565,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): device_name = config_entry.data[ATTR_DEVICE_NAME] resp = {} + entity_registry = er.async_get(hass) for sensor in data: entity_type = sensor[ATTR_SENSOR_TYPE] @@ -559,9 +574,10 @@ async def webhook_update_sensor_states(hass, config_entry, data): unique_store_key = _gen_unique_id(config_entry.data[CONF_WEBHOOK_ID], unique_id) - entity_registry = er.async_get(hass) - if not entity_registry.async_get_entity_id( - entity_type, DOMAIN, unique_store_key + if not ( + entity_id := entity_registry.async_get_entity_id( + entity_type, DOMAIN, unique_store_key + ) ): _LOGGER.debug( "Refusing to update %s non-registered sensor: %s", @@ -601,6 +617,12 @@ async def webhook_update_sensor_states(hass, config_entry, data): resp[unique_id] = {"success": True} + # Check if disabled + entry = entity_registry.async_get(entity_id) + + if entry.disabled_by: + resp[unique_id]["is_disabled"] = True + return webhook_response(resp, registration=config_entry.data) @@ -637,6 +659,21 @@ async def webhook_get_config(hass, config_entry, data): with suppress(hass.components.cloud.CloudNotAvailable): resp[CONF_REMOTE_UI_URL] = cloud.async_remote_ui_url(hass) + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + + entities = {} + for entry in er.async_entries_for_config_entry( + er.async_get(hass), config_entry.entry_id + ): + if entry.domain in ("binary_sensor", "sensor"): + unique_id = _extract_sensor_unique_id(webhook_id, entry.unique_id) + else: + unique_id = entry.unique_id + + entities[unique_id] = {"disabled": entry.disabled} + + resp["entities"] = entities + return webhook_response(resp, registration=config_entry.data) diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index 301c49381f7..c0f7f126a49 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -355,7 +355,7 @@ async def test_default_disabling_entity(hass, create_registrations, webhook_clie "name": "Battery State", "type": "sensor", "unique_id": "battery_state", - "default_disabled": True, + "disabled": True, }, }, ) @@ -373,3 +373,70 @@ async def test_default_disabling_entity(hass, create_registrations, webhook_clie er.async_get(hass).async_get("sensor.test_1_battery_state").disabled_by == er.RegistryEntryDisabler.INTEGRATION ) + + +async def test_updating_disabled_sensor(hass, create_registrations, webhook_client): + """Test that sensors return error if disabled in instance.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Battery State", + "state": None, + "type": "sensor", + "unique_id": "battery_state", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": 123, + "type": "sensor", + "unique_id": "battery_state", + }, + ], + }, + ) + + assert update_resp.status == HTTPStatus.OK + + json = await update_resp.json() + assert json["battery_state"]["success"] is True + assert "is_disabled" not in json["battery_state"] + + er.async_get(hass).async_update_entity( + "sensor.test_1_battery_state", disabled_by=er.RegistryEntryDisabler.USER + ) + + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": 123, + "type": "sensor", + "unique_id": "battery_state", + }, + ], + }, + ) + + assert update_resp.status == HTTPStatus.OK + + json = await update_resp.json() + assert json["battery_state"]["success"] is True + assert json["battery_state"]["is_disabled"] is True diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 3eac2d97b19..0bc237b1c11 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -254,10 +254,30 @@ async def test_webhook_handle_get_zones(hass, create_registrations, webhook_clie async def test_webhook_handle_get_config(hass, create_registrations, webhook_client): """Test that we can get config properly.""" - resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), - json={"type": "get_config"}, - ) + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + # Create two entities + for sensor in ( + { + "name": "Battery State", + "type": "sensor", + "unique_id": "battery-state-id", + }, + { + "name": "Battery Charging", + "type": "sensor", + "unique_id": "battery-charging-id", + "disabled": True, + }, + ): + reg_resp = await webhook_client.post( + webhook_url, + json={"type": "register_sensor", "data": sensor}, + ) + assert reg_resp.status == HTTPStatus.CREATED + + resp = await webhook_client.post(webhook_url, json={"type": "get_config"}) assert resp.status == HTTPStatus.OK @@ -279,6 +299,11 @@ async def test_webhook_handle_get_config(hass, create_registrations, webhook_cli "components": hass_config["components"], "version": hass_config["version"], "theme_color": "#03A9F4", # Default frontend theme color + "entities": { + "mock-device-id": {"disabled": False}, + "battery-state-id": {"disabled": False}, + "battery-charging-id": {"disabled": True}, + }, } assert expected_dict == json @@ -902,6 +927,7 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): assert entry.unit_of_measurement is None assert entry.entity_category is None assert entry.original_icon == "mdi:cellphone" + assert entry.disabled_by is None reg_resp = await webhook_client.post( webhook_url, @@ -917,6 +943,7 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): "entity_category": "diagnostic", "icon": "mdi:new-icon", "unit_of_measurement": "%", + "disabled": True, }, }, ) @@ -928,3 +955,21 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): assert entry.unit_of_measurement == "%" assert entry.entity_category == "diagnostic" assert entry.original_icon == "mdi:new-icon" + assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "New Name", + "type": "sensor", + "unique_id": "abcd", + "disabled": False, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.disabled_by is None