diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 0d2c05df518..fc325b1b6e9 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for mobile_app.""" from __future__ import annotations +from datetime import date, datetime from typing import Any from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass @@ -10,6 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from .const import ( @@ -99,7 +101,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement @property - def native_value(self): + def native_value(self) -> StateType | date | datetime: """Return the state of the sensor.""" if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): return None @@ -122,7 +124,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): return state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement this sensor expresses itself in.""" return self._config.get(ATTR_SENSOR_UOM) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 7a86755bc5d..6476b681256 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -22,9 +22,7 @@ from homeassistant.components import ( notify as hass_notify, tag, ) -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES as BINARY_SENSOR_CLASSES, -) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.device_tracker import ( ATTR_BATTERY, @@ -33,10 +31,7 @@ from homeassistant.components.device_tracker import ( ATTR_LOCATION_NAME, ) from homeassistant.components.frontend import MANIFEST_JSON -from homeassistant.components.sensor import ( - DEVICE_CLASSES as SENSOR_CLASSES, - STATE_CLASSES as SENSOSR_STATE_CLASSES, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -58,7 +53,7 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.decorator import Registry from .const import ( @@ -131,8 +126,7 @@ WEBHOOK_COMMANDS: Registry[ str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]] ] = Registry() -COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) -SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] +SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR) WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( { @@ -507,19 +501,27 @@ def _extract_sensor_unique_id(webhook_id: str, unique_id: str) -> str: vol.All( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( - vol.Lower, vol.In(COMBINED_CLASSES) + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.Any( + None, + vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass)), + vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)), ), vol.Required(ATTR_SENSOR_NAME): cv.string, vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Optional(ATTR_SENSOR_UOM): cv.string, + vol.Optional(ATTR_SENSOR_UOM): vol.Any(None, cv.string), vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any( - None, bool, str, int, float + None, bool, int, float, str + ), + vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): vol.Any( + None, vol.Coerce(EntityCategory) + ), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( + None, cv.icon + ), + vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.Any( + None, vol.Coerce(SensorStateClass) ), - 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_DISABLED): bool, }, _validate_state_class_sensor, @@ -619,8 +621,10 @@ async def webhook_update_sensor_states( sensor_schema_full = vol.Schema( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, str, int, float), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( + None, cv.icon + ), + vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, int, float, str), vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, } diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 996471c939f..a6ab5797a11 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -979,6 +979,32 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): entry = ent_reg.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 = ent_reg.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, create_registrations, webhook_client, mock_agent @@ -1017,3 +1043,57 @@ async def test_webhook_handle_conversation_process( }, "conversation_id": None, } + + +async def test_sending_sensor_state(hass, create_registrations, webhook_client, caplog): + """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 + + ent_reg = er.async_get(hass) + entry = ent_reg.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" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": { + "state": 50.0000, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + 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"