From 56c577c8321d543197c469eceaaefe660ed2b698 Mon Sep 17 00:00:00 2001 From: Jonathan Keslin Date: Mon, 10 Jan 2022 09:39:31 -0800 Subject: [PATCH] Expose power & energy usage of VeSync outlets as separate sensors (#61837) --- .coveragerc | 1 + homeassistant/components/vesync/__init__.py | 20 ++- homeassistant/components/vesync/common.py | 54 +++++-- homeassistant/components/vesync/const.py | 1 + homeassistant/components/vesync/sensor.py | 152 ++++++++++++++++++++ homeassistant/components/vesync/switch.py | 10 -- 6 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/vesync/sensor.py diff --git a/.coveragerc b/.coveragerc index 6f036bb9449..020b7d5cf2b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1248,6 +1248,7 @@ omit = homeassistant/components/vesync/const.py homeassistant/components/vesync/fan.py homeassistant/components/vesync/light.py + homeassistant/components/vesync/sensor.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/binary_sensor.py diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 97a91f1209e..3dd30b96f40 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -18,10 +18,11 @@ from .const import ( VS_FANS, VS_LIGHTS, VS_MANAGER, + VS_SENSORS, VS_SWITCHES, ) -PLATFORMS = [Platform.SWITCH, Platform.FAN, Platform.LIGHT] +PLATFORMS = [Platform.SWITCH, Platform.FAN, Platform.LIGHT, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -53,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b switches = hass.data[DOMAIN][VS_SWITCHES] = [] fans = hass.data[DOMAIN][VS_FANS] = [] lights = hass.data[DOMAIN][VS_LIGHTS] = [] + sensors = hass.data[DOMAIN][VS_SENSORS] = [] hass.data[DOMAIN][VS_DISPATCHERS] = [] @@ -68,17 +70,23 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b lights.extend(device_dict[VS_LIGHTS]) hass.async_create_task(forward_setup(config_entry, Platform.LIGHT)) + if device_dict[VS_SENSORS]: + sensors.extend(device_dict[VS_SENSORS]) + hass.async_create_task(forward_setup(config_entry, Platform.SENSOR)) + async def async_new_device_discovery(service: ServiceCall) -> None: """Discover if new devices should be added.""" manager = hass.data[DOMAIN][VS_MANAGER] switches = hass.data[DOMAIN][VS_SWITCHES] fans = hass.data[DOMAIN][VS_FANS] lights = hass.data[DOMAIN][VS_LIGHTS] + sensors = hass.data[DOMAIN][VS_SENSORS] dev_dict = await async_process_devices(hass, manager) switch_devs = dev_dict.get(VS_SWITCHES, []) fan_devs = dev_dict.get(VS_FANS, []) light_devs = dev_dict.get(VS_LIGHTS, []) + sensor_devs = dev_dict.get(VS_SENSORS, []) switch_set = set(switch_devs) new_switches = list(switch_set.difference(switches)) @@ -110,6 +118,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b lights.extend(new_lights) hass.async_create_task(forward_setup(config_entry, "light")) + sensor_set = set(sensor_devs) + new_sensors = list(sensor_set.difference(sensors)) + if new_sensors and sensors: + sensors.extend(new_sensors) + async_dispatcher_send(hass, VS_DISCOVERY.format(VS_SENSORS), new_sensors) + return + if new_sensors and not sensors: + sensors.extend(new_sensors) + hass.async_create_task(forward_setup(config_entry, "sensor")) + hass.services.async_register( DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery ) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index d51da7a375b..1104a84e6b6 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -1,9 +1,9 @@ """Common utilities for VeSync Component.""" import logging -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import Entity, ToggleEntity -from .const import VS_FANS, VS_LIGHTS, VS_SWITCHES +from .const import DOMAIN, VS_FANS, VS_LIGHTS, VS_SENSORS, VS_SWITCHES _LOGGER = logging.getLogger(__name__) @@ -14,6 +14,7 @@ async def async_process_devices(hass, manager): devices[VS_SWITCHES] = [] devices[VS_FANS] = [] devices[VS_LIGHTS] = [] + devices[VS_SENSORS] = [] await hass.async_add_executor_job(manager.update) @@ -27,6 +28,8 @@ async def async_process_devices(hass, manager): if manager.outlets: devices[VS_SWITCHES].extend(manager.outlets) + # Expose outlets' power & energy usage as separate sensors + devices[VS_SENSORS].extend(manager.outlets) _LOGGER.info("%d VeSync outlets found", len(manager.outlets)) if manager.switches: @@ -40,39 +43,66 @@ async def async_process_devices(hass, manager): return devices -class VeSyncDevice(ToggleEntity): - """Base class for VeSync Device Representations.""" +class VeSyncBaseEntity(Entity): + """Base class for VeSync Entity Representations.""" def __init__(self, device): """Initialize the VeSync device.""" self.device = device @property - def unique_id(self): + def base_unique_id(self): """Return the ID of this device.""" if isinstance(self.device.sub_device_no, int): return f"{self.device.cid}{str(self.device.sub_device_no)}" return self.device.cid @property - def name(self): + def unique_id(self): + """Return the ID of this device.""" + # The unique_id property may be overridden in subclasses, such as in sensors. Maintaining base_unique_id allows + # us to group related entities under a single device. + return self.base_unique_id + + @property + def base_name(self): """Return the name of the device.""" return self.device.device_name @property - def is_on(self): - """Return True if device is on.""" - return self.device.device_status == "on" + def name(self): + """Return the name of the entity (may be overridden).""" + return self.base_name @property def available(self) -> bool: """Return True if device is available.""" return self.device.connection_status == "online" - def turn_off(self, **kwargs): - """Turn the device off.""" - self.device.turn_off() + @property + def device_info(self): + """Return device information.""" + return { + "identifiers": {(DOMAIN, self.base_unique_id)}, + "name": self.base_name, + "model": self.device.device_type, + "default_manufacturer": "VeSync", + "sw_version": self.device.current_firm_version, + } def update(self): """Update vesync device.""" self.device.update() + + +class VeSyncDevice(VeSyncBaseEntity, ToggleEntity): + """Base class for VeSync Device Representations.""" + + @property + def is_on(self): + """Return True if device is on.""" + return self.device.device_status == "on" + + def turn_off(self, **kwargs): + """Turn the device off.""" + self.device.turn_off() diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index 5d9dfc8aa5d..9a2cb2a1281 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -8,4 +8,5 @@ SERVICE_UPDATE_DEVS = "update_devices" VS_SWITCHES = "switches" VS_FANS = "fans" VS_LIGHTS = "lights" +VS_SENSORS = "sensors" VS_MANAGER = "manager" diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py new file mode 100644 index 00000000000..c217844f256 --- /dev/null +++ b/homeassistant/components/vesync/sensor.py @@ -0,0 +1,152 @@ +"""Support for power & energy sensors for VeSync outlets.""" +from datetime import datetime +import logging + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory + +from .common import VeSyncBaseEntity +from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_SENSORS +from .switch import DEV_TYPE_TO_HA + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up switches.""" + + async def async_discover(devices): + """Add new devices to platform.""" + _async_setup_entities(devices, async_add_entities) + + disp = async_dispatcher_connect( + hass, VS_DISCOVERY.format(VS_SENSORS), async_discover + ) + hass.data[DOMAIN][VS_DISPATCHERS].append(disp) + + _async_setup_entities(hass.data[DOMAIN][VS_SENSORS], async_add_entities) + return True + + +@callback +def _async_setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + dev_list = [] + for dev in devices: + if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet": + dev_list.append(VeSyncPowerSensor(dev)) + dev_list.append(VeSyncEnergySensor(dev)) + else: + # Not an outlet that supports energy/power, so do not create sensor entities + continue + + async_add_entities(dev_list, update_before_add=True) + + +class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity): + """Representation of a sensor describing diagnostics of a VeSync outlet.""" + + def __init__(self, plug): + """Initialize the VeSync outlet device.""" + super().__init__(plug) + self.smartplug = plug + + @property + def entity_category(self): + """Return the diagnostic entity category.""" + return EntityCategory.DIAGNOSTIC + + +class VeSyncPowerSensor(VeSyncSensorEntity): + """Representation of current power use for a VeSync outlet.""" + + @property + def unique_id(self): + """Return unique ID for power sensor on device.""" + return f"{super().unique_id}-power" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} current power" + + @property + def device_class(self): + """Return the power device class.""" + return SensorDeviceClass.POWER + + @property + def native_value(self): + """Return the current power usage in W.""" + return self.smartplug.power + + @property + def native_unit_of_measurement(self): + """Return the Watt unit of measurement.""" + return POWER_WATT + + @property + def state_class(self): + """Return the measurement state class.""" + return SensorStateClass.MEASUREMENT + + def update(self): + """Update outlet details and energy usage.""" + self.smartplug.update() + self.smartplug.update_energy() + + +class VeSyncEnergySensor(VeSyncSensorEntity): + """Representation of current day's energy use for a VeSync outlet.""" + + def __init__(self, plug): + """Initialize the VeSync outlet device.""" + super().__init__(plug) + self.smartplug = plug + + @property + def unique_id(self): + """Return unique ID for power sensor on device.""" + return f"{super().unique_id}-energy" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} energy use today" + + @property + def device_class(self): + """Return the energy device class.""" + return SensorDeviceClass.ENERGY + + @property + def last_reset(self): + """Return datetime representing beginning of day.""" + return datetime.today() + + @property + def native_value(self): + """Return the today total energy usage in kWh.""" + return self.smartplug.energy_today + + @property + def native_unit_of_measurement(self): + """Return the kWh unit of measurement.""" + return ENERGY_KILO_WATT_HOUR + + @property + def state_class(self): + """Return the total_increasing state class.""" + return SensorStateClass.TOTAL_INCREASING + + def update(self): + """Update outlet details and energy usage.""" + self.smartplug.update() + self.smartplug.update_energy() diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 2aeb000c50d..1894ad8eff5 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -88,16 +88,6 @@ class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): "yearly_energy_total": self.smartplug.yearly_energy_total, } - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self.smartplug.power - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - return self.smartplug.energy_today - def update(self): """Update outlet details and energy usage.""" self.smartplug.update()