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
74 changes: 67 additions & 7 deletions homeassistant/components/fitbit/sensor.py
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 @@ class FitbitSensorEntityDescription(SensorEntityDescription):

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 @@ def is_allowed_resource(description: FitbitSensorEntityDescription) -> bool:
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 @@ def is_allowed_resource(description: FitbitSensorEntityDescription) -> bool:
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 @@ -714,7 +735,7 @@ async def async_update(self) -> None:


class FitbitBatterySensor(CoordinatorEntity, SensorEntity):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
"""Implementation of a Fitbit sensor."""
"""Implementation of a Fitbit battery sensor."""

entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
Expand All @@ -731,10 +752,12 @@ def __init__(
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,40 @@ def _handle_coordinator_update(self) -> None:
self.device = self.coordinator.data[self.device.id]
self._attr_native_value = self.device.battery
self.async_write_ha_state()


class FitbitBatteryLevelSensor(CoordinatorEntity, SensorEntity):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
"""Implementation of a Fitbit battery sensor."""
allenporter marked this conversation as resolved.
Show resolved Hide resolved

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()
10 changes: 10 additions & 0 deletions homeassistant/components/fitbit/strings.json
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
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