Dynamically add sensors for new measurements in Withings (#102022)

* Dynamically add sensors for new data points in Withings

* Dynamically add sensors for new data points in Withings

* Add test

* Change docstring

* Store new measurements

* Fix feedback

* Add test back

* Add test back

* Add test back
This commit is contained in:
Joost Lekkerkerker 2023-10-15 18:00:52 +02:00 committed by GitHub
parent 05ee28cae5
commit dcb5faa305
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 25 deletions

View File

@ -57,8 +57,10 @@ class WithingsMeasurementSensorEntityDescription(
"""Immutable class for describing withings data."""
MEASUREMENT_SENSORS = [
WithingsMeasurementSensorEntityDescription(
MEASUREMENT_SENSORS: dict[
MeasurementType, WithingsMeasurementSensorEntityDescription
] = {
MeasurementType.WEIGHT: WithingsMeasurementSensorEntityDescription(
key="weight_kg",
measurement_type=MeasurementType.WEIGHT,
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
@ -66,7 +68,7 @@ MEASUREMENT_SENSORS = [
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.FAT_MASS_WEIGHT: WithingsMeasurementSensorEntityDescription(
key="fat_mass_kg",
measurement_type=MeasurementType.FAT_MASS_WEIGHT,
translation_key="fat_mass",
@ -75,7 +77,7 @@ MEASUREMENT_SENSORS = [
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.FAT_FREE_MASS: WithingsMeasurementSensorEntityDescription(
key="fat_free_mass_kg",
measurement_type=MeasurementType.FAT_FREE_MASS,
translation_key="fat_free_mass",
@ -84,7 +86,7 @@ MEASUREMENT_SENSORS = [
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.MUSCLE_MASS: WithingsMeasurementSensorEntityDescription(
key="muscle_mass_kg",
measurement_type=MeasurementType.MUSCLE_MASS,
translation_key="muscle_mass",
@ -93,7 +95,7 @@ MEASUREMENT_SENSORS = [
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.BONE_MASS: WithingsMeasurementSensorEntityDescription(
key="bone_mass_kg",
measurement_type=MeasurementType.BONE_MASS,
translation_key="bone_mass",
@ -103,7 +105,7 @@ MEASUREMENT_SENSORS = [
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.HEIGHT: WithingsMeasurementSensorEntityDescription(
key="height_m",
measurement_type=MeasurementType.HEIGHT,
translation_key="height",
@ -113,14 +115,14 @@ MEASUREMENT_SENSORS = [
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.TEMPERATURE: WithingsMeasurementSensorEntityDescription(
key="temperature_c",
measurement_type=MeasurementType.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.BODY_TEMPERATURE: WithingsMeasurementSensorEntityDescription(
key="body_temperature_c",
measurement_type=MeasurementType.BODY_TEMPERATURE,
translation_key="body_temperature",
@ -128,7 +130,7 @@ MEASUREMENT_SENSORS = [
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.SKIN_TEMPERATURE: WithingsMeasurementSensorEntityDescription(
key="skin_temperature_c",
measurement_type=MeasurementType.SKIN_TEMPERATURE,
translation_key="skin_temperature",
@ -136,7 +138,7 @@ MEASUREMENT_SENSORS = [
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.FAT_RATIO: WithingsMeasurementSensorEntityDescription(
key="fat_ratio_pct",
measurement_type=MeasurementType.FAT_RATIO,
translation_key="fat_ratio",
@ -144,21 +146,21 @@ MEASUREMENT_SENSORS = [
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.DIASTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription(
key="diastolic_blood_pressure_mmhg",
measurement_type=MeasurementType.DIASTOLIC_BLOOD_PRESSURE,
translation_key="diastolic_blood_pressure",
native_unit_of_measurement=UOM_MMHG,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.SYSTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription(
key="systolic_blood_pressure_mmhg",
measurement_type=MeasurementType.SYSTOLIC_BLOOD_PRESSURE,
translation_key="systolic_blood_pressure",
native_unit_of_measurement=UOM_MMHG,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.HEART_RATE: WithingsMeasurementSensorEntityDescription(
key="heart_pulse_bpm",
measurement_type=MeasurementType.HEART_RATE,
translation_key="heart_pulse",
@ -166,14 +168,14 @@ MEASUREMENT_SENSORS = [
icon="mdi:heart-pulse",
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.SP02: WithingsMeasurementSensorEntityDescription(
key="spo2_pct",
measurement_type=MeasurementType.SP02,
translation_key="spo2",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.HYDRATION: WithingsMeasurementSensorEntityDescription(
key="hydration",
measurement_type=MeasurementType.HYDRATION,
translation_key="hydration",
@ -183,7 +185,7 @@ MEASUREMENT_SENSORS = [
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsMeasurementSensorEntityDescription(
MeasurementType.PULSE_WAVE_VELOCITY: WithingsMeasurementSensorEntityDescription(
key="pulse_wave_velocity",
measurement_type=MeasurementType.PULSE_WAVE_VELOCITY,
translation_key="pulse_wave_velocity",
@ -191,7 +193,7 @@ MEASUREMENT_SENSORS = [
device_class=SensorDeviceClass.SPEED,
state_class=SensorStateClass.MEASUREMENT,
),
]
}
@dataclass
@ -371,11 +373,32 @@ async def async_setup_entry(
measurement_coordinator: WithingsMeasurementDataUpdateCoordinator = hass.data[
DOMAIN
][entry.entry_id][MEASUREMENT_COORDINATOR]
current_measurement_types = set(measurement_coordinator.data.keys())
entities: list[SensorEntity] = []
entities.extend(
WithingsMeasurementSensor(measurement_coordinator, attribute)
for attribute in MEASUREMENT_SENSORS
WithingsMeasurementSensor(
measurement_coordinator, MEASUREMENT_SENSORS[measurement_type]
)
for measurement_type in measurement_coordinator.data
if measurement_type in MEASUREMENT_SENSORS
)
def _async_measurement_listener() -> None:
"""Listen for new measurements and add sensors if they did not exist."""
received_measurement_types = set(measurement_coordinator.data.keys())
new_measurement_types = received_measurement_types - current_measurement_types
if new_measurement_types:
current_measurement_types.update(new_measurement_types)
async_add_entities(
WithingsMeasurementSensor(
measurement_coordinator, MEASUREMENT_SENSORS[measurement_type]
)
for measurement_type in new_measurement_types
)
measurement_coordinator.async_add_listener(_async_measurement_listener)
sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
][SLEEP_COORDINATOR]

View File

@ -14,11 +14,6 @@
"unit": 0,
"value": 71
},
{
"type": 8,
"unit": 0,
"value": 5
},
{
"type": 5,
"unit": 0,

View File

@ -17,6 +17,7 @@ from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_array_fixture,
load_json_object_fixture,
)
@ -103,3 +104,33 @@ async def test_update_updates_incrementally(
assert state is not None
assert state.state == "71"
assert len(withings.get_measurement_in_period.call_args_list) == 1
async def test_update_new_measurement_creates_new_sensor(
hass: HomeAssistant,
withings: AsyncMock,
polling_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test fetching a new measurement will add a new sensor."""
meas_json = load_json_array_fixture("withings/get_meas_1.json")
measurement_groups = [
MeasurementGroup.from_api(measurement) for measurement in meas_json
]
withings.get_measurement_in_period.return_value = measurement_groups
await setup_integration(hass, polling_config_entry, False)
assert hass.states.get("sensor.henk_fat_mass") is None
meas_json = load_json_object_fixture("withings/get_meas.json")
measurement_groups = [
MeasurementGroup.from_api(measurement)
for measurement in meas_json["measuregrps"]
]
withings.get_measurement_in_period.return_value = measurement_groups
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("sensor.henk_fat_mass") is not None