def _get_device_class(info: dict) -> Type[SmartDevice]: """Find SmartDevice subclass for device described by passed data.""" if "system" in info and "get_sysinfo" in info["system"]: sysinfo = info["system"]["get_sysinfo"] if "type" in sysinfo: type_ = sysinfo["type"] elif "mic_type" in sysinfo: type_ = sysinfo["mic_type"] else: raise SmartDeviceException( "Unable to find the device type field!") else: raise SmartDeviceException( "No 'system' nor 'get_sysinfo' in response") if ("smartlife.iot.dimmer" in info and "get_dimmer_parameters" in info["smartlife.iot.dimmer"]): return SmartDimmer elif "smartplug" in type_.lower() and "children" in sysinfo: return SmartStrip elif "smartplug" in type_.lower(): return SmartPlug elif "smartbulb" in type_.lower(): return SmartBulb raise SmartDeviceException("Unknown device type: %s", type_)
def _get_device_class(info: dict) -> Type[SmartDevice]: """Find SmartDevice subclass for device described by passed data.""" if "system" not in info or "get_sysinfo" not in info["system"]: raise SmartDeviceException( "No 'system' or 'get_sysinfo' in response") sysinfo = info["system"]["get_sysinfo"] type_ = sysinfo.get("type", sysinfo.get("mic_type")) if type_ is None: raise SmartDeviceException("Unable to find the device type field!") if "dev_name" in sysinfo and "Dimmer" in sysinfo["dev_name"]: return SmartDimmer if "smartplug" in type_.lower(): if "children" in sysinfo: return SmartStrip return SmartPlug if "smartbulb" in type_.lower(): if "length" in sysinfo: # strips have length return SmartLightStrip return SmartBulb raise SmartDeviceException("Unknown device type: %s" % type_)
def valid_temperature_range(self) -> Tuple[int, int]: """Return the device-specific white temperature range (in Kelvin). :return: White temperature range in Kelvin (minimum, maximum) """ if not self.is_variable_color_temp: raise SmartDeviceException("Color temperature not supported") for model, temp_range in TPLINK_KELVIN.items(): sys_info = self.sys_info if re.match(model, sys_info["model"]): return temp_range raise SmartDeviceException( "Unknown color temperature range, please open an issue on github")
def _get_new_device_class(info: dict) -> Type[SmartDevice]: """Find SmartDevice subclass given new discovery payload.""" if "result" not in info: raise SmartDeviceException("No 'result' in discovery response") if "device_type" not in info["result"]: raise SmartDeviceException("No 'device_type' in discovery result") dtype = info["result"]["device_type"] if dtype == "IOT.SMARTPLUGSWITCH": return SmartPlug raise SmartDeviceException("Unknown device type: %s", dtype)
def brightness(self) -> int: """Return the current brightness in percentage.""" if not self.is_dimmable: # pragma: no cover raise SmartDeviceException("Bulb is not dimmable.") light_state = self.light_state return int(light_state["brightness"])
def color_temp(self) -> int: """Return color temperature of the device in kelvin.""" if not self.is_variable_color_temp: raise SmartDeviceException("Bulb does not support colortemp.") light_state = self.light_state return int(light_state["color_temp"])
async def get_emeter_realtime(self) -> EmeterStatus: """Retrieve current energy readings.""" if not self.has_emeter: raise SmartDeviceException("Device has no emeter") plug_emeter = False for plug in self.children: if plug.has_emeter: plug_emeter = True if not plug_emeter: return EmeterStatus(await self._query_helper(self.emeter_type, "get_realtime")) emeter_rt: DefaultDict[int, float] = defaultdict(lambda: 0.0) count = 0 for plug in self.children: if not plug.has_emeter: continue count += 1 plug_emeter_rt = await plug.get_emeter_realtime() for field, value in plug_emeter_rt.items(): emeter_rt[field] += value # Voltage is averaged emeter_rt['voltage_mv'] /= count return EmeterStatus(emeter_rt)
def get_plug_by_name(self, name: str) -> "SmartStripPlug": """Return child plug for given name.""" for p in self.plugs: if p.alias == name: return p raise SmartDeviceException(f"Device has no child with {name}")
def _get_child_info(self) -> Dict: """Return the subdevice information for this device.""" for plug in self.parent.sys_info["children"]: if plug["id"] == self.child_id: return plug raise SmartDeviceException(f"Unable to find children {self.child_id}")
async def set_hsv(self, hue: int, saturation: int, value: int): """Set new HSV. :param int hue: hue in degrees :param int saturation: saturation in percentage [0,100] :param int value: value in percentage [0, 100] """ if not self.is_color: raise SmartDeviceException("Bulb does not support color.") if not isinstance(hue, int) or not (0 <= hue <= 360): raise ValueError("Invalid hue value: {} " "(valid range: 0-360)".format(hue)) if not isinstance(saturation, int) or not (0 <= saturation <= 100): raise ValueError("Invalid saturation value: {} " "(valid range: 0-100%)".format(saturation)) self._raise_for_invalid_brightness(value) light_state = { "hue": hue, "saturation": saturation, "brightness": value, "color_temp": 0, } await self.set_light_state(light_state)
async def set_brightness(self, brightness: int, *, transition: int = None): """Set the new dimmer brightness level in percentage. :param int transition: transition duration in milliseconds. Using a transition will cause the dimmer to turn on. """ if not self.is_dimmable: raise SmartDeviceException("Device is not dimmable.") if not isinstance(brightness, int): raise ValueError("Brightness must be integer, " "not of %s.", type(brightness)) if not 0 <= brightness <= 100: raise ValueError("Brightness value %s is not valid." % brightness) # Dimmers do not support a brightness of 0, but bulbs do. # Coerce 0 to 1 to maintain the same interface between dimmers and bulbs. if brightness == 0: brightness = 1 if transition is not None: return await self.set_dimmer_transition(brightness, transition) return await self._query_helper(self.DIMMER_SERVICE, "set_brightness", {"brightness": brightness})
def _get_new_owner(info: dict) -> Optional[str]: """Find owner given new-style discovery payload.""" if "result" not in info: raise SmartDeviceException("No 'result' in discovery response") if "owner" not in info["result"]: return None return info["result"]["owner"]
async def set_brightness(self, brightness: int) -> None: """Set the brightness in percentage.""" if not self.is_dimmable: # pragma: no cover raise SmartDeviceException("Bulb is not dimmable.") self._raise_for_invalid_brightness(brightness) light_state = {"brightness": brightness} await self.set_light_state(light_state)
def brightness(self) -> int: """Return current brightness on dimmers. Will return a range between 0 - 100. """ if not self.is_dimmable: raise SmartDeviceException("Device is not dimmable.") sys_info = self.sys_info return int(sys_info["brightness"])
async def set_brightness(self, brightness: int, *, transition: int = None) -> Dict: """Set the brightness in percentage. :param int brightness: brightness in percent :param int transition: transition in milliseconds. """ if not self.is_dimmable: # pragma: no cover raise SmartDeviceException("Bulb is not dimmable.") self._raise_for_invalid_brightness(brightness) light_state = {"brightness": brightness} return await self.set_light_state(light_state, transition=transition)
def color_temp(self) -> int: """Return color temperature of the device. :return: Color temperature in Kelvin :rtype: int """ if not self.is_variable_color_temp: raise SmartDeviceException("Bulb does not support colortemp.") light_state = self.light_state if not self.is_on: return int(light_state["dft_on_state"]["color_temp"]) else: return int(light_state["color_temp"])
def brightness(self) -> int: """Return the current brightness. :return: brightness in percent :rtype: int """ if not self.is_dimmable: # pragma: no cover raise SmartDeviceException("Bulb is not dimmable.") light_state = self.light_state if not self.is_on: return int(light_state["dft_on_state"]["brightness"]) else: return int(light_state["brightness"])
async def set_brightness(self, value: int): """Set the new dimmer brightness level in percentage.""" if not self.is_dimmable: raise SmartDeviceException("Device is not dimmable.") if not isinstance(value, int): raise ValueError("Brightness must be integer, " "not of %s.", type(value)) elif 0 <= value <= 100: return await self._query_helper("smartlife.iot.dimmer", "set_brightness", {"brightness": value}) else: raise ValueError("Brightness value %s is not valid." % value)
async def set_color_temp(self, temp: int) -> None: """Set the color temperature of the device in kelvin.""" if not self.is_variable_color_temp: raise SmartDeviceException("Bulb does not support colortemp.") valid_temperature_range = self.valid_temperature_range if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]: raise ValueError( "Temperature should be between {} " "and {}".format(*valid_temperature_range) ) light_state = {"color_temp": temp} await self.set_light_state(light_state)
def hsv(self) -> Tuple[int, int, int]: """Return the current HSV state of the bulb. :return: hue, saturation and value (degrees, %, %) """ if not self.is_color: raise SmartDeviceException("Bulb does not support color.") light_state = cast(dict, self.light_state) hue = light_state["hue"] saturation = light_state["saturation"] value = light_state["brightness"] return hue, saturation, value
def light_state(self) -> Dict[str, str]: """Query the light state.""" light_state = self._last_update["system"]["get_sysinfo"]["light_state"] if light_state is None: raise SmartDeviceException( "The device has no light_state or you have not called update()" ) # if the bulb is off, its state is stored under a different key # as is_on property depends on on_off itself, we check it here manually is_on = light_state["on_off"] if not is_on: off_state = {**light_state["dft_on_state"], "on_off": is_on} return cast(dict, off_state) return light_state
async def discover_single(host: str) -> SmartDevice: """Discover a single device by the given IP address. :param host: Hostname of device to query :rtype: SmartDevice :return: Object for querying/controlling found device. """ protocol = TPLinkSmartHomeProtocol() info = await protocol.query(host, Discover.DISCOVERY_QUERY) device_class = Discover._get_device_class(info) if device_class is not None: return device_class(host) raise SmartDeviceException("Unable to discover device, received: %s" % info)
async def set_brightness(self, value: int): """Set the new dimmer brightness level. Note: When setting brightness, if the light is not already on, it will be turned on automatically. :param value: integer between 1 and 100 """ if not self.is_dimmable: raise SmartDeviceException("Device is not dimmable.") if not isinstance(value, int): raise ValueError("Brightness must be integer, " "not of %s.", type(value)) elif 0 < value <= 100: await self.turn_on() await self._query_helper("smartlife.iot.dimmer", "set_brightness", {"brightness": value}) await self.update() else: raise ValueError("Brightness value %s is not valid." % value)
async def set_color_temp( self, temp: int, *, brightness=None, transition: int = None ) -> Dict: """Set the color temperature of the device in kelvin. :param int temp: The new color temperature, in Kelvin :param int transition: transition in milliseconds. """ if not self.is_variable_color_temp: raise SmartDeviceException("Bulb does not support colortemp.") valid_temperature_range = self.valid_temperature_range if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]: raise ValueError( "Temperature should be between {} " "and {}".format(*valid_temperature_range) ) light_state = {"color_temp": temp} if brightness is not None: light_state["brightness"] = brightness return await self.set_light_state(light_state, transition=transition)
async def discover_single(host: str, complete: bool) -> SmartDevice: """Discover a single device by the given IP address. :param host: Hostname of device to query :param complete: Whether to discover only with get_sysinfo or all options :rtype: SmartDevice :return: Object for querying/controlling found device. """ protocol = TPLinkSmartHomeProtocol() if complete: info = await protocol.query(host, Discover.COMPLETE_DISCOVERY_QUERY) else: info = await protocol.query(host, Discover.DISCOVERY_QUERY) device_class = Discover._get_device_class(info) if device_class is not None: dev = device_class(host) await dev.update() return dev raise SmartDeviceException("Unable to discover device, received: %s" % info)
def get_plug_by_index(self, index: int) -> "SmartStripPlug": """Return child plug for given index.""" if index + 1 > len(self.plugs) or index < 0: raise SmartDeviceException( f"Invalid index {index}, device has {len(self.plugs)} plugs") return self.plugs[index]