core/homeassistant/components/smartthings/sensor.py

739 lines
21 KiB
Python

"""Support for sensors through the SmartThings cloud API."""
from __future__ import annotations
from collections.abc import Sequence
from typing import NamedTuple
from pysmartthings import Attribute, Capability
from pysmartthings.device import DeviceEntity
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
UnitOfArea,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfMass,
UnitOfPower,
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DATA_BROKERS, DOMAIN
from .entity import SmartThingsEntity
class Map(NamedTuple):
"""Tuple for mapping Smartthings capabilities to Home Assistant sensors."""
attribute: str
name: str
default_unit: str | None
device_class: SensorDeviceClass | None
state_class: SensorStateClass | None
entity_category: EntityCategory | None
CAPABILITY_TO_SENSORS: dict[str, list[Map]] = {
Capability.activity_lighting_mode: [
Map(
Attribute.lighting_mode,
"Activity Lighting Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.air_conditioner_mode: [
Map(
Attribute.air_conditioner_mode,
"Air Conditioner Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.air_quality_sensor: [
Map(
Attribute.air_quality,
"Air Quality",
"CAQI",
None,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None, None, None)],
Capability.audio_volume: [
Map(Attribute.volume, "Volume", PERCENTAGE, None, None, None)
],
Capability.battery: [
Map(
Attribute.battery,
"Battery",
PERCENTAGE,
SensorDeviceClass.BATTERY,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.body_mass_index_measurement: [
Map(
Attribute.bmi_measurement,
"Body Mass Index",
f"{UnitOfMass.KILOGRAMS}/{UnitOfArea.SQUARE_METERS}",
None,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.body_weight_measurement: [
Map(
Attribute.body_weight_measurement,
"Body Weight",
UnitOfMass.KILOGRAMS,
SensorDeviceClass.WEIGHT,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.carbon_dioxide_measurement: [
Map(
Attribute.carbon_dioxide,
"Carbon Dioxide Measurement",
CONCENTRATION_PARTS_PER_MILLION,
SensorDeviceClass.CO2,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.carbon_monoxide_detector: [
Map(
Attribute.carbon_monoxide,
"Carbon Monoxide Detector",
None,
None,
None,
None,
)
],
Capability.carbon_monoxide_measurement: [
Map(
Attribute.carbon_monoxide_level,
"Carbon Monoxide Measurement",
CONCENTRATION_PARTS_PER_MILLION,
SensorDeviceClass.CO,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.dishwasher_operating_state: [
Map(
Attribute.machine_state, "Dishwasher Machine State", None, None, None, None
),
Map(
Attribute.dishwasher_job_state,
"Dishwasher Job State",
None,
None,
None,
None,
),
Map(
Attribute.completion_time,
"Dishwasher Completion Time",
None,
SensorDeviceClass.TIMESTAMP,
None,
None,
),
],
Capability.dryer_mode: [
Map(
Attribute.dryer_mode,
"Dryer Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.dryer_operating_state: [
Map(Attribute.machine_state, "Dryer Machine State", None, None, None, None),
Map(Attribute.dryer_job_state, "Dryer Job State", None, None, None, None),
Map(
Attribute.completion_time,
"Dryer Completion Time",
None,
SensorDeviceClass.TIMESTAMP,
None,
None,
),
],
Capability.dust_sensor: [
Map(
Attribute.fine_dust_level,
"Fine Dust Level",
None,
None,
SensorStateClass.MEASUREMENT,
None,
),
Map(
Attribute.dust_level,
"Dust Level",
None,
None,
SensorStateClass.MEASUREMENT,
None,
),
],
Capability.energy_meter: [
Map(
Attribute.energy,
"Energy Meter",
UnitOfEnergy.KILO_WATT_HOUR,
SensorDeviceClass.ENERGY,
SensorStateClass.TOTAL_INCREASING,
None,
)
],
Capability.equivalent_carbon_dioxide_measurement: [
Map(
Attribute.equivalent_carbon_dioxide_measurement,
"Equivalent Carbon Dioxide Measurement",
CONCENTRATION_PARTS_PER_MILLION,
SensorDeviceClass.CO2,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.formaldehyde_measurement: [
Map(
Attribute.formaldehyde_level,
"Formaldehyde Measurement",
CONCENTRATION_PARTS_PER_MILLION,
None,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.gas_meter: [
Map(
Attribute.gas_meter,
"Gas Meter",
UnitOfEnergy.KILO_WATT_HOUR,
SensorDeviceClass.ENERGY,
SensorStateClass.MEASUREMENT,
None,
),
Map(
Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None, None, None
),
Map(
Attribute.gas_meter_time,
"Gas Meter Time",
None,
SensorDeviceClass.TIMESTAMP,
None,
None,
),
Map(
Attribute.gas_meter_volume,
"Gas Meter Volume",
UnitOfVolume.CUBIC_METERS,
SensorDeviceClass.GAS,
SensorStateClass.MEASUREMENT,
None,
),
],
Capability.illuminance_measurement: [
Map(
Attribute.illuminance,
"Illuminance",
LIGHT_LUX,
SensorDeviceClass.ILLUMINANCE,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.infrared_level: [
Map(
Attribute.infrared_level,
"Infrared Level",
PERCENTAGE,
None,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.media_input_source: [
Map(Attribute.input_source, "Media Input Source", None, None, None, None)
],
Capability.media_playback_repeat: [
Map(
Attribute.playback_repeat_mode,
"Media Playback Repeat",
None,
None,
None,
None,
)
],
Capability.media_playback_shuffle: [
Map(
Attribute.playback_shuffle, "Media Playback Shuffle", None, None, None, None
)
],
Capability.media_playback: [
Map(Attribute.playback_status, "Media Playback Status", None, None, None, None)
],
Capability.odor_sensor: [
Map(Attribute.odor_level, "Odor Sensor", None, None, None, None)
],
Capability.oven_mode: [
Map(
Attribute.oven_mode,
"Oven Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.oven_operating_state: [
Map(Attribute.machine_state, "Oven Machine State", None, None, None, None),
Map(Attribute.oven_job_state, "Oven Job State", None, None, None, None),
Map(Attribute.completion_time, "Oven Completion Time", None, None, None, None),
],
Capability.oven_setpoint: [
Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None, None)
],
Capability.power_consumption_report: [],
Capability.power_meter: [
Map(
Attribute.power,
"Power Meter",
UnitOfPower.WATT,
SensorDeviceClass.POWER,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.power_source: [
Map(
Attribute.power_source,
"Power Source",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.refrigeration_setpoint: [
Map(
Attribute.refrigeration_setpoint,
"Refrigeration Setpoint",
None,
SensorDeviceClass.TEMPERATURE,
None,
None,
)
],
Capability.relative_humidity_measurement: [
Map(
Attribute.humidity,
"Relative Humidity Measurement",
PERCENTAGE,
SensorDeviceClass.HUMIDITY,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.robot_cleaner_cleaning_mode: [
Map(
Attribute.robot_cleaner_cleaning_mode,
"Robot Cleaner Cleaning Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.robot_cleaner_movement: [
Map(
Attribute.robot_cleaner_movement,
"Robot Cleaner Movement",
None,
None,
None,
None,
)
],
Capability.robot_cleaner_turbo_mode: [
Map(
Attribute.robot_cleaner_turbo_mode,
"Robot Cleaner Turbo Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.signal_strength: [
Map(
Attribute.lqi,
"LQI Signal Strength",
None,
None,
SensorStateClass.MEASUREMENT,
EntityCategory.DIAGNOSTIC,
),
Map(
Attribute.rssi,
"RSSI Signal Strength",
None,
SensorDeviceClass.SIGNAL_STRENGTH,
SensorStateClass.MEASUREMENT,
EntityCategory.DIAGNOSTIC,
),
],
Capability.smoke_detector: [
Map(Attribute.smoke, "Smoke Detector", None, None, None, None)
],
Capability.temperature_measurement: [
Map(
Attribute.temperature,
"Temperature Measurement",
None,
SensorDeviceClass.TEMPERATURE,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.thermostat_cooling_setpoint: [
Map(
Attribute.cooling_setpoint,
"Thermostat Cooling Setpoint",
None,
SensorDeviceClass.TEMPERATURE,
None,
None,
)
],
Capability.thermostat_fan_mode: [
Map(
Attribute.thermostat_fan_mode,
"Thermostat Fan Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.thermostat_heating_setpoint: [
Map(
Attribute.heating_setpoint,
"Thermostat Heating Setpoint",
None,
SensorDeviceClass.TEMPERATURE,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.thermostat_mode: [
Map(
Attribute.thermostat_mode,
"Thermostat Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.thermostat_operating_state: [
Map(
Attribute.thermostat_operating_state,
"Thermostat Operating State",
None,
None,
None,
None,
)
],
Capability.thermostat_setpoint: [
Map(
Attribute.thermostat_setpoint,
"Thermostat Setpoint",
None,
SensorDeviceClass.TEMPERATURE,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.three_axis: [],
Capability.tv_channel: [
Map(Attribute.tv_channel, "Tv Channel", None, None, None, None),
Map(Attribute.tv_channel_name, "Tv Channel Name", None, None, None, None),
],
Capability.tvoc_measurement: [
Map(
Attribute.tvoc_level,
"Tvoc Measurement",
CONCENTRATION_PARTS_PER_MILLION,
None,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.ultraviolet_index: [
Map(
Attribute.ultraviolet_index,
"Ultraviolet Index",
None,
None,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.voltage_measurement: [
Map(
Attribute.voltage,
"Voltage Measurement",
UnitOfElectricPotential.VOLT,
SensorDeviceClass.VOLTAGE,
SensorStateClass.MEASUREMENT,
None,
)
],
Capability.washer_mode: [
Map(
Attribute.washer_mode,
"Washer Mode",
None,
None,
None,
EntityCategory.DIAGNOSTIC,
)
],
Capability.washer_operating_state: [
Map(Attribute.machine_state, "Washer Machine State", None, None, None, None),
Map(Attribute.washer_job_state, "Washer Job State", None, None, None, None),
Map(
Attribute.completion_time,
"Washer Completion Time",
None,
SensorDeviceClass.TIMESTAMP,
None,
None,
),
],
}
UNITS = {
"C": UnitOfTemperature.CELSIUS,
"F": UnitOfTemperature.FAHRENHEIT,
"lux": LIGHT_LUX,
}
THREE_AXIS_NAMES = ["X Coordinate", "Y Coordinate", "Z Coordinate"]
POWER_CONSUMPTION_REPORT_NAMES = [
"energy",
"power",
"deltaEnergy",
"powerEnergy",
"energySaved",
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add sensors for a config entry."""
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
entities: list[SensorEntity] = []
for device in broker.devices.values():
for capability in broker.get_assigned(device.device_id, "sensor"):
if capability == Capability.three_axis:
entities.extend(
[
SmartThingsThreeAxisSensor(device, index)
for index in range(len(THREE_AXIS_NAMES))
]
)
elif capability == Capability.power_consumption_report:
entities.extend(
[
SmartThingsPowerConsumptionSensor(device, report_name)
for report_name in POWER_CONSUMPTION_REPORT_NAMES
]
)
else:
maps = CAPABILITY_TO_SENSORS[capability]
entities.extend(
[
SmartThingsSensor(
device,
m.attribute,
m.name,
m.default_unit,
m.device_class,
m.state_class,
m.entity_category,
)
for m in maps
]
)
if broker.any_assigned(device.device_id, "switch"):
for capability in (Capability.energy_meter, Capability.power_meter):
maps = CAPABILITY_TO_SENSORS[capability]
entities.extend(
[
SmartThingsSensor(
device,
m.attribute,
m.name,
m.default_unit,
m.device_class,
m.state_class,
m.entity_category,
)
for m in maps
]
)
async_add_entities(entities)
def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
return [
capability for capability in CAPABILITY_TO_SENSORS if capability in capabilities
]
class SmartThingsSensor(SmartThingsEntity, SensorEntity):
"""Define a SmartThings Sensor."""
def __init__(
self,
device: DeviceEntity,
attribute: str,
name: str,
default_unit: str | None,
device_class: SensorDeviceClass | None,
state_class: str | None,
entity_category: EntityCategory | None,
) -> None:
"""Init the class."""
super().__init__(device)
self._attribute = attribute
self._attr_name = f"{device.label} {name}"
self._attr_unique_id = f"{device.device_id}.{attribute}"
self._attr_device_class = device_class
self._default_unit = default_unit
self._attr_state_class = state_class
self._attr_entity_category = entity_category
@property
def native_value(self):
"""Return the state of the sensor."""
value = self._device.status.attributes[self._attribute].value
if self.device_class != SensorDeviceClass.TIMESTAMP:
return value
return dt_util.parse_datetime(value)
@property
def native_unit_of_measurement(self):
"""Return the unit this state is expressed in."""
unit = self._device.status.attributes[self._attribute].unit
return UNITS.get(unit, unit) if unit else self._default_unit
class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity):
"""Define a SmartThings Three Axis Sensor."""
def __init__(self, device, index):
"""Init the class."""
super().__init__(device)
self._index = index
self._attr_name = f"{device.label} {THREE_AXIS_NAMES[index]}"
self._attr_unique_id = f"{device.device_id} {THREE_AXIS_NAMES[index]}"
@property
def native_value(self):
"""Return the state of the sensor."""
three_axis = self._device.status.attributes[Attribute.three_axis].value
try:
return three_axis[self._index]
except (TypeError, IndexError):
return None
class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity):
"""Define a SmartThings Sensor."""
def __init__(
self,
device: DeviceEntity,
report_name: str,
) -> None:
"""Init the class."""
super().__init__(device)
self.report_name = report_name
self._attr_name = f"{device.label} {report_name}"
self._attr_unique_id = f"{device.device_id}.{report_name}_meter"
if self.report_name == "power":
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_device_class = SensorDeviceClass.POWER
self._attr_native_unit_of_measurement = UnitOfPower.WATT
else:
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
self._attr_device_class = SensorDeviceClass.ENERGY
self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
@property
def native_value(self):
"""Return the state of the sensor."""
value = self._device.status.attributes[Attribute.power_consumption].value
if value is None or value.get(self.report_name) is None:
return None
if self.report_name == "power":
return value[self.report_name]
return value[self.report_name] / 1000
@property
def extra_state_attributes(self):
"""Return specific state attributes."""
if self.report_name == "power":
attributes = [
"power_consumption_start",
"power_consumption_end",
]
state_attributes = {}
for attribute in attributes:
value = getattr(self._device.status, attribute)
if value is not None:
state_attributes[attribute] = value
return state_attributes
return None