diff options
Diffstat (limited to 'ShellyPy/wrapper.py')
-rw-r--r-- | ShellyPy/wrapper.py | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/ShellyPy/wrapper.py b/ShellyPy/wrapper.py new file mode 100644 index 0000000..adb34e4 --- /dev/null +++ b/ShellyPy/wrapper.py @@ -0,0 +1,280 @@ +from sys import version_info + +if version_info.major == 3: + from json.decoder import JSONDecodeError +else: + JSONDecodeError = ValueError + + +from requests import post +from requests.auth import HTTPBasicAuth + +from .error import * + + +def confirm_ip(ip): + """ + @brief Confirm IPv4 adress + + @param ip IP given as either a string or List containing strings or integers + + @return returns True if ip is valid + """ + + if isinstance(ip, str): + ip = ip.split(".") + if len(ip) != 4: + return False + + if isinstance(ip, list): + for value in ip: + if isinstance(value, str): + try: + value = int(value) + except ValueError: + value = 256 + if value > 255 or value < 0: + return False + return True + + return False + + +class Shelly: + + def __init__(self, ip, port = "80", *args, **kwargs): + """ + @param ip the target IP of the shelly device. Can be a string, list of strings or list of integers + @param port target port, may be useful for non Shelly devices that have the same HTTP Api + @param login dict of login credentials. Keys needed are "username" and "password" + @param timeout specify the amount of time until requests are aborted. + @param debug enable debug printing + """ + + # TODO add domain support + + self.__debugging__ = kwargs.get("debug", None) + + self.__PROTOCOL__ = "http" + + if not confirm_ip(ip): + raise MalformedIP("IP is is malformed or not IPv4") + + login = kwargs.get("login", {}) + + if isinstance(ip, list): + self.__ip__ = ".".join([str(val) for val in ip]) + else: + self.__ip__ = ip + + self.__port__ = port + + self.__timeout__ = kwargs.get("timeout", 5) + + self.__credentials__ = HTTPBasicAuth( + login.get("username", ""), login.get("password", "")) + + self.update() + + def __repr__(self): + return "<{} {} ({})>".format(self.__name__, self.__type__, self.__ip__) + + def __str__(self): + return str(self.__name__) + + def update(self): + """ + @brief update the Shelly attributes + """ + status = self.settings() + + self.__type__ = status['device']['type'] + self.__name__ = status['device']['hostname'] + + self.__relay__ = status.get("relays", []) + self.__roller__ = status.get("rollers", []) + self.__light__ = status.get("lights", []) + + # documentation for the Shelly Sense is very weird + # FIXME + self.__ir__ = status.get("ir", []) + # There isn't even an example of the response for the RGBW + # FIXME + self.__color__ = status.get("color", []) + + self.__emeter__ = status.get("emeter", []) + + def post(self, page, values = None): + """ + @brief returns settings + + @param page page to be accesed. Use the Shelly HTTP API Reference to see whats possible + + @return returns json response + """ + + url = "{}://{}:{}/{}?".format(self.__PROTOCOL__, self.__ip__, self.__port__, page) + + if values: + url += "&".join(["{}={}".format(key, value) for key, value in values.items()]) + + if self.__debugging__: + print("Target Adress: {}\n" + "Authentication: {}\n" + "Timeout: {}" + "".format(url, any(self.__credentials__.username + self.__credentials__.password), self.__timeout__)) + + response = post(url, auth=self.__credentials__, + timeout=self.__timeout__) + + if response.status_code == 401: + raise BadLogin() + elif response.status_code == 404: + raise NotFound("Not Found") + + try: + return response.json() + except JSONDecodeError: + raise BadResponse("Bad JSON") + + def settings(self, subpage = None): + """ + @brief returns settings + + @param page page to be accesed. Use the Shelly HTTP API Reference to see whats possible + + @return returns settings as a dict + """ + + page = "settings" + if subpage: + page += "/" + subpage + + return self.post(page) + + def relay(self, index, *args, **kwargs): + """ + @brief Interacts with a relay at the given index + + @param self The object + @param index index of the relay + @param turn Will turn the relay on or off + @param timer a one-shot flip-back timer in seconds + """ + + values = {} + + turn = kwargs.get("turn", None) + timer = kwargs.get("timer", None) + + if turn is not None: + if turn: + values["turn"] = "on" + else: + values["turn"] = "off" + + if timer: + values["timer"] = timer + + return self.post("relay/{}".format(index), values) + + def roller(self, index, *args, **kwargs): + """ + @brief Interacts with a roller at a given index + + @param self The object + @param index index of the roller. When in doubt use 0 + @param go way of the roller to go. Accepted are "open", "close", "stop", "to_pos" + @param roller_pos the wanted position in percent + @param duration how long it will take to get to that position + """ + + go = kwargs.get("go", None) + roller_pos = kwargs.get("roller_pos", None) + duration = kwargs.get("duration", None) + + def clamp_percentage(val): + """ + clamp given percentage to a range from 0 to 100 + """ + if val > 100: + val = 100 + elif val < 0: + val = 0 + return val + + values = {} + + if go: + values["go"] = go + + if roller_pos is not None: + values["roller_pos"] = clamp_percentage(roller_pos) + + if duration is not None: + values["duration"] = duration + + return self.post("roller/{index}".format(index), values) + + def light(self, index, *args, **kwargs): + + mode = kwargs.get("mode", None) + timer = kwargs.get("timer", None) + turn = kwargs.get("turn", None) + red = kwargs.get("red", None) + green = kwargs.get("green", None) + blue = kwargs.get("blue", None) + white = kwargs.get("white", None) + gain = kwargs.get("gain", None) + temp = kwargs.get("temp", None) + brightness = kwargs.get("brightness", None) + + def clamp(val): + """clamp any number to 8 bit""" + if val > 255: + val = 255 + elif val < 0: + val = 0 + + return val + + values = {} + + if mode: + values["mode"] = mode + + if timer is not None: + values["timer"] = timer + + if turn is not None: + if turn: + values["turn"] = "on" + else: + values["turn"] = "off" + + if red is not None: + values["red"] = clamp(red) + + if green is not None: + values["green"] = clamp(green) + + if blue is not None: + values["blue"] = clamp(blue) + + if white is not None: + values["white"] = clamp(white) + + if gain is not None: + values["gain"] = clamp(gain) + + if temp is not None: + values["temp"] = temp + + if brightness is not None: + values["brightness"] = brightness + + return self.post("light/{index}".format(index), values) + + def emeter(self, i, *args, **kwargs): + #TODO + return |