379 lines
13 KiB
Python
Raw Normal View History

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