/ adafruit_hue.py
adafruit_hue.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2019 Brent Rubell for Adafruit Industries
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  `adafruit_hue`
 24  ================================================================================
 25  
 26  CircuitPython helper library for the Philips Hue
 27  
 28  * Author(s): Brent Rubell
 29  
 30  Implementation Notes
 31  --------------------
 32  
 33  **Software and Dependencies:**
 34  
 35  * Adafruit CircuitPython firmware for the supported boards:
 36    https://github.com/adafruit/circuitpython/releases
 37  
 38  * Adafruit ESP32SPI or ESP_ATcontrol library:
 39      https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI
 40      https://github.com/adafruit/Adafruit_CircuitPython_ESP_ATcontrol
 41  
 42  * SimpleIO library:
 43      https://github.com/adafruit/Adafruit_CircuitPython_SimpleIO
 44  """
 45  import time
 46  from random import randint
 47  from simpleio import map_range
 48  
 49  __version__ = "0.0.0-auto.0"
 50  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Hue.git"
 51  
 52  
 53  class Bridge:
 54      """
 55      HTTP Interface for interacting with a Philips Hue Bridge.
 56      """
 57  
 58      def __init__(self, wifi_manager, bridge_ip=None, username=None):
 59          """
 60          Creates an instance of the Philips Hue Bridge Interface.
 61          :param wifi_manager wifi_manager: WiFiManager from ESPSPI_WiFiManager/ESPAT_WiFiManager
 62          """
 63          wifi_type = str(type(wifi_manager))
 64          if "ESPSPI_WiFiManager" in wifi_type or "ESPAT_WiFiManager" in wifi_type:
 65              self._wifi = wifi_manager
 66          else:
 67              raise TypeError("This library requires a WiFiManager object.")
 68          self._ip = bridge_ip
 69          self._username = username
 70          if bridge_ip and username is not None:
 71              self._bridge_url = "http://{}/api".format(self._ip)
 72              self._username_url = self._bridge_url + "/" + self._username
 73  
 74      @staticmethod
 75      def rgb_to_hsb(rgb):
 76          """Returns RGB values as a HSL tuple.
 77          :param list rgb: RGB Values
 78          """
 79          r = rgb[0] / 255
 80          g = rgb[1] / 255
 81          b = rgb[2] / 255
 82          c_max = max(r, g, b)
 83          c_min = min(r, g, b)
 84          delta = c_max - c_min
 85          light = (c_max + c_min) / 2
 86          if delta == 0.0:
 87              hue = 0
 88              sat = 0
 89          else:
 90              if light < 0.5:
 91                  sat = (c_max - c_min) / (c_max + c_min)
 92              else:
 93                  sat = (c_max - c_min) / (2.0 - c_max - c_min)
 94              if c_max == r:
 95                  hue = (g - b) / (c_max - c_min)
 96              elif c_max == g:
 97                  hue = 2.0 + (b - r) / (c_max - c_min)
 98              else:
 99                  hue = 4.0 + (r - g) / (c_max - c_min)
100              hue *= 60
101              if hue < 0:
102                  hue += 360
103          hue = map_range(hue, 0, 360, 0, 65535)
104          sat = map_range(sat * 100, 0, 100, 0, 254)
105          light = map_range(light * 100, 0, 100, 0, 254)
106          return round(hue), round(sat, 3), round(light, 2)
107  
108      # Hue Core API
109      def discover_bridge(self):
110          """Discovers Philips Hue Bridge IP from the hosted broker discovery service.
111          Returns the bridge's IP address.
112          """
113          try:
114              resp = self._wifi.get("https://discovery.meethue.com")
115              json_data = resp.json()
116              bridge_ip = json_data[0]["internalipaddress"]
117              resp.close()
118          except:
119              raise TypeError(
120                  "Ensure the Philips Bridge and CircuitPython device\
121                               are both on the same WiFi network."
122              )
123          self._ip = bridge_ip
124          # set up hue bridge address path
125          self._bridge_url = "http://{}/api".format(self._ip)
126          return self._ip
127  
128      def register_username(self):
129          """Attempts to register a Hue application username for use with your bridge.
130          Provides a 30 second delay to press the link button on the bridge.
131          Returns username or None.
132          """
133          self._bridge_url = "http://{}/api".format(self._ip)
134          data = {"devicetype": "CircuitPython#pyportal{0}".format(randint(0, 100))}
135          resp = self._wifi.post(self._bridge_url, json=data)
136          connection_attempts = 1
137          username = None
138          while username is None and connection_attempts > 0:
139              resp = self._wifi.post(self._bridge_url, json=data)
140              json = resp.json()[0]
141              if json.get("success"):
142                  username = str(json["success"]["username"])
143                  self._username_url = self._bridge_url + "/" + username
144              connection_attempts -= 1
145              time.sleep(1)
146          resp.close()
147          return username
148  
149      # Lights API
150      def show_light_info(self, light_id):
151          """Gets the attributes and state of a given light.
152          :param int light_id: Light identifier.
153          """
154          resp = self._get("{0}/lights/{1}".format(self._username_url, light_id))
155          return resp
156  
157      def set_light(self, light_id, **kwargs):
158          """Allows the user to turn the light on and off, modify the hue and effects.
159          You can pass the following as valid kwargs into this method:
160          :param bool on: On/Off state of the light
161          :param int bri: Brightness value of the light, 0-100% (1 to 254)
162          :param int hue: Hue value to set the light, in degrees (0 to 360) (0 to 65535)
163          :param int sat: Saturation of the light, 0-100% (0 to 254)
164          (more settings at:
165          https://developers.meethue.com/develop/hue-api/lights-api/#set-light-state )
166          """
167          resp = self._put(
168              "{0}/lights/{1}/state".format(self._username_url, light_id), kwargs
169          )
170          return resp
171  
172      def toggle_light(self, light_id):
173          """Gets and toggles the current state of a specified light.
174          :param int light_id: Light identifier.
175          """
176          light_state = self.get_light(light_id)
177          light_state = not light_state["state"]["on"]
178          resp = self.set_light(light_id, on=light_state)
179          return resp
180  
181      def get_light(self, light_id):
182          """Gets the attributes and state of a provided light.
183          :param int light_id: Light identifier.
184          """
185          resp = self._get("{0}/lights/{1}".format(self._username_url, light_id))
186          return resp
187  
188      def get_lights(self):
189          """Returns all the light resources available for a bridge.
190          """
191          resp = self._get(self._username_url + "/lights")
192          return resp
193  
194      # Groups API
195      def create_group(self, lights, group_id):
196          """Creates a new group containing the lights specified and optional name.
197          :param list lights: List of light identifiers.
198          :param str group_id: Optional group name.
199          """
200          data = {"lights": lights, "name": group_id, "type": group_id}
201          resp = self._post(self._username_url + "/groups", data)
202          return resp
203  
204      def set_group(self, group_id, **kwargs):
205          """Allows the user to turn the light on and off, modify the hue and effects.
206          :param int group_id: Group identifier.
207          You can pass the following as (optional) valid kwargs into this method:
208          :param bool on: On/Off state of the light
209          :param int bri: Brightness value of the light (1 to 254)
210          :param int hue: Hue value to set the light to (0 to 65535)
211          :param int sat: Saturation of the light (0 to 254)
212          (more settings at
213          https://developers.meethue.com/develop/hue-api/lights-api/#set-light-state )
214          """
215          resp = self._put(
216              "{0}/groups/{1}/action".format(self._username_url, group_id), kwargs
217          )
218          return resp
219  
220      def get_groups(self):
221          """Returns all the light groups available for a bridge.
222          """
223          resp = self._get(self._username_url + "/groups")
224          return resp
225  
226      # Scene API
227      def set_scene(self, group_id, scene_id):
228          """Sets a group scene.
229          :param str scene: The scene identifier
230          """
231          # To recall an existing scene, use the Groups API.
232          self.set_group(group_id, scene=scene_id)
233  
234      def get_scenes(self):
235          """Returns a list of all scenes currently stored in the bridge.
236          """
237          resp = self._get(self._username_url + "/scenes")
238          return resp
239  
240      # HTTP Helpers for the Hue API
241      def _post(self, path, data):
242          """POST data
243          :param str path: Formatted Hue API URL
244          :param json data: JSON data to POST to the Hue API.
245          """
246          resp = self._wifi.post(path, json=data)
247          resp_json = resp.json()
248          resp.close()
249          return resp_json
250  
251      def _put(self, path, data):
252          """PUT data
253          :param str path: Formatted Hue API URL
254          :param json data: JSON data to PUT to the Hue API.
255          """
256          resp = self._wifi.put(path, json=data)
257          resp_json = resp.json()
258          resp.close()
259          return resp_json
260  
261      def _get(self, path, data=None):
262          """GET data
263          :param str path: Formatted Hue API URL
264          :param json data: JSON data to GET from the Hue API.
265          """
266          resp = self._wifi.get(path, json=data)
267          resp_json = resp.json()
268          resp.close()
269          return resp_json