Add modernized fitbit battery level sensor (#102500)

* Add modernized fitbit battery level sensor

* Use entity names for existing battery sensors

* Use icon from device class

* Update homeassistant/components/fitbit/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update tests with lower case naming

* Swap the names of the device battery sensors

* Revert "Swap the names of the device battery sensors"

This reverts commit c9516f6d06d77a27c5d7528cdbaa69153546bbbb.

* Update homeassistant/components/fitbit/sensor.py

Co-authored-by: Jan Vaníček <janvanicek@post.cz>

* Improve typing

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Jan Vaníček <janvanicek@post.cz>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Allen Porter 2023-11-02 02:22:27 -07:00 committed by GitHub
parent 76115ce766
commit fe482af561
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 119 additions and 9 deletions

View File

@ -39,6 +39,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResultType
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@ -504,10 +505,20 @@ SLEEP_START_TIME_12HR = FitbitSensorEntityDescription(
FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription(
key="devices/battery",
name="Battery",
translation_key="battery",
icon="mdi:battery",
scope=FitbitScope.DEVICE,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
)
FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription(
key="devices/battery_level",
translation_key="battery_level",
scope=FitbitScope.DEVICE,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
)
FITBIT_RESOURCES_KEYS: Final[list[str]] = [
@ -678,7 +689,7 @@ async def async_setup_entry(
async_add_entities(entities)
if data.device_coordinator and is_allowed_resource(FITBIT_RESOURCE_BATTERY):
async_add_entities(
battery_entities: list[SensorEntity] = [
FitbitBatterySensor(
data.device_coordinator,
user_profile.encoded_id,
@ -687,7 +698,17 @@ async def async_setup_entry(
enable_default_override=is_explicit_enable(FITBIT_RESOURCE_BATTERY),
)
for device in data.device_coordinator.data.values()
]
battery_entities.extend(
FitbitBatteryLevelSensor(
data.device_coordinator,
user_profile.encoded_id,
FITBIT_RESOURCE_BATTERY_LEVEL,
device=device,
)
for device in data.device_coordinator.data.values()
)
async_add_entities(battery_entities)
class FitbitSensor(SensorEntity):
@ -742,8 +763,8 @@ class FitbitSensor(SensorEntity):
self.async_schedule_update_ha_state(force_refresh=True)
class FitbitBatterySensor(CoordinatorEntity, SensorEntity):
"""Implementation of a Fitbit sensor."""
class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEntity):
"""Implementation of a Fitbit battery sensor."""
entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
@ -760,10 +781,12 @@ class FitbitBatterySensor(CoordinatorEntity, SensorEntity):
super().__init__(coordinator)
self.entity_description = description
self.device = device
self._attr_unique_id = f"{user_profile_id}_{description.key}"
if device is not None:
self._attr_name = f"{device.device_version} Battery"
self._attr_unique_id = f"{self._attr_unique_id}_{device.id}"
self._attr_unique_id = f"{user_profile_id}_{description.key}_{device.id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{user_profile_id}_{device.id}")},
name=device.device_version,
model=device.device_version,
)
if enable_default_override:
self._attr_entity_registry_enabled_default = True
@ -794,3 +817,42 @@ class FitbitBatterySensor(CoordinatorEntity, SensorEntity):
self.device = self.coordinator.data[self.device.id]
self._attr_native_value = self.device.battery
self.async_write_ha_state()
class FitbitBatteryLevelSensor(
CoordinatorEntity[FitbitDeviceCoordinator], SensorEntity
):
"""Implementation of a Fitbit battery level sensor."""
entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
def __init__(
self,
coordinator: FitbitDeviceCoordinator,
user_profile_id: str,
description: FitbitSensorEntityDescription,
device: FitbitDevice,
) -> None:
"""Initialize the Fitbit sensor."""
super().__init__(coordinator)
self.entity_description = description
self.device = device
self._attr_unique_id = f"{user_profile_id}_{description.key}_{device.id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{user_profile_id}_{device.id}")},
name=device.device_version,
model=device.device_version,
)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass update state from existing coordinator data."""
await super().async_added_to_hass()
self._handle_coordinator_update()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.device = self.coordinator.data[self.device.id]
self._attr_native_value = self.device.battery_level
self.async_write_ha_state()

View File

@ -28,6 +28,16 @@
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
},
"entity": {
"sensor": {
"battery": {
"name": "Battery"
},
"battery_level": {
"name": "Battery level"
}
}
},
"issues": {
"deprecated_yaml_no_import": {
"title": "Fitbit YAML configuration is being removed",

View File

@ -241,7 +241,7 @@ async def test_sensors(
("devices_response", "monitored_resources"),
[([DEVICE_RESPONSE_CHARGE_2, DEVICE_RESPONSE_ARIA_AIR], ["devices/battery"])],
)
async def test_device_battery_level(
async def test_device_battery(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
@ -285,6 +285,43 @@ async def test_device_battery_level(
assert entry.unique_id == f"{PROFILE_USER_ID}_devices/battery_016713257"
@pytest.mark.parametrize(
("devices_response", "monitored_resources"),
[([DEVICE_RESPONSE_CHARGE_2, DEVICE_RESPONSE_ARIA_AIR], ["devices/battery"])],
)
async def test_device_battery_level(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
entity_registry: er.EntityRegistry,
) -> None:
"""Test battery level sensor for devices."""
assert await sensor_platform_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
state = hass.states.get("sensor.charge_2_battery_level")
assert state
assert state.state == "60"
assert state.attributes == {
"attribution": "Data provided by Fitbit.com",
"friendly_name": "Charge 2 Battery level",
"device_class": "battery",
"unit_of_measurement": "%",
}
state = hass.states.get("sensor.aria_air_battery_level")
assert state
assert state.state == "95"
assert state.attributes == {
"attribution": "Data provided by Fitbit.com",
"friendly_name": "Aria Air Battery level",
"device_class": "battery",
"unit_of_measurement": "%",
}
@pytest.mark.parametrize(
(
"monitored_resources",
@ -558,6 +595,7 @@ async def test_settings_scope_config_entry(
states = hass.states.async_all()
assert [s.entity_id for s in states] == [
"sensor.charge_2_battery",
"sensor.charge_2_battery_level",
]