Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add modernized fitbit battery level sensor #102500

Merged
merged 9 commits into from
Nov 2, 2023
78 changes: 70 additions & 8 deletions homeassistant/components/fitbit/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
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
Expand Down Expand Up @@ -503,10 +504,20 @@

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",
allenporter marked this conversation as resolved.
Show resolved Hide resolved
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]] = [
Expand Down Expand Up @@ -657,7 +668,7 @@
async_add_entities(entities, True)

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,
Expand All @@ -666,7 +677,17 @@
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):
Expand Down Expand Up @@ -713,8 +734,8 @@
self._attr_native_value = self.entity_description.value_fn(result)


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
Expand All @@ -731,10 +752,12 @@
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}"
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -765,3 +788,42 @@
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

Check warning on line 799 in homeassistant/components/fitbit/sensor.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/fitbit/sensor.py#L799

Added line #L799 was not covered by tests

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()
10 changes: 10 additions & 0 deletions homeassistant/components/fitbit/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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",
Expand Down
40 changes: 39 additions & 1 deletion tests/components/fitbit/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,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]],
Expand Down Expand Up @@ -272,6 +272,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",
Expand Down Expand Up @@ -552,6 +589,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",
]


Expand Down