├── LICENSE ├── README.md └── hue.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Steven Jacobs 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hue Bridge Client 2 | An easy-to-use, MicroPython-compatible class to access and control lights on a Philips Hue Bridge. 3 | 4 | ## Features 5 | * Automatic bridge discovery using the UPnP SSDP protocol 6 | * Handles developer API username registration for API access 7 | * Stores bridge IP and username for instant access on restarts 8 | * Simple methods to get lists of lights and groups 9 | * Set one or many, light or group state parameters with a single call 10 | * Works equally well in micropython or python 2.7 and up 11 | * Easy to extend with new methods to access more bridge functionality 12 | 13 | ## Usage 14 | The first time the `Bridge` class is used, it will discovery the bridge and then initiate the necessary API username registration. 15 | ``` 16 | import hue 17 | h = hue.Bridge() 18 | ``` 19 | You'll be prompted to press the link button on the Hue Bridge. 20 | ``` 21 | Bridge located at 192.168.0.7 22 | >>> Press link button on Hue bridge to register <<< 23 | ``` 24 | **If you are using the ESP8266, you'll need to disable the AP WLAN interface for the discovery process to work.** This also requires that the STA WLAN (station) interface be connected to the same network as the bridge. Here's how to disable the AP WLAN interface. 25 | ``` 26 | import network 27 | ap=network.WLAN(network.AP_IF) 28 | ap.active(False) 29 | ``` 30 | Once you've connected to the bridge, you can interact with *lights* and *groups*. Individual lights or groups are referenced by an ID number. You can get the list of IDs with the methods `getLights` and `getGroups`. 31 | ``` 32 | h.getGroups() 33 | {1: 'Dining room', 2: 'Bedroom', ... , 9: 'Porch', 10: 'Spare bedroom'} 34 | ``` 35 | To get details about a specific light our group, use the `getLight` and `getGroup` methods. 36 | ``` 37 | h.getLight(1) 38 | {u'name': u'Dining 1', u'swversion': u'5.38.2.19136', u'manufacturername': u'Philips', u'state': {u'on': True, u'reachable': True, u'bri': 200, u'alert': u'none'}, u'uniqueid': u'00:17:88:01:10:40:2c:14-0b', u'type': u'Dimmable light', u'modelid': u'LWB006'} 39 | ``` 40 | Here's an example of how to access individual settings from the response: 41 | ``` 42 | light = h.getLight(1) 43 | light['state']['on'] 44 | True 45 | light['state']['bri'] 46 | 200 47 | ``` 48 | To set light or group state parameters use the `setLight` and `setGroup` methods. 49 | ``` 50 | h.setLight(1,bri=254,transitiontime=20) 51 | h.setGroup(10,on=False) 52 | h.setLight(2,bri_inc=-100) 53 | ``` 54 | 55 | ## Installation 56 | Just download the `hue.py` file from this repository and place in a folder with your python code. 57 | 58 | ## More In-Depth Info 59 | If you'd like to know more about the Hue Bridge API, head over to [https://developers.meethue.com/philips-hue-api]. You'll need to register as a developer to gain access, which is free. 60 | 61 | To access other aspects of the bridge API, use the `get()` and `put()` methods. For example, to get bridge configuration along with the current whitelist of usernames, try the folowing: 62 | ``` 63 | h.get('config') 64 | ``` 65 | The methods `post()` and `delete()` are not currently implemented, but can be easily added, by modelling them after the get and put methods. These methods are used to change bridge and device configurations. 66 | 67 | When first instantiating this class, `setup()` runs, which locates the bridge's IP address and attempts to get an API username from the bridge. Both of these values will be stored to the file `bridge.dat` within the current working directory. The next time the Bridge class instantiated, it will load the saved IP and username values automatically. 68 | If the bridge changes IP, you can edit the `bridge.dat` file. It is recommended that you reserve the bridges IP address on your router, so it doesn't hop around. Alternatively, you can force a `discover()` whenever you connect to the bridge to get its current IP. To do this you'll need to disable autosetup when instantiating the Bridge class. Here's the steps necessary: 69 | ``` 70 | h = hue.Bridge(autosetup=False) 71 | h.loadSettings() #This will load the last known IP and API Username 72 | h.discover() #Find the bridge's current IP 73 | h.saveSettings() #Optionally, you can save the updated IP 74 | ``` 75 | 76 | ## Troubleshooting 77 | If you are having trouble with the saved settings and would just like to start fresh, call the `reset()` method. The IP discovery and username process will take place, and the new settings will be saved. 78 | ``` 79 | h = hue.Bridge(autosetup=False) 80 | h.reset() #Clear saved settings 81 | ``` 82 | If you are having get the bridge to respond to commands, enable *debug* level 2 to get feedback from the method calls. 83 | ``` 84 | h = hue.Bridge(debug=2) #Enable debug output 85 | ``` 86 | Be aware that issuing *set* commands too quickly can lead to the bridge refusing to process the request. Philips suggest that lights be updated no quicker than 10 times per second and groups be updated, at most, once per second. These are just guidelines to follow. It is also best practice to minimize how many state values you change in each call. For instance, once the light is 'on', you can adjust brightness, in successive calls, without setting 'on' again. 87 | -------------------------------------------------------------------------------- /hue.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from time import sleep 3 | import json 4 | try: 5 | import requests 6 | except: 7 | import urequests as requests 8 | 9 | # UPnP SSDP Search request header 10 | HEADER = b"""M-SEARCH * HTTP/1.1\r 11 | HOST: 239.255.255.250:1900\r 12 | MAN: "ssdp:discover"\r 13 | ST: ssdp:all\r 14 | MX: 3\r 15 | \r 16 | """ 17 | 18 | class Bridge: 19 | """Provides methods for connecting to and using Hue Bridge. Supports 20 | Micropython, Python 2, and 3.""" 21 | 22 | def __init__(self,autosetup=True, debug=1): 23 | self.debug = debug #0=no prints, 1=messages, 2=debug 24 | self.IP = None 25 | self.username = None 26 | if autosetup: 27 | self.setup() 28 | 29 | 30 | def show(self,str,level=1): 31 | """ Show debug output. """ 32 | if self.debug >= level: 33 | print(str) 34 | 35 | 36 | def setup(self): 37 | """ Loads bridge settings or attempts to establish them, if needed.""" 38 | success = self.loadSettings() 39 | if success: 40 | # verify bridge settings work 41 | try: 42 | self.idLights() 43 | success = True 44 | except: 45 | success = False 46 | if not success: 47 | if self.discover(): 48 | self.show('Bridge located at {}'.format(self.IP)) 49 | self.show('>>> Press link button on Hue bridge to register <<<') 50 | if self.getUsername(): 51 | success = self.saveSettings() 52 | else: 53 | self.show("Couldn't get username from bridge.") 54 | else: 55 | self.show("Couldn't find bridge on LAN.") 56 | return success 57 | 58 | 59 | def discover(self): 60 | """ Locate Hue Bridge IP using UPnP SSDP search. Discovery will return 61 | when bridge is found or 3 seconds after last device response. Returns IP 62 | address or None.""" 63 | #On ESP8266, disable AP WLAN to force use of STA interface 64 | #import network 65 | #ap = network.WLAN(network.AP_IF) 66 | #ap.active(False) 67 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 68 | s.sendto(HEADER, ('239.255.255.250',1900)) #UPnP Multicast 69 | s.settimeout(3) 70 | 71 | IP = None 72 | while IP == None: 73 | data, addr = s.recvfrom(1024) 74 | self.show(str(data),2) 75 | lines = data.split(b'\r\n') 76 | for l in lines: 77 | tokens = l.split(b' ') 78 | if tokens[0] == b'SERVER:': 79 | product = tokens[3].split(b'/') 80 | if product[0] == b'IpBridge': 81 | IP = str(addr[0]) 82 | break 83 | 84 | s.close() 85 | self.IP = IP 86 | return IP 87 | 88 | 89 | def getUsername(self): 90 | """ Get a developer API username from bridge. 91 | Requires that the bridge link button be pressed sometime while polling. 92 | Polls for 20 seconds (20 attempts at 1 second intervals). 93 | Can timeout with error if bridge is non-responsive. 94 | Returns username on success or None on failure.""" 95 | url = 'http://{}/api'.format(self.IP) 96 | data = '{"devicetype":"TapLight#mydevice"}' 97 | username = None 98 | count = 20 99 | while count > 0 and username == None: 100 | resp = requests.post(url,data=data) 101 | if resp.status_code == 200: 102 | j = resp.json()[0] 103 | self.show(j,2) 104 | if j.get('success'): 105 | username = str(j['success']['username']) 106 | self.username = username 107 | sleep(1) 108 | count -= 1 109 | return username 110 | 111 | 112 | def saveSettings(self): 113 | """ Save bridge IP and username to bridge.dat file. 114 | Returns True on success.""" 115 | if self.IP and self.username: 116 | f=open('bridge.dat','w') 117 | f.write(json.dumps([self.IP,self.username])) 118 | f.close() 119 | return True 120 | else: 121 | return None 122 | 123 | 124 | def loadSettings(self): 125 | """ Load bridge IP and username from bridge.dat file and set base URL. 126 | Returns True on success. """ 127 | try: 128 | f=open('bridge.dat') 129 | except: 130 | return None 131 | l = json.load(f) 132 | f.close() 133 | self.IP = str(l[0]) 134 | self.username = str(l[1]) 135 | self.show('Loaded settings {} {}'.format(self.IP,self.username),2) 136 | return True 137 | 138 | 139 | def resetSettings(self): 140 | """Delete current saved bridge settings and reinitiate.""" 141 | from os import remove 142 | remove('bridge.dat') 143 | self.IP = None 144 | self.username = None 145 | self.setup() 146 | 147 | 148 | def url(self,path): 149 | """Return url for API calls.""" 150 | return 'http://{}/api/{}/{}'.format(self.IP,self.username,path) 151 | 152 | 153 | def get(self, path): 154 | """Perform GET request and return json result.""" 155 | url = self.url(path) 156 | self.show(url,2) 157 | resp = requests.get(url).json() 158 | self.show(resp,2) 159 | return resp 160 | 161 | 162 | def put(self, path, data): 163 | """Perform PUT request and return response.""" 164 | url = self.url(path) 165 | self.show(url,2) 166 | data = json.dumps(data) 167 | self.show(data,2) 168 | resp = requests.put(url, data=data).json() 169 | self.show(resp,2) 170 | return resp 171 | 172 | 173 | def allLights(self): 174 | """Returns dictionary containing all lights, with detail.""" 175 | """Large return set, not ideal for controllers with limited RAM.""" 176 | return self.get('lights') 177 | 178 | 179 | def idLights(self): 180 | """Returns list of all light IDs.""" 181 | ids = self.get('groups/0')['lights'] 182 | for i in range(len(ids)): 183 | ids[i] = int(ids[i]) 184 | return ids 185 | 186 | 187 | def getLight(self,id): 188 | """Returns dictionary of light details for given ID.""" 189 | return self.get('lights/{}'.format(str(id))) 190 | 191 | 192 | def getLights(self): 193 | """Iterates through each light to build and return a dictionary 194 | of light IDs and names.""" 195 | dict = {} 196 | for i in self.idLights(): 197 | dict[i] = str(self.getLight(i)['name']) 198 | return dict 199 | 200 | 201 | def setLight(self,id,**kwargs): 202 | """Set one or more states of a light. 203 | Ex: setLight(1,on=True,bri=254,hue=50000,sat=254)""" 204 | self.put('lights/{}/state'.format(str(id)),kwargs) 205 | 206 | 207 | def allGroups(self): 208 | """Returns dictionary containing all groups, with detail.""" 209 | return self.get('groups') 210 | 211 | 212 | def getGroup(self,id): 213 | """Returns dictionary of group details.""" 214 | return self.get('groups/{}'.format(str(id))) 215 | 216 | 217 | def getGroups(self): 218 | """Returns dictionary of group IDs and names.""" 219 | dict = {} 220 | groups = self.allGroups() 221 | for g in groups: 222 | dict[int(g)] = str(groups[g]['name']) 223 | return dict 224 | 225 | 226 | def setGroup(self,id,**kwargs): 227 | """Set one or more states of a group. 228 | Ex: setGroup(1,bri_inc=100,transitiontime=40)""" 229 | self.put('groups/{}/action'.format(str(id)),kwargs) 230 | 231 | 232 | --------------------------------------------------------------------------------