Add battery sensor to Elgato (#87680)

This commit is contained in:
Franck Nijhof 2023-02-08 10:22:12 +01:00 committed by GitHub
parent e749521b29
commit 6582367917
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 227 additions and 4 deletions

View file

@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import ElgatoDataUpdateCoordinator
PLATFORMS = [Platform.BUTTON, Platform.LIGHT]
PLATFORMS = [Platform.BUTTON, Platform.LIGHT, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View file

@ -1,7 +1,7 @@
"""DataUpdateCoordinator for Elgato."""
from dataclasses import dataclass
from elgato import Elgato, ElgatoConnectionError, Info, Settings, State
from elgato import BatteryInfo, Elgato, ElgatoConnectionError, Info, Settings, State
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
@ -16,6 +16,7 @@ from .const import DOMAIN, LOGGER, SCAN_INTERVAL
class ElgatoData:
"""Elgato data stored in the DataUpdateCoordinator."""
battery: BatteryInfo | None
info: Info
settings: Settings
state: State
@ -25,6 +26,7 @@ class ElgatoDataUpdateCoordinator(DataUpdateCoordinator[ElgatoData]):
"""Class to manage fetching Elgato data."""
config_entry: ConfigEntry
has_battery: bool | None = None
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
@ -44,7 +46,11 @@ class ElgatoDataUpdateCoordinator(DataUpdateCoordinator[ElgatoData]):
async def _async_update_data(self) -> ElgatoData:
"""Fetch data from the Elgato device."""
try:
if self.has_battery is None:
self.has_battery = await self.client.has_battery()
return ElgatoData(
battery=await self.client.battery() if self.has_battery else None,
info=await self.client.info(),
settings=await self.client.settings(),
state=await self.client.state(),

View file

@ -0,0 +1,94 @@
"""Support for Elgato sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import 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 ElgatoData, ElgatoDataUpdateCoordinator
from .entity import ElgatoEntity
@dataclass
class ElgatoEntityDescriptionMixin:
"""Mixin values for Elgato entities."""
value_fn: Callable[[ElgatoData], float | int | None]
@dataclass
class ElgatoSensorEntityDescription(
SensorEntityDescription, ElgatoEntityDescriptionMixin
):
"""Class describing Elgato sensor entities."""
has_fn: Callable[[ElgatoData], bool] = lambda _: True
SENSORS = [
ElgatoSensorEntityDescription(
key="battery",
name="Battery",
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda x: x.battery is not None,
value_fn=lambda x: x.battery.level if x.battery else None,
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Elgato sensor based on a config entry."""
coordinator: ElgatoDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
ElgatoSensorEntity(
coordinator=coordinator,
description=description,
)
for description in SENSORS
if description.has_fn(coordinator.data)
)
class ElgatoSensorEntity(ElgatoEntity, SensorEntity):
"""Representation of a Elgato sensor."""
entity_description: ElgatoSensorEntityDescription
def __init__(
self,
coordinator: ElgatoDataUpdateCoordinator,
description: ElgatoSensorEntityDescription,
) -> None:
"""Initiate Elgato sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.data.info.serial_number}_{description.key}"
)
@property
def native_value(self) -> float | int | None:
"""Return the sensor value."""
return self.entity_description.value_fn(self.coordinator.data)

View file

@ -2,14 +2,14 @@
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
from elgato import Info, Settings, State
from elgato import BatteryInfo, ElgatoNoBatteryError, Info, Settings, State
import pytest
from homeassistant.components.elgato.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
from tests.common import MockConfigEntry, get_fixture_path, load_fixture
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@ -90,6 +90,17 @@ def mock_elgato(
elgato.settings.return_value = Settings.parse_raw(
load_fixture(f"{device_fixtures}/settings.json", DOMAIN)
)
# This may, or may not, be a battery-powered device
if get_fixture_path(f"{device_fixtures}/battery.json", DOMAIN).exists():
elgato.has_battery.return_value = True
elgato.battery.return_value = BatteryInfo.parse_raw(
load_fixture(f"{device_fixtures}/battery.json", DOMAIN)
)
else:
elgato.has_battery.return_value = False
elgato.battery.side_effect = ElgatoNoBatteryError
yield elgato

View file

@ -0,0 +1,8 @@
{
"powerSource": 1,
"level": 78.57,
"status": 2,
"currentBatteryVoltage": 3860,
"inputChargeVoltage": 4208,
"inputChargeCurrent": 3008
}

View file

@ -0,0 +1,16 @@
{
"productName": "Elgato Key Light Mini",
"hardwareBoardType": 202,
"hardwareRevision": 0.1,
"macAddress": "11:22:33:44:55:66",
"firmwareBuildNumber": 229,
"firmwareVersion": "1.0.4",
"serialNumber": "GW24L1A02987",
"displayName": "Frenck",
"features": ["lights"],
"wifi-info": {
"ssid": "Frenck-IoT",
"frequencyMHz": 2400,
"rssi": -41
}
}

View file

@ -0,0 +1,20 @@
{
"powerOnBehavior": 1,
"powerOnBrightness": 20,
"powerOnTemperature": 230,
"switchOnDurationMs": 100,
"switchOffDurationMs": 300,
"colorChangeDurationMs": 100,
"battery": {
"energySaving": {
"enable": 0,
"minimumBatteryLevel": 15.0,
"disableWifi": 0,
"adjustBrightness": {
"enable": 0,
"brightness": 10.0
}
},
"bypass": 0
}
}

View file

@ -0,0 +1,5 @@
{
"on": 1,
"brightness": 21,
"temperature": 297
}

View file

@ -0,0 +1,63 @@
"""Tests for the Elgato sensor platform."""
import pytest
from homeassistant.components.elgato.const import DOMAIN
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity import EntityCategory
@pytest.mark.parametrize("device_fixtures", ["key-light-mini"])
@pytest.mark.usefixtures(
"device_fixtures",
"entity_registry_enabled_by_default",
"init_integration",
"mock_elgato",
)
async def test_battery_sensor(hass: HomeAssistant) -> None:
"""Test the Elgato battery sensor."""
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.frenck_battery")
assert state
assert state.state == "78.57"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck Battery"
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert not state.attributes.get(ATTR_ICON)
entry = entity_registry.async_get("sensor.frenck_battery")
assert entry
assert entry.unique_id == "GW24L1A02987_battery"
assert entry.entity_category == EntityCategory.DIAGNOSTIC
assert entry.options == {"sensor": {"suggested_display_precision": 0}}
assert entry.device_id
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.configuration_url is None
assert device_entry.connections == {
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
}
assert device_entry.entry_type is None
assert device_entry.identifiers == {(DOMAIN, "GW24L1A02987")}
assert device_entry.manufacturer == "Elgato"
assert device_entry.model == "Elgato Key Light Mini"
assert device_entry.name == "Frenck"
assert device_entry.sw_version == "1.0.4 (229)"
assert device_entry.hw_version == "202"