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
77 changes: 68 additions & 9 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,19 @@ 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_device_class",
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,16 +667,26 @@ 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(
FitbitBatterySensor(
battery_entities: list[SensorEntity] = [
FitbitBatteryStringSensor(
data.device_coordinator,
user_profile.encoded_id,
FITBIT_RESOURCE_BATTERY,
device=device,
enable_default_override=is_explicit_enable(FITBIT_RESOURCE_BATTERY),
)
for device in data.device_coordinator.data.values()
]
battery_entities.extend(
FitbitBatterySensor(
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 +733,8 @@ async def async_update(self) -> None:
self._attr_native_value = self.entity_description.value_fn(result)


class FitbitBatterySensor(CoordinatorEntity, SensorEntity):
"""Implementation of a Fitbit sensor."""
class FitbitBatteryStringSensor(CoordinatorEntity, SensorEntity):
"""Implementation of a Fitbit battery sensor."""

entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
Expand All @@ -731,10 +751,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 +787,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 FitbitBatterySensor(CoordinatorEntity, SensorEntity):
"""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()
7 changes: 7 additions & 0 deletions homeassistant/components/fitbit/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
},
"entity": {
"sensor": {
"battery": {
"name": "Battery level"
}
}
},
"issues": {
"deprecated_yaml_no_import": {
"title": "Fitbit YAML configuration is being removed",
Expand Down
37 changes: 29 additions & 8 deletions tests/components/fitbit/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,32 +242,52 @@ async def test_device_battery_level(

state = hass.states.get("sensor.charge_2_battery")
assert state
assert state.state == "Medium"
assert state.state == "60"
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
assert state.attributes == {
"attribution": "Data provided by Fitbit.com",
"friendly_name": "Charge 2 Battery",
"device_class": "battery",
"unit_of_measurement": "%",
}

state = hass.states.get("sensor.charge_2_battery_level")
assert state
assert state.state == "Medium"
assert state.attributes == {
"attribution": "Data provided by Fitbit.com",
"friendly_name": "Charge 2 Battery level",
"icon": "mdi:battery-50",
"model": "Charge 2",
"type": "tracker",
}

entry = entity_registry.async_get("sensor.charge_2_battery")
entry = entity_registry.async_get("sensor.charge_2_battery_level")
assert entry
assert entry.unique_id == f"{PROFILE_USER_ID}_devices/battery_816713257"

state = hass.states.get("sensor.aria_air_battery")
assert state
assert state.state == "High"
assert state.state == "95"
assert state.attributes == {
"attribution": "Data provided by Fitbit.com",
"friendly_name": "Aria Air Battery",
"device_class": "battery",
"unit_of_measurement": "%",
}

state = hass.states.get("sensor.aria_air_battery_level")
assert state
assert state.state == "High"
assert state.attributes == {
"attribution": "Data provided by Fitbit.com",
"friendly_name": "Aria Air Battery level",
"icon": "mdi:battery",
"model": "Aria Air",
"type": "scale",
}

entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.aria_air_battery")
entry = entity_registry.async_get("sensor.aria_air_battery_level")
assert entry
assert entry.unique_id == f"{PROFILE_USER_ID}_devices/battery_016713257"

Expand Down Expand Up @@ -550,9 +570,10 @@ async def test_settings_scope_config_entry(
assert await integration_setup()

states = hass.states.async_all()
assert [s.entity_id for s in states] == [
assert {s.entity_id for s in states} == {
"sensor.charge_2_battery",
]
"sensor.charge_2_battery_level",
}


@pytest.mark.parametrize(
Expand Down Expand Up @@ -656,7 +677,7 @@ async def test_device_battery_level_update_failed(

state = hass.states.get("sensor.charge_2_battery")
assert state
assert state.state == "Medium"
assert state.state == "60"

# Request an update for the entity which will fail
await async_update_entity(hass, "sensor.charge_2_battery")
Expand Down Expand Up @@ -706,7 +727,7 @@ async def test_device_battery_level_reauth_required(

state = hass.states.get("sensor.charge_2_battery")
assert state
assert state.state == "Medium"
assert state.state == "60"

# Request an update for the entity which will fail
await async_update_entity(hass, "sensor.charge_2_battery")
Expand Down