mirror of
https://github.com/home-assistant/core.git
synced 2026-09-08 06:31:35 +01:00
352 lines
13 KiB
Python
352 lines
13 KiB
Python
"""Sensor platform for Specialized Turbo integration."""
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from typing import override
|
|
|
|
from specialized_turbo import AssistLevel, TelemetrySnapshot
|
|
|
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
|
PassiveBluetoothCoordinatorEntity,
|
|
)
|
|
from homeassistant.components.sensor import (
|
|
SensorDeviceClass,
|
|
SensorEntity,
|
|
SensorEntityDescription,
|
|
SensorStateClass,
|
|
)
|
|
from homeassistant.const import (
|
|
CONF_ADDRESS,
|
|
PERCENTAGE,
|
|
REVOLUTIONS_PER_MINUTE,
|
|
EntityCategory,
|
|
UnitOfElectricCurrent,
|
|
UnitOfElectricPotential,
|
|
UnitOfEnergy,
|
|
UnitOfEnergyDistance,
|
|
UnitOfLength,
|
|
UnitOfPower,
|
|
UnitOfSpeed,
|
|
UnitOfTemperature,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.device_registry import (
|
|
CONNECTION_BLUETOOTH,
|
|
DeviceInfo,
|
|
format_mac,
|
|
)
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.helpers.typing import StateType
|
|
|
|
from . import SpecializedTurboConfigEntry
|
|
from .coordinator import SpecializedTurboCoordinator
|
|
|
|
PARALLEL_UPDATES = 0
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class SpecializedSensorEntityDescription(SensorEntityDescription):
|
|
"""Describes a Specialized Turbo sensor entity."""
|
|
|
|
value_fn: Callable[[TelemetrySnapshot], StateType]
|
|
|
|
|
|
def _assist_level_name(snap: TelemetrySnapshot) -> str | None:
|
|
"""Return assist level as a lowercase string, or None if unknown."""
|
|
level = snap.motor.assist_level
|
|
if level is None:
|
|
return None
|
|
if isinstance(level, AssistLevel):
|
|
return level.name.lower()
|
|
return None
|
|
|
|
|
|
SENSOR_DESCRIPTIONS: tuple[SpecializedSensorEntityDescription, ...] = (
|
|
# --- Battery ---
|
|
SpecializedSensorEntityDescription(
|
|
key="battery_charge_percent",
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
device_class=SensorDeviceClass.BATTERY,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
value_fn=lambda s: s.battery.charge_pct,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="battery_capacity_wh",
|
|
translation_key="battery_capacity_wh",
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
value_fn=lambda s: s.battery.capacity_wh,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="battery_remaining_wh",
|
|
translation_key="battery_remaining_wh",
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
value_fn=lambda s: s.battery.remaining_wh,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="battery_health",
|
|
translation_key="battery_health",
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.battery.health_pct,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="battery_temp",
|
|
translation_key="battery_temp",
|
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
device_class=SensorDeviceClass.TEMPERATURE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.battery.temp_c,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="battery_charge_cycles",
|
|
translation_key="battery_charge_cycles",
|
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.battery.charge_cycles,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="battery_voltage",
|
|
translation_key="battery_voltage",
|
|
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
device_class=SensorDeviceClass.VOLTAGE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.battery.voltage_v,
|
|
suggested_display_precision=1,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="battery_current",
|
|
translation_key="battery_current",
|
|
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
device_class=SensorDeviceClass.CURRENT,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.battery.current_a,
|
|
suggested_display_precision=1,
|
|
),
|
|
# --- Motor / Rider ---
|
|
SpecializedSensorEntityDescription(
|
|
key="speed",
|
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
|
device_class=SensorDeviceClass.SPEED,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
value_fn=lambda s: s.motor.speed_kmh,
|
|
suggested_display_precision=1,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="rider_power",
|
|
translation_key="rider_power",
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
device_class=SensorDeviceClass.POWER,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
value_fn=lambda s: s.motor.rider_power_w,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="motor_power",
|
|
translation_key="motor_power",
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
device_class=SensorDeviceClass.POWER,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
value_fn=lambda s: s.motor.motor_power_w,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="cadence",
|
|
translation_key="cadence",
|
|
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
value_fn=lambda s: s.motor.cadence_rpm,
|
|
suggested_display_precision=0,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="odometer",
|
|
translation_key="odometer",
|
|
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
|
device_class=SensorDeviceClass.DISTANCE,
|
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
value_fn=lambda s: s.motor.odometer_km,
|
|
suggested_display_precision=1,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="motor_temp",
|
|
translation_key="motor_temp",
|
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
device_class=SensorDeviceClass.TEMPERATURE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.motor.motor_temp_c,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="assist_level",
|
|
translation_key="assist_level",
|
|
device_class=SensorDeviceClass.ENUM,
|
|
options=["off", "eco", "trail", "turbo"],
|
|
value_fn=_assist_level_name,
|
|
),
|
|
# --- Settings (informational, disabled by default) ---
|
|
SpecializedSensorEntityDescription(
|
|
key="assist_eco_pct",
|
|
translation_key="assist_eco_pct",
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
entity_registry_enabled_default=False,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.settings.assist_lev1_pct,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="assist_trail_pct",
|
|
translation_key="assist_trail_pct",
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
entity_registry_enabled_default=False,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.settings.assist_lev2_pct,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="assist_turbo_pct",
|
|
translation_key="assist_turbo_pct",
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
entity_registry_enabled_default=False,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.settings.assist_lev3_pct,
|
|
),
|
|
# --- System (TCX2+ only, disabled by default) ---
|
|
SpecializedSensorEntityDescription(
|
|
key="range_long",
|
|
translation_key="range_long",
|
|
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
|
device_class=SensorDeviceClass.DISTANCE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_registry_enabled_default=False,
|
|
value_fn=lambda s: s.system.range_long_km,
|
|
suggested_display_precision=1,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="range_short",
|
|
translation_key="range_short",
|
|
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
|
device_class=SensorDeviceClass.DISTANCE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_registry_enabled_default=False,
|
|
value_fn=lambda s: s.system.range_short_km,
|
|
suggested_display_precision=1,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="altitude",
|
|
translation_key="altitude",
|
|
native_unit_of_measurement=UnitOfLength.METERS,
|
|
device_class=SensorDeviceClass.DISTANCE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_registry_enabled_default=False,
|
|
value_fn=lambda s: s.system.altitude_m,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="altitude_gain",
|
|
translation_key="altitude_gain",
|
|
native_unit_of_measurement=UnitOfLength.METERS,
|
|
device_class=SensorDeviceClass.DISTANCE,
|
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
entity_registry_enabled_default=False,
|
|
value_fn=lambda s: s.system.altitude_gain_m,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="gradient",
|
|
translation_key="gradient",
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_registry_enabled_default=False,
|
|
value_fn=lambda s: s.system.gradient_pct,
|
|
suggested_display_precision=1,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="system_temp",
|
|
translation_key="system_temp",
|
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
device_class=SensorDeviceClass.TEMPERATURE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_registry_enabled_default=False,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=lambda s: s.system.system_temp_c,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="consumption",
|
|
translation_key="consumption",
|
|
native_unit_of_measurement=UnitOfEnergyDistance.WATT_HOUR_PER_KM,
|
|
device_class=SensorDeviceClass.ENERGY_DISTANCE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
entity_registry_enabled_default=False,
|
|
value_fn=lambda s: s.system.consumption_wh_km,
|
|
suggested_display_precision=1,
|
|
),
|
|
SpecializedSensorEntityDescription(
|
|
key="kcal",
|
|
translation_key="kcal",
|
|
native_unit_of_measurement=UnitOfEnergy.KILO_CALORIE,
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
entity_registry_enabled_default=False,
|
|
value_fn=lambda s: s.system.kcal,
|
|
),
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: SpecializedTurboConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up Specialized Turbo sensors from a config entry."""
|
|
coordinator = entry.runtime_data
|
|
|
|
async_add_entities(
|
|
SpecializedTurboSensor(coordinator, description, entry)
|
|
for description in SENSOR_DESCRIPTIONS
|
|
)
|
|
|
|
|
|
class SpecializedTurboSensor(
|
|
PassiveBluetoothCoordinatorEntity[SpecializedTurboCoordinator],
|
|
SensorEntity,
|
|
):
|
|
"""One telemetry field from a Specialized Turbo bike."""
|
|
|
|
entity_description: SpecializedSensorEntityDescription
|
|
_attr_has_entity_name = True
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: SpecializedTurboCoordinator,
|
|
description: SpecializedSensorEntityDescription,
|
|
entry: SpecializedTurboConfigEntry,
|
|
) -> None:
|
|
"""Initialize the sensor."""
|
|
super().__init__(coordinator)
|
|
self.entity_description = description
|
|
self._attr_unique_id = (
|
|
f"{format_mac(entry.data[CONF_ADDRESS])}_{description.key}"
|
|
)
|
|
self._attr_device_info = DeviceInfo(
|
|
connections={(CONNECTION_BLUETOOTH, format_mac(entry.data[CONF_ADDRESS]))},
|
|
manufacturer="Specialized",
|
|
model="Turbo",
|
|
)
|
|
|
|
@property
|
|
@override
|
|
def available(self) -> bool:
|
|
"""Return True when the bike is connected and has sent data."""
|
|
return (
|
|
super().available
|
|
and self.coordinator.connected
|
|
and self.coordinator.snapshot.message_count > 0
|
|
)
|
|
|
|
@property
|
|
@override
|
|
def native_value(self) -> StateType:
|
|
"""Return the sensor value from the coordinator's snapshot."""
|
|
return self.entity_description.value_fn(self.coordinator.snapshot)
|