Extract smartthings switch energy attributes into sensors (#53719)

This commit is contained in:
Michael 2021-07-30 06:50:02 +02:00 committed by GitHub
parent 8972fae0ca
commit 2b2cddb5f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 257 additions and 75 deletions

View file

@ -9,6 +9,7 @@ import logging
from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError
from pysmartapp.event import EVENT_TYPE_DEVICE
from pysmartthings import Attribute, Capability, SmartThings
from pysmartthings.device import DeviceEntity
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
@ -412,7 +413,7 @@ class DeviceBroker:
class SmartThingsEntity(Entity):
"""Defines a SmartThings entity."""
def __init__(self, device):
def __init__(self, device: DeviceEntity) -> None:
"""Initialize the instance."""
self._device = device
self._dispatcher_remove = None

View file

@ -3,18 +3,26 @@ from __future__ import annotations
from collections import namedtuple
from collections.abc import Sequence
from datetime import datetime
from pysmartthings import Attribute, Capability
from pysmartthings.device import DeviceEntity
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import (
AREA_SQUARE_METERS,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CO,
DEVICE_CLASS_CO2,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLTAGE,
ELECTRIC_POTENTIAL_VOLT,
ENERGY_KILO_WATT_HOUR,
LIGHT_LUX,
@ -25,26 +33,27 @@ from homeassistant.const import (
TEMP_FAHRENHEIT,
VOLUME_CUBIC_METERS,
)
from homeassistant.util.dt import utc_from_timestamp
from . import SmartThingsEntity
from .const import DATA_BROKERS, DOMAIN
Map = namedtuple("map", "attribute name default_unit device_class")
Map = namedtuple("map", "attribute name default_unit device_class state_class")
CAPABILITY_TO_SENSORS = {
Capability.activity_lighting_mode: [
Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None)
Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None, None)
],
Capability.air_conditioner_mode: [
Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None)
Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None, None)
],
Capability.air_quality_sensor: [
Map(Attribute.air_quality, "Air Quality", "CAQI", None)
Map(Attribute.air_quality, "Air Quality", "CAQI", None, STATE_CLASS_MEASUREMENT)
],
Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None)],
Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None)],
Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None, None)],
Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None, None)],
Capability.battery: [
Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY)
Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY, None)
],
Capability.body_mass_index_measurement: [
Map(
@ -52,57 +61,80 @@ CAPABILITY_TO_SENSORS = {
"Body Mass Index",
f"{MASS_KILOGRAMS}/{AREA_SQUARE_METERS}",
None,
STATE_CLASS_MEASUREMENT,
)
],
Capability.body_weight_measurement: [
Map(Attribute.body_weight_measurement, "Body Weight", MASS_KILOGRAMS, None)
Map(
Attribute.body_weight_measurement,
"Body Weight",
MASS_KILOGRAMS,
None,
STATE_CLASS_MEASUREMENT,
)
],
Capability.carbon_dioxide_measurement: [
Map(
Attribute.carbon_dioxide,
"Carbon Dioxide Measurement",
CONCENTRATION_PARTS_PER_MILLION,
None,
DEVICE_CLASS_CO2,
STATE_CLASS_MEASUREMENT,
)
],
Capability.carbon_monoxide_detector: [
Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None)
Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None, None)
],
Capability.carbon_monoxide_measurement: [
Map(
Attribute.carbon_monoxide_level,
"Carbon Monoxide Measurement",
CONCENTRATION_PARTS_PER_MILLION,
None,
DEVICE_CLASS_CO,
STATE_CLASS_MEASUREMENT,
)
],
Capability.dishwasher_operating_state: [
Map(Attribute.machine_state, "Dishwasher Machine State", None, None),
Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None),
Map(Attribute.machine_state, "Dishwasher Machine State", None, None, None),
Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None, None),
Map(
Attribute.completion_time,
"Dishwasher Completion Time",
None,
DEVICE_CLASS_TIMESTAMP,
None,
),
],
Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None)],
Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None, None)],
Capability.dryer_operating_state: [
Map(Attribute.machine_state, "Dryer Machine State", None, None),
Map(Attribute.dryer_job_state, "Dryer Job State", None, None),
Map(Attribute.machine_state, "Dryer Machine State", None, None, None),
Map(Attribute.dryer_job_state, "Dryer Job State", None, None, None),
Map(
Attribute.completion_time,
"Dryer Completion Time",
None,
DEVICE_CLASS_TIMESTAMP,
None,
),
],
Capability.dust_sensor: [
Map(Attribute.fine_dust_level, "Fine Dust Level", None, None),
Map(Attribute.dust_level, "Dust Level", None, None),
Map(
Attribute.fine_dust_level,
"Fine Dust Level",
None,
None,
STATE_CLASS_MEASUREMENT,
),
Map(Attribute.dust_level, "Dust Level", None, None, STATE_CLASS_MEASUREMENT),
],
Capability.energy_meter: [
Map(Attribute.energy, "Energy Meter", ENERGY_KILO_WATT_HOUR, None)
Map(
Attribute.energy,
"Energy Meter",
ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
)
],
Capability.equivalent_carbon_dioxide_measurement: [
Map(
@ -110,6 +142,7 @@ CAPABILITY_TO_SENSORS = {
"Equivalent Carbon Dioxide Measurement",
CONCENTRATION_PARTS_PER_MILLION,
None,
STATE_CLASS_MEASUREMENT,
)
],
Capability.formaldehyde_measurement: [
@ -118,50 +151,94 @@ CAPABILITY_TO_SENSORS = {
"Formaldehyde Measurement",
CONCENTRATION_PARTS_PER_MILLION,
None,
STATE_CLASS_MEASUREMENT,
)
],
Capability.gas_meter: [
Map(Attribute.gas_meter, "Gas Meter", ENERGY_KILO_WATT_HOUR, None),
Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None),
Map(Attribute.gas_meter_time, "Gas Meter Time", None, DEVICE_CLASS_TIMESTAMP),
Map(Attribute.gas_meter_volume, "Gas Meter Volume", VOLUME_CUBIC_METERS, None),
Map(
Attribute.gas_meter,
"Gas Meter",
ENERGY_KILO_WATT_HOUR,
None,
STATE_CLASS_MEASUREMENT,
),
Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None, None),
Map(
Attribute.gas_meter_time,
"Gas Meter Time",
None,
DEVICE_CLASS_TIMESTAMP,
None,
),
Map(
Attribute.gas_meter_volume,
"Gas Meter Volume",
VOLUME_CUBIC_METERS,
None,
STATE_CLASS_MEASUREMENT,
),
],
Capability.illuminance_measurement: [
Map(Attribute.illuminance, "Illuminance", LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE)
Map(
Attribute.illuminance,
"Illuminance",
LIGHT_LUX,
DEVICE_CLASS_ILLUMINANCE,
STATE_CLASS_MEASUREMENT,
)
],
Capability.infrared_level: [
Map(Attribute.infrared_level, "Infrared Level", PERCENTAGE, None)
Map(
Attribute.infrared_level,
"Infrared Level",
PERCENTAGE,
None,
STATE_CLASS_MEASUREMENT,
)
],
Capability.media_input_source: [
Map(Attribute.input_source, "Media Input Source", None, None)
Map(Attribute.input_source, "Media Input Source", None, None, None)
],
Capability.media_playback_repeat: [
Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None)
Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None, None)
],
Capability.media_playback_shuffle: [
Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None)
Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None, None)
],
Capability.media_playback: [
Map(Attribute.playback_status, "Media Playback Status", None, None)
Map(Attribute.playback_status, "Media Playback Status", None, None, None)
],
Capability.odor_sensor: [Map(Attribute.odor_level, "Odor Sensor", None, None)],
Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None)],
Capability.odor_sensor: [
Map(Attribute.odor_level, "Odor Sensor", None, None, None)
],
Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None, None)],
Capability.oven_operating_state: [
Map(Attribute.machine_state, "Oven Machine State", None, None),
Map(Attribute.oven_job_state, "Oven Job State", None, None),
Map(Attribute.completion_time, "Oven Completion Time", None, None),
Map(Attribute.machine_state, "Oven Machine State", None, None, None),
Map(Attribute.oven_job_state, "Oven Job State", None, None, None),
Map(Attribute.completion_time, "Oven Completion Time", None, None, None),
],
Capability.oven_setpoint: [
Map(Attribute.oven_setpoint, "Oven Set Point", None, None)
Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None)
],
Capability.power_meter: [
Map(
Attribute.power,
"Power Meter",
POWER_WATT,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
)
],
Capability.power_source: [
Map(Attribute.power_source, "Power Source", None, None, None)
],
Capability.power_meter: [Map(Attribute.power, "Power Meter", POWER_WATT, None)],
Capability.power_source: [Map(Attribute.power_source, "Power Source", None, None)],
Capability.refrigeration_setpoint: [
Map(
Attribute.refrigeration_setpoint,
"Refrigeration Setpoint",
None,
DEVICE_CLASS_TEMPERATURE,
None,
)
],
Capability.relative_humidity_measurement: [
@ -170,6 +247,7 @@ CAPABILITY_TO_SENSORS = {
"Relative Humidity Measurement",
PERCENTAGE,
DEVICE_CLASS_HUMIDITY,
STATE_CLASS_MEASUREMENT,
)
],
Capability.robot_cleaner_cleaning_mode: [
@ -178,25 +256,43 @@ CAPABILITY_TO_SENSORS = {
"Robot Cleaner Cleaning Mode",
None,
None,
None,
)
],
Capability.robot_cleaner_movement: [
Map(Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None)
Map(
Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None, None
)
],
Capability.robot_cleaner_turbo_mode: [
Map(Attribute.robot_cleaner_turbo_mode, "Robot Cleaner Turbo Mode", None, None)
Map(
Attribute.robot_cleaner_turbo_mode,
"Robot Cleaner Turbo Mode",
None,
None,
None,
)
],
Capability.signal_strength: [
Map(Attribute.lqi, "LQI Signal Strength", None, None),
Map(Attribute.rssi, "RSSI Signal Strength", None, None),
Map(Attribute.lqi, "LQI Signal Strength", None, None, STATE_CLASS_MEASUREMENT),
Map(
Attribute.rssi,
"RSSI Signal Strength",
None,
DEVICE_CLASS_SIGNAL_STRENGTH,
STATE_CLASS_MEASUREMENT,
),
],
Capability.smoke_detector: [
Map(Attribute.smoke, "Smoke Detector", None, None, None)
],
Capability.smoke_detector: [Map(Attribute.smoke, "Smoke Detector", None, None)],
Capability.temperature_measurement: [
Map(
Attribute.temperature,
"Temperature Measurement",
None,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
)
],
Capability.thermostat_cooling_setpoint: [
@ -205,10 +301,11 @@ CAPABILITY_TO_SENSORS = {
"Thermostat Cooling Setpoint",
None,
DEVICE_CLASS_TEMPERATURE,
None,
)
],
Capability.thermostat_fan_mode: [
Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None)
Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None, None)
],
Capability.thermostat_heating_setpoint: [
Map(
@ -216,10 +313,11 @@ CAPABILITY_TO_SENSORS = {
"Thermostat Heating Setpoint",
None,
DEVICE_CLASS_TEMPERATURE,
None,
)
],
Capability.thermostat_mode: [
Map(Attribute.thermostat_mode, "Thermostat Mode", None, None)
Map(Attribute.thermostat_mode, "Thermostat Mode", None, None, None)
],
Capability.thermostat_operating_state: [
Map(
@ -227,6 +325,7 @@ CAPABILITY_TO_SENSORS = {
"Thermostat Operating State",
None,
None,
None,
)
],
Capability.thermostat_setpoint: [
@ -235,12 +334,13 @@ CAPABILITY_TO_SENSORS = {
"Thermostat Setpoint",
None,
DEVICE_CLASS_TEMPERATURE,
None,
)
],
Capability.three_axis: [],
Capability.tv_channel: [
Map(Attribute.tv_channel, "Tv Channel", None, None),
Map(Attribute.tv_channel_name, "Tv Channel Name", None, None),
Map(Attribute.tv_channel, "Tv Channel", None, None, None),
Map(Attribute.tv_channel_name, "Tv Channel Name", None, None, None),
],
Capability.tvoc_measurement: [
Map(
@ -248,23 +348,39 @@ CAPABILITY_TO_SENSORS = {
"Tvoc Measurement",
CONCENTRATION_PARTS_PER_MILLION,
None,
STATE_CLASS_MEASUREMENT,
)
],
Capability.ultraviolet_index: [
Map(Attribute.ultraviolet_index, "Ultraviolet Index", None, None)
Map(
Attribute.ultraviolet_index,
"Ultraviolet Index",
None,
None,
STATE_CLASS_MEASUREMENT,
)
],
Capability.voltage_measurement: [
Map(Attribute.voltage, "Voltage Measurement", ELECTRIC_POTENTIAL_VOLT, None)
Map(
Attribute.voltage,
"Voltage Measurement",
ELECTRIC_POTENTIAL_VOLT,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
)
],
Capability.washer_mode: [
Map(Attribute.washer_mode, "Washer Mode", None, None, None)
],
Capability.washer_mode: [Map(Attribute.washer_mode, "Washer Mode", None, None)],
Capability.washer_operating_state: [
Map(Attribute.machine_state, "Washer Machine State", None, None),
Map(Attribute.washer_job_state, "Washer Job State", None, None),
Map(Attribute.machine_state, "Washer Machine State", None, None, None),
Map(Attribute.washer_job_state, "Washer Job State", None, None, None),
Map(
Attribute.completion_time,
"Washer Completion Time",
None,
DEVICE_CLASS_TIMESTAMP,
None,
),
],
}
@ -292,11 +408,34 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
sensors.extend(
[
SmartThingsSensor(
device, m.attribute, m.name, m.default_unit, m.device_class
device,
m.attribute,
m.name,
m.default_unit,
m.device_class,
m.state_class,
)
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]
sensors.extend(
[
SmartThingsSensor(
device,
m.attribute,
m.name,
m.default_unit,
m.device_class,
m.state_class,
)
for m in maps
]
)
async_add_entities(sensors)
@ -311,14 +450,21 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
"""Define a SmartThings Sensor."""
def __init__(
self, device, attribute: str, name: str, default_unit: str, device_class: str
):
self,
device: DeviceEntity,
attribute: str,
name: str,
default_unit: str,
device_class: str,
state_class: str | None,
) -> None:
"""Init the class."""
super().__init__(device)
self._attribute = attribute
self._name = name
self._device_class = device_class
self._default_unit = default_unit
self._attr_state_class = state_class
@property
def name(self) -> str:
@ -346,6 +492,13 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
unit = self._device.status.attributes[self._attribute].unit
return UNITS.get(unit, unit) if unit else self._default_unit
@property
def last_reset(self) -> datetime | None:
"""Return the time when the sensor was last reset, if any."""
if self._attribute == Attribute.energy:
return utc_from_timestamp(0)
return None
class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity):
"""Define a SmartThings Three Axis Sensor."""

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Sequence
from pysmartthings import Attribute, Capability
from pysmartthings import Capability
from homeassistant.components.switch import SwitchEntity
@ -48,16 +48,6 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity):
# the entity state ahead of receiving the confirming push updates
self.async_write_ha_state()
@property
def current_power_w(self):
"""Return the current power usage in W."""
return self._device.status.attributes[Attribute.power].value
@property
def today_energy_kwh(self):
"""Return the today total energy usage in kWh."""
return self._device.status.attributes[Attribute.energy].value
@property
def is_on(self) -> bool:
"""Return true if light is on."""

View file

@ -6,7 +6,11 @@ real HTTP calls are not initiated during testing.
"""
from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability
from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN
from homeassistant.components.sensor import (
DEVICE_CLASSES,
DOMAIN as SENSOR_DOMAIN,
STATE_CLASSES,
)
from homeassistant.components.smartthings import sensor
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
from homeassistant.config_entries import ConfigEntryState
@ -33,6 +37,8 @@ async def test_mapping_integrity():
assert (
sensor_map.device_class in DEVICE_CLASSES
), sensor_map.device_class
if sensor_map.state_class:
assert sensor_map.state_class in STATE_CLASSES, sensor_map.state_class
async def test_entity_state(hass, device_factory):
@ -95,6 +101,44 @@ async def test_entity_and_device_attributes(hass, device_factory):
assert entry.manufacturer == "Unavailable"
async def test_energy_sensors_for_switch_device(hass, device_factory):
"""Test the attributes of the entity are correct."""
# Arrange
device = device_factory(
"Switch_1",
[Capability.switch, Capability.power_meter, Capability.energy_meter],
{Attribute.switch: "off", Attribute.power: 355, Attribute.energy: 11.422},
)
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
# Act
await setup_platform(hass, SENSOR_DOMAIN, devices=[device])
# Assert
state = hass.states.get("sensor.switch_1_energy_meter")
assert state
assert state.state == "11.422"
entry = entity_registry.async_get("sensor.switch_1_energy_meter")
assert entry
assert entry.unique_id == f"{device.device_id}.{Attribute.energy}"
entry = device_registry.async_get_device({(DOMAIN, device.device_id)})
assert entry
assert entry.name == device.label
assert entry.model == device.device_type_name
assert entry.manufacturer == "Unavailable"
state = hass.states.get("sensor.switch_1_power_meter")
assert state
assert state.state == "355"
entry = entity_registry.async_get("sensor.switch_1_power_meter")
assert entry
assert entry.unique_id == f"{device.device_id}.{Attribute.power}"
entry = device_registry.async_get_device({(DOMAIN, device.device_id)})
assert entry
assert entry.name == device.label
assert entry.model == device.device_type_name
assert entry.manufacturer == "Unavailable"
async def test_update_from_signal(hass, device_factory):
"""Test the binary_sensor updates when receiving a signal."""
# Arrange

View file

@ -7,11 +7,7 @@ real HTTP calls are not initiated during testing.
from pysmartthings import Attribute, Capability
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
from homeassistant.components.switch import (
ATTR_CURRENT_POWER_W,
ATTR_TODAY_ENERGY_KWH,
DOMAIN as SWITCH_DOMAIN,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.helpers import device_registry as dr, entity_registry as er
@ -72,8 +68,6 @@ async def test_turn_on(hass, device_factory):
state = hass.states.get("switch.switch_1")
assert state is not None
assert state.state == "on"
assert state.attributes[ATTR_CURRENT_POWER_W] == 355
assert state.attributes[ATTR_TODAY_ENERGY_KWH] == 11.422
async def test_update_from_signal(hass, device_factory):