Vesync air quality (#72658)
This commit is contained in:
parent
7ecb527648
commit
8c16ac2e47
5 changed files with 169 additions and 164 deletions
|
@ -20,6 +20,8 @@ async def async_process_devices(hass, manager):
|
|||
|
||||
if manager.fans:
|
||||
devices[VS_FANS].extend(manager.fans)
|
||||
# Expose fan sensors separately
|
||||
devices[VS_SENSORS].extend(manager.fans)
|
||||
_LOGGER.info("%d VeSync fans found", len(manager.fans))
|
||||
|
||||
if manager.bulbs:
|
||||
|
@ -49,31 +51,25 @@ class VeSyncBaseEntity(Entity):
|
|||
def __init__(self, device):
|
||||
"""Initialize the VeSync device."""
|
||||
self.device = device
|
||||
self._attr_unique_id = self.base_unique_id
|
||||
self._attr_name = self.base_name
|
||||
|
||||
@property
|
||||
def base_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.
|
||||
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 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."""
|
||||
# Same story here as `base_unique_id` above
|
||||
return self.device.device_name
|
||||
|
||||
@property
|
||||
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."""
|
||||
|
@ -98,6 +94,11 @@ class VeSyncBaseEntity(Entity):
|
|||
class VeSyncDevice(VeSyncBaseEntity, ToggleEntity):
|
||||
"""Base class for VeSync Device Representations."""
|
||||
|
||||
@property
|
||||
def details(self):
|
||||
"""Provide access to the device details dictionary."""
|
||||
return self.device.details
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if device is on."""
|
||||
|
|
|
@ -9,3 +9,31 @@ VS_FANS = "fans"
|
|||
VS_LIGHTS = "lights"
|
||||
VS_SENSORS = "sensors"
|
||||
VS_MANAGER = "manager"
|
||||
|
||||
DEV_TYPE_TO_HA = {
|
||||
"wifi-switch-1.3": "outlet",
|
||||
"ESW03-USA": "outlet",
|
||||
"ESW01-EU": "outlet",
|
||||
"ESW15-USA": "outlet",
|
||||
"ESWL01": "switch",
|
||||
"ESWL03": "switch",
|
||||
"ESO15-TB": "outlet",
|
||||
}
|
||||
|
||||
SKU_TO_BASE_DEVICE = {
|
||||
"LV-PUR131S": "LV-PUR131S",
|
||||
"LV-RH131S": "LV-PUR131S", # Alt ID Model LV-PUR131S
|
||||
"Core200S": "Core200S",
|
||||
"LAP-C201S-AUSR": "Core200S", # Alt ID Model Core200S
|
||||
"LAP-C202S-WUSR": "Core200S", # Alt ID Model Core200S
|
||||
"Core300S": "Core300S",
|
||||
"LAP-C301S-WJP": "Core300S", # Alt ID Model Core300S
|
||||
"Core400S": "Core400S",
|
||||
"LAP-C401S-WJP": "Core400S", # Alt ID Model Core400S
|
||||
"LAP-C401S-WUSR": "Core400S", # Alt ID Model Core400S
|
||||
"LAP-C401S-WAAA": "Core400S", # Alt ID Model Core400S
|
||||
"Core600S": "Core600S",
|
||||
"LAP-C601S-WUS": "Core600S", # Alt ID Model Core600S
|
||||
"LAP-C601S-WUSR": "Core600S", # Alt ID Model Core600S
|
||||
"LAP-C601S-WEU": "Core600S", # Alt ID Model Core600S
|
||||
}
|
||||
|
|
|
@ -14,26 +14,16 @@ from homeassistant.util.percentage import (
|
|||
)
|
||||
|
||||
from .common import VeSyncDevice
|
||||
from .const import DOMAIN, VS_DISCOVERY, VS_FANS
|
||||
from .const import DOMAIN, SKU_TO_BASE_DEVICE, VS_DISCOVERY, VS_FANS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEV_TYPE_TO_HA = {
|
||||
"LV-PUR131S": "fan",
|
||||
"LV-RH131S": "fan", # Alt ID Model LV-PUR131S
|
||||
"Core200S": "fan",
|
||||
"LAP-C201S-AUSR": "fan", # Alt ID Model Core200S
|
||||
"LAP-C202S-WUSR": "fan", # Alt ID Model Core200S
|
||||
"Core300S": "fan",
|
||||
"LAP-C301S-WJP": "fan", # Alt ID Model Core300S
|
||||
"Core400S": "fan",
|
||||
"LAP-C401S-WJP": "fan", # Alt ID Model Core400S
|
||||
"LAP-C401S-WUSR": "fan", # Alt ID Model Core400S
|
||||
"LAP-C401S-WAAA": "fan", # Alt ID Model Core400S
|
||||
"Core600S": "fan",
|
||||
"LAP-C601S-WUS": "fan", # Alt ID Model Core600S
|
||||
"LAP-C601S-WUSR": "fan", # Alt ID Model Core600S
|
||||
"LAP-C601S-WEU": "fan", # Alt ID Model Core600S
|
||||
}
|
||||
|
||||
FAN_MODE_AUTO = "auto"
|
||||
|
@ -41,37 +31,17 @@ FAN_MODE_SLEEP = "sleep"
|
|||
|
||||
PRESET_MODES = {
|
||||
"LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP],
|
||||
"LV-RH131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model LV-PUR131S
|
||||
"Core200S": [FAN_MODE_SLEEP],
|
||||
"LAP-C201S-AUSR": [FAN_MODE_SLEEP], # Alt ID Model Core200S
|
||||
"LAP-C202S-WUSR": [FAN_MODE_SLEEP], # Alt ID Model Core200S
|
||||
"Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP],
|
||||
"LAP-C301S-WJP": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core300S
|
||||
"Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP],
|
||||
"LAP-C401S-WJP": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S
|
||||
"LAP-C401S-WUSR": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S
|
||||
"LAP-C401S-WAAA": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S
|
||||
"Core600S": [FAN_MODE_AUTO, FAN_MODE_SLEEP],
|
||||
"LAP-C601S-WUS": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S
|
||||
"LAP-C601S-WUSR": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S
|
||||
"LAP-C601S-WEU": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S
|
||||
}
|
||||
SPEED_RANGE = { # off is not included
|
||||
"LV-PUR131S": (1, 3),
|
||||
"LV-RH131S": (1, 3), # ALt ID Model LV-PUR131S
|
||||
"Core200S": (1, 3),
|
||||
"LAP-C201S-AUSR": (1, 3), # ALt ID Model Core200S
|
||||
"LAP-C202S-WUSR": (1, 3), # ALt ID Model Core200S
|
||||
"Core300S": (1, 3),
|
||||
"LAP-C301S-WJP": (1, 3), # ALt ID Model Core300S
|
||||
"Core400S": (1, 4),
|
||||
"LAP-C401S-WJP": (1, 4), # ALt ID Model Core400S
|
||||
"LAP-C401S-WUSR": (1, 4), # ALt ID Model Core400S
|
||||
"LAP-C401S-WAAA": (1, 4), # ALt ID Model Core400S
|
||||
"Core600S": (1, 4),
|
||||
"LAP-C601S-WUS": (1, 4), # ALt ID Model Core600S
|
||||
"LAP-C601S-WUSR": (1, 4), # ALt ID Model Core600S
|
||||
"LAP-C601S-WEU": (1, 4), # ALt ID Model Core600S
|
||||
}
|
||||
|
||||
|
||||
|
@ -99,7 +69,7 @@ def _setup_entities(devices, async_add_entities):
|
|||
"""Check if device is online and add entity."""
|
||||
entities = []
|
||||
for dev in devices:
|
||||
if DEV_TYPE_TO_HA.get(dev.device_type) == "fan":
|
||||
if DEV_TYPE_TO_HA.get(SKU_TO_BASE_DEVICE.get(dev.device_type)) == "fan":
|
||||
entities.append(VeSyncFanHA(dev))
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
|
@ -128,19 +98,21 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
|
|||
and (current_level := self.smartfan.fan_level) is not None
|
||||
):
|
||||
return ranged_value_to_percentage(
|
||||
SPEED_RANGE[self.device.device_type], current_level
|
||||
SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], current_level
|
||||
)
|
||||
return None
|
||||
|
||||
@property
|
||||
def speed_count(self) -> int:
|
||||
"""Return the number of speeds the fan supports."""
|
||||
return int_states_in_range(SPEED_RANGE[self.device.device_type])
|
||||
return int_states_in_range(
|
||||
SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]]
|
||||
)
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Get the list of available preset modes."""
|
||||
return PRESET_MODES[self.device.device_type]
|
||||
return PRESET_MODES[SKU_TO_BASE_DEVICE.get(self.device.device_type)]
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
|
@ -171,15 +143,9 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
|
|||
if hasattr(self.smartfan, "night_light"):
|
||||
attr["night_light"] = self.smartfan.night_light
|
||||
|
||||
if self.smartfan.details.get("air_quality_value") is not None:
|
||||
attr["air_quality"] = self.smartfan.details["air_quality_value"]
|
||||
|
||||
if hasattr(self.smartfan, "mode"):
|
||||
attr["mode"] = self.smartfan.mode
|
||||
|
||||
if hasattr(self.smartfan, "filter_life"):
|
||||
attr["filter_life"] = self.smartfan.filter_life
|
||||
|
||||
return attr
|
||||
|
||||
def set_percentage(self, percentage):
|
||||
|
@ -195,7 +161,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
|
|||
self.smartfan.change_fan_speed(
|
||||
math.ceil(
|
||||
percentage_to_ranged_value(
|
||||
SPEED_RANGE[self.device.device_type], percentage
|
||||
SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], percentage
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,25 +1,119 @@
|
|||
"""Support for power & energy sensors for VeSync outlets."""
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
PERCENTAGE,
|
||||
POWER_WATT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .common import VeSyncBaseEntity
|
||||
from .const import DOMAIN, VS_DISCOVERY, VS_SENSORS
|
||||
from .switch import DEV_TYPE_TO_HA
|
||||
from .common import VeSyncBaseEntity, VeSyncDevice
|
||||
from .const import DEV_TYPE_TO_HA, DOMAIN, SKU_TO_BASE_DEVICE, VS_DISCOVERY, VS_SENSORS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VeSyncSensorEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[VeSyncDevice], StateType]
|
||||
|
||||
|
||||
@dataclass
|
||||
class VeSyncSensorEntityDescription(
|
||||
SensorEntityDescription, VeSyncSensorEntityDescriptionMixin
|
||||
):
|
||||
"""Describe VeSync sensor entity."""
|
||||
|
||||
exists_fn: Callable[[VeSyncDevice], bool] = lambda _: True
|
||||
update_fn: Callable[[VeSyncDevice], None] = lambda _: None
|
||||
|
||||
|
||||
def update_energy(device):
|
||||
"""Update outlet details and energy usage."""
|
||||
device.update()
|
||||
device.update_energy()
|
||||
|
||||
|
||||
def sku_supported(device, supported):
|
||||
"""Get the base device of which a device is an instance."""
|
||||
return SKU_TO_BASE_DEVICE.get(device.device_type) in supported
|
||||
|
||||
|
||||
def ha_dev_type(device):
|
||||
"""Get the homeassistant device_type for a given device."""
|
||||
return DEV_TYPE_TO_HA.get(device.device_type)
|
||||
|
||||
|
||||
FILTER_LIFE_SUPPORTED = ["LV-PUR131S", "Core200S", "Core300S", "Core400S", "Core600S"]
|
||||
AIR_QUALITY_SUPPORTED = ["LV-PUR131S", "Core400S", "Core600S"]
|
||||
PM25_SUPPORTED = ["Core400S", "Core600S"]
|
||||
|
||||
SENSORS: tuple[VeSyncSensorEntityDescription, ...] = (
|
||||
VeSyncSensorEntityDescription(
|
||||
key="filter-life",
|
||||
name="Filter Life",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda device: device.details["filter_life"],
|
||||
exists_fn=lambda device: sku_supported(device, FILTER_LIFE_SUPPORTED),
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="air-quality",
|
||||
name="Air Quality",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.details["air_quality"],
|
||||
exists_fn=lambda device: sku_supported(device, AIR_QUALITY_SUPPORTED),
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="pm25",
|
||||
name="PM2.5",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.details["air_quality_value"],
|
||||
exists_fn=lambda device: sku_supported(device, PM25_SUPPORTED),
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="power",
|
||||
name="current power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.details["power"],
|
||||
update_fn=update_energy,
|
||||
exists_fn=lambda device: ha_dev_type(device) == "outlet",
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="energy",
|
||||
name="energy use today",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.details["energy"],
|
||||
update_fn=update_energy,
|
||||
exists_fn=lambda device: ha_dev_type(device) == "outlet",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
|
@ -44,107 +138,33 @@ def _setup_entities(devices, async_add_entities):
|
|||
"""Check if device is online and add entity."""
|
||||
entities = []
|
||||
for dev in devices:
|
||||
if DEV_TYPE_TO_HA.get(dev.device_type) != "outlet":
|
||||
# Not an outlet that supports energy/power, so do not create sensor entities
|
||||
continue
|
||||
entities.append(VeSyncPowerSensor(dev))
|
||||
entities.append(VeSyncEnergySensor(dev))
|
||||
|
||||
for description in SENSORS:
|
||||
if description.exists_fn(dev):
|
||||
entities.append(VeSyncSensorEntity(dev, description))
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity):
|
||||
"""Representation of a sensor describing diagnostics of a VeSync outlet."""
|
||||
"""Representation of a sensor describing a VeSync device."""
|
||||
|
||||
def __init__(self, plug):
|
||||
entity_description: VeSyncSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: VeSyncDevice,
|
||||
description: VeSyncSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the VeSync outlet device."""
|
||||
super().__init__(plug)
|
||||
self.smartplug = plug
|
||||
super().__init__(device)
|
||||
self.entity_description = description
|
||||
self._attr_name = f"{super().name} {description.name}"
|
||||
self._attr_unique_id = f"{super().unique_id}-{description.key}"
|
||||
|
||||
@property
|
||||
def entity_category(self):
|
||||
"""Return the diagnostic entity category."""
|
||||
return EntityCategory.DIAGNOSTIC
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.device)
|
||||
|
||||
|
||||
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 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()
|
||||
def update(self) -> None:
|
||||
"""Run the update function defined for the sensor."""
|
||||
return self.entity_description.update_fn(self.device)
|
||||
|
|
|
@ -8,20 +8,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .common import VeSyncDevice
|
||||
from .const import DOMAIN, VS_DISCOVERY, VS_SWITCHES
|
||||
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_SWITCHES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEV_TYPE_TO_HA = {
|
||||
"wifi-switch-1.3": "outlet",
|
||||
"ESW03-USA": "outlet",
|
||||
"ESW01-EU": "outlet",
|
||||
"ESW15-USA": "outlet",
|
||||
"ESWL01": "switch",
|
||||
"ESWL03": "switch",
|
||||
"ESO15-TB": "outlet",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue