"""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()