Remove local pyvesync lib and migrate to public one. (#25)

* move to pip pyvesync library

* remove pyvesync

* improve extra attributes

* fix style
This commit is contained in:
Vincent Le Bourlot 2022-06-08 11:08:19 +02:00 committed by GitHub
parent 4f3c7fd36f
commit d10928ecca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 29 additions and 3423 deletions

View File

@ -6,6 +6,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from pyvesync.vesync import VeSync
from .common import async_process_devices
from .const import (
@ -21,7 +22,6 @@ from .const import (
VS_SENSORS,
VS_SWITCHES,
)
from .pyvesync.vesync import VeSync
PLATFORMS = {
Platform.SWITCH: VS_SWITCHES,

View File

@ -2,6 +2,7 @@
import logging
from homeassistant.helpers.entity import Entity, ToggleEntity
from pyvesync.vesyncfan import model_features
from .const import (
DOMAIN,
@ -13,7 +14,6 @@ from .const import (
VS_SENSORS,
VS_SWITCHES,
)
from .pyvesync.vesyncfan import model_features
_LOGGER = logging.getLogger(__name__)

View File

@ -8,9 +8,9 @@ from homeassistant.components import dhcp
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from pyvesync.vesync import VeSync
from .const import DOMAIN
from .pyvesync.vesync import VeSync
_LOGGER = logging.getLogger(__name__)

View File

@ -16,6 +16,8 @@ VS_MANAGER = "manager"
VS_MODE_AUTO = "auto"
VS_MODE_MANUAL = "manual"
VS_TO_HA_ATTRIBUTES = {"humidity": "current_humidity"}
DEV_TYPE_TO_HA = {
"Core200S": "fan",
"Core300S": "fan",

View File

@ -14,7 +14,7 @@ from homeassistant.util.percentage import (
)
from .common import VeSyncDevice
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_FANS
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_FANS, VS_TO_HA_ATTRIBUTES
_LOGGER = logging.getLogger(__name__)
@ -114,28 +114,13 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
def extra_state_attributes(self):
"""Return the state attributes of the fan."""
attr = {}
if hasattr(self.smartfan, "active_time"):
attr["active_time"] = self.smartfan.active_time
if hasattr(self.smartfan, "screen_status"):
attr["screen_status"] = self.smartfan.screen_status
if hasattr(self.smartfan, "child_lock"):
attr["child_lock"] = self.smartfan.child_lock
if hasattr(self.smartfan, "night_light"):
attr["night_light"] = self.smartfan.night_light
if hasattr(self.smartfan, "air_quality"):
attr["air_quality"] = self.smartfan.air_quality
if hasattr(self.smartfan, "mode"):
attr["mode"] = self.smartfan.mode
if hasattr(self.smartfan, "filter_life"):
attr["filter_life"] = self.smartfan.filter_life
for k, v in self.smarthumidifier.details.items():
if k in VS_TO_HA_ATTRIBUTES:
attr[VS_TO_HA_ATTRIBUTES[k]] = v
elif k in self.state_attributes:
attr[f"vs_{k}"] = v
else:
attr[k] = v
return attr
def set_percentage(self, percentage):

View File

@ -14,7 +14,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import VeSyncDevice, is_humidifier
from .const import DOMAIN, VS_DISCOVERY, VS_HUMIDIFIERS, VS_MODE_AUTO, VS_MODE_MANUAL
from .const import (
DOMAIN,
VS_DISCOVERY,
VS_HUMIDIFIERS,
VS_MODE_AUTO,
VS_MODE_MANUAL,
VS_TO_HA_ATTRIBUTES,
)
_LOGGER = logging.getLogger(__name__)
@ -22,7 +29,6 @@ MAX_HUMIDITY = 80
MIN_HUMIDITY = 30
MODES = [MODE_AUTO, MODE_NORMAL, MODE_SLEEP]
VESYNC_TO_HA_ATTRIBUTES = {"humidity": "current_humidity"}
async def async_setup_entry(
@ -117,9 +123,11 @@ class VeSyncHumidifierHA(VeSyncDevice, HumidifierEntity):
attr = {}
for k, v in self.smarthumidifier.details.items():
if k in VESYNC_TO_HA_ATTRIBUTES:
attr[VESYNC_TO_HA_ATTRIBUTES[k]] = v
elif k not in self.state_attributes:
if k in VS_TO_HA_ATTRIBUTES:
attr[VS_TO_HA_ATTRIBUTES[k]] = v
elif k in self.state_attributes:
attr[f"vs_{k}"] = v
else:
attr[k] = v
return attr

View File

@ -1,6 +0,0 @@
"""VeSync API Library."""
import logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)5s - %(message)s"
)

View File

@ -1,241 +0,0 @@
"""Helper functions for VeSync API."""
import hashlib
import logging
import time
import requests
logger = logging.getLogger(__name__)
API_BASE_URL = "https://smartapi.vesync.com"
API_RATE_LIMIT = 30
API_TIMEOUT = 5
DEFAULT_TZ = "America/New_York"
DEFAULT_REGION = "US"
APP_VERSION = "2.8.6"
PHONE_BRAND = "SM N9005"
PHONE_OS = "Android"
MOBILE_ID = "1234567890123456"
USER_TYPE = "1"
BYPASS_APP_V = "VeSync 3.0.51"
class Helpers:
"""VeSync Helper Functions."""
@staticmethod
def req_headers(manager) -> dict:
"""Build header for api requests."""
return {
"accept-language": "en",
"accountId": manager.account_id,
"appVersion": APP_VERSION,
"content-type": "application/json",
"tk": manager.token,
"tz": manager.time_zone,
}
@staticmethod
def req_body_base(manager) -> dict:
"""Return universal keys for body of api requests."""
return {"timeZone": manager.time_zone, "acceptLanguage": "en"}
@staticmethod
def req_body_auth(manager) -> dict:
"""Keys for authenticating api requests."""
return {"accountID": manager.account_id, "token": manager.token}
@staticmethod
def req_body_details() -> dict:
"""Detail keys for api requests."""
return {
"appVersion": APP_VERSION,
"phoneBrand": PHONE_BRAND,
"phoneOS": PHONE_OS,
"traceId": str(int(time.time())),
}
@classmethod
def req_body(cls, manager, type_) -> dict:
"""Builder for body of api requests."""
body = {}
if type_ == "login":
body = {**cls.req_body_base(manager), **cls.req_body_details()}
body["email"] = manager.username
body["password"] = cls.hash_password(manager.password)
body["devToken"] = ""
body["userType"] = USER_TYPE
body["method"] = "login"
elif type_ == "devicedetail":
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body["method"] = "devicedetail"
body["mobileId"] = MOBILE_ID
elif type_ == "devicelist":
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body["method"] = "devices"
body["pageNo"] = "1"
body["pageSize"] = "100"
elif type_ == "devicestatus":
body = {**cls.req_body_base(manager), **cls.req_body_auth(manager)}
elif type_ == "energy_week":
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body["method"] = "energyweek"
body["mobileId"] = MOBILE_ID
elif type_ == "energy_month":
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body["method"] = "energymonth"
body["mobileId"] = MOBILE_ID
elif type_ == "energy_year":
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body["method"] = "energyyear"
body["mobileId"] = MOBILE_ID
elif type_ == "bypass":
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body["method"] = "bypass"
elif type_ == "bypass_config":
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body["method"] = "firmwareUpdateInfo"
return body
@staticmethod
def calculate_hex(hex_string) -> float:
"""Credit for conversion to itsnotlupus/vesync_wsproxy."""
hex_conv = hex_string.split(":")
return (int(hex_conv[0], 16) + int(hex_conv[1], 16)) / 8192
@staticmethod
def hash_password(string) -> str:
"""Encode password."""
return hashlib.md5(string.encode("utf-8")).hexdigest()
@staticmethod
def call_api(
api: str, method: str, json: dict = None, headers: dict = None
) -> tuple:
"""Make API calls by passing endpoint, header and body."""
response = None
status_code = None
try:
logger.debug("[%s] calling '%s' api", method, api)
if method.lower() == "get":
r = requests.get(
API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT
)
elif method.lower() == "post":
r = requests.post(
API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT
)
elif method.lower() == "put":
r = requests.put(
API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT
)
except requests.exceptions.RequestException as e:
logger.debug(e)
except Exception as e:
logger.debug(e)
else:
if r.status_code == 200:
status_code = 200
if r.content:
response = r.json()
else:
logger.debug("Unable to fetch %s%s", API_BASE_URL, api)
return response, status_code
@staticmethod
def code_check(r: dict) -> bool:
"""Test if code == 0 for successful API call."""
return isinstance(r, dict) and r.get("code") == 0
@staticmethod
def build_details_dict(r: dict) -> dict:
"""Build details dictionary from API response."""
return {
"active_time": r.get("activeTime", 0),
"energy": r.get("energy", 0),
"night_light_status": r.get("nightLightStatus", None),
"night_light_brightness": r.get("nightLightBrightness", None),
"night_light_automode": r.get("nightLightAutomode", None),
"power": r.get("power", 0),
"voltage": r.get("voltage", 0),
}
@staticmethod
def build_energy_dict(r: dict) -> dict:
"""Build energy dictionary from API response."""
return {
"energy_consumption_of_today": r.get("energyConsumptionOfToday", 0),
"cost_per_kwh": r.get("costPerKWH", 0),
"max_energy": r.get("maxEnergy", 0),
"total_energy": r.get("totalEnergy", 0),
"currency": r.get("currency", 0),
"data": r.get("data", 0),
}
@staticmethod
def build_config_dict(r: dict) -> dict:
"""Build configuration dictionary from API response."""
if r.get("threshold") is not None:
threshold = r.get("threshold")
else:
threshold = r.get("thresHold")
return {
"current_firmware_version": r.get("currentFirmVersion"),
"latest_firmware_version": r.get("latestFirmVersion"),
"maxPower": r.get("maxPower"),
"threshold": threshold,
"power_protection": r.get("powerProtectionStatus"),
"energy_saving_status": r.get("energySavingStatus"),
}
@classmethod
def bypass_body_v2(cls, manager):
"""Build body dict for bypass calls."""
bdy = {}
bdy.update(**cls.req_body(manager, "bypass"))
bdy["method"] = "bypassV2"
bdy["debugMode"] = False
bdy["deviceRegion"] = DEFAULT_REGION
return bdy
@staticmethod
def bypass_header():
"""Build bypass header dict."""
return {
"Content-Type": "application/json; charset=UTF-8",
"User-Agent": "okhttp/3.12.1",
}

View File

@ -1,300 +0,0 @@
"""VeSync API Device Library."""
import logging
import re
import time
from itertools import chain
from typing import Tuple
from . import vesyncbulb as bulb_mods
from . import vesyncfan as fan_mods
from . import vesyncoutlet as outlet_mods
from . import vesyncswitch as switch_mods
from .helpers import Helpers
from .vesyncbasedevice import VeSyncBaseDevice
logger = logging.getLogger(__name__)
API_RATE_LIMIT: int = 30
DEFAULT_TZ: str = "America/New_York"
DEFAULT_ENER_UP_INT: int = 21600
def object_factory(dev_type, config, manager) -> Tuple[str, VeSyncBaseDevice]:
"""Get device type and instantiate class."""
def fans(dev_type, config, manager):
fan_cls = fan_mods.fan_modules[dev_type]
fan_obj = getattr(fan_mods, fan_cls)
return "fans", fan_obj(config, manager)
def outlets(dev_type, config, manager):
outlet_cls = outlet_mods.outlet_modules[dev_type]
outlet_obj = getattr(outlet_mods, outlet_cls)
return "outlets", outlet_obj(config, manager)
def switches(dev_type, config, manager):
switch_cls = switch_mods.switch_modules[dev_type]
switch_obj = getattr(switch_mods, switch_cls)
return "switches", switch_obj(config, manager)
def bulbs(dev_type, config, manager):
bulb_cls = bulb_mods.bulb_modules[dev_type]
bulb_obj = getattr(bulb_mods, bulb_cls)
return "bulbs", bulb_obj(config, manager)
if dev_type in fan_mods.fan_modules:
type_str, dev_obj = fans(dev_type, config, manager)
elif dev_type in outlet_mods.outlet_modules:
type_str, dev_obj = outlets(dev_type, config, manager)
elif dev_type in switch_mods.switch_modules:
type_str, dev_obj = switches(dev_type, config, manager)
elif dev_type in bulb_mods.bulb_modules:
type_str, dev_obj = bulbs(dev_type, config, manager)
else:
logger.debug(
"Unknown device named %s model %s",
config.get("deviceName", ""),
config.get("deviceType", ""),
)
type_str = "unknown"
dev_obj = None
return type_str, dev_obj
class VeSync:
"""VeSync API functions."""
def __init__(self, username, password, time_zone=DEFAULT_TZ, debug=False):
"""Initialize VeSync class with username, password and time zone."""
self.debug = debug
if debug:
logger.setLevel(logging.DEBUG)
self.username = username
self.password = password
self.token = None
self.account_id = None
self.devices = None
self.enabled = False
self.update_interval = API_RATE_LIMIT
self.last_update_ts = None
self.in_process = False
self._energy_update_interval = DEFAULT_ENER_UP_INT
self._energy_check = True
self._dev_list = {}
self.outlets = []
self.switches = []
self.fans = []
self.bulbs = []
self.scales = []
self._dev_list = {
"fans": self.fans,
"outlets": self.outlets,
"switches": self.switches,
"bulbs": self.bulbs,
}
if isinstance(time_zone, str) and time_zone:
reg_test = r"[^a-zA-Z/_]"
if bool(re.search(reg_test, time_zone)):
self.time_zone = DEFAULT_TZ
logger.debug("Invalid characters in time zone - %s", time_zone)
else:
self.time_zone = time_zone
else:
self.time_zone = DEFAULT_TZ
logger.debug("Time zone is not a string")
@property
def energy_update_interval(self) -> int:
"""Return energy update interval."""
return self._energy_update_interval
@energy_update_interval.setter
def energy_update_interval(self, new_energy_update: int) -> None:
"""Set energy update interval in seconds."""
if new_energy_update > 0:
self._energy_update_interval = new_energy_update
@staticmethod
def remove_dev_test(device, new_list: list) -> bool:
"""Test if device should be removed - False = Remove."""
if isinstance(new_list, list) and device.cid:
for item in new_list:
device_found = False
if "cid" in item:
if device.cid == item["cid"]:
device_found = True
break
else:
logger.debug("No cid found in - %s", str(item))
if not device_found:
logger.debug(
"Device removed - %s - %s", device.device_name, device.device_type
)
return False
return True
def add_dev_test(self, new_dev: dict) -> bool:
"""Test if new device should be added - True = Add."""
if "cid" in new_dev:
for _, v in self._dev_list.items():
for dev in v:
if (
dev.cid == new_dev.get("cid")
and new_dev.get("subDeviceNo", 0) == dev.sub_device_no
):
return False
return True
def remove_old_devices(self, devices: list) -> bool:
"""Remove devices not found in device list return."""
for k, v in self._dev_list.items():
before = len(v)
v[:] = [x for x in v if self.remove_dev_test(x, devices)]
after = len(v)
if before != after:
logger.debug("%s %s removed", str(before - after), k)
return True
@staticmethod
def set_dev_id(devices: list) -> list:
"""Correct devices without cid or uuid."""
dev_rem = []
for dev_num, dev in enumerate(devices):
if dev.get("cid") is None:
if dev.get("macID") is not None:
dev["cid"] = dev["macID"]
elif dev.get("uuid") is not None:
dev["cid"] = dev["uuid"]
else:
dev_rem.append(dev_num)
logger.warning("Device with no ID - %s", dev.get("deviceName"))
if dev_rem:
devices = [i for j, i in enumerate(devices) if j not in dev_rem]
return devices
def process_devices(self, dev_list: list) -> bool:
"""Instantiate Device Objects."""
devices = VeSync.set_dev_id(dev_list)
num_devices = sum(
len(v) if isinstance(v, list) else 1 for _, v in self._dev_list.items()
)
if not devices:
logger.warning("No devices found in api return")
return False
if num_devices == 0:
logger.debug("New device list initialized")
else:
self.remove_old_devices(devices)
devices[:] = [x for x in devices if self.add_dev_test(x)]
detail_keys = ["deviceType", "deviceName", "deviceStatus"]
for dev in devices:
if any(k not in dev for k in detail_keys):
logger.debug("Error adding device")
continue
dev_type = dev.get("deviceType")
try:
device_str, device_obj = object_factory(dev_type, dev, self)
device_list = getattr(self, device_str)
device_list.append(device_obj)
except AttributeError as err:
logger.debug("Error - %s", err)
logger.debug("%s device not added", dev_type)
continue
return True
def get_devices(self) -> bool:
"""Return tuple listing outlets, switches, and fans of devices."""
if not self.enabled:
return False
self.in_process = True
proc_return = False
response, _ = Helpers.call_api(
"/cloud/v1/deviceManaged/devices",
"post",
headers=Helpers.req_headers(self),
json=Helpers.req_body(self, "devicelist"),
)
if response and Helpers.code_check(response):
if "result" in response and "list" in response["result"]:
device_list = response["result"]["list"]
if self.debug:
logger.debug(str(device_list))
proc_return = self.process_devices(device_list)
else:
logger.error("Device list in response not found")
else:
logger.warning("Error retrieving device list")
self.in_process = False
return proc_return
def login(self) -> bool:
"""Return True if log in request succeeds."""
user_check = isinstance(self.username, str) and len(self.username) > 0
pass_check = isinstance(self.password, str) and len(self.password) > 0
if not user_check:
logger.error("Username invalid")
return False
if not pass_check:
logger.error("Password invalid")
return False
response, _ = Helpers.call_api(
"/cloud/v1/user/login", "post", json=Helpers.req_body(self, "login")
)
if Helpers.code_check(response) and "result" in response:
self.token = response.get("result").get("token")
self.account_id = response.get("result").get("accountID")
self.enabled = True
return True
logger.error("Error logging in with username and password")
return False
def device_time_check(self) -> bool:
"""Test if update interval has been exceeded."""
return (
self.last_update_ts is None
or (time.time() - self.last_update_ts) > self.update_interval
)
def update(self) -> None:
"""Fetch updated information about devices."""
if self.device_time_check():
if not self.enabled:
logger.error("Not logged in to VeSync")
return
self.get_devices()
devices = list(self._dev_list.values())
for device in chain(*devices):
device.update()
self.last_update_ts = time.time()
def update_energy(self, bypass_check=False) -> None:
"""Fetch updated energy information about devices."""
if self.outlets:
for outlet in self.outlets:
outlet.update_energy(bypass_check)
def update_all_devices(self) -> None:
"""Run get_details() for each device."""
devices = list(self._dev_list.keys())
for dev in chain(*devices):
dev.get_details()

View File

@ -1,117 +0,0 @@
"""Base class for all VeSync devices."""
import collections
import json
import logging
from typing import Optional, Union
logger = logging.getLogger(__name__)
class VeSyncBaseDevice:
"""Properties shared across all VeSync devices."""
def __init__(self, details: dict, manager):
"""Initialize VeSync device base class."""
self.manager = manager
if "cid" in details and details["cid"] is not None:
self.device_name: str = details.get("deviceName", None)
self.device_image: Optional[str] = details.get("deviceImg", None)
self.cid: str = details.get("cid", None)
self.connection_status: str = details.get("connectionStatus", None)
self.connection_type: Optional[str] = details.get("connectionType", None)
self.device_type: str = details.get("deviceType", None)
self.type: str = details.get("type", None)
self.uuid: Optional[str] = details.get("uuid", None)
self.config_module: str = details.get("configModule", None)
self.mac_id: Optional[str] = details.get("macID", None)
self.mode: Optional[str] = details.get("mode", None)
self.speed: Union[str, int, None] = details.get("speed", None)
self.extension = details.get("extension", None)
self.current_firm_version = details.get("currentFirmVersion", None)
self.sub_device_no = details.get("subDeviceNo", 0)
self.config: dict = {}
if isinstance(details.get("extension"), dict):
ext = details["extension"]
self.speed = ext.get("fanSpeedLevel")
self.mode = ext.get("mode")
if self.connection_status != "online":
self.device_status = "off"
else:
self.device_status = details.get("deviceStatus", None)
else:
logger.error("No cid found for %s", self.__class__.__name__)
def __eq__(self, other):
"""Use device CID and subdevice number to test equality."""
return bool(other.cid == self.cid and other.sub_device_no == self.sub_device_no)
def __hash__(self):
"""Use CID and sub-device number to make device hash."""
if isinstance(self.sub_device_no, int) and self.sub_device_no > 0:
return hash(self.cid + str(self.sub_device_no))
return hash(self.cid)
def __str__(self):
"""Use device info for string represtation of class."""
return f"Device Name: {self.device_name}, \
Device Type: {self.device_type},\
SubDevice No.: {self.sub_device_no},\
Status: {self.device_status}"
def __repr__(self):
"""Representation of device details."""
return f"DevClass: {self.__class__.__name__},\
Name:{self.device_name}, Device No: {self.sub_device_no},\
DevStatus: {self.device_status}, CID: {self.cid}"
@property
def is_on(self) -> bool:
"""Return true if device is on."""
if self.device_status == "on":
return True
return False
@property
def firmware_update(self) -> bool:
"""Return True if firmware update available."""
cfv = self.config.get("current_firmware_version")
lfv = self.config.get("latest_firmware_version")
if cfv is not None and lfv is not None:
if cfv != lfv:
return True
else:
logger.debug("Call device.get_config() to get firmware versions")
return False
def display(self) -> None:
"""Print formatted device info to stdout."""
disp = [
("Device Name:", self.device_name),
("Model: ", self.device_type),
("Subdevice No: ", str(self.sub_device_no)),
("Status: ", self.device_status),
("Online: ", self.connection_status),
("Type: ", self.type),
("CID: ", self.cid),
]
if self.uuid is not None:
disp.append(("UUID: ", self.uuid))
disp1 = collections.OrderedDict(disp)
for k, v in disp1.items():
print(f"{k:.<15} {v:<15}")
def displayJSON(self) -> str:
"""JSON API for device details."""
return json.dumps(
{
"Device Name": self.device_name,
"Model": self.device_type,
"Subdevice No": str(self.sub_device_no),
"Status": self.device_status,
"Online": self.connection_status,
"Type": self.type,
"CID": self.cid,
}
)

View File

@ -1,378 +0,0 @@
"""Etekcity Smart Light Bulb."""
import json
import logging
from abc import ABCMeta, abstractmethod
from typing import Dict, Union
from .helpers import Helpers as helpers
from .vesyncbasedevice import VeSyncBaseDevice
logger = logging.getLogger(__name__)
# Possible features - dimmable, color_temp, rgb_shift
feature_dict: dict = {
"ESL100": {"module": "VeSyncBulbESL100", "features": ["dimmable"]},
"ESL100CW": {
"module": "VeSyncBulbESL100CW",
"features": ["dimmable", "color_temp"],
},
}
bulb_modules: dict = {k: v["module"] for k, v in feature_dict.items()}
__all__: list = list(bulb_modules.values()) + ["bulb_modules"]
def pct_to_kelvin(pct: float, max_k: int = 6500, min_k: int = 2700) -> float:
"""Convert percent to kelvin."""
return ((max_k - min_k) * pct / 100) + min_k
class VeSyncBulb(VeSyncBaseDevice):
"""Base class for VeSync Bulbs."""
__metaclass__ = ABCMeta
def __init__(self, details: Dict[str, Union[str, list]], manager):
"""Initialize VeSync smart bulb base class."""
super().__init__(details, manager)
self._brightness = 0
self._color_temp = 0
self.features = feature_dict.get(self.device_type, {}).get("features")
if self.features is None:
logger.error("No configuration set for - %s", self.device_name)
raise RuntimeError(f"No configuration set for - {self.device_name}")
@property
def brightness(self) -> int:
"""Return brightness of vesync bulb."""
if self.dimmable_feature and self._brightness is not None:
return self._brightness
return 0
@property
def color_temp_kelvin(self) -> int:
"""Return Color Temp of bulb if supported in Kelvin."""
if self.color_temp_feature and self._color_temp is not None:
return int(pct_to_kelvin(self._color_temp))
return 0
@property
def color_temp_pct(self) -> int:
"""Return color temperature of bulb in percent."""
if self.color_temp_feature and self._color_temp is not None:
return int(self._color_temp)
return 0
@property
def dimmable_feature(self) -> bool:
"""Return true if dimmable bulb."""
return "dimmable" in self.features
@property
def color_temp_feature(self) -> bool:
"""Return true if bulb supports color temperature changes."""
return "color_temp" in feature_dict[self.device_type]
@property
def rgb_shift_feature(self) -> bool:
"""Return True if bulb supports changing color."""
return "rgb_shift" in feature_dict[self.device_type]
@abstractmethod
def get_details(self) -> None:
"""Get vesync bulb details."""
@abstractmethod
def toggle(self, status: str) -> bool:
"""Toggle vesync lightbulb."""
@abstractmethod
def get_config(self) -> None:
"""Call api to get configuration details and firmware."""
def turn_on(self) -> bool:
"""Turn on vesync bulbs."""
return self._toggle("on")
def turn_off(self) -> bool:
"""Turn off vesync bulbs."""
return self._toggle("off")
def _toggle(self, state: str):
if self.toggle(state):
self.device_status = state
return True
logger.warning("Error turning %s %s", self.device_name, state)
return False
def update(self) -> None:
"""Update bulb details."""
self.get_details()
def display(self) -> None:
"""Return formatted bulb info to stdout."""
super().display()
if self.connection_status == "online" and self.dimmable_feature:
disp1 = [("Brightness: ", self.brightness, "%")]
for line in disp1:
print(f"{line[0]:.<17} {line[1]} {line[2]}")
def displayJSON(self) -> str:
"""Return bulb device info in JSON format."""
sup = super().displayJSON()
sup_val = json.loads(sup)
if self.connection_status == "online":
if self.dimmable_feature:
sup_val.update({"Brightness": str(self.brightness)})
if self.color_temp_feature:
sup_val.update({"Kelvin": str(self.color_temp_kelvin)})
return sup_val
class VeSyncBulbESL100(VeSyncBulb):
"""Object to hold VeSync ESL100 light bulb."""
def __init__(self, details, manager):
"""Initialize Etekcity ESL100 Dimmable Bulb."""
super().__init__(details, manager)
self.details: dict = {}
def get_details(self) -> None:
"""Get details of dimmable bulb."""
body = helpers.req_body(self.manager, "devicedetail")
body["uuid"] = self.uuid
r, _ = helpers.call_api(
"/SmartBulb/v1/device/devicedetail",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r):
self.connection_status = r.get("connectionStatus")
self.device_status = r.get("deviceStatus")
if self.dimmable_feature:
self._brightness = int(r.get("brightNess"))
else:
logger.debug("Error getting %s details", self.device_name)
def get_config(self) -> None:
"""Get configuration of dimmable bulb."""
body = helpers.req_body(self.manager, "devicedetail")
body["method"] = "configurations"
body["uuid"] = self.uuid
r, _ = helpers.call_api(
"/SmartBulb/v1/device/configurations",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r):
self.config = helpers.build_config_dict(r)
else:
logger.warning("Error getting %s config info", self.device_name)
def toggle(self, status) -> bool:
"""Toggle dimmable bulb."""
body = self._get_body(status)
r, _ = helpers.call_api(
"/SmartBulb/v1/device/devicestatus",
"put",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r):
self.device_status = status
return True
return False
def set_brightness(self, brightness: int) -> bool:
"""Set brightness of dimmable bulb."""
if not self.dimmable_feature:
logger.debug("%s is not dimmable", self.device_name)
return False
if isinstance(brightness, int) and (brightness <= 0 or brightness > 100):
logger.warning("Invalid brightness")
return False
body = self._get_body("on")
body["brightNess"] = str(brightness)
r, _ = helpers.call_api(
"/SmartBulb/v1/device/updateBrightness",
"put",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r):
self._brightness = brightness
return True
logger.debug("Error setting brightness for %s", self.device_name)
return False
def _get_body(self, status: str):
body = helpers.req_body(self.manager, "devicestatus")
body["uuid"] = self.uuid
body["status"] = status
return body
class VeSyncBulbESL100CW(VeSyncBulb):
"""VeSync Tunable and Dimmable White Bulb."""
def __init__(self, details, manager):
"""Initialize Etekcity Tunable white bulb."""
super().__init__(details, manager)
def get_details(self) -> None:
"""Get details of tunable bulb."""
body = helpers.req_body(self.manager, "bypass")
body["cid"] = self.cid
body["jsonCmd"] = {"getLightStatus": "get"}
body["configModule"] = self.config_module
r, _ = helpers.call_api(
"/cloud/v1/deviceManaged/bypass",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if not isinstance(r, dict) or not helpers.code_check(r):
logger.debug("Error calling %s", self.device_name)
return
response = r
if response.get("result", {}).get("light") is not None:
light = response.get("result", {}).get("light")
self.connection_status = "online"
self.device_status = light.get("action", "off")
if self.dimmable_feature:
self._brightness = light.get("brightness")
if self.color_temp_feature:
self._color_temp = light.get("colorTempe")
elif response.get("code") == -11300027:
logger.debug("%s device offline", self.device_name)
self.connection_status = "offline"
self.device_status = "off"
else:
logger.debug(
"%s - Unknown return code - %d with message %s",
self.device_name,
response.get("code"),
response.get("msg"),
)
def get_config(self) -> None:
"""Get configuration and firmware info of tunable bulb."""
body = helpers.req_body(self.manager, "bypass_config")
body["uuid"] = self.uuid
r, _ = helpers.call_api(
"/cloud/v1/deviceManaged/configurations",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r):
self.config = helpers.build_config_dict(r)
else:
logger.debug("Error getting %s config info", self.device_name)
def toggle(self, status) -> bool:
"""Toggle tunable bulb."""
body = helpers.req_body(self.manager, "bypass")
body["cid"] = self.cid
body["configModule"] = self.config_module
body["jsonCmd"] = {"light": {"action": status}}
r, _ = helpers.call_api(
"/cloud/v1/deviceManaged/bypass",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r) == 0:
self.device_status = status
return True
logger.debug("%s offline", self.device_name)
self.device_status = "off"
self.connection_status = "offline"
return False
def set_brightness(self, brightness: int) -> bool:
"""Set brightness of tunable bulb."""
if not self.dimmable_feature:
logger.debug("%s is not dimmable", self.device_name)
return False
if brightness <= 0 or brightness > 100:
logger.debug("Invalid brightness")
return False
body = helpers.req_body(self.manager, "bypass")
body["cid"] = self.cid
body["configModule"] = self.config_module
if self.device_status == "off":
light_dict = {"action": "on", "brightness": brightness}
else:
light_dict = {"brightness": brightness}
body["jsonCmd"] = {"light": light_dict}
r, _ = helpers.call_api(
"/cloud/v1/deviceManaged/bypass",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r):
self._brightness = brightness
return True
self.device_status = "off"
self.connection_status = "offline"
logger.debug("%s offline", self.device_name)
return False
def set_color_temp(self, color_temp: int) -> bool:
"""Set Color Temperature of Bulb in pct (1 - 100)."""
if color_temp < 0 or color_temp > 100:
logger.debug("Invalid color temperature - only between 0 and 100")
return False
body = helpers.req_body(self.manager, "bypass")
body["cid"] = self.cid
body["jsonCmd"] = {"light": {}}
if self.device_status == "off":
light_dict = {"action": "on", "colorTempe": color_temp}
else:
light_dict = {"colorTempe": color_temp}
body["jsonCmd"]["light"] = light_dict
r, _ = helpers.call_api(
"/cloud/v1/deviceManaged/bypass",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if not helpers.code_check(r):
return False
if r.get("code") == -11300027:
logger.debug("%s device offline", self.device_name)
self.connection_status = "offline"
self.device_status = "off"
return False
if r.get("code") == 0:
self.device_status = "on"
self._color_temp = color_temp
return True
logger.debug(
"%s - Unknown return code - %d with message %s",
self.device_name,
r.get("code"),
r.get("msg"),
)
return False

File diff suppressed because it is too large Load Diff

View File

@ -1,703 +0,0 @@
"""Etekcity Outlets."""
import json
import logging
import time
from abc import ABCMeta, abstractmethod
from .helpers import Helpers
from .vesyncbasedevice import VeSyncBaseDevice
logger = logging.getLogger(__name__)
outlet_config = {
"wifi-switch-1.3": {"module": "VeSyncOutlet7A"},
"ESW03-USA": {"module": "VeSyncOutlet10A"},
"ESW01-EU": {"module": "VeSyncOutlet10A"},
"ESW15-USA": {"module": "VeSyncOutlet15A"},
"ESO15-TB": {"module": "VeSyncOutdoorPlug"},
}
outlet_modules = {k: v["module"] for k, v in outlet_config.items()}
__all__ = list(outlet_modules.values()) + ["outlet_modules"]
class VeSyncOutlet(VeSyncBaseDevice):
"""Base class for Etekcity Outlets."""
__metaclass__ = ABCMeta
def __init__(self, details, manager):
"""Initialize VeSync Outlet base class."""
super().__init__(details, manager)
self.details = {}
self.energy = {}
self.update_energy_ts = None
self._energy_update_interval = manager.energy_update_interval
@property
def update_time_check(self) -> bool:
"""Test if energy update interval has been exceeded."""
if self.update_energy_ts is None:
return True
if (time.time() - self.update_energy_ts) > self._energy_update_interval:
return True
return False
@abstractmethod
def turn_on(self) -> bool:
"""Return True if device has beeeen turned on."""
@abstractmethod
def turn_off(self) -> bool:
"""Return True if device has beeeen turned off."""
@abstractmethod
def get_details(self) -> None:
"""Build details dictionary."""
@abstractmethod
def get_weekly_energy(self) -> None:
"""Build weekly energy history dictionary."""
@abstractmethod
def get_monthly_energy(self) -> None:
"""Build Monthly Energy History Dictionary."""
@abstractmethod
def get_yearly_energy(self):
"""Build Yearly Energy Dictionary."""
@abstractmethod
def get_config(self):
"""Get configuration and firmware details."""
def update(self):
"""Get Device Energy and Status."""
self.get_details()
def update_energy(self, bypass_check: bool = False):
"""Build weekly, monthly and yearly dictionaries."""
if bypass_check or self.update_time_check:
self.update_energy_ts = time.time()
self.get_weekly_energy()
if "week" in self.energy:
self.get_monthly_energy()
self.get_yearly_energy()
if not bypass_check:
self.update_energy_ts = time.time()
@property
def active_time(self) -> int:
"""Return active time of a device in minutes."""
return self.details.get("active_time", 0)
@property
def energy_today(self) -> float:
"""Return energy."""
return self.details.get("energy", 0)
@property
def power(self) -> float:
"""Return current power in watts."""
return float(self.details.get("power", 0))
@property
def voltage(self) -> float:
"""Return current voltage."""
return float(self.details.get("voltage", 0))
@property
def monthly_energy_total(self) -> float:
"""Return total energy usage over the month."""
return self.energy.get("month", {}).get("total_energy", 0)
@property
def weekly_energy_total(self) -> float:
"""Return total energy usage over the week."""
return self.energy.get("week", {}).get("total_energy", 0)
@property
def yearly_energy_total(self) -> float:
"""Return total energy usage over the year."""
return self.energy.get("year", {}).get("total_energy", 0)
def display(self):
"""Return formatted device info to stdout."""
super().display()
disp1 = [
("Active Time : ", self.active_time, " minutes"),
("Energy: ", self.energy_today, " kWh"),
("Power: ", self.power, " Watts"),
("Voltage: ", self.voltage, " Volts"),
("Energy Week: ", self.weekly_energy_total, " kWh"),
("Energy Month: ", self.monthly_energy_total, " kWh"),
("Energy Year: ", self.yearly_energy_total, " kWh"),
]
for line in disp1:
print(f"{line[0]:.<15} {line[1]} {line[2]}")
def displayJSON(self):
"""Return JSON details for outlet."""
sup = super().displayJSON()
sup_val = json.loads(sup)
sup_val.update(
{
"Active Time": str(self.active_time),
"Energy": str(self.energy_today),
"Power": str(self.power),
"Voltage": str(self.voltage),
"Energy Week": str(self.weekly_energy_total),
"Energy Month": str(self.monthly_energy_total),
"Energy Year": str(self.yearly_energy_total),
}
)
return sup_val
class VeSyncOutlet7A(VeSyncOutlet):
"""Etekcity 7A Round Outlet Class."""
def __init__(self, details, manager):
"""Initialize Etekcity 7A round outlet class."""
super().__init__(details, manager)
self.det_keys = ["deviceStatus", "activeTime", "energy", "power", "voltage"]
self.energy_keys = ["energyConsumptionOfToday", "maxEnergy", "totalEnergy"]
def get_details(self) -> None:
"""Get 7A outlet details."""
r, _ = Helpers.call_api(
f"/v1/device/{self.cid}/detail",
"get",
headers=Helpers.req_headers(self.manager),
)
if r is not None and all(x in r for x in self.det_keys):
self.device_status = r.get("deviceStatus", self.device_status)
self.details["active_time"] = r.get("activeTime", 0)
self.details["energy"] = r.get("energy", 0)
power = r.get("power", "0:0")
power = round(float(Helpers.calculate_hex(power)), 2)
self.details["power"] = power
voltage = r.get("voltage", "0:0")
voltage = round(float(Helpers.calculate_hex(voltage)), 2)
self.details["voltage"] = voltage
else:
logger.debug("Unable to get %s details", self.device_name)
def get_weekly_energy(self) -> None:
"""Get 7A outlet weekly energy info and build weekly energy dict."""
r, _ = Helpers.call_api(
f"/v1/device/{self.cid}/energy/week",
"get",
headers=Helpers.req_headers(self.manager),
)
if r is not None and all(x in r for x in self.energy_keys):
self.energy["week"] = Helpers.build_energy_dict(r)
else:
logger.debug("Unable to get %s weekly data", self.device_name)
def get_monthly_energy(self) -> None:
"""Get 7A outlet monthly energy info and build monthly energy dict."""
r, _ = Helpers.call_api(
f"/v1/device/{self.cid}/energy/month",
"get",
headers=Helpers.req_headers(self.manager),
)
if r is not None and all(x in r for x in self.energy_keys):
self.energy["month"] = Helpers.build_energy_dict(r)
else:
logger.warning("Unable to get %s monthly data", self.device_name)
def get_yearly_energy(self) -> None:
"""Get 7A outlet yearly energy info and build yearly energy dict."""
r, _ = Helpers.call_api(
f"/v1/device/{self.cid}/energy/year",
"get",
headers=Helpers.req_headers(self.manager),
)
if r is not None and all(x in r for x in self.energy_keys):
self.energy["year"] = Helpers.build_energy_dict(r)
else:
logger.debug("Unable to get %s yearly data", self.device_name)
def turn_on(self) -> bool:
"""Turn 7A outlet on - return True if successful."""
return self._toggle("on")
def turn_off(self) -> bool:
"""Turn 7A outlet off - return True if successful."""
return self._toggle("off")
def _toggle(self, state):
_, status_code = Helpers.call_api(
f"/v1/wifi-switch-1.3/{self.cid}/status/{state}",
"put",
headers=Helpers.req_headers(self.manager),
)
if status_code is not None and status_code == 200:
self.device_status = state
return True
logger.warning("Error turning %s %s", self.device_name, state)
return False
def get_config(self) -> None:
"""Get 7A outlet configuration info."""
r, _ = Helpers.call_api(
f"/v1/device/{self.cid}/configurations",
"get",
headers=Helpers.req_headers(self.manager),
)
if "currentFirmVersion" in r:
self.config = Helpers.build_config_dict(r)
else:
logger.debug("Error getting configuration info for %s", self.device_name)
class VeSyncOutlet10A(VeSyncOutlet):
"""Etekcity 10A Round Outlets."""
def __init__(self, details, manager):
"""Initialize 10A outlet class."""
super().__init__(details, manager)
def get_details(self) -> None:
"""Get 10A outlet details."""
body = Helpers.req_body(self.manager, "devicedetail")
body["uuid"] = self.uuid
r, _ = Helpers.call_api(
"/10a/v1/device/devicedetail",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(r):
self.device_status = r.get("deviceStatus", self.device_status)
self.connection_status = r.get("connectionStatus", self.connection_status)
self.details = Helpers.build_details_dict(r)
else:
logger.debug("Unable to get %s details", self.device_name)
def get_config(self) -> None:
"""Get 10A outlet configuration info."""
body = Helpers.req_body(self.manager, "devicedetail")
body["method"] = "configurations"
body["uuid"] = self.uuid
r, _ = Helpers.call_api(
"/10a/v1/device/configurations",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(r):
self.config = Helpers.build_config_dict(r)
else:
logger.debug("Error getting %s config info", self.device_name)
def get_weekly_energy(self) -> None:
"""Get 10A outlet weekly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_week")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/10a/v1/device/energyweek",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["week"] = Helpers.build_energy_dict(response)
else:
logger.debug("Unable to get %s weekly data", self.device_name)
def get_monthly_energy(self) -> None:
"""Get 10A outlet monthly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_month")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/10a/v1/device/energymonth",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["month"] = Helpers.build_energy_dict(response)
else:
logger.debug("Unable to get %s monthly data", self.device_name)
def get_yearly_energy(self) -> None:
"""Get 10A outlet yearly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_year")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/10a/v1/device/energyyear",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["year"] = Helpers.build_energy_dict(response)
else:
logger.debug("Unable to get %s yearly data", self.device_name)
def turn_on(self) -> bool:
"""Turn 10A outlet on - return True if successful."""
body = Helpers.req_body(self.manager, "devicestatus")
body["uuid"] = self.uuid
body["status"] = "on"
response, _ = Helpers.call_api(
"/10a/v1/device/devicestatus",
"put",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.device_status = "on"
return True
logger.warning("Error turning %s on", self.device_name)
return False
def turn_off(self) -> bool:
"""Turn 10A outlet off - return True if successful."""
body = Helpers.req_body(self.manager, "devicestatus")
body["uuid"] = self.uuid
body["status"] = "off"
response, _ = Helpers.call_api(
"/10a/v1/device/devicestatus",
"put",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.device_status = "off"
return True
logger.warning("Error turning %s off", self.device_name)
return False
class VeSyncOutlet15A(VeSyncOutlet):
"""Class for Etekcity 15A Rectangular Outlets."""
def __init__(self, details, manager):
"""Initialize 15A rectangular outlets."""
super().__init__(details, manager)
self.nightlight_status = "off"
self.nightlight_brightness = 0
def get_details(self) -> None:
"""Get 15A outlet details."""
body = Helpers.req_body(self.manager, "devicedetail")
body["uuid"] = self.uuid
r, _ = Helpers.call_api(
"/15a/v1/device/devicedetail",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
attr_list = (
"deviceStatus",
"activeTime",
"energy",
"power",
"voltage",
"nightLightStatus",
"nightLightAutomode",
"nightLightBrightness",
)
if Helpers.code_check(r) and all(k in r for k in attr_list):
self.device_status = r.get("deviceStatus")
self.connection_status = r.get("connectionStatus")
self.nightlight_status = r.get("nightLightStatus")
self.nightlight_brightness = r.get("nightLightBrightness")
self.details = Helpers.build_details_dict(r)
else:
logger.debug("Unable to get %s details", self.device_name)
def get_config(self) -> None:
"""Get 15A outlet configuration info."""
body = Helpers.req_body(self.manager, "devicedetail")
body["method"] = "configurations"
body["uuid"] = self.uuid
r, _ = Helpers.call_api(
"/15a/v1/device/configurations",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(r):
self.config = Helpers.build_config_dict(r)
else:
logger.debug("Unable to get %s config info", self.device_name)
def get_weekly_energy(self) -> None:
"""Get 15A outlet weekly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_week")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/15a/v1/device/energyweek",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["week"] = Helpers.build_energy_dict(response)
else:
logger.debug("Unable to get %s weekly data", self.device_name)
def get_monthly_energy(self) -> None:
"""Get 15A outlet monthly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_month")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/15a/v1/device/energymonth",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["month"] = Helpers.build_energy_dict(response)
else:
logger.debug("Unable to get %s monthly data", self.device_name)
def get_yearly_energy(self) -> None:
"""Get 15A outlet yearly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_year")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/15a/v1/device/energyyear",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["year"] = Helpers.build_energy_dict(response)
else:
logger.debug("Unable to get %s yearly data", self.device_name)
def turn_on(self) -> bool:
"""Turn 15A outlet on - return True if successful."""
body = Helpers.req_body(self.manager, "devicestatus")
body["uuid"] = self.uuid
body["status"] = "on"
response, _ = Helpers.call_api(
"/15a/v1/device/devicestatus",
"put",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.device_status = "on"
return True
logger.warning("Error turning %s on", self.device_name)
return False
def turn_off(self) -> bool:
"""Turn 15A outlet off - return True if successful."""
body = Helpers.req_body(self.manager, "devicestatus")
body["uuid"] = self.uuid
body["status"] = "off"
response, _ = Helpers.call_api(
"/15a/v1/device/devicestatus",
"put",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.device_status = "off"
return True
logger.warning("Error turning %s off", self.device_name)
return False
def turn_on_nightlight(self) -> bool:
"""Turn on nightlight."""
return self._extracted_from_turn_off_nightlight_3("auto")
def turn_off_nightlight(self) -> bool:
"""Turn Off Nightlight."""
return self._extracted_from_turn_off_nightlight_3("manual")
def _extracted_from_turn_off_nightlight_3(self, mode):
body = Helpers.req_body(self.manager, "devicestatus")
body["uuid"] = self.uuid
body["mode"] = mode
response, _ = Helpers.call_api(
"/15a/v1/device/nightlightstatus",
"put",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
return True
logger.debug(
"Error turning %s %s nightlight",
"on" if mode == "auto" else "off",
self.device_name,
)
return False
class VeSyncOutdoorPlug(VeSyncOutlet):
"""Class to hold Etekcity outdoor outlets."""
def __init__(self, details, manager):
"""Initialize Etekcity Outdoor Plug class."""
super().__init__(details, manager)
def get_details(self) -> None:
"""Get details for outdoor outlet."""
body = Helpers.req_body(self.manager, "devicedetail")
body["uuid"] = self.uuid
r, _ = Helpers.call_api(
"/outdoorsocket15a/v1/device/devicedetail",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(r):
self.details = Helpers.build_details_dict(r)
self.connection_status = r.get("connectionStatus")
dev_no = self.sub_device_no
sub_device_list = r.get("subDevices")
if sub_device_list and dev_no <= len(sub_device_list):
self.device_status = sub_device_list[(dev_no + -1)].get(
"subDeviceStatus"
)
return
logger.debug("Unable to get %s details", self.device_name)
def get_config(self) -> None:
"""Get configuration info for outdoor outlet."""
body = Helpers.req_body(self.manager, "devicedetail")
body["method"] = "configurations"
body["uuid"] = self.uuid
r, _ = Helpers.call_api(
"/outdoorsocket15a/v1/device/configurations",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(r):
self.config = Helpers.build_config_dict(r)
logger.debug("Error getting %s config info", self.device_name)
def get_weekly_energy(self) -> None:
"""Get outdoor outlet weekly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_week")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/outdoorsocket15a/v1/device/energyweek",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["week"] = Helpers.build_energy_dict(response)
else:
logger.debug("Unable to get %s weekly data", self.device_name)
def get_monthly_energy(self) -> None:
"""Get outdoor outlet monthly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_month")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/outdoorsocket15a/v1/device/energymonth",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["month"] = Helpers.build_energy_dict(response)
logger.debug("Unable to get %s monthly data", self.device_name)
def get_yearly_energy(self) -> None:
"""Get outdoor outlet yearly energy info and populate energy dict."""
body = Helpers.req_body(self.manager, "energy_year")
body["uuid"] = self.uuid
response, _ = Helpers.call_api(
"/outdoorsocket15a/v1/device/energyyear",
"post",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.energy["year"] = Helpers.build_energy_dict(response)
else:
logger.debug("Unable to get %s yearly data", self.device_name)
def toggle(self, status) -> bool:
"""Toggle power for outdoor outlet."""
body = Helpers.req_body(self.manager, "devicestatus")
body["uuid"] = self.uuid
body["status"] = status
body["switchNo"] = self.sub_device_no
response, _ = Helpers.call_api(
"/outdoorsocket15a/v1/device/devicestatus",
"put",
headers=Helpers.req_headers(self.manager),
json=body,
)
if Helpers.code_check(response):
self.device_status = status
return True
logger.warning("Error turning %s %s", self.device_name, status)
return False
def turn_on(self) -> bool:
"""Turn outdoor outlet on and return True if successful."""
return bool(self.toggle("on"))
def turn_off(self) -> bool:
"""Turn outdoor outlet off and return True if successful."""
return bool(self.toggle("off"))

View File

@ -1,347 +0,0 @@
"""Classes for VeSync Switch Devices."""
import json
import logging
from abc import ABCMeta, abstractmethod
from typing import Dict, Union
from .helpers import Helpers as helpers
from .vesyncbasedevice import VeSyncBaseDevice
logger = logging.getLogger(__name__)
feature_dict: Dict[str, Dict[str, Union[list, str]]] = {
"ESWL01": {"module": "VeSyncWallSwitch", "features": []},
"ESWD16": {"module": "VeSyncDimmerSwitch", "features": ["dimmable"]},
"ESWL03": {"module": "VeSyncWallSwitch", "features": []},
}
switch_modules: dict = {k: v["module"] for k, v in feature_dict.items()}
__all__: list = list(switch_modules.values()) + ["switch_modules"]
class VeSyncSwitch(VeSyncBaseDevice):
"""Etekcity Switch Base Class."""
__metaclasss__ = ABCMeta
def __init__(self, details, manager):
"""Initialize Switch Base Class."""
super().__init__(details, manager)
self.features = feature_dict.get(self.device_type, {}).get("features")
if self.features is None:
logger.error("No configuration set for - %s", self.device_name)
raise RuntimeError(f"No configuration set for - {self.device_name}")
self.details = {}
def is_dimmable(self) -> bool:
"""Return True if switch is dimmable."""
return bool("dimmable" in self.features)
@abstractmethod
def get_details(self) -> None:
"""Get Device Details."""
@abstractmethod
def turn_on(self) -> bool:
"""Turn Switch On."""
@abstractmethod
def turn_off(self) -> bool:
"""Turn switch off."""
@abstractmethod
def get_config(self) -> None:
"""Get configuration and firmware deatils."""
@property
def active_time(self) -> int:
"""Get active time of switch."""
return self.details.get("active_time", 0)
def update(self) -> None:
"""Update device details."""
self.get_details()
class VeSyncWallSwitch(VeSyncSwitch):
"""Etekcity standard wall switch class."""
def __init__(self, details, manager):
"""Initialize standard etekcity wall switch class."""
super().__init__(details, manager)
def get_details(self) -> None:
"""Get switch device details."""
body = helpers.req_body(self.manager, "devicedetail")
body["uuid"] = self.uuid
head = helpers.req_headers(self.manager)
r, _ = helpers.call_api(
"/inwallswitch/v1/device/devicedetail", "post", headers=head, json=body
)
if r is not None and helpers.code_check(r):
self.device_status = r.get("deviceStatus", self.device_status)
self.details["active_time"] = r.get("activeTime", 0)
self.connection_status = r.get("connectionStatus", self.connection_status)
else:
logger.debug("Error getting %s details", self.device_name)
def get_config(self) -> None:
"""Get switch device configuration info."""
body = helpers.req_body(self.manager, "devicedetail")
body["method"] = "configurations"
body["uuid"] = self.uuid
r, _ = helpers.call_api(
"/inwallswitch/v1/device/configurations",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r):
self.config = helpers.build_config_dict(r)
else:
logger.warning("Unable to get %s config info", self.device_name)
def turn_off(self) -> bool:
"""Turn off switch device."""
body = helpers.req_body(self.manager, "devicestatus")
body["status"] = "off"
body["uuid"] = self.uuid
head = helpers.req_headers(self.manager)
r, _ = helpers.call_api(
"/inwallswitch/v1/device/devicestatus", "put", headers=head, json=body
)
if r is not None and helpers.code_check(r):
self.device_status = "off"
return True
logger.warning("Error turning %s off", self.device_name)
return False
def turn_on(self) -> bool:
"""Turn on switch device."""
body = helpers.req_body(self.manager, "devicestatus")
body["status"] = "on"
body["uuid"] = self.uuid
head = helpers.req_headers(self.manager)
r, _ = helpers.call_api(
"/inwallswitch/v1/device/devicestatus", "put", headers=head, json=body
)
if r is not None and helpers.code_check(r):
self.device_status = "on"
return True
logger.warning("Error turning %s on", self.device_name)
return False
class VeSyncDimmerSwitch(VeSyncSwitch):
"""Vesync Dimmer Switch Class with RGB Faceplate."""
def __init__(self, details, manager):
"""Initialize dimmer switch class."""
super().__init__(details, manager)
self._brightness = 0
self._rgb_value = {"red": 0, "blue": 0, "green": 0}
self._rgb_status = "unknown"
self._indicator_light = "unknown"
def get_details(self) -> None:
"""Get dimmer switch details."""
body = helpers.req_body(self.manager, "devicedetail")
body["uuid"] = self.uuid
head = helpers.req_headers(self.manager)
r, _ = helpers.call_api(
"/dimmer/v1/device/devicedetail", "post", headers=head, json=body
)
if r is not None and helpers.code_check(r):
self.device_status = r.get("deviceStatus", self.device_status)
self.details["active_time"] = r.get("activeTime", 0)
self.connection_status = r.get("connectionStatus", self.connection_status)
self._brightness = r.get("brightness")
self._rgb_status = r.get("rgbStatus")
self._rgb_value = r.get("rgbValue")
self._indicator_light = r.get("indicatorlightStatus")
else:
logger.debug("Error getting %s details", self.device_name)
@property
def brightness(self) -> float:
"""Return brightness in percent."""
return self._brightness
@property
def indicator_light_status(self) -> str:
"""Faceplate brightness light status."""
return self._indicator_light
@property
def rgb_light_status(self) -> str:
"""RGB Faceplate light status."""
return self._rgb_status
@property
def rgb_light_value(self) -> dict:
"""RGB Light Values."""
return self._rgb_value
def switch_toggle(self, status: str) -> bool:
"""Toggle switch status."""
if status not in ["on", "off"]:
logger.debug("Invalid status passed to wall switch")
return False
body = helpers.req_body(self.manager, "devicestatus")
body["status"] = status
body["uuid"] = self.uuid
head = helpers.req_headers(self.manager)
r, _ = helpers.call_api(
"/dimmer/v1/device/devicestatus", "put", headers=head, json=body
)
if r is not None and helpers.code_check(r):
self.device_status = status
return True
logger.warning("Error turning %s %s", self.device_name, status)
return False
def turn_on(self) -> bool:
"""Turn switch on."""
return self.switch_toggle("on")
def turn_off(self) -> bool:
"""Turn switch off."""
return self.switch_toggle("off")
def indicator_light_toggle(self, status: str) -> bool:
"""Toggle indicator light."""
if status not in ["on", "off"]:
logger.debug("Invalid status for wall switch")
return False
body = helpers.req_body(self.manager, "devicestatus")
body["status"] = status
body["uuid"] = self.uuid
head = helpers.req_headers(self.manager)
r, _ = helpers.call_api(
"/dimmer/v1/device/indicatorlightstatus", "put", headers=head, json=body
)
if r is not None and helpers.code_check(r):
self.device_status = status
return True
logger.warning("Error turning %s indicator light %s", self.device_name, status)
return False
def indicator_light_on(self) -> bool:
"""Turn Indicator light on."""
return self.indicator_light_toggle("on")
def indicator_light_off(self) -> bool:
"""Turn indicator light off."""
return self.indicator_light_toggle("off")
def rgb_color_status(
self, status: str, red: int = None, blue: int = None, green: int = None
) -> bool:
"""Set faceplate RGB color."""
body = helpers.req_body(self.manager, "devicestatus")
body["status"] = status
body["uuid"] = self.uuid
head = helpers.req_headers(self.manager)
if red is not None and blue is not None and green is not None:
body["rgbValue"] = {"red": red, "blue": blue, "green": green}
r, _ = helpers.call_api(
"/dimmer/v1/device/devicergbstatus", "put", headers=head, json=body
)
if r is not None and helpers.code_check(r):
self._rgb_status = status
if body.get("rgbValue") is not None:
self._rgb_value = {"red": red, "blue": blue, "green": green}
return True
logger.warning("Error turning %s off", self.device_name)
return False
def rgb_color_off(self) -> bool:
"""Turn RGB Color Off."""
return self.rgb_color_status("off")
def rgb_color_on(self) -> bool:
"""Turn RGB Color Off."""
return self.rgb_color_status("on")
def rgb_color_set(self, red: int, green: int, blue: int) -> bool:
"""Set RGB color of faceplate."""
if isinstance(red, int) and isinstance(green, int) and isinstance(blue, int):
for color in [red, green, blue]:
if color < 0 or color > 255:
logger.warning("Invalid RGB value")
return False
return bool(self.rgb_color_status("on", red, green, blue))
return False
def set_brightness(self, brightness: int) -> bool:
"""Set brightness of dimmer - 1 - 100."""
if isinstance(brightness, int) and (brightness > 0 or brightness <= 100):
body = helpers.req_body(self.manager, "devicestatus")
body["brightness"] = brightness
body["uuid"] = self.uuid
head = helpers.req_headers(self.manager)
r, _ = helpers.call_api(
"/dimmer/v1/device/updatebrightness", "put", headers=head, json=body
)
if r is not None and helpers.code_check(r):
self._brightness = brightness
return True
logger.warning("Error setting %s brightness", self.device_name)
else:
logger.warning("Invalid brightness")
return False
def displayJSON(self) -> str:
"""JSON API for dimmer switch."""
sup_val = json.loads(super().displayJSON())
if self.is_dimmable:
sup_val.update(
{
"Indicator Light": str(self.active_time),
"Brightness": str(self._brightness),
"RGB Light": str(self._rgb_status),
}
)
return sup_val
def get_config(self) -> None:
"""Get dimmable switch device configuration info."""
body = helpers.req_body(self.manager, "devicedetail")
body["method"] = "configurations"
body["uuid"] = self.uuid
r, _ = helpers.call_api(
"/dimmer/v1/device/configurations",
"post",
headers=helpers.req_headers(self.manager),
json=body,
)
if helpers.code_check(r):
self.config = helpers.build_config_dict(r)
else:
logger.warning("Unable to get %s config info", self.device_name)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
pyvesync==2.0.3

View File

@ -1,4 +1,5 @@
homeassistant
-r requirements.txt
homeassistant==2022.6.4
black
isort
flake8