From 26e79163677645943ff009b2dbe5922365681c25 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Feb 2021 20:18:03 +0100 Subject: [PATCH] Migrate mobile_app to RestoreEntity (#46391) --- .../components/mobile_app/__init__.py | 6 - .../components/mobile_app/binary_sensor.py | 52 ++-- .../components/mobile_app/config_flow.py | 6 +- homeassistant/components/mobile_app/const.py | 2 - homeassistant/components/mobile_app/entity.py | 55 ++-- .../components/mobile_app/helpers.py | 4 - homeassistant/components/mobile_app/sensor.py | 46 +-- .../components/mobile_app/webhook.py | 30 +- tests/components/mobile_app/conftest.py | 8 - .../mobile_app/test_binary_sensor.py | 271 ++++++++++++++++++ .../{test_entity.py => test_sensor.py} | 14 + tests/components/mobile_app/test_webhook.py | 2 +- 12 files changed, 395 insertions(+), 101 deletions(-) create mode 100644 tests/components/mobile_app/test_binary_sensor.py rename tests/components/mobile_app/{test_entity.py => test_sensor.py} (92%) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 3bc95bf3e05..54fa3398ee2 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -17,11 +17,9 @@ from .const import ( ATTR_MODEL, ATTR_OS_VERSION, CONF_CLOUDHOOK_URL, - DATA_BINARY_SENSOR, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES, - DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY, @@ -40,18 +38,14 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): app_config = await store.async_load() if app_config is None: app_config = { - DATA_BINARY_SENSOR: {}, DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], - DATA_SENSOR: {}, } hass.data[DOMAIN] = { - DATA_BINARY_SENSOR: app_config.get(DATA_BINARY_SENSOR, {}), DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: app_config.get(DATA_DELETED_IDS, []), DATA_DEVICES: {}, - DATA_SENSOR: app_config.get(DATA_SENSOR, {}), DATA_STORE: store, } diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index ae8efc0c113..36897dd9f69 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -2,18 +2,25 @@ from functools import partial from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( + ATTR_DEVICE_NAME, + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, + ATTR_SENSOR_TYPE, ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE, ATTR_SENSOR_UNIQUE_ID, DATA_DEVICES, DOMAIN, ) -from .entity import MobileAppEntity, sensor_id +from .entity import MobileAppEntity, unique_id async def async_setup_entry(hass, config_entry, async_add_entities): @@ -22,13 +29,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): webhook_id = config_entry.data[CONF_WEBHOOK_ID] - for config in hass.data[DOMAIN][ENTITY_TYPE].values(): - if config[CONF_WEBHOOK_ID] != webhook_id: + entity_registry = await er.async_get_registry(hass) + entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) + for entry in entries: + if entry.domain != ENTITY_TYPE or entry.disabled_by: continue - - device = hass.data[DOMAIN][DATA_DEVICES][webhook_id] - - entities.append(MobileAppBinarySensor(config, device, config_entry)) + config = { + ATTR_SENSOR_ATTRIBUTES: {}, + ATTR_SENSOR_DEVICE_CLASS: entry.device_class, + ATTR_SENSOR_ICON: entry.original_icon, + ATTR_SENSOR_NAME: entry.original_name, + ATTR_SENSOR_STATE: None, + ATTR_SENSOR_TYPE: entry.domain, + ATTR_SENSOR_UNIQUE_ID: entry.unique_id, + } + entities.append(MobileAppBinarySensor(config, entry.device_id, config_entry)) async_add_entities(entities) @@ -37,14 +52,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if data[CONF_WEBHOOK_ID] != webhook_id: return - unique_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) - - entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id] - - if "added" in entity: - return - - entity["added"] = True + data[CONF_UNIQUE_ID] = unique_id( + data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID] + ) + data[ + CONF_NAME + ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] @@ -64,3 +77,10 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): def is_on(self): """Return the state of the binary sensor.""" return self._config[ATTR_SENSOR_STATE] + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + + super().async_restore_last_state(last_state) + self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index 08fdecf364d..80b6c8db5e1 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -3,7 +3,7 @@ import uuid from homeassistant import config_entries from homeassistant.components import person -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry as er from .const import ATTR_APP_ID, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN @@ -36,8 +36,8 @@ class MobileAppFlowHandler(config_entries.ConfigFlow): user_input[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") # Register device tracker entity and add to person registering app - ent_reg = await entity_registry.async_get_registry(self.hass) - devt_entry = ent_reg.async_get_or_create( + entity_registry = await er.async_get_registry(self.hass) + devt_entry = entity_registry.async_get_or_create( "device_tracker", DOMAIN, user_input[ATTR_DEVICE_ID], diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index b35468a6fb3..b603e117c4c 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -9,11 +9,9 @@ CONF_REMOTE_UI_URL = "remote_ui_url" CONF_SECRET = "secret" CONF_USER_ID = "user_id" -DATA_BINARY_SENSOR = "binary_sensor" DATA_CONFIG_ENTRIES = "config_entries" DATA_DELETED_IDS = "deleted_ids" DATA_DEVICES = "devices" -DATA_SENSOR = "sensor" DATA_STORE = "store" DATA_NOTIFY = "notify" diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 7a12f617740..748f680da5e 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,57 +1,70 @@ """A entity class for mobile_app.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.restore_state import RestoreEntity from .const import ( - ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON, - ATTR_SENSOR_NAME, + ATTR_SENSOR_STATE, ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, - DOMAIN, SIGNAL_SENSOR_UPDATE, ) from .helpers import device_info -def sensor_id(webhook_id, unique_id): +def unique_id(webhook_id, sensor_unique_id): """Return a unique sensor ID.""" - return f"{webhook_id}_{unique_id}" + return f"{webhook_id}_{sensor_unique_id}" -class MobileAppEntity(Entity): +class MobileAppEntity(RestoreEntity): """Representation of an mobile app entity.""" def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry): - """Initialize the sensor.""" + """Initialize the entity.""" self._config = config self._device = device self._entry = entry self._registration = entry.data - self._sensor_id = sensor_id( - self._registration[CONF_WEBHOOK_ID], config[ATTR_SENSOR_UNIQUE_ID] - ) + self._unique_id = config[CONF_UNIQUE_ID] self._entity_type = config[ATTR_SENSOR_TYPE] self.unsub_dispatcher = None - self._name = f"{entry.data[ATTR_DEVICE_NAME]} {config[ATTR_SENSOR_NAME]}" + self._name = config[CONF_NAME] async def async_added_to_hass(self): """Register callbacks.""" self.unsub_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update ) + state = await self.async_get_last_state() + + if state is None: + return + + self.async_restore_last_state(state) async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" if self.unsub_dispatcher is not None: self.unsub_dispatcher() + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + self._config[ATTR_SENSOR_STATE] = last_state.state + self._config[ATTR_SENSOR_ATTRIBUTES] = { + **last_state.attributes, + **self._config[ATTR_SENSOR_ATTRIBUTES], + } + if ATTR_ICON in last_state.attributes: + self._config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON] + @property def should_poll(self) -> bool: """Declare that this entity pushes its state to HA.""" @@ -80,27 +93,19 @@ class MobileAppEntity(Entity): @property def unique_id(self): """Return the unique ID of this sensor.""" - return self._sensor_id + return self._unique_id @property def device_info(self): """Return device registry information for this entity.""" return device_info(self._registration) - async def async_update(self): - """Get the latest state of the sensor.""" - data = self.hass.data[DOMAIN] - try: - self._config = data[self._entity_type][self._sensor_id] - except KeyError: - return - @callback def _handle_update(self, data): """Handle async event updates.""" - incoming_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) - if incoming_id != self._sensor_id: + incoming_id = unique_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) + if incoming_id != self._unique_id: return - self._config = data + self._config = {**self._config, **data} self.async_write_ha_state() diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 7c5cbd135ed..a9079be4f04 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -25,9 +25,7 @@ from .const import ( ATTR_SUPPORTS_ENCRYPTION, CONF_SECRET, CONF_USER_ID, - DATA_BINARY_SENSOR, DATA_DELETED_IDS, - DATA_SENSOR, DOMAIN, ) @@ -138,9 +136,7 @@ def safe_registration(registration: Dict) -> Dict: def savable_state(hass: HomeAssistantType) -> Dict: """Return a clean object containing things that should be saved.""" return { - DATA_BINARY_SENSOR: hass.data[DOMAIN][DATA_BINARY_SENSOR], DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS], - DATA_SENSOR: hass.data[DOMAIN][DATA_SENSOR], } diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 11e07ed5e79..b09ef86453b 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,19 +1,26 @@ """Sensor platform for mobile_app.""" from functools import partial -from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( + ATTR_DEVICE_NAME, + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, + ATTR_SENSOR_TYPE, ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SENSOR_UOM, DATA_DEVICES, DOMAIN, ) -from .entity import MobileAppEntity, sensor_id +from .entity import MobileAppEntity, unique_id async def async_setup_entry(hass, config_entry, async_add_entities): @@ -22,13 +29,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): webhook_id = config_entry.data[CONF_WEBHOOK_ID] - for config in hass.data[DOMAIN][ENTITY_TYPE].values(): - if config[CONF_WEBHOOK_ID] != webhook_id: + entity_registry = await er.async_get_registry(hass) + entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) + for entry in entries: + if entry.domain != ENTITY_TYPE or entry.disabled_by: continue - - device = hass.data[DOMAIN][DATA_DEVICES][webhook_id] - - entities.append(MobileAppSensor(config, device, config_entry)) + config = { + ATTR_SENSOR_ATTRIBUTES: {}, + ATTR_SENSOR_DEVICE_CLASS: entry.device_class, + ATTR_SENSOR_ICON: entry.original_icon, + ATTR_SENSOR_NAME: entry.original_name, + ATTR_SENSOR_STATE: None, + ATTR_SENSOR_TYPE: entry.domain, + ATTR_SENSOR_UNIQUE_ID: entry.unique_id, + ATTR_SENSOR_UOM: entry.unit_of_measurement, + } + entities.append(MobileAppSensor(config, entry.device_id, config_entry)) async_add_entities(entities) @@ -37,14 +53,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if data[CONF_WEBHOOK_ID] != webhook_id: return - unique_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) - - entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id] - - if "added" in entity: - return - - entity["added"] = True + data[CONF_UNIQUE_ID] = unique_id( + data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID] + ) + data[ + CONF_NAME + ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 043a555b6b7..3044f2df212 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -36,6 +36,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceNotFound from homeassistant.helpers import ( config_validation as cv, device_registry as dr, + entity_registry as er, template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -79,7 +80,6 @@ from .const import ( CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, - DATA_STORE, DOMAIN, ERR_ENCRYPTION_ALREADY_ENABLED, ERR_ENCRYPTION_NOT_AVAILABLE, @@ -95,7 +95,6 @@ from .helpers import ( error_response, registration_context, safe_registration, - savable_state, supports_encryption, webhook_response, ) @@ -415,7 +414,10 @@ async def webhook_register_sensor(hass, config_entry, data): device_name = config_entry.data[ATTR_DEVICE_NAME] unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - existing_sensor = unique_store_key in hass.data[DOMAIN][entity_type] + entity_registry = await er.async_get_registry(hass) + existing_sensor = entity_registry.async_get_entity_id( + entity_type, DOMAIN, unique_store_key + ) data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] @@ -424,16 +426,7 @@ async def webhook_register_sensor(hass, config_entry, data): _LOGGER.debug( "Re-register for %s of existing sensor %s", device_name, unique_id ) - entry = hass.data[DOMAIN][entity_type][unique_store_key] - data = {**entry, **data} - hass.data[DOMAIN][entity_type][unique_store_key] = data - - hass.data[DOMAIN][DATA_STORE].async_delay_save( - lambda: savable_state(hass), DELAY_SAVE - ) - - if existing_sensor: async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data) else: register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" @@ -485,7 +478,10 @@ async def webhook_update_sensor_states(hass, config_entry, data): unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - if unique_store_key not in hass.data[DOMAIN][entity_type]: + entity_registry = await er.async_get_registry(hass) + if not entity_registry.async_get_entity_id( + entity_type, DOMAIN, unique_store_key + ): _LOGGER.error( "Refusing to update %s non-registered sensor: %s", device_name, @@ -498,7 +494,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): } continue - entry = hass.data[DOMAIN][entity_type][unique_store_key] + entry = {CONF_WEBHOOK_ID: config_entry.data[CONF_WEBHOOK_ID]} try: sensor = sensor_schema_full(sensor) @@ -518,16 +514,10 @@ async def webhook_update_sensor_states(hass, config_entry, data): new_state = {**entry, **sensor} - hass.data[DOMAIN][entity_type][unique_store_key] = new_state - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) resp[unique_id] = {"success": True} - hass.data[DOMAIN][DATA_STORE].async_delay_save( - lambda: savable_state(hass), DELAY_SAVE - ) - return webhook_response(resp, registration=config_entry.data) diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 7c611eb1010..db4843c126a 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -7,14 +7,6 @@ from homeassistant.setup import async_setup_component from .const import REGISTER, REGISTER_CLEARTEXT -from tests.common import mock_device_registry - - -@pytest.fixture -def registry(hass): - """Return a configured device registry.""" - return mock_device_registry(hass) - @pytest.fixture async def create_registrations(hass, authed_api_client): diff --git a/tests/components/mobile_app/test_binary_sensor.py b/tests/components/mobile_app/test_binary_sensor.py new file mode 100644 index 00000000000..5ada948a5d6 --- /dev/null +++ b/tests/components/mobile_app/test_binary_sensor.py @@ -0,0 +1,271 @@ +"""Entity tests for mobile_app.""" +from homeassistant.const import STATE_OFF +from homeassistant.helpers import device_registry + + +async def test_sensor(hass, create_registrations, webhook_client): + """Test that sensors can be registered and updated.""" + 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": { + "attributes": {"foo": "bar"}, + "device_class": "plug", + "icon": "mdi:power-plug", + "name": "Is Charging", + "state": True, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + }, + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + + assert entity.attributes["device_class"] == "plug" + assert entity.attributes["icon"] == "mdi:power-plug" + assert entity.attributes["foo"] == "bar" + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Is Charging" + assert entity.state == "on" + + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": False, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + # This invalid data should not invalidate whole request + { + "type": "binary_sensor", + "unique_id": "invalid_state", + "invalid": "data", + }, + ], + }, + ) + + assert update_resp.status == 200 + + json = await update_resp.json() + assert json["invalid_state"]["success"] is False + + updated_entity = hass.states.get("binary_sensor.test_1_is_charging") + assert updated_entity.state == "off" + assert "foo" not in updated_entity.attributes + + dev_reg = await device_registry.async_get_registry(hass) + assert len(dev_reg.devices) == len(create_registrations) + + # Reload to verify state is restored + config_entry = hass.config_entries.async_entries("mobile_app")[1] + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + unloaded_entity = hass.states.get("binary_sensor.test_1_is_charging") + assert unloaded_entity.state == "unavailable" + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + restored_entity = hass.states.get("binary_sensor.test_1_is_charging") + assert restored_entity.state == updated_entity.state + assert restored_entity.attributes == updated_entity.attributes + + +async def test_sensor_must_register(hass, create_registrations, webhook_client): + """Test that sensors must be registered before updating.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + {"state": True, "type": "binary_sensor", "unique_id": "battery_state"} + ], + }, + ) + + assert resp.status == 200 + + json = await resp.json() + assert json["battery_state"]["success"] is False + assert json["battery_state"]["error"]["code"] == "not_registered" + + +async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, caplog): + """Test that a duplicate unique ID in registration updates the sensor.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + payload = { + "type": "register_sensor", + "data": { + "attributes": {"foo": "bar"}, + "device_class": "plug", + "icon": "mdi:power-plug", + "name": "Is Charging", + "state": True, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + } + + reg_resp = await webhook_client.post(webhook_url, json=payload) + + assert reg_resp.status == 201 + + reg_json = await reg_resp.json() + assert reg_json == {"success": True} + await hass.async_block_till_done() + + assert "Re-register" not in caplog.text + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + + assert entity.attributes["device_class"] == "plug" + assert entity.attributes["icon"] == "mdi:power-plug" + assert entity.attributes["foo"] == "bar" + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Is Charging" + assert entity.state == "on" + + payload["data"]["state"] = False + dupe_resp = await webhook_client.post(webhook_url, json=payload) + + assert dupe_resp.status == 201 + dupe_reg_json = await dupe_resp.json() + assert dupe_reg_json == {"success": True} + await hass.async_block_till_done() + + assert "Re-register" in caplog.text + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + + assert entity.attributes["device_class"] == "plug" + assert entity.attributes["icon"] == "mdi:power-plug" + assert entity.attributes["foo"] == "bar" + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Is Charging" + assert entity.state == "off" + + +async def test_register_sensor_no_state(hass, create_registrations, webhook_client): + """Test that sensors can be registered, when there is no (unknown) state.""" + 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": "Is Charging", + "state": None, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + }, + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Is Charging" + assert entity.state == STATE_OFF # Binary sensor defaults to off + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Backup Is Charging", + "type": "binary_sensor", + "unique_id": "backup_is_charging", + }, + }, + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.test_1_backup_is_charging") + assert entity + + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Backup Is Charging" + assert entity.state == STATE_OFF # Binary sensor defaults to off + + +async def test_update_sensor_no_state(hass, create_registrations, webhook_client): + """Test that sensors can be updated, when there is no (unknown) state.""" + 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": "Is Charging", + "state": True, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + }, + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + assert entity.state == "on" + + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + {"state": None, "type": "binary_sensor", "unique_id": "is_charging"} + ], + }, + ) + + assert update_resp.status == 200 + + json = await update_resp.json() + assert json == {"is_charging": {"success": True}} + + updated_entity = hass.states.get("binary_sensor.test_1_is_charging") + assert updated_entity.state == STATE_OFF # Binary sensor defaults to off diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_sensor.py similarity index 92% rename from tests/components/mobile_app/test_entity.py rename to tests/components/mobile_app/test_sensor.py index ba121d766ac..0ba1cf3096d 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_sensor.py @@ -66,10 +66,24 @@ async def test_sensor(hass, create_registrations, webhook_client): updated_entity = hass.states.get("sensor.test_1_battery_state") assert updated_entity.state == "123" + assert "foo" not in updated_entity.attributes dev_reg = await device_registry.async_get_registry(hass) assert len(dev_reg.devices) == len(create_registrations) + # Reload to verify state is restored + config_entry = hass.config_entries.async_entries("mobile_app")[1] + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + unloaded_entity = hass.states.get("sensor.test_1_battery_state") + assert unloaded_entity.state == "unavailable" + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + restored_entity = hass.states.get("sensor.test_1_battery_state") + assert restored_entity.state == updated_entity.state + assert restored_entity.attributes == updated_entity.attributes + async def test_sensor_must_register(hass, create_registrations, webhook_client): """Test that sensors must be registered before updating.""" diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 831c8250d7a..a7dc675b7b7 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -109,7 +109,7 @@ async def test_webhook_handle_fire_event(hass, create_registrations, webhook_cli @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_event", store_event)