1
0
mirror of https://github.com/home-assistant/core.git synced 2025-04-29 19:57:52 +00:00
2025-04-19 09:16:45 +00:00

1141 lines
41 KiB
Python

"""Support for sensors through the SmartThings cloud API."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime
from typing import Any, cast
from pysmartthings import Attribute, Capability, ComponentStatus, SmartThings, Status
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
UnitOfArea,
UnitOfEnergy,
UnitOfMass,
UnitOfPower,
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from . import FullDevice, SmartThingsConfigEntry
from .const import MAIN
from .entity import SmartThingsEntity
from .util import deprecate_entity
THERMOSTAT_CAPABILITIES = {
Capability.TEMPERATURE_MEASUREMENT,
Capability.THERMOSTAT_HEATING_SETPOINT,
Capability.THERMOSTAT_MODE,
}
JOB_STATE_MAP = {
"airWash": "air_wash",
"airwash": "air_wash",
"aIRinse": "ai_rinse",
"aISpin": "ai_spin",
"aIWash": "ai_wash",
"aIDrying": "ai_drying",
"internalCare": "internal_care",
"continuousDehumidifying": "continuous_dehumidifying",
"thawingFrozenInside": "thawing_frozen_inside",
"delayWash": "delay_wash",
"weightSensing": "weight_sensing",
"freezeProtection": "freeze_protection",
"preDrain": "pre_drain",
"preWash": "pre_wash",
"prewash": "pre_wash",
"wrinklePrevent": "wrinkle_prevent",
"unknown": None,
}
OVEN_JOB_STATE_MAP = {
"scheduledStart": "scheduled_start",
"fastPreheat": "fast_preheat",
"scheduledEnd": "scheduled_end",
"stone_heating": "stone_heating",
"timeHoldPreheat": "time_hold_preheat",
}
MEDIA_PLAYBACK_STATE_MAP = {
"fast forwarding": "fast_forwarding",
}
ROBOT_CLEANER_TURBO_MODE_STATE_MAP = {
"extraSilence": "extra_silence",
}
ROBOT_CLEANER_MOVEMENT_MAP = {
"powerOff": "off",
}
OVEN_MODE = {
"Conventional": "conventional",
"Bake": "bake",
"BottomHeat": "bottom_heat",
"ConvectionBake": "convection_bake",
"ConvectionRoast": "convection_roast",
"Broil": "broil",
"ConvectionBroil": "convection_broil",
"SteamCook": "steam_cook",
"SteamBake": "steam_bake",
"SteamRoast": "steam_roast",
"SteamBottomHeatplusConvection": "steam_bottom_heat_plus_convection",
"Microwave": "microwave",
"MWplusGrill": "microwave_plus_grill",
"MWplusConvection": "microwave_plus_convection",
"MWplusHotBlast": "microwave_plus_hot_blast",
"MWplusHotBlast2": "microwave_plus_hot_blast_2",
"SlimMiddle": "slim_middle",
"SlimStrong": "slim_strong",
"SlowCook": "slow_cook",
"Proof": "proof",
"Dehydrate": "dehydrate",
"Others": "others",
"StrongSteam": "strong_steam",
"Descale": "descale",
"Rinse": "rinse",
}
WASHER_OPTIONS = ["pause", "run", "stop"]
def power_attributes(status: dict[str, Any]) -> dict[str, Any]:
"""Return the power attributes."""
state = {}
for attribute in ("start", "end"):
if (value := status.get(attribute)) is not None:
state[f"power_consumption_{attribute}"] = value
return state
@dataclass(frozen=True, kw_only=True)
class SmartThingsSensorEntityDescription(SensorEntityDescription):
"""Describe a SmartThings sensor entity."""
value_fn: Callable[[Any], str | float | int | datetime | None] = lambda value: value
extra_state_attributes_fn: Callable[[Any], dict[str, Any]] | None = None
capability_ignore_list: list[set[Capability]] | None = None
options_attribute: Attribute | None = None
exists_fn: Callable[[Status], bool] | None = None
use_temperature_unit: bool = False
deprecated: Callable[[ComponentStatus], str | None] | None = None
CAPABILITY_TO_SENSORS: dict[
Capability, dict[Attribute, list[SmartThingsSensorEntityDescription]]
] = {
# Haven't seen at devices yet
Capability.ACTIVITY_LIGHTING_MODE: {
Attribute.LIGHTING_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.LIGHTING_MODE,
translation_key="lighting_mode",
entity_category=EntityCategory.DIAGNOSTIC,
)
]
},
Capability.AIR_CONDITIONER_MODE: {
Attribute.AIR_CONDITIONER_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.AIR_CONDITIONER_MODE,
translation_key="air_conditioner_mode",
entity_category=EntityCategory.DIAGNOSTIC,
capability_ignore_list=[
{
Capability.TEMPERATURE_MEASUREMENT,
Capability.THERMOSTAT_COOLING_SETPOINT,
}
],
)
]
},
Capability.AIR_QUALITY_SENSOR: {
Attribute.AIR_QUALITY: [
SmartThingsSensorEntityDescription(
key=Attribute.AIR_QUALITY,
translation_key="air_quality",
native_unit_of_measurement="CAQI",
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.ALARM: {
Attribute.ALARM: [
SmartThingsSensorEntityDescription(
key=Attribute.ALARM,
translation_key="alarm",
options=["both", "strobe", "siren", "off"],
device_class=SensorDeviceClass.ENUM,
)
]
},
Capability.AUDIO_VOLUME: {
Attribute.VOLUME: [
SmartThingsSensorEntityDescription(
key=Attribute.VOLUME,
translation_key="audio_volume",
native_unit_of_measurement=PERCENTAGE,
deprecated=(
lambda status: "media_player"
if Capability.AUDIO_MUTE in status
else None
),
)
]
},
Capability.BATTERY: {
Attribute.BATTERY: [
SmartThingsSensorEntityDescription(
key=Attribute.BATTERY,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
)
]
},
# Haven't seen at devices yet
Capability.BODY_MASS_INDEX_MEASUREMENT: {
Attribute.BMI_MEASUREMENT: [
SmartThingsSensorEntityDescription(
key=Attribute.BMI_MEASUREMENT,
translation_key="body_mass_index",
native_unit_of_measurement=f"{UnitOfMass.KILOGRAMS}/{UnitOfArea.SQUARE_METERS}",
state_class=SensorStateClass.MEASUREMENT,
)
]
},
# Haven't seen at devices yet
Capability.BODY_WEIGHT_MEASUREMENT: {
Attribute.BODY_WEIGHT_MEASUREMENT: [
SmartThingsSensorEntityDescription(
key=Attribute.BODY_WEIGHT_MEASUREMENT,
translation_key="body_weight",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.CARBON_DIOXIDE_MEASUREMENT: {
Attribute.CARBON_DIOXIDE: [
SmartThingsSensorEntityDescription(
key=Attribute.CARBON_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
# Haven't seen at devices yet
Capability.CARBON_MONOXIDE_DETECTOR: {
Attribute.CARBON_MONOXIDE: [
SmartThingsSensorEntityDescription(
key=Attribute.CARBON_MONOXIDE,
translation_key="carbon_monoxide_detector",
options=["detected", "clear", "tested"],
device_class=SensorDeviceClass.ENUM,
)
]
},
# Haven't seen at devices yet
Capability.CARBON_MONOXIDE_MEASUREMENT: {
Attribute.CARBON_MONOXIDE_LEVEL: [
SmartThingsSensorEntityDescription(
key=Attribute.CARBON_MONOXIDE_LEVEL,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.DISHWASHER_OPERATING_STATE: {
Attribute.MACHINE_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.MACHINE_STATE,
translation_key="dishwasher_machine_state",
options=WASHER_OPTIONS,
device_class=SensorDeviceClass.ENUM,
)
],
Attribute.DISHWASHER_JOB_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.DISHWASHER_JOB_STATE,
translation_key="dishwasher_job_state",
options=[
"air_wash",
"cooling",
"drying",
"finish",
"pre_drain",
"pre_wash",
"rinse",
"spin",
"wash",
"wrinkle_prevent",
],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: JOB_STATE_MAP.get(value, value),
)
],
Attribute.COMPLETION_TIME: [
SmartThingsSensorEntityDescription(
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
)
],
},
# part of the proposed spec, Haven't seen at devices yet
Capability.DRYER_MODE: {
Attribute.DRYER_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.DRYER_MODE,
translation_key="dryer_mode",
entity_category=EntityCategory.DIAGNOSTIC,
)
]
},
Capability.DRYER_OPERATING_STATE: {
Attribute.MACHINE_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.MACHINE_STATE,
translation_key="dryer_machine_state",
options=WASHER_OPTIONS,
device_class=SensorDeviceClass.ENUM,
)
],
Attribute.DRYER_JOB_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.DRYER_JOB_STATE,
translation_key="dryer_job_state",
options=[
"cooling",
"delay_wash",
"drying",
"finished",
"none",
"refreshing",
"weight_sensing",
"wrinkle_prevent",
"dehumidifying",
"ai_drying",
"sanitizing",
"internal_care",
"freeze_protection",
"continuous_dehumidifying",
"thawing_frozen_inside",
],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: JOB_STATE_MAP.get(value, value),
)
],
Attribute.COMPLETION_TIME: [
SmartThingsSensorEntityDescription(
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
)
],
},
Capability.DUST_SENSOR: {
Attribute.DUST_LEVEL: [
SmartThingsSensorEntityDescription(
key=Attribute.DUST_LEVEL,
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
)
],
Attribute.FINE_DUST_LEVEL: [
SmartThingsSensorEntityDescription(
key=Attribute.FINE_DUST_LEVEL,
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
)
],
},
Capability.ENERGY_METER: {
Attribute.ENERGY: [
SmartThingsSensorEntityDescription(
key=Attribute.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
)
]
},
# Haven't seen at devices yet
Capability.EQUIVALENT_CARBON_DIOXIDE_MEASUREMENT: {
Attribute.EQUIVALENT_CARBON_DIOXIDE_MEASUREMENT: [
SmartThingsSensorEntityDescription(
key=Attribute.EQUIVALENT_CARBON_DIOXIDE_MEASUREMENT,
translation_key="equivalent_carbon_dioxide",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
# Haven't seen at devices yet
Capability.FORMALDEHYDE_MEASUREMENT: {
Attribute.FORMALDEHYDE_LEVEL: [
SmartThingsSensorEntityDescription(
key=Attribute.FORMALDEHYDE_LEVEL,
translation_key="formaldehyde",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.GAS_METER: {
Attribute.GAS_METER: [
SmartThingsSensorEntityDescription(
key=Attribute.GAS_METER,
translation_key="gas_meter",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
)
],
Attribute.GAS_METER_CALORIFIC: [
SmartThingsSensorEntityDescription(
key=Attribute.GAS_METER_CALORIFIC,
translation_key="gas_meter_calorific",
)
],
Attribute.GAS_METER_TIME: [
SmartThingsSensorEntityDescription(
key=Attribute.GAS_METER_TIME,
translation_key="gas_meter_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
)
],
Attribute.GAS_METER_VOLUME: [
SmartThingsSensorEntityDescription(
key=Attribute.GAS_METER_VOLUME,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL,
)
],
},
# Haven't seen at devices yet
Capability.ILLUMINANCE_MEASUREMENT: {
Attribute.ILLUMINANCE: [
SmartThingsSensorEntityDescription(
key=Attribute.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
# Haven't seen at devices yet
Capability.INFRARED_LEVEL: {
Attribute.INFRARED_LEVEL: [
SmartThingsSensorEntityDescription(
key=Attribute.INFRARED_LEVEL,
translation_key="infrared_level",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.MEDIA_INPUT_SOURCE: {
Attribute.INPUT_SOURCE: [
SmartThingsSensorEntityDescription(
key=Attribute.INPUT_SOURCE,
translation_key="media_input_source",
device_class=SensorDeviceClass.ENUM,
options_attribute=Attribute.SUPPORTED_INPUT_SOURCES,
value_fn=lambda value: value.lower() if value else None,
deprecated=lambda _: "media_player",
)
]
},
Capability.MEDIA_PLAYBACK_REPEAT: {
Attribute.PLAYBACK_REPEAT_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.PLAYBACK_REPEAT_MODE,
translation_key="media_playback_repeat",
deprecated=lambda _: "media_player",
)
]
},
Capability.MEDIA_PLAYBACK_SHUFFLE: {
Attribute.PLAYBACK_SHUFFLE: [
SmartThingsSensorEntityDescription(
key=Attribute.PLAYBACK_SHUFFLE,
translation_key="media_playback_shuffle",
deprecated=lambda _: "media_player",
)
]
},
Capability.MEDIA_PLAYBACK: {
Attribute.PLAYBACK_STATUS: [
SmartThingsSensorEntityDescription(
key=Attribute.PLAYBACK_STATUS,
translation_key="media_playback_status",
options=[
"paused",
"playing",
"stopped",
"fast_forwarding",
"rewinding",
"buffering",
],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: MEDIA_PLAYBACK_STATE_MAP.get(value, value),
deprecated=lambda _: "media_player",
)
]
},
Capability.ODOR_SENSOR: {
Attribute.ODOR_LEVEL: [
SmartThingsSensorEntityDescription(
key=Attribute.ODOR_LEVEL,
translation_key="odor_sensor",
)
]
},
Capability.OVEN_MODE: {
Attribute.OVEN_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.OVEN_MODE,
translation_key="oven_mode",
entity_category=EntityCategory.DIAGNOSTIC,
options=list(OVEN_MODE.values()),
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: OVEN_MODE.get(value, value),
)
]
},
Capability.OVEN_OPERATING_STATE: {
Attribute.MACHINE_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.MACHINE_STATE,
translation_key="oven_machine_state",
options=["ready", "running", "paused"],
device_class=SensorDeviceClass.ENUM,
)
],
Attribute.OVEN_JOB_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.OVEN_JOB_STATE,
translation_key="oven_job_state",
options=[
"cleaning",
"cooking",
"cooling",
"draining",
"preheat",
"ready",
"rinsing",
"finished",
"scheduled_start",
"warming",
"defrosting",
"sensing",
"searing",
"fast_preheat",
"scheduled_end",
"stone_heating",
"time_hold_preheat",
],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: OVEN_JOB_STATE_MAP.get(value, value),
)
],
Attribute.COMPLETION_TIME: [
SmartThingsSensorEntityDescription(
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
)
],
},
Capability.OVEN_SETPOINT: {
Attribute.OVEN_SETPOINT: [
SmartThingsSensorEntityDescription(
key=Attribute.OVEN_SETPOINT,
translation_key="oven_setpoint",
device_class=SensorDeviceClass.TEMPERATURE,
use_temperature_unit=True,
# Set the value to None if it is 0 F (-17 C)
value_fn=lambda value: None if value in {0, -17} else value,
)
]
},
Capability.POWER_CONSUMPTION_REPORT: {
Attribute.POWER_CONSUMPTION: [
SmartThingsSensorEntityDescription(
key="energy_meter",
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energy" in value
),
),
SmartThingsSensorEntityDescription(
key="power_meter",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
value_fn=lambda value: value["power"],
extra_state_attributes_fn=power_attributes,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "power" in value
),
),
SmartThingsSensorEntityDescription(
key="deltaEnergy_meter",
translation_key="energy_difference",
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["deltaEnergy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "deltaEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="powerEnergy_meter",
translation_key="power_energy",
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["powerEnergy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "powerEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="energySaved_meter",
translation_key="energy_saved",
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energySaved"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energySaved" in value
),
),
]
},
Capability.POWER_METER: {
Attribute.POWER: [
SmartThingsSensorEntityDescription(
key=Attribute.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
# Haven't seen at devices yet
Capability.POWER_SOURCE: {
Attribute.POWER_SOURCE: [
SmartThingsSensorEntityDescription(
key=Attribute.POWER_SOURCE,
translation_key="power_source",
entity_category=EntityCategory.DIAGNOSTIC,
)
]
},
# part of the proposed spec
Capability.REFRIGERATION_SETPOINT: {
Attribute.REFRIGERATION_SETPOINT: [
SmartThingsSensorEntityDescription(
key=Attribute.REFRIGERATION_SETPOINT,
translation_key="refrigeration_setpoint",
device_class=SensorDeviceClass.TEMPERATURE,
)
]
},
Capability.RELATIVE_BRIGHTNESS: {
Attribute.BRIGHTNESS_INTENSITY: [
SmartThingsSensorEntityDescription(
key=Attribute.BRIGHTNESS_INTENSITY,
translation_key="brightness_intensity",
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.RELATIVE_HUMIDITY_MEASUREMENT: {
Attribute.HUMIDITY: [
SmartThingsSensorEntityDescription(
key=Attribute.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.ROBOT_CLEANER_CLEANING_MODE: {
Attribute.ROBOT_CLEANER_CLEANING_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.ROBOT_CLEANER_CLEANING_MODE,
translation_key="robot_cleaner_cleaning_mode",
options=["auto", "part", "repeat", "manual", "stop", "map"],
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
)
],
},
Capability.ROBOT_CLEANER_MOVEMENT: {
Attribute.ROBOT_CLEANER_MOVEMENT: [
SmartThingsSensorEntityDescription(
key=Attribute.ROBOT_CLEANER_MOVEMENT,
translation_key="robot_cleaner_movement",
options=[
"homing",
"idle",
"charging",
"alarm",
"off",
"reserve",
"point",
"after",
"cleaning",
"pause",
],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: ROBOT_CLEANER_MOVEMENT_MAP.get(value, value),
)
]
},
Capability.ROBOT_CLEANER_TURBO_MODE: {
Attribute.ROBOT_CLEANER_TURBO_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.ROBOT_CLEANER_TURBO_MODE,
translation_key="robot_cleaner_turbo_mode",
options=["on", "off", "silence", "extra_silence"],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: ROBOT_CLEANER_TURBO_MODE_STATE_MAP.get(
value, value
),
entity_category=EntityCategory.DIAGNOSTIC,
)
]
},
# Haven't seen at devices yet
Capability.SIGNAL_STRENGTH: {
Attribute.LQI: [
SmartThingsSensorEntityDescription(
key=Attribute.LQI,
translation_key="link_quality",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
)
],
Attribute.RSSI: [
SmartThingsSensorEntityDescription(
key=Attribute.RSSI,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
)
],
},
# Haven't seen at devices yet
Capability.SMOKE_DETECTOR: {
Attribute.SMOKE: [
SmartThingsSensorEntityDescription(
key=Attribute.SMOKE,
translation_key="smoke_detector",
options=["detected", "clear", "tested"],
device_class=SensorDeviceClass.ENUM,
)
]
},
Capability.TEMPERATURE_MEASUREMENT: {
Attribute.TEMPERATURE: [
SmartThingsSensorEntityDescription(
key=Attribute.TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.THERMOSTAT_COOLING_SETPOINT: {
Attribute.COOLING_SETPOINT: [
SmartThingsSensorEntityDescription(
key=Attribute.COOLING_SETPOINT,
translation_key="thermostat_cooling_setpoint",
device_class=SensorDeviceClass.TEMPERATURE,
capability_ignore_list=[
{
Capability.AIR_CONDITIONER_FAN_MODE,
Capability.TEMPERATURE_MEASUREMENT,
Capability.AIR_CONDITIONER_MODE,
},
THERMOSTAT_CAPABILITIES,
],
)
]
},
# Haven't seen at devices yet
Capability.THERMOSTAT_FAN_MODE: {
Attribute.THERMOSTAT_FAN_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.THERMOSTAT_FAN_MODE,
translation_key="thermostat_fan_mode",
entity_category=EntityCategory.DIAGNOSTIC,
capability_ignore_list=[THERMOSTAT_CAPABILITIES],
)
]
},
# Haven't seen at devices yet
Capability.THERMOSTAT_HEATING_SETPOINT: {
Attribute.HEATING_SETPOINT: [
SmartThingsSensorEntityDescription(
key=Attribute.HEATING_SETPOINT,
translation_key="thermostat_heating_setpoint",
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
capability_ignore_list=[THERMOSTAT_CAPABILITIES],
)
]
},
# Haven't seen at devices yet
Capability.THERMOSTAT_MODE: {
Attribute.THERMOSTAT_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.THERMOSTAT_MODE,
translation_key="thermostat_mode",
entity_category=EntityCategory.DIAGNOSTIC,
capability_ignore_list=[THERMOSTAT_CAPABILITIES],
)
]
},
# Haven't seen at devices yet
Capability.THERMOSTAT_OPERATING_STATE: {
Attribute.THERMOSTAT_OPERATING_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.THERMOSTAT_OPERATING_STATE,
translation_key="thermostat_operating_state",
capability_ignore_list=[THERMOSTAT_CAPABILITIES],
)
]
},
# deprecated capability
Capability.THERMOSTAT_SETPOINT: {
Attribute.THERMOSTAT_SETPOINT: [
SmartThingsSensorEntityDescription(
key=Attribute.THERMOSTAT_SETPOINT,
translation_key="thermostat_setpoint",
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
)
]
},
Capability.THREE_AXIS: {
Attribute.THREE_AXIS: [
SmartThingsSensorEntityDescription(
key="x_coordinate",
translation_key="x_coordinate",
value_fn=lambda value: value[0],
),
SmartThingsSensorEntityDescription(
key="y_coordinate",
translation_key="y_coordinate",
value_fn=lambda value: value[1],
),
SmartThingsSensorEntityDescription(
key="z_coordinate",
translation_key="z_coordinate",
value_fn=lambda value: value[2],
),
]
},
Capability.TV_CHANNEL: {
Attribute.TV_CHANNEL: [
SmartThingsSensorEntityDescription(
key=Attribute.TV_CHANNEL,
translation_key="tv_channel",
)
],
Attribute.TV_CHANNEL_NAME: [
SmartThingsSensorEntityDescription(
key=Attribute.TV_CHANNEL_NAME,
translation_key="tv_channel_name",
)
],
},
# Haven't seen at devices yet
Capability.TVOC_MEASUREMENT: {
Attribute.TVOC_LEVEL: [
SmartThingsSensorEntityDescription(
key=Attribute.TVOC_LEVEL,
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
# Haven't seen at devices yet
Capability.ULTRAVIOLET_INDEX: {
Attribute.ULTRAVIOLET_INDEX: [
SmartThingsSensorEntityDescription(
key=Attribute.ULTRAVIOLET_INDEX,
translation_key="uv_index",
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.VERY_FINE_DUST_SENSOR: {
Attribute.VERY_FINE_DUST_LEVEL: [
SmartThingsSensorEntityDescription(
key=Attribute.VERY_FINE_DUST_LEVEL,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
Capability.VOLTAGE_MEASUREMENT: {
Attribute.VOLTAGE: [
SmartThingsSensorEntityDescription(
key=Attribute.VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
)
]
},
# part of the proposed spec
Capability.WASHER_MODE: {
Attribute.WASHER_MODE: [
SmartThingsSensorEntityDescription(
key=Attribute.WASHER_MODE,
translation_key="washer_mode",
entity_category=EntityCategory.DIAGNOSTIC,
)
]
},
Capability.WASHER_OPERATING_STATE: {
Attribute.MACHINE_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.MACHINE_STATE,
translation_key="washer_machine_state",
options=WASHER_OPTIONS,
device_class=SensorDeviceClass.ENUM,
)
],
Attribute.WASHER_JOB_STATE: [
SmartThingsSensorEntityDescription(
key=Attribute.WASHER_JOB_STATE,
translation_key="washer_job_state",
options=[
"air_wash",
"ai_rinse",
"ai_spin",
"ai_wash",
"cooling",
"delay_wash",
"drying",
"finish",
"none",
"pre_wash",
"rinse",
"spin",
"wash",
"weight_sensing",
"wrinkle_prevent",
"freeze_protection",
],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: JOB_STATE_MAP.get(value, value),
)
],
Attribute.COMPLETION_TIME: [
SmartThingsSensorEntityDescription(
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
)
],
},
}
UNITS = {
"C": UnitOfTemperature.CELSIUS,
"F": UnitOfTemperature.FAHRENHEIT,
"ccf": UnitOfVolume.CENTUM_CUBIC_FEET,
"lux": LIGHT_LUX,
"mG": None,
"μg/m^3": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
}
async def async_setup_entry(
hass: HomeAssistant,
entry: SmartThingsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add sensors for a config entry."""
entry_data = entry.runtime_data
entities = []
entity_registry = er.async_get(hass)
for device in entry_data.devices.values(): # pylint: disable=too-many-nested-blocks
for capability, attributes in CAPABILITY_TO_SENSORS.items():
if capability in device.status[MAIN]:
for attribute, descriptions in attributes.items():
for description in descriptions:
if (
not description.capability_ignore_list
or not any(
all(
capability in device.status[MAIN]
for capability in capability_list
)
for capability_list in description.capability_ignore_list
)
) and (
not description.exists_fn
or description.exists_fn(
device.status[MAIN][capability][attribute]
)
):
if (
description.deprecated
and (
reason := description.deprecated(
device.status[MAIN]
)
)
is not None
):
if deprecate_entity(
hass,
entity_registry,
SENSOR_DOMAIN,
f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{description.key}",
f"deprecated_{reason}",
):
entities.append(
SmartThingsSensor(
entry_data.client,
device,
description,
capability,
attribute,
)
)
continue
entities.append(
SmartThingsSensor(
entry_data.client,
device,
description,
capability,
attribute,
)
)
async_add_entities(entities)
class SmartThingsSensor(SmartThingsEntity, SensorEntity):
"""Define a SmartThings Sensor."""
entity_description: SmartThingsSensorEntityDescription
def __init__(
self,
client: SmartThings,
device: FullDevice,
entity_description: SmartThingsSensorEntityDescription,
capability: Capability,
attribute: Attribute,
) -> None:
"""Init the class."""
capabilities_to_subscribe = {capability}
if entity_description.use_temperature_unit:
capabilities_to_subscribe.add(Capability.TEMPERATURE_MEASUREMENT)
super().__init__(client, device, capabilities_to_subscribe)
self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{entity_description.key}"
self._attribute = attribute
self.capability = capability
self.entity_description = entity_description
@property
def native_value(self) -> str | float | datetime | int | None:
"""Return the state of the sensor."""
res = self.get_attribute_value(self.capability, self._attribute)
return self.entity_description.value_fn(res)
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit this state is expressed in."""
if self.entity_description.use_temperature_unit:
unit = self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
else:
unit = self._internal_state[self.capability][self._attribute].unit
return (
UNITS.get(unit, unit)
if unit
else self.entity_description.native_unit_of_measurement
)
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes."""
if self.entity_description.extra_state_attributes_fn:
return self.entity_description.extra_state_attributes_fn(
self.get_attribute_value(self.capability, self._attribute)
)
return None
@property
def options(self) -> list[str] | None:
"""Return the options for this sensor."""
if self.entity_description.options_attribute:
if (
options := self.get_attribute_value(
self.capability, self.entity_description.options_attribute
)
) is None:
return []
return [option.lower() for option in options]
return super().options