diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 943f5c69cbe..a4c71168f4e 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/sensor.py b/homeassistant/components/fully_kiosk/sensor.py new file mode 100644 index 00000000000..6b3ca3fbcb0 --- /dev/null +++ b/homeassistant/components/fully_kiosk/sensor.py @@ -0,0 +1,138 @@ +"""Fully Kiosk Browser sensor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_MEGABYTES, PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +def round_storage(value: int) -> float: + """Convert storage values from bytes to megabytes.""" + return round(value * 0.000001, 1) + + +@dataclass +class FullySensorEntityDescription(SensorEntityDescription): + """Fully Kiosk Browser sensor description.""" + + state_fn: Callable | None = None + + +SENSORS: tuple[FullySensorEntityDescription, ...] = ( + FullySensorEntityDescription( + key="batteryLevel", + name="Battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="screenOrientation", + name="Screen orientation", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="foregroundApp", + name="Foreground app", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="currentPage", + name="Current page", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="internalStorageFreeSpace", + name="Internal storage free space", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="internalStorageTotalSpace", + name="Internal storage total space", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="ramFreeMemory", + name="Free memory", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="ramTotalMemory", + name="Total memory", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser sensor.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + async_add_entities( + FullySensor(coordinator, description) + for description in SENSORS + if description.key in coordinator.data + ) + + +class FullySensor(FullyKioskEntity, SensorEntity): + """Representation of a Fully Kiosk Browser sensor.""" + + entity_description: FullySensorEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + sensor: FullySensorEntityDescription, + ) -> None: + """Initialize the sensor entity.""" + self.entity_description = sensor + + self._attr_unique_id = f"{coordinator.data['deviceID']}-{sensor.key}" + + super().__init__(coordinator) + + @property + def native_value(self) -> Any: + """Return the state of the sensor.""" + if (value := self.coordinator.data.get(self.entity_description.key)) is None: + return None + + if self.entity_description.state_fn is not None: + return self.entity_description.state_fn(value) + + return value diff --git a/tests/components/fully_kiosk/test_sensor.py b/tests/components/fully_kiosk/test_sensor.py new file mode 100644 index 00000000000..b54627f85b2 --- /dev/null +++ b/tests/components/fully_kiosk/test_sensor.py @@ -0,0 +1,156 @@ +"""Test the Fully Kiosk Browser sensors.""" +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError + +from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_sensors_sensors( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.amazon_fire_battery") + assert state + assert state.state == "100" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Battery" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_battery") + assert entry + assert entry.unique_id == "abcdef-123456-batteryLevel" + + state = hass.states.get("sensor.amazon_fire_screen_orientation") + assert state + assert state.state == "90" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Screen orientation" + + entry = entity_registry.async_get("sensor.amazon_fire_screen_orientation") + assert entry + assert entry.unique_id == "abcdef-123456-screenOrientation" + + state = hass.states.get("sensor.amazon_fire_foreground_app") + assert state + assert state.state == "de.ozerov.fully" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Foreground app" + + entry = entity_registry.async_get("sensor.amazon_fire_foreground_app") + assert entry + assert entry.unique_id == "abcdef-123456-foregroundApp" + + state = hass.states.get("sensor.amazon_fire_current_page") + assert state + assert state.state == "https://homeassistant.local" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Current page" + + entry = entity_registry.async_get("sensor.amazon_fire_current_page") + assert entry + assert entry.unique_id == "abcdef-123456-currentPage" + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == "11675.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Amazon Fire Internal storage free space" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_internal_storage_free_space") + assert entry + assert entry.unique_id == "abcdef-123456-internalStorageFreeSpace" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_internal_storage_total_space") + assert state + assert state.state == "12938.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Amazon Fire Internal storage total space" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_internal_storage_total_space") + assert entry + assert entry.unique_id == "abcdef-123456-internalStorageTotalSpace" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_free_memory") + assert state + assert state.state == "362.4" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Free memory" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_free_memory") + assert entry + assert entry.unique_id == "abcdef-123456-ramFreeMemory" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_total_memory") + assert state + assert state.state == "1440.1" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Total memory" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_total_memory") + assert entry + assert entry.unique_id == "abcdef-123456-ramTotalMemory" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + # Test unknown/missing data + mock_fully_kiosk.getDeviceInfo.return_value = {} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == STATE_UNKNOWN + + # Test failed update + mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == STATE_UNAVAILABLE