diff --git a/README.md b/README.md index 577b67b..3119dcb 100644 --- a/README.md +++ b/README.md @@ -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. -[![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. diff --git a/custom_components/vesync/config_flow.py b/custom_components/vesync/config_flow.py index 4ea05a0..a8e1fde 100644 --- a/custom_components/vesync/config_flow.py +++ b/custom_components/vesync/config_flow.py @@ -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 diff --git a/custom_components/vesync/fan.py b/custom_components/vesync/fan.py index e69742f..5d6ff9f 100644 --- a/custom_components/vesync/fan.py +++ b/custom_components/vesync/fan.py @@ -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): @@ -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: @@ -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, @@ -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() diff --git a/custom_components/vesync/light.py b/custom_components/vesync/light.py index c2d68bd..cd27599 100644 --- a/custom_components/vesync/light.py +++ b/custom_components/vesync/light.py @@ -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__) @@ -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 @@ -62,25 +65,19 @@ 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)) @@ -88,129 +85,82 @@ def _ha_brightness_to_vesync(ha_brightness): 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): @@ -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) diff --git a/custom_components/vesync/manifest.json b/custom_components/vesync/manifest.json index a25b1fb..64225e3 100644 --- a/custom_components/vesync/manifest.json +++ b/custom_components/vesync/manifest.json @@ -1,7 +1,10 @@ { "domain": "vesync", "name": "VeSync", + "version": "1.4.2", + "integration_type": "hub", "codeowners": [ + "@jdaleo23", "@markperdue", "@webdjoe", "@thegardenmonkey", @@ -24,6 +27,5 @@ ], "requirements": [ "pyvesync==2.1.15" - ], - "version": "1.3.3" + ] }