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
9 changes: 4 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py37-plus]
args: [--py314-plus]
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 25.1.0
hooks:
- id: black
additional_dependencies: ['click==8.0.4']
args:
- --safe
- --quiet
Expand All @@ -31,7 +30,7 @@ repos:
- pydocstyle==6.3.0
files: ^(custom_components)/.+\.py$
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/adrienverge/yamllint.git
Expand Down
4 changes: 2 additions & 2 deletions custom_components/vesync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b

manager = VeSync(username, password, time_zone)

login = await hass.async_add_executor_job(manager.login)
login = await manager.login()

if not login:
_LOGGER.error("Unable to login to the VeSync server")
Expand All @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_update_data():
"""Fetch data from API endpoint."""
try:
await hass.async_add_executor_job(manager.update)
await manager.update()
except Exception as err:
raise UpdateFailed(f"Update failed: {err}") from err

Expand Down
13 changes: 6 additions & 7 deletions custom_components/vesync/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def _setup_entities(devices, async_add_entities, coordinator):
"""Check if device is online and add entity."""
entities = []
for dev in devices:
if hasattr(dev, "fryer_status"):
if hasattr(dev.state, "cook_set_temp"):
for stype in BINARY_SENSOR_TYPES_AIRFRYER.values():
entities.append( # noqa: PERF401
VeSyncairfryerSensor(
Expand Down Expand Up @@ -90,9 +90,8 @@ def name(self):

@property
def is_on(self) -> bool:
"""Return a value indicating whether the Humidifier's water tank is lifted."""
return getattr(self.airfryer, self.stype[0], None)
# return self.smarthumidifier.details["water_tank_lifted"]
"""Return a value indicating whether the fryer state attribute is active."""
return getattr(self.airfryer.state, self.stype[0], None)

@property
def icon(self):
Expand Down Expand Up @@ -130,7 +129,7 @@ def name(self):
@property
def is_on(self) -> bool:
"""Return a value indicating whether the Humidifier is out of water."""
return self.smarthumidifier.details["water_lacks"]
return self.smarthumidifier.state.water_lacks


class VeSyncWaterTankLiftedSensor(VeSyncBinarySensorEntity):
Expand All @@ -149,7 +148,7 @@ def name(self):
@property
def is_on(self) -> bool:
"""Return a value indicating whether the Humidifier's water tank is lifted."""
return self.smarthumidifier.details["water_tank_lifted"]
return self.smarthumidifier.state.water_tank_lifted


class VeSyncFilterOpenStateSensor(VeSyncBinarySensorEntity):
Expand All @@ -168,4 +167,4 @@ def name(self):
@property
def is_on(self) -> bool:
"""Return a value indicating whether the Humidifier's filter is open."""
return self.smarthumidifier.details["filter_open_state"]
return self.smarthumidifier.state.filter_open_state
8 changes: 4 additions & 4 deletions custom_components/vesync/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _setup_entities(devices, async_add_entities, coordinator):
"""Check if device is online and add entity."""
entities = []
for dev in devices:
if hasattr(dev, "cook_set_temp"):
if hasattr(dev.state, "cook_set_temp"):
for stype in SENSOR_TYPES_CS158.values():
entities.append( # noqa: PERF401
VeSyncairfryerButton(
Expand Down Expand Up @@ -91,6 +91,6 @@ def icon(self):
"""Return the icon to use in the frontend, if any."""
return self.stype[2]

def press(self) -> None:
"""Return True if device is on."""
self.airfryer.end()
async def async_press(self) -> None:
"""Press the button."""
await self.airfryer.end()
138 changes: 63 additions & 75 deletions custom_components/vesync/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,13 @@
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.helpers.entity import Entity, ToggleEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from pyvesync.vesyncfan import model_features as fan_model_features
from pyvesync.vesynckitchen import model_features as kitchen_model_features

from .const import (
DOMAIN,
VS_AIRFRYER_TYPES,
VS_BINARY_SENSORS,
VS_BUTTON,
VS_FAN_TYPES,
VS_FANS,
VS_HUMIDIFIERS,
VS_HUMIDIFIERS_TYPES,
VS_LIGHTS,
VS_NUMBERS,
VS_SENSORS,
Expand All @@ -26,8 +21,21 @@
_LOGGER = logging.getLogger(__name__)


_CONFIG_DICT_ATTR_MAP = {
"levels": "fan_levels",
"mist_levels": "mist_levels",
"warm_mist_levels": "warm_mist_levels",
"modes": "modes",
}


def has_feature(device, dictionary, attribute):
"""Return the detail of the attribute."""
if dictionary in ("details", "config"):
return getattr(device.state, attribute, None) is not None
elif dictionary == "_config_dict":
device_attr = _CONFIG_DICT_ATTR_MAP.get(attribute)
return device_attr is not None and len(getattr(device, device_attr, ())) > 0
return getattr(device, dictionary, {}).get(attribute, None) is not None


Expand All @@ -44,79 +52,59 @@ async def async_process_devices(hass, manager):
VS_BUTTON: [],
}

all_devices = list(manager.devices)
redacted = async_redact_data(
{k: [d.__dict__ for d in v] for k, v in manager._dev_list.items()},
[d.to_dict() for d in all_devices],
["cid", "uuid", "mac_id"],
)

_LOGGER.debug(
"Found the following devices: %s",
redacted,
)
_LOGGER.debug("Found the following devices: %s", redacted)

if (
manager.bulbs is None
and manager.fans is None
and manager.kitchen is None
and manager.outlets is None
and manager.switches is None
):
if len(manager.devices) == 0:
_LOGGER.error("Could not find any device to add in %s", redacted)

if manager.fans:
for fan in manager.fans:
# VeSync classifies humidifiers as fans
if fan_model_features(fan.device_type)["module"] in VS_HUMIDIFIERS_TYPES:
devices[VS_HUMIDIFIERS].append(fan)
elif fan_model_features(fan.device_type)["module"] in VS_FAN_TYPES:
devices[VS_FANS].append(fan)
else:
_LOGGER.warning(
"Unknown fan type %s %s (enable debug for more info)",
fan.device_name,
fan.device_type,
)
continue
devices[VS_NUMBERS].append(fan)
devices[VS_SWITCHES].append(fan)
devices[VS_SENSORS].append(fan)
devices[VS_BINARY_SENSORS].append(fan)
devices[VS_LIGHTS].append(fan)

if manager.bulbs:
devices[VS_LIGHTS].extend(manager.bulbs)

if manager.outlets:
devices[VS_SWITCHES].extend(manager.outlets)
# Expose outlets' power & energy usage as separate sensors
devices[VS_SENSORS].extend(manager.outlets)

if manager.switches:
for switch in manager.switches:
if not switch.is_dimmable():
devices[VS_SWITCHES].append(switch)
else:
devices[VS_LIGHTS].append(switch)

if manager.kitchen:
for airfryer in manager.kitchen:
if (
kitchen_model_features(airfryer.device_type)["module"]
in VS_AIRFRYER_TYPES
):
_LOGGER.warning(
"Found air fryer %s, support in progress.\n", airfryer.device_name
)
devices[VS_SENSORS].append(airfryer)
devices[VS_BINARY_SENSORS].append(airfryer)
devices[VS_SWITCHES].append(airfryer)
devices[VS_BUTTON].append(airfryer)
else:
_LOGGER.warning(
"Unknown device type %s %s (enable debug for more info)",
airfryer.device_name,
airfryer.device_type,
)
for purifier in manager.devices.air_purifiers:
devices[VS_FANS].append(purifier)
devices[VS_NUMBERS].append(purifier)
devices[VS_SWITCHES].append(purifier)
devices[VS_SENSORS].append(purifier)
devices[VS_BINARY_SENSORS].append(purifier)
devices[VS_LIGHTS].append(purifier)

for fan in manager.devices.fans:
devices[VS_FANS].append(fan)
devices[VS_NUMBERS].append(fan)
devices[VS_SWITCHES].append(fan)
devices[VS_SENSORS].append(fan)
devices[VS_BINARY_SENSORS].append(fan)
devices[VS_LIGHTS].append(fan)

for humidifier in manager.devices.humidifiers:
devices[VS_HUMIDIFIERS].append(humidifier)
devices[VS_NUMBERS].append(humidifier)
devices[VS_SWITCHES].append(humidifier)
devices[VS_SENSORS].append(humidifier)
devices[VS_BINARY_SENSORS].append(humidifier)
devices[VS_LIGHTS].append(humidifier)

if manager.devices.bulbs:
devices[VS_LIGHTS].extend(manager.devices.bulbs)

if manager.devices.outlets:
devices[VS_SWITCHES].extend(manager.devices.outlets)
devices[VS_SENSORS].extend(manager.devices.outlets)

for switch in manager.devices.switches:
devices[VS_LIGHTS if switch.is_dimmable() else VS_SWITCHES].append(switch)

for airfryer in manager.devices.air_fryers:
_LOGGER.warning(
"Found air fryer %s, support in progress.\n", airfryer.device_name
)
devices[VS_SENSORS].append(airfryer)
devices[VS_BINARY_SENSORS].append(airfryer)
devices[VS_SWITCHES].append(airfryer)
devices[VS_BUTTON].append(airfryer)

return devices

Expand Down Expand Up @@ -156,7 +144,7 @@ def name(self):
@property
def available(self) -> bool:
"""Return True if device is available."""
return self.device.connection_status == "online"
return self.device.state.connection_status == "online"

@property
def device_info(self):
Expand Down Expand Up @@ -186,8 +174,8 @@ def __init__(self, device, coordinator) -> None:
@property
def is_on(self):
"""Return True if device is on."""
return self.device.device_status == "on"
return self.device.is_on

def turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
self.device.turn_off()
await self.device.turn_off()
58 changes: 28 additions & 30 deletions custom_components/vesync/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Config flow utilities."""

from __future__ import annotations

import logging
from collections.abc import Mapping
from typing import Any
Expand Down Expand Up @@ -43,6 +41,32 @@ def reauth_schema(
}


class VeSyncOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle VeSync integration options."""

async def async_step_init(self, user_input=None):
"""Manage options."""

return await self.async_step_vesync_options()

async def async_step_vesync_options(self, user_input=None):
"""Manage the VeSync options."""

if user_input is not None:
return self.async_create_entry(title="", data=user_input)

options = {
vol.Required(
POLLING_INTERVAL,
default=self.config_entry.options.get(POLLING_INTERVAL, 60),
): int,
}

return self.async_show_form(
step_id="vesync_options", data_schema=vol.Schema(options)
)


class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""

Expand Down Expand Up @@ -76,7 +100,7 @@ async def async_step_reauth_confirm(
password = user_input[CONF_PASSWORD]
polling_interval = user_input[POLLING_INTERVAL]
manager = VeSync(username, password)
login = await self.hass.async_add_executor_job(manager.login)
login = await manager.login()
if not login:
errors["base"] = "invalid_auth"
else:
Expand Down Expand Up @@ -121,7 +145,7 @@ async def async_step_user(
password = user_input[CONF_PASSWORD]
polling_interval = user_input[POLLING_INTERVAL]
manager = VeSync(username, password)
login = await self.hass.async_add_executor_job(manager.login)
login = await manager.login()
if not login:
errors["base"] = "invalid_auth"
else:
Expand All @@ -148,29 +172,3 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes
_LOGGER.debug("DHCP discovery detected device %s", hostname)
self.context["title_placeholders"] = {"gateway_id": hostname}
return await self.async_step_user()


class VeSyncOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle VeSync integration options."""

async def async_step_init(self, user_input=None):
"""Manage options."""

return await self.async_step_vesync_options()

async def async_step_vesync_options(self, user_input=None):
"""Manage the VeSync options."""

if user_input is not None:
return self.async_create_entry(title="", data=user_input)

options = {
vol.Required(
POLLING_INTERVAL,
default=self.config_entry.options.get(POLLING_INTERVAL, 60),
): int,
}

return self.async_show_form(
step_id="vesync_options", data_schema=vol.Schema(options)
)
4 changes: 0 additions & 4 deletions custom_components/vesync/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@

VS_TO_HA_ATTRIBUTES = {"humidity": "current_humidity"}

VS_FAN_TYPES = ["VeSyncAirBypass", "VeSyncAir131", "VeSyncAirBaseV2"]
VS_HUMIDIFIERS_TYPES = ["VeSyncHumid200300S", "VeSyncHumid200S", "VeSyncHumid1000S"]
VS_AIRFRYER_TYPES = ["VeSyncAirFryer158"]


DEV_TYPE_TO_HA = {
"ESL100": "bulb-dimmable",
Expand Down
Loading
Loading