"""Webhook tests for mobile_app.""" from binascii import unhexlify from http import HTTPStatus import json from unittest.mock import ANY, patch from nacl.encoding import Base64Encoder from nacl.secret import SecretBox import pytest from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN from homeassistant.components.tag import EVENT_TAG_SCANNED from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( CONF_WEBHOOK_ID, STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE from tests.common import async_capture_events, async_mock_service from tests.components.conversation import MockAgent @pytest.fixture async def homeassistant(hass): """Load the homeassistant integration.""" await async_setup_component(hass, "homeassistant", {}) def encrypt_payload(secret_key, payload, encode_json=True): """Return a encrypted payload given a key and dictionary of data.""" prepped_key = unhexlify(secret_key) if encode_json: payload = json.dumps(payload) payload = payload.encode("utf-8") return ( SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") ) def encrypt_payload_legacy(secret_key, payload, encode_json=True): """Return a encrypted payload given a key and dictionary of data.""" keylen = SecretBox.KEY_SIZE prepped_key = secret_key.encode("utf-8") prepped_key = prepped_key[:keylen] prepped_key = prepped_key.ljust(keylen, b"\0") if encode_json: payload = json.dumps(payload) payload = payload.encode("utf-8") return ( SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") ) def decrypt_payload(secret_key, encrypted_data): """Return a decrypted payload given a key and a string of encrypted data.""" prepped_key = unhexlify(secret_key) decrypted_data = SecretBox(prepped_key).decrypt( encrypted_data, encoder=Base64Encoder ) decrypted_data = decrypted_data.decode("utf-8") return json.loads(decrypted_data) def decrypt_payload_legacy(secret_key, encrypted_data): """Return a decrypted payload given a key and a string of encrypted data.""" keylen = SecretBox.KEY_SIZE prepped_key = secret_key.encode("utf-8") prepped_key = prepped_key[:keylen] prepped_key = prepped_key.ljust(keylen, b"\0") decrypted_data = SecretBox(prepped_key).decrypt( encrypted_data, encoder=Base64Encoder ) decrypted_data = decrypted_data.decode("utf-8") return json.loads(decrypted_data) async def test_webhook_handle_render_template( create_registrations, webhook_client ) -> None: """Test that we render templates properly.""" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "render_template", "data": { "one": {"template": "Hello world"}, "two": {"template": "{{ now() | random }}"}, "three": {"template": "{{ now() 3 }}"}, }, }, ) assert resp.status == HTTPStatus.OK json = await resp.json() assert json == { "one": "Hello world", "two": {"error": "TypeError: object of type 'datetime.datetime' has no len()"}, "three": { "error": "TemplateSyntaxError: expected token 'end of print statement', got 'integer'" }, } async def test_webhook_handle_call_services( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test that we call services properly.""" calls = async_mock_service(hass, "test", "mobile_app") resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json=CALL_SERVICE, ) assert resp.status == HTTPStatus.OK assert len(calls) == 1 async def test_webhook_handle_fire_event( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test that we can fire events.""" events = [] @callback def store_event(event): """Help store events.""" events.append(event) hass.bus.async_listen("test_event", store_event) resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json=FIRE_EVENT ) assert resp.status == HTTPStatus.OK json = await resp.json() assert json == {} assert len(events) == 1 assert events[0].data["hello"] == "yo world" async def test_webhook_update_registration(webhook_client) -> None: """Test that a we can update an existing registration via webhook.""" register_resp = await webhook_client.post( "/api/mobile_app/registrations", json=REGISTER_CLEARTEXT ) assert register_resp.status == HTTPStatus.CREATED register_json = await register_resp.json() webhook_id = register_json[CONF_WEBHOOK_ID] update_container = {"type": "update_registration", "data": UPDATE} update_resp = await webhook_client.post( f"/api/webhook/{webhook_id}", json=update_container ) assert update_resp.status == HTTPStatus.OK update_json = await update_resp.json() assert update_json["app_version"] == "2.0.0" assert CONF_WEBHOOK_ID not in update_json assert CONF_SECRET not in update_json async def test_webhook_handle_get_zones( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test that we can get zones properly.""" # Zone is already loaded as part of the fixture, # so we just trigger a reload. with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ ZONE_DOMAIN: [ { "name": "School", "latitude": 32.8773367, "longitude": -117.2494053, "radius": 250, "icon": "mdi:school", }, { "name": "Work", "latitude": 33.8773367, "longitude": -118.2494053, }, ] }, ): await hass.services.async_call(ZONE_DOMAIN, "reload", blocking=True) resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={"type": "get_zones"}, ) assert resp.status == HTTPStatus.OK json = await resp.json() assert len(json) == 3 zones = sorted(json, key=lambda entry: entry["entity_id"]) assert zones[0]["entity_id"] == "zone.home" assert zones[1]["entity_id"] == "zone.school" assert zones[1]["attributes"]["icon"] == "mdi:school" assert zones[1]["attributes"]["latitude"] == 32.8773367 assert zones[1]["attributes"]["longitude"] == -117.2494053 assert zones[1]["attributes"]["radius"] == 250 assert zones[2]["entity_id"] == "zone.work" assert "icon" not in zones[2]["attributes"] assert zones[2]["attributes"]["latitude"] == 33.8773367 assert zones[2]["attributes"]["longitude"] == -118.2494053 async def test_webhook_handle_get_config( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test that we can get config properly.""" 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 json = await resp.json() if "components" in json: json["components"] = set(json["components"]) if "allowlist_external_dirs" in json: json["allowlist_external_dirs"] = set(json["allowlist_external_dirs"]) hass_config = hass.config.as_dict() expected_dict = { "latitude": hass_config["latitude"], "longitude": hass_config["longitude"], "elevation": hass_config["elevation"], "unit_system": hass_config["unit_system"], "location_name": hass_config["location_name"], "time_zone": hass_config["time_zone"], "components": set(hass_config["components"]), "version": hass_config["version"], "theme_color": ANY, "entities": { "mock-device-id": {"disabled": False}, "battery-state-id": {"disabled": False}, "battery-charging-id": {"disabled": True}, }, } assert expected_dict == json async def test_webhook_returns_error_incorrect_json( webhook_client, create_registrations, caplog: pytest.LogCaptureFixture ) -> None: """Test that an error is returned when JSON is invalid.""" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), data="not json" ) assert resp.status == HTTPStatus.BAD_REQUEST json = await resp.json() assert json == {} assert "invalid JSON" in caplog.text @pytest.mark.parametrize( ("msg", "generate_response"), [ (RENDER_TEMPLATE, lambda hass: {"one": "Hello world"}), ( {"type": "get_zones", "data": {}}, lambda hass: [hass.states.get("zone.home").as_dict()], ), ], ) async def test_webhook_handle_decryption( hass: HomeAssistant, webhook_client, create_registrations, msg, generate_response ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] data = encrypt_payload(key, msg["data"]) container = {"type": msg["type"], "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert "encrypted_data" in webhook_json decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) assert decrypted_data == generate_response(hass) async def test_webhook_handle_decryption_legacy( webhook_client, create_registrations ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert "encrypted_data" in webhook_json decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) assert decrypted_data == {"one": "Hello world"} async def test_webhook_handle_decryption_fail( webhook_client, create_registrations, caplog: pytest.LogCaptureFixture ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] # Send valid data data = encrypt_payload(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) assert decrypted_data == {"one": "Hello world"} caplog.clear() # Send invalid JSON data data = encrypt_payload(key, "{not_valid", encode_json=False) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK assert await resp.json() == {} assert "Ignoring invalid JSON in encrypted payload" in caplog.text caplog.clear() # Break the key, and send JSON data data = encrypt_payload(key[::-1], RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK assert await resp.json() == {} assert "Ignoring encrypted payload because unable to decrypt" in caplog.text async def test_webhook_handle_decryption_legacy_fail( webhook_client, create_registrations, caplog: pytest.LogCaptureFixture ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] # Send valid data using legacy method data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) assert decrypted_data == {"one": "Hello world"} caplog.clear() # Send invalid JSON data data = encrypt_payload_legacy(key, "{not_valid", encode_json=False) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK assert await resp.json() == {} assert "Ignoring invalid JSON in encrypted payload" in caplog.text caplog.clear() # Break the key, and send JSON data data = encrypt_payload_legacy(key[::-1], RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK assert await resp.json() == {} assert "Ignoring encrypted payload because unable to decrypt" in caplog.text async def test_webhook_handle_decryption_legacy_upgrade( webhook_client, create_registrations ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] # Send using legacy method data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert "encrypted_data" in webhook_json decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) assert decrypted_data == {"one": "Hello world"} # Send using new method data = encrypt_payload(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert "encrypted_data" in webhook_json decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) assert decrypted_data == {"one": "Hello world"} # Send using legacy method - no longer possible data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) assert resp.status == HTTPStatus.OK assert await resp.json() == {} async def test_webhook_requires_encryption( webhook_client, create_registrations ) -> None: """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=RENDER_TEMPLATE, ) assert resp.status == HTTPStatus.BAD_REQUEST webhook_json = await resp.json() assert "error" in webhook_json assert webhook_json["success"] is False assert webhook_json["error"]["code"] == "encryption_required" async def test_webhook_update_location_without_locations( hass: HomeAssistant, webhook_client, create_registrations ) -> None: """Test that location can be updated.""" # start off with a location set by name resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "update_location", "data": {"location_name": STATE_HOME}, }, ) assert resp.status == HTTPStatus.OK state = hass.states.get("device_tracker.test_1_2") assert state is not None assert state.state == STATE_HOME # set location to an 'unknown' state resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "update_location", "data": {"altitude": 123}, }, ) assert resp.status == HTTPStatus.OK state = hass.states.get("device_tracker.test_1_2") assert state is not None assert state.state == STATE_UNKNOWN assert state.attributes["altitude"] == 123 async def test_webhook_update_location_with_gps( hass: HomeAssistant, webhook_client, create_registrations ) -> None: """Test that location can be updated.""" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "update_location", "data": {"gps": [1, 2], "gps_accuracy": 10, "altitude": -10}, }, ) assert resp.status == HTTPStatus.OK state = hass.states.get("device_tracker.test_1_2") assert state is not None assert state.attributes["latitude"] == 1.0 assert state.attributes["longitude"] == 2.0 assert state.attributes["gps_accuracy"] == 10 assert state.attributes["altitude"] == -10 async def test_webhook_update_location_with_gps_without_accuracy( hass: HomeAssistant, webhook_client, create_registrations ) -> None: """Test that location can be updated.""" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "update_location", "data": {"gps": [1, 2]}, }, ) assert resp.status == HTTPStatus.OK state = hass.states.get("device_tracker.test_1_2") assert state.state == STATE_UNKNOWN async def test_webhook_update_location_with_location_name( hass: HomeAssistant, webhook_client, create_registrations ) -> None: """Test that location can be updated.""" with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ ZONE_DOMAIN: [ { "name": "zone_name", "latitude": 1.23, "longitude": -4.56, "radius": 200, "icon": "mdi:test-tube", }, ] }, ): await hass.services.async_call(ZONE_DOMAIN, "reload", blocking=True) resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "update_location", "data": {"location_name": "zone_name"}, }, ) assert resp.status == HTTPStatus.OK state = hass.states.get("device_tracker.test_1_2") assert state.state == "zone_name" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "update_location", "data": {"location_name": STATE_HOME}, }, ) assert resp.status == HTTPStatus.OK state = hass.states.get("device_tracker.test_1_2") assert state.state == STATE_HOME resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "update_location", "data": {"location_name": STATE_NOT_HOME}, }, ) assert resp.status == HTTPStatus.OK state = hass.states.get("device_tracker.test_1_2") assert state.state == STATE_NOT_HOME async def test_webhook_enable_encryption( hass: HomeAssistant, webhook_client, create_registrations ) -> None: """Test that encryption can be added to a reg initially created without.""" webhook_id = create_registrations[1]["webhook_id"] enable_enc_resp = await webhook_client.post( f"/api/webhook/{webhook_id}", json={"type": "enable_encryption"}, ) assert enable_enc_resp.status == HTTPStatus.OK enable_enc_json = await enable_enc_resp.json() assert len(enable_enc_json) == 1 assert CONF_SECRET in enable_enc_json key = enable_enc_json["secret"] enc_required_resp = await webhook_client.post( f"/api/webhook/{webhook_id}", json=RENDER_TEMPLATE, ) assert enc_required_resp.status == HTTPStatus.BAD_REQUEST enc_required_json = await enc_required_resp.json() assert "error" in enc_required_json assert enc_required_json["success"] is False assert enc_required_json["error"]["code"] == "encryption_required" enc_data = encrypt_payload(key, RENDER_TEMPLATE["data"]) container = { "type": "render_template", "encrypted": True, "encrypted_data": enc_data, } enc_resp = await webhook_client.post(f"/api/webhook/{webhook_id}", json=container) assert enc_resp.status == HTTPStatus.OK enc_json = await enc_resp.json() assert "encrypted_data" in enc_json decrypted_data = decrypt_payload(key, enc_json["encrypted_data"]) assert decrypted_data == {"one": "Hello world"} async def test_webhook_camera_stream_non_existent( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test fetching camera stream URLs for a non-existent camera.""" webhook_id = create_registrations[1]["webhook_id"] resp = await webhook_client.post( f"/api/webhook/{webhook_id}", json={ "type": "stream_camera", "data": {"camera_entity_id": "camera.doesnt_exist"}, }, ) assert resp.status == HTTPStatus.BAD_REQUEST webhook_json = await resp.json() assert webhook_json["success"] is False async def test_webhook_camera_stream_non_hls( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test fetching camera stream URLs for a non-HLS/stream-supporting camera.""" hass.states.async_set("camera.non_stream_camera", "idle", {"supported_features": 0}) webhook_id = create_registrations[1]["webhook_id"] resp = await webhook_client.post( f"/api/webhook/{webhook_id}", json={ "type": "stream_camera", "data": {"camera_entity_id": "camera.non_stream_camera"}, }, ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert webhook_json["hls_path"] is None assert ( webhook_json["mjpeg_path"] == "/api/camera_proxy_stream/camera.non_stream_camera" ) async def test_webhook_camera_stream_stream_available( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test fetching camera stream URLs for an HLS/stream-supporting camera.""" hass.states.async_set( "camera.stream_camera", "idle", {"supported_features": CameraEntityFeature.STREAM}, ) webhook_id = create_registrations[1]["webhook_id"] with patch( "homeassistant.components.camera.async_request_stream", return_value="/api/streams/some_hls_stream", ): resp = await webhook_client.post( f"/api/webhook/{webhook_id}", json={ "type": "stream_camera", "data": {"camera_entity_id": "camera.stream_camera"}, }, ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert webhook_json["hls_path"] == "/api/streams/some_hls_stream" assert webhook_json["mjpeg_path"] == "/api/camera_proxy_stream/camera.stream_camera" async def test_webhook_camera_stream_stream_available_but_errors( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test fetching camera stream URLs for an HLS/stream-supporting camera but that streaming errors.""" hass.states.async_set( "camera.stream_camera", "idle", {"supported_features": CameraEntityFeature.STREAM}, ) webhook_id = create_registrations[1]["webhook_id"] with patch( "homeassistant.components.camera.async_request_stream", side_effect=HomeAssistantError(), ): resp = await webhook_client.post( f"/api/webhook/{webhook_id}", json={ "type": "stream_camera", "data": {"camera_entity_id": "camera.stream_camera"}, }, ) assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert webhook_json["hls_path"] is None assert webhook_json["mjpeg_path"] == "/api/camera_proxy_stream/camera.stream_camera" async def test_webhook_handle_scan_tag( hass: HomeAssistant, device_registry: dr.DeviceRegistry, create_registrations, webhook_client, ) -> None: """Test that we can scan tags.""" device = device_registry.async_get_device(identifiers={(DOMAIN, "mock-device-id")}) assert device is not None events = async_capture_events(hass, EVENT_TAG_SCANNED) resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={"type": "scan_tag", "data": {"tag_id": "mock-tag-id"}}, ) assert resp.status == HTTPStatus.OK json = await resp.json() assert json == {} assert len(events) == 1 assert events[0].data["tag_id"] == "mock-tag-id" assert events[0].data["device_id"] == device.id async def test_register_sensor_limits_state_class( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test that we limit state classes to sensors only.""" 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": 100, "type": "sensor", "state_class": "total", "unique_id": "abcd", }, }, ) assert reg_resp.status == HTTPStatus.CREATED reg_resp = await webhook_client.post( webhook_url, json={ "type": "register_sensor", "data": { "name": "Battery State", "state": 100, "type": "binary_sensor", "state_class": "total", "unique_id": "efgh", }, }, ) # This means it was ignored. assert reg_resp.status == HTTPStatus.OK async def test_reregister_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, create_registrations, webhook_client, ) -> None: """Test that we can add more info in re-registration.""" 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": 100, "type": "sensor", "unique_id": "abcd", }, }, ) assert reg_resp.status == HTTPStatus.CREATED entry = entity_registry.async_get("sensor.test_1_battery_state") assert entry.original_name == "Test 1 Battery State" assert entry.device_class is None 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, json={ "type": "register_sensor", "data": { "name": "New Name", "state": 100, "type": "sensor", "unique_id": "abcd", "state_class": "measurement", "device_class": "battery", "entity_category": "diagnostic", "icon": "mdi:new-icon", "unit_of_measurement": "%", "disabled": True, }, }, ) assert reg_resp.status == HTTPStatus.CREATED entry = entity_registry.async_get("sensor.test_1_battery_state") assert entry.original_name == "Test 1 New Name" assert entry.device_class == "battery" 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 = entity_registry.async_get("sensor.test_1_battery_state") assert entry.disabled_by is None reg_resp = await webhook_client.post( webhook_url, json={ "type": "register_sensor", "data": { "name": "New Name 2", "state": 100, "type": "sensor", "unique_id": "abcd", "state_class": None, "device_class": None, "entity_category": None, "icon": None, "unit_of_measurement": None, }, }, ) assert reg_resp.status == HTTPStatus.CREATED entry = entity_registry.async_get("sensor.test_1_battery_state") assert entry.original_name == "Test 1 New Name 2" assert entry.device_class is None assert entry.unit_of_measurement is None assert entry.entity_category is None assert entry.original_icon is None async def test_webhook_handle_conversation_process( hass: HomeAssistant, homeassistant, create_registrations, webhook_client, mock_conversation_agent: MockAgent, ) -> None: """Test that we can converse.""" webhook_client.server.app.router._frozen = False with patch( "homeassistant.components.conversation.agent_manager.async_get_agent", return_value=mock_conversation_agent, ): resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ "type": "conversation_process", "data": { "text": "Turn the kitchen light off", }, }, ) assert resp.status == HTTPStatus.OK json = await resp.json() assert json == { "response": { "response_type": "action_done", "card": {}, "speech": { "plain": { "extra_data": None, "speech": "Test response", } }, "language": hass.config.language, "data": { "targets": [], "success": [], "failed": [], }, }, "conversation_id": None, } async def test_sending_sensor_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, create_registrations, webhook_client, caplog: pytest.LogCaptureFixture, ) -> None: """Test that we can register and send sensor state as number and None.""" 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": 100, "type": "sensor", "unique_id": "abcd", }, }, ) assert reg_resp.status == HTTPStatus.CREATED reg_resp = await webhook_client.post( webhook_url, json={ "type": "register_sensor", "data": { "name": "Battery Health", "state": "good", "type": "sensor", "unique_id": "health-id", }, }, ) assert reg_resp.status == HTTPStatus.CREATED entry = entity_registry.async_get("sensor.test_1_battery_state") assert entry.original_name == "Test 1 Battery State" assert entry.device_class is None 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 await hass.async_block_till_done() state = hass.states.get("sensor.test_1_battery_state") assert state is not None assert state.state == "100" state = hass.states.get("sensor.test_1_battery_health") assert state is not None assert state.state == "good" # Now with a list. reg_resp = await webhook_client.post( webhook_url, json={ "type": "update_sensor_states", "data": [ { "state": 50.0000, "type": "sensor", "unique_id": "abcd", }, { "state": "okay-ish", "type": "sensor", "unique_id": "health-id", }, ], }, ) await hass.async_block_till_done() state = hass.states.get("sensor.test_1_battery_state") assert state is not None assert state.state == "50.0" state = hass.states.get("sensor.test_1_battery_health") assert state is not None assert state.state == "okay-ish"