From 46de665176f836c978f9bb26647aea5e0a5b4856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller=20=28ChaoticByte=29?= Date: Fri, 21 Jul 2023 20:40:19 +0200 Subject: [PATCH] Added first working version of the library; updated README, .gitignore and LICENSE --- .gitignore | 2 + LICENSE | 2 +- README.md | 3 +- yahuelib/__init_.py | 0 yahuelib/controller.py | 182 +++++++++++++++++++++++++++++++++++++++++ yahuelib/exceptions.py | 7 ++ yahuelib/utils.py | 14 ++++ 7 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 yahuelib/__init_.py create mode 100644 yahuelib/controller.py create mode 100644 yahuelib/exceptions.py create mode 100644 yahuelib/utils.py diff --git a/.gitignore b/.gitignore index 68bc17f..88e592d 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +test.py diff --git a/LICENSE b/LICENSE index 2225de2..001657d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Julian Müller +Copyright (c) 2023 Julian Müller (ChaoticByte) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e914d12..4bb95a9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # YaHueLib -Yet Another Philips Hue API Library + +Yet Another Philips Hue API Library for Python. This project only implements a subset of the API. diff --git a/yahuelib/__init_.py b/yahuelib/__init_.py new file mode 100644 index 0000000..e69de29 diff --git a/yahuelib/controller.py b/yahuelib/controller.py new file mode 100644 index 0000000..e7d9cbe --- /dev/null +++ b/yahuelib/controller.py @@ -0,0 +1,182 @@ +# Copyright (c) 2023 Julian Müller (ChaoticByte) + + +from json import dumps as _json_dumps +from json import loads as _json_loads +from urllib import request as _request + + +from .exceptions import * + + +# Have to use the following SSL context because the +# TLS certificate of a Hue Bridge is self signed +ssl_context_unverified = _request.ssl._create_unverified_context() + + +class _BaseController: + + _api_endpoint_all = "" + _api_endpoint_specific = "" + + def __init__(self, number:int, bridge_ip_address:str, bridge_api_user:str): + assert type(number) == int + assert type(bridge_ip_address) == str + assert type(bridge_api_user) == str + self.number = number + self.bridge_ip_address = bridge_ip_address + self.bridge_api_user = bridge_api_user + + @classmethod + def from_name(cls, name:str, bridge_ip_address:str, bridge_api_user:str): + assert type(bridge_ip_address) == str + assert type(bridge_api_user) == str + assert type(name) == str + api_request = _request.Request(cls._api_endpoint_all.format( + bridge_ip_address=bridge_ip_address, + bridge_api_user=bridge_api_user)) + with _request.urlopen(api_request, context=ssl_context_unverified) as r: + data = _json_loads(r.read()) + for n in data: + if data[n]["name"] == name: + return cls(int(n), bridge_ip_address, bridge_api_user) + raise LightOrGroupNotFound() + + def _api_request(self, method="GET", path:str="", data:dict={}): + assert type(method) == str + assert type(path) == str + assert type(data) == dict + api_request = _request.Request( + self._api_endpoint_specific.format( + bridge_ip_address=self.bridge_ip_address, + bridge_api_user=self.bridge_api_user, + number=self.number + ) + path, + method=method, + data=_json_dumps(data).encode()) + with _request.urlopen(api_request, context=ssl_context_unverified) as r: + response_data = _json_loads(r.read()) + if type(response_data) == list and len(response_data) > 0: + if "error" in response_data[0]: + raise APIError(response_data) + return response_data + + +class LightController(_BaseController): + + _api_endpoint_all = "https://{bridge_ip_address}/api/{bridge_api_user}/lights" + _api_endpoint_specific = "https://{bridge_ip_address}/api/{bridge_api_user}/lights/{number}" + + @property + def reachable(self) -> bool: + data = self._api_request() + return data["state"]["reachable"] + + @property + def on(self) -> bool: + data = self._api_request() + return data["state"]["on"] + + @on.setter + def on(self, on:bool): + assert type(on) == bool + self._api_request("PUT", "/state", {"on": on}) + + @property + def brightness(self): + data = self._api_request() + return data["state"]["bri"] + + @brightness.setter + def brightness(self, brightness:float): + assert type(brightness) == float or type(brightness) == int + bri_ = min(max(int(brightness * 254), 0), 254) + self._api_request("PUT", "/state", {"bri": bri_}) + + @property + def hue(self): + data = self._api_request() + return data["state"]["hue"] + + @hue.setter + def hue(self, hue:float): + assert type(hue) == float or type(hue) == int + hue_ = min(max(int(hue * 65535), 0), 65535) + self._api_request("PUT", "/state", {"hue": hue_}) + + @property + def saturation(self): + data = self._api_request() + return data["state"]["sat"] + + @saturation.setter + def saturation(self, saturation:float): + assert type(saturation) == float or type(saturation) == int + sat_ = min(max(int(saturation * 254), 0), 254) + self._api_request("PUT", "/state", {"sat": sat_}) + + def alert(self): + self._api_request("PUT", "/state", {"alert": "select"}) + + def alert_long(self): + self._api_request("PUT", "/state", {"alert": "lselect"}) + + +class GroupController(_BaseController): + + _api_endpoint_all = "https://{bridge_ip_address}/api/{bridge_api_user}/groups" + _api_endpoint_specific = "https://{bridge_ip_address}/api/{bridge_api_user}/groups/{number}" + + @property + def any_on(self) -> bool: + data = self._api_request() + return data["state"]["any_on"] + + @property + def all_on(self) -> bool: + data = self._api_request() + return data["state"]["all_on"] + + @all_on.setter + def all_on(self, on:bool): + assert type(on) == bool + self._api_request("PUT", "/action", {"on": on}) + + @property + def brightness(self): + data = self._api_request() + return data["action"]["bri"] + + @brightness.setter + def brightness(self, brightness:float): + assert type(brightness) == float or type(brightness) == int + bri_ = min(max(int(brightness * 254), 0), 254) + self._api_request("PUT", "/action", {"bri": bri_}) + + @property + def hue(self): + data = self._api_request() + return data["action"]["hue"] + + @hue.setter + def hue(self, hue:float): + assert type(hue) == float or type(hue) == int + hue_ = min(max(int(hue * 65535), 0), 65535) + self._api_request("PUT", "/action", {"hue": hue_}) + + @property + def saturation(self): + data = self._api_request() + return data["action"]["sat"] + + @saturation.setter + def saturation(self, saturation:float): + assert type(saturation) == float or type(saturation) == int + sat_ = min(max(int(saturation * 254), 0), 254) + self._api_request("PUT", "/action", {"sat": sat_}) + + def alert(self): + self._api_request("PUT", "/action", {"alert": "select"}) + + def alert_long(self): + self._api_request("PUT", "/action", {"alert": "lselect"}) diff --git a/yahuelib/exceptions.py b/yahuelib/exceptions.py new file mode 100644 index 0000000..a977f7d --- /dev/null +++ b/yahuelib/exceptions.py @@ -0,0 +1,7 @@ +# Copyright (c) 2023 Julian Müller (ChaoticByte) + +class LightOrGroupNotFound(Exception): + pass + +class APIError(Exception): + pass diff --git a/yahuelib/utils.py b/yahuelib/utils.py new file mode 100644 index 0000000..a588dec --- /dev/null +++ b/yahuelib/utils.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Julian Müller (ChaoticByte) + + +from colorsys import rgb_to_hsv as _rgb_to_hsv + + +def rgb_to_hsv(r:int, g:int, b:int) -> float: + assert type(r) == int + assert type(g) == int + assert type(b) == int + r_ = r / 255.0 + g_ = g / 255.0 + b_ = b / 255.0 + return _rgb_to_hsv(r_, g_, b_)