diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index d0d939ce67e..336a6620035 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -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() diff --git a/homeassistant/components/fitbit/strings.json b/homeassistant/components/fitbit/strings.json index 889b56f1bbd..7e85e232099 100644 --- a/homeassistant/components/fitbit/strings.json +++ b/homeassistant/components/fitbit/strings.json @@ -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", diff --git a/tests/components/fitbit/test_sensor.py b/tests/components/fitbit/test_sensor.py index 5421a652125..9aa6f633e63 100644 --- a/tests/components/fitbit/test_sensor.py +++ b/tests/components/fitbit/test_sensor.py @@ -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", ]