Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
> This a fork of the archived project created by [vlebourl](https://github.com/vlebourl/custom_vesync), and previously maintained by [micahqcade](https://github.com/micahqcade/). Please contribute here.

<!---[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs)-->
[![GitHub release](https://img.shields.io/github/v/release/haext/custom_vesync.svg)](https://GitHub.com/haext/custom_vesync/releases/)

# Legacy Information...
# VeSync custom component for Home Assistant

Custom component for Home Assistant to interact with smart devices via the VeSync platform.
Expand Down
2 changes: 1 addition & 1 deletion custom_components/vesync/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ async def async_step_user(
errors=errors,
)

async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult:
"""Handle DHCP discovery."""
hostname = discovery_info.hostname

Expand Down
31 changes: 18 additions & 13 deletions custom_components/vesync/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,19 @@ def __init__(self, fan, coordinator) -> None:
self._speed_range = (1, 3)

@property
def supported_features(self):
def supported_features(self) -> FanEntityFeature:
"""Flag supported features."""
return (
FanEntityFeature.TURN_ON
| FanEntityFeature.TURN_OFF
| FanEntityFeature.SET_SPEED
| FanEntityFeature.PRESET_MODE
if self.speed_count > 1
else FanEntityFeature.TURN_ON
| FanEntityFeature.TURN_OFF
| FanEntityFeature.SET_SPEED
)
# Start with power features which are now required for the turn_on/off actions to work
features = FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF

# Add speed features if applicable
features |= FanEntityFeature.SET_SPEED

# Add preset features if more than one speed exists
if self.speed_count > 1:
features |= FanEntityFeature.PRESET_MODE

return features

@property
def percentage(self):
Expand Down Expand Up @@ -159,7 +160,7 @@ def set_preset_mode(self, preset_mode):
"""Set the preset mode of device."""
if preset_mode not in self.preset_modes:
raise ValueError(
"{preset_mode} is not one of the valid preset modes: {self.preset_modes}"
f"{preset_mode} is not one of the valid preset modes: {self.preset_modes}"
)

if not self.smartfan.is_on:
Expand All @@ -181,7 +182,6 @@ def set_preset_mode(self, preset_mode):

def turn_on(
self,
# speed: str | None = None,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs,
Expand All @@ -193,3 +193,8 @@ def turn_on(
if percentage is None:
percentage = 50
self.set_percentage(percentage)

def turn_off(self, **kwargs) -> None:
"""Turn the device off."""
self.smartfan.turn_off()
self.schedule_update_ha_state()
174 changes: 56 additions & 118 deletions custom_components/vesync/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.color import (
color_temperature_kelvin_to_mired,
color_temperature_mired_to_kelvin,
)

from .common import VeSyncDevice, has_feature
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_FAN_TYPES, VS_LIGHTS
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_LIGHTS

_LOGGER = logging.getLogger(__name__)

Expand All @@ -26,7 +30,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up lights."""

coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]

@callback
Expand Down Expand Up @@ -62,155 +65,102 @@ def _setup_entities(devices, async_add_entities, coordinator):

def _vesync_brightness_to_ha(vesync_brightness):
try:
# check for validity of brightness value received
brightness_value = int(vesync_brightness)
except ValueError:
# deal if any unexpected/non numeric value
_LOGGER.debug(
"VeSync - received unexpected 'brightness' value from pyvesync api: %s",
vesync_brightness,
)
return None
# convert percent brightness to ha expected range
return round((max(1, brightness_value) / 100) * 255)


def _ha_brightness_to_vesync(ha_brightness):
# get brightness from HA data
brightness = int(ha_brightness)
# ensure value between 1-255
brightness = max(1, min(brightness, 255))
# convert to percent that vesync api expects
brightness = round((brightness / 255) * 100)
return max(1, min(brightness, 100))


class VeSyncBaseLight(VeSyncDevice, LightEntity):
"""Base class for VeSync Light Devices Representations."""

def __init_(self, light, coordinator):
"""Initialize the VeSync light device."""
super().__init__(light, coordinator)

@property
def brightness(self):
"""Get light brightness."""
# get value from pyvesync library api,
return _vesync_brightness_to_ha(self.device.brightness)

def turn_on(self, **kwargs):
"""Turn the device on."""
attribute_adjustment_only = False
# set white temperature
if (
self.color_mode in (ColorMode.COLOR_TEMP,)
and ATTR_COLOR_TEMP_KELVIN in kwargs
):
# get white temperature from HA data
color_temp = int(kwargs[ATTR_COLOR_TEMP_KELVIN])
# ensure value between min-max supported Mireds
color_temp = max(self.min_mireds, min(color_temp, self.max_mireds))
# convert Mireds to Percent value that api expects
color_temp = round(
((color_temp - self.min_mireds) / (self.max_mireds - self.min_mireds))
* 100

# Handle Color Temperature (Kelvin)
if self.color_mode == ColorMode.COLOR_TEMP and ATTR_COLOR_TEMP_KELVIN in kwargs:
kelvin = int(kwargs[ATTR_COLOR_TEMP_KELVIN])
# Ensure within Kelvin bounds
kelvin = max(self.min_color_temp_kelvin, min(kelvin, self.max_color_temp_kelvin))

# VeSync API expects 0-100 percentage (0=Cold, 100=Warm)
# Map Kelvin to Percent
mireds = color_temperature_kelvin_to_mired(kelvin)
color_temp_pct = round(
((mireds - self.min_mireds) / (self.max_mireds - self.min_mireds)) * 100
)
# flip cold/warm to what pyvesync api expects
color_temp = 100 - color_temp
# ensure value between 0-100
color_temp = max(0, min(color_temp, 100))
# call pyvesync library api method to set color_temp
self.device.set_color_temp(color_temp)
# flag attribute_adjustment_only, so it doesn't turn_on the device redundantly
# Flip logic for pyvesync (100 - pct)
color_temp_pct = 100 - color_temp_pct
color_temp_pct = max(0, min(color_temp_pct, 100))

self.device.set_color_temp(color_temp_pct)
attribute_adjustment_only = True
# set brightness level
if (
self.color_mode in (ColorMode.BRIGHTNESS, ColorMode.COLOR_TEMP)
and ATTR_BRIGHTNESS in kwargs
):
# get brightness from HA data

# Handle Brightness
if ATTR_BRIGHTNESS in kwargs:
brightness = _ha_brightness_to_vesync(kwargs[ATTR_BRIGHTNESS])
self.device.set_brightness(brightness)
# flag attribute_adjustment_only, so it doesn't turn_on the device redundantly
attribute_adjustment_only = True
# check flag if should skip sending the turn_on command

if attribute_adjustment_only:
return
# send turn_on command to pyvesync api

self.device.turn_on()


class VeSyncDimmableLightHA(VeSyncBaseLight, LightEntity):
class VeSyncDimmableLightHA(VeSyncBaseLight):
"""Representation of a VeSync dimmable light device."""

def __init__(self, device, coordinator) -> None:
"""Initialize the VeSync dimmable light device."""
super().__init__(device, coordinator)

@property
def color_mode(self):
"""Set color mode for this entity."""
return ColorMode.BRIGHTNESS

@property
def supported_color_modes(self):
"""Flag supported color_modes (in an array format)."""
return [ColorMode.BRIGHTNESS]
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}


class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity):
class VeSyncTunableWhiteLightHA(VeSyncBaseLight):
"""Representation of a VeSync Tunable White Light device."""

def __init__(self, device, coordinator) -> None:
"""Initialize the VeSync Tunable White Light device."""
super().__init__(device, coordinator)
_attr_color_mode = ColorMode.COLOR_TEMP
_attr_supported_color_modes = {ColorMode.COLOR_TEMP}

@property
def color_temp(self):
"""Get device white temperature."""
# get value from pyvesync library api,
def color_temp_kelvin(self):
"""Get device white temperature in Kelvin."""
result = self.device.color_temp_pct
try:
# check for validity of brightness value received
color_temp_value = int(result)
except ValueError:
# deal if any unexpected/non numeric value
_LOGGER.debug(
"VeSync - received unexpected 'color_temp_pct' value from pyvesync api: %s",
result,
)
return 0
# flip cold/warm
color_temp_value = 100 - color_temp_value
# ensure value between 0-100
color_temp_value = max(0, min(color_temp_value, 100))
# convert percent value to Mireds
color_temp_value = round(
self.min_mireds
+ ((self.max_mireds - self.min_mireds) / 100 * color_temp_value)
)
# ensure value between minimum and maximum Mireds
return max(self.min_mireds, min(color_temp_value, self.max_mireds))
pct = int(result)
except (ValueError, TypeError):
return self.min_color_temp_kelvin

# Convert percent back to Mireds
pct = 100 - pct
mireds = self.min_mireds + ((self.max_mireds - self.min_mireds) / 100 * pct)
return color_temperature_mired_to_kelvin(mireds)

@property
def min_mireds(self):
"""Set device coldest white temperature."""
return 154 # 154 Mireds ( 1,000,000 divided by 6500 Kelvin = 154 Mireds)

def min_mireds(self): return 154 # 6500K
@property
def max_mireds(self):
"""Set device warmest white temperature."""
return 370 # 370 Mireds ( 1,000,000 divided by 2700 Kelvin = 370 Mireds)
def max_mireds(self): return 370 # 2700K

@property
def color_mode(self):
"""Set color mode for this entity."""
return ColorMode.COLOR_TEMP

def min_color_temp_kelvin(self): return 2700
@property
def supported_color_modes(self):
"""Flag supported color_modes (in an array format)."""
return [ColorMode.COLOR_TEMP]
def max_color_temp_kelvin(self): return 6500


class VeSyncNightLightHA(VeSyncDimmableLightHA):
Expand All @@ -220,59 +170,47 @@ def __init__(self, device, coordinator) -> None:
"""Initialize the VeSync device."""
super().__init__(device, coordinator)
self.device = device
self.has_brightness = has_feature(
self.device, "details", "night_light_brightness"
)
self.has_brightness = has_feature(self.device, "details", "night_light_brightness")

@property
def unique_id(self):
"""Return the ID of this device."""
return f"{super().unique_id}-night-light"

@property
def name(self):
"""Return the name of the device."""
return f"{super().name} night light"

@property
def brightness(self):
"""Get night light brightness."""
return (
_vesync_brightness_to_ha(self.device.details["night_light_brightness"])
if self.has_brightness
else {"on": 255, "dim": 125, "off": 0}[self.device.details["night_light"]]
)
if self.has_brightness:
return _vesync_brightness_to_ha(self.device.details["night_light_brightness"])
return {"on": 255, "dim": 125, "off": 0}.get(self.device.details["night_light"], 0)

@property
def is_on(self):
"""Return True if night light is on."""
if has_feature(self.device, "details", "night_light"):
return self.device.details["night_light"] in ["on", "dim"]
if self.has_brightness:
return self.device.details["night_light_brightness"] > 0
return self.device.details.get("night_light_brightness", 0) > 0
return False

@property
def entity_category(self):
"""Return the configuration entity category."""
return EntityCategory.CONFIG

def turn_on(self, **kwargs):
"""Turn the night light on."""
if self.device._config_dict["module"] in VS_FAN_TYPES:
if self.device._config_dict.get("module") in VS_FAN_TYPES:
if ATTR_BRIGHTNESS in kwargs and kwargs[ATTR_BRIGHTNESS] < 255:
self.device.set_night_light("dim")
else:
self.device.set_night_light("on")
elif ATTR_BRIGHTNESS in kwargs:
self.device.set_night_light_brightness(
_ha_brightness_to_vesync(kwargs[ATTR_BRIGHTNESS])
)
self.device.set_night_light_brightness(_ha_brightness_to_vesync(kwargs[ATTR_BRIGHTNESS]))
else:
self.device.set_night_light_brightness(100)

def turn_off(self, **kwargs):
"""Turn the night light off."""
if self.device._config_dict["module"] in VS_FAN_TYPES:
if self.device._config_dict.get("module") in VS_FAN_TYPES:
self.device.set_night_light("off")
else:
self.device.set_night_light_brightness(0)
6 changes: 4 additions & 2 deletions custom_components/vesync/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"domain": "vesync",
"name": "VeSync",
"version": "1.4.2",
"integration_type": "hub",
"codeowners": [
"@jdaleo23",
"@markperdue",
"@webdjoe",
"@thegardenmonkey",
Expand All @@ -24,6 +27,5 @@
],
"requirements": [
"pyvesync==2.1.15"
],
"version": "1.3.3"
]
}
Loading