2022-04-01 15:02:13 +02:00
|
|
|
"""Etekcity Smart Light Bulb."""
|
|
|
|
|
|
|
|
import json
|
|
|
|
import logging
|
2022-04-29 11:38:08 +02:00
|
|
|
from abc import ABCMeta, abstractmethod
|
2022-04-01 15:02:13 +02:00
|
|
|
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")
|
|
|
|
|
2022-05-26 10:52:14 +02:00
|
|
|
def _toggle(self, state: str):
|
2022-04-01 15:02:13 +02:00
|
|
|
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
|