async def query(host: str, request: Union[str, Dict], retry_count: int = 3) -> Dict: """Request information from a TP-Link SmartHome Device. :param str host: host name or ip address of the device :param request: command to send to the device (can be either dict or json string) :param retry_count: how many retries to do in case of failure :return: response dict """ if isinstance(request, dict): request = json.dumps(request) timeout = TPLinkSmartHomeProtocol.DEFAULT_TIMEOUT writer = None for retry in range(retry_count + 1): try: task = asyncio.open_connection( host, TPLinkSmartHomeProtocol.DEFAULT_PORT ) reader, writer = await asyncio.wait_for(task, timeout=timeout) _LOGGER.debug("> (%i) %s", len(request), request) writer.write(TPLinkSmartHomeProtocol.encrypt(request)) await writer.drain() buffer = bytes() # Some devices send responses with a length header of 0 and # terminate with a zero size chunk. Others send the length and # will hang if we attempt to read more data. length = -1 while True: chunk = await reader.read(4096) if length == -1: length = struct.unpack(">I", chunk[0:4])[0] buffer += chunk if (length > 0 and len(buffer) >= length + 4) or not chunk: break response = TPLinkSmartHomeProtocol.decrypt(buffer[4:]) json_payload = json.loads(response) _LOGGER.debug("< (%i) %s", len(response), pf(json_payload)) return json_payload except Exception as ex: if retry >= retry_count: _LOGGER.debug("Giving up after %s retries", retry) raise SmartDeviceException( "Unable to query the device: %s" % ex ) from ex _LOGGER.debug("Unable to query the device, retrying: %s", ex) finally: if writer: writer.close() await writer.wait_closed() # make mypy happy, this should never be reached.. raise SmartDeviceException("Query reached somehow to unreachable")
def get_plug_by_name(self, name: str) -> "SmartDevice": """Return child device for the given name.""" for p in self.children: if p.alias == name: return p raise SmartDeviceException(f"Device has no child with {name}")
def get_plug_by_index(self, index: int) -> "SmartDevice": """Return child device for the given index.""" if index + 1 > len(self.children) or index < 0: raise SmartDeviceException( f"Invalid index {index}, device has {len(self.children)} plugs" ) return self.children[index]
async def current_consumption(self) -> float: """Get the current power consumption in Watt.""" if not self.has_emeter: raise SmartDeviceException("Device has no emeter") response = EmeterStatus(await self.get_emeter_realtime()) return response["power"]
async def erase_emeter_stats(self) -> Dict: """Erase energy meter statistics.""" if not self.has_emeter: raise SmartDeviceException("Device has no emeter") return await self._query_helper(self.emeter_type, "erase_emeter_stat", None)
def __getitem__(self, item): valid_keys = [ "voltage_mv", "power_mw", "current_ma", "energy_wh", "total_wh", "voltage", "power", "current", "total", "energy", ] # 1. if requested data is available, return it if item in super().keys(): return super().__getitem__(item) # otherwise decide how to convert it else: if item not in valid_keys: raise KeyError(item) if "_" in item: # upscale return super().__getitem__(item[:item.find("_")]) * 1000 else: # downscale for i in super().keys(): if i.startswith(item): return self.__getitem__(i) / 1000 raise SmartDeviceException("Unable to find a value for '%s'" % item)
async def get_emeter_realtime(self) -> EmeterStatus: """Retrieve current energy readings.""" if not self.has_emeter: raise SmartDeviceException("Device has no emeter") return EmeterStatus(await self._query_helper(self.emeter_type, "get_realtime"))
async def get_emeter_daily(self, year: int = None, month: int = None, kwh: bool = True) -> Dict: """Retrieve daily statistics for a given month. :param year: year for which to retrieve statistics (default: this year) :param month: month for which to retrieve statistics (default: this month) :param kwh: return usage in kWh (default: True) :return: mapping of day of month to value """ if not self.has_emeter: raise SmartDeviceException("Device has no emeter") if year is None: year = datetime.now().year if month is None: month = datetime.now().month response = await self._query_helper(self.emeter_type, "get_daystat", { "month": month, "year": year }) return self._emeter_convert_emeter_data(response["day_list"], kwh)
def emeter_realtime(self) -> EmeterStatus: """Return current energy readings.""" if not self.has_emeter: raise SmartDeviceException("Device has no emeter") return EmeterStatus( self._last_update[self.emeter_type]["get_realtime"])
async def _query_helper(self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None) -> Any: """Query device, return results or raise an exception. :param target: Target system {system, time, emeter, ..} :param cmd: Command to execute :param arg: payload dict to be send to the device :param child_ids: ids of child devices :return: Unwrapped result for the call. """ request = self._create_request(target, cmd, arg, child_ids) try: response = await self.protocol.query(host=self.host, request=request) except Exception as ex: raise SmartDeviceException( f"Communication error on {target}:{cmd}") from ex if target not in response: raise SmartDeviceException( f"No required {target} in response: {response}") result = response[target] if "err_code" in result and result["err_code"] != 0: raise SmartDeviceException(f"Error on {target}.{cmd}: {result}") if cmd not in result: raise SmartDeviceException(f"No command in response: {response}") result = result[cmd] if "err_code" in result and result["err_code"] != 0: raise SmartDeviceException(f"Error on {target} {cmd}: {result}") if "err_code" in result: del result["err_code"] return result
def emeter_this_month(self) -> Optional[float]: """Return this month's energy consumption in kWh.""" if not self.has_emeter: raise SmartDeviceException("Device has no emeter") raw_data = self._last_update[ self.emeter_type]["get_monthstat"]["month_list"] data = self._emeter_convert_emeter_data(raw_data) current_month = datetime.now().month if current_month in data: return data[current_month] return None
def emeter_today(self) -> Optional[float]: """Return today's energy consumption in kWh.""" if not self.has_emeter: raise SmartDeviceException("Device has no emeter") raw_data = self._last_update[ self.emeter_type]["get_daystat"]["day_list"] data = self._emeter_convert_emeter_data(raw_data) today = datetime.now().day if today in data: return data[today] return None
def mac(self) -> str: """Return mac address. :return: mac address in hexadecimal with colons, e.g. 01:23:45:67:89:ab """ sys_info = self.sys_info if "mac" in sys_info: return str(sys_info["mac"]) elif "mic_mac" in sys_info: return ":".join( format(s, "02x") for s in bytes.fromhex(sys_info["mic_mac"])) raise SmartDeviceException( "Unknown mac, please submit a bug report with sys_info output.")
async def wifi_scan(self) -> List[WifiNetwork]: # noqa: D202 """Scan for available wifi networks.""" async def _scan(target): return await self._query_helper(target, "get_scaninfo", {"refresh": 1}) try: info = await _scan("netif") except SmartDeviceException as ex: _LOGGER.debug( "Unable to scan using 'netif', retrying with 'softaponboarding': %s", ex) info = await _scan("smartlife.iot.common.softaponboarding") if "ap_list" not in info: raise SmartDeviceException("Invalid response for wifi scan: %s" % info) return [WifiNetwork(**x) for x in info["ap_list"]]
async def wrapped(*args, **kwargs): self = args[0] if self._last_update is None: raise SmartDeviceException( "You need to await update() to access the data") return await f(*args, **kwargs)