From 28dcb2b4ba2181626f0e268f00d335c813d99042 Mon Sep 17 00:00:00 2001 From: joost witteveen Date: Mon, 21 Dec 2020 13:22:14 +0100 Subject: [PATCH] Initial commit: Simple python script to control Philips Hue Lamps on a Bridge --- hue.py | 308 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100755 hue.py diff --git a/hue.py b/hue.py new file mode 100755 index 0000000..1b8bdd9 --- /dev/null +++ b/hue.py @@ -0,0 +1,308 @@ +#!/usr/bin/python3 + +""" + +Script to set colours of HUE lamps (via the bridge). +Author: joost witteveen (joosteto@gmail.com) + +Examples: + +Get a valid access code from the bridge: +1) press the button on the bridge +2) Run the following command: + python3 hue.py --getuser yourusername +3) The access code will be saved in the file 'accesscode', and used for next commands. + +Make all lamps as bright as possible, and green: +python3 hue.py --hue 2 --bri 1 + +Show json response for a '/lights/2' http GET request: +python3 hue.py --get /lights/2 + +Continuously check for long 'off' button press on switch id=4, and if +python3 hue.py --checklong0 4 + + + +More info about the HUE lamps: + +https://domoticproject.com/controlling-philips-hue-lights-with-raspberry-pi/ + + +sudo apt-get install avahi-utils python3-requests + +# Get address of the bridge: +avahi-browse -rt _hue._tcp + +################################################# +python3 hue.py --get /lights/2 +{'capabilities': {'certified': True, + 'control': {'colorgamut': [[0.6915, 0.3083], + [0.17, 0.7], + [0.1532, 0.0475]], + 'colorgamuttype': 'C', + 'ct': {'max': 500, 'min': 153}, + 'maxlumen': 800, + 'mindimlevel': 200}, + 'streaming': {'proxy': True, 'renderer': True}}, + 'config': {'archetype': 'classicbulb', + 'direction': 'omnidirectional', + 'function': 'mixed', + 'startup': {'configured': True, 'mode': 'powerfail'}}, + 'manufacturername': 'Signify Netherlands B.V.', + 'modelid': 'LCA001', + 'name': 'Hue color lamp 2', + 'productid': 'Philips-LCA001-5-A19ECLv6', + 'productname': 'Hue color lamp', + 'state': {'alert': 'select', + 'bri': 25, + 'colormode': 'hs', + 'ct': 500, + 'effect': 'none', + 'hue': 0, + 'mode': 'homeautomation', + 'on': True, + 'reachable': True, + 'sat': 127, + 'xy': [0.5359, 0.3425]}, + 'swconfigid': 'BD38721C', + 'swupdate': {'lastinstall': '2020-12-05T13:42:29', 'state': 'noupdates'}, + 'swversion': '1.65.9_hB3217DF', + 'type': 'Extended color light', + 'uniqueid': '00:17:88:01:06:7a:a7:59-0b'} + +""" + +bridgeAddr="Philips-hue.local" +user=None +verbose=None + +import argparse +import os +import pprint +import requests +import time + +class LinkButtonNotPressed(Exception): + def __init__(self, info): + self.info=info +class BridgeError(Exception): + def __init__(self, info): + self.info=info + +def getcmd(arg=""): + if arg[0]=='/': + arg=arg[1:] + url = f'http://{bridgeAddr}/api/{user}/{arg}' + headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} + if verbose: + print(f"URL={url}") + r = requests.get(url,headers=headers) + answ=r.json() + try: + if 'error' in answ[0]: + raise BridgeError(answ[0]['error']) + except KeyError: #normally answ is a dict, and test for 'error' in answ[0] will fail + pass + if verbose: + print(f" -> {r.json()}") + return answ + +def getuser(name): + """ +curl -d '{"devicetype":"[joosteto]"}' -H "Content-Type: application/json" -X POST 'http://philips-hue.local/api' +[{"success":{"username":"asdjkflasdjfksdfjsklfjsdkla-ajfdklasdjk"}}] + +""" + url = f'http://{bridgeAddr}/api/' + payload =f'{{"devicetype": "[{name}]"}}' + #print(f"payload={payload}") + headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} + r=requests.post(url, data=payload, headers=headers) + #print(f"r.text={r.text}") + answ=r.json()[0] + #answ=[{"success":{"yourusername":"asdjkflasdjfksdfjsklfjsdkla-ajfdklasdjk"}}][0] + if 'success' in answ: + usercode=answ['success'] + name=tuple(usercode.keys())[0] + code=usercode[name] + print(f'access code for name {name}: {code}') + if os.path.exists('accesscode'): + print("Erase old accesscode file to store new accesscode?") + input("^C to abort, Enter to continue ") + open('accesscode', 'w').write(code) + else: + if 'error' in answ: + if answ['error']['description']=='link button not pressed': + raise LinkButtonNotPressed(answ['error']) + print(f'Error: {answ["error"]["description"]}') + else: + print(f'Error: {answ}') + +def parsecolorgamuts(lightinfo): + cg={} + for (lightid, li) in lightinfo.items(): + cg[lightid]=li['capabilities']['control']['colorgamut'] + return cg + +def set_lightstate(lightid, name, value): + url = f'http://{bridgeAddr}/api/{user}/lights/{lightid}/state' + payload =f'{{"{name}":{value}, "on": true}}' + headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} + if verbose: + print(f'url={url}\ndata={payload}\nheaders={headers}') + r = requests.put(url, data=payload, headers=headers) + if verbose: + print(f" -> response: {r.text}") + + +def loopcolors(cg): + tStart=time.time() + while True: + t=(time.time()-tStart)/5 + for lightid in cg: + tid=(t+int(lightid))%3 + it0=int(tid) + it1=(it0+1)%3 + xy0=cg[lightid][it0] + xy1=cg[lightid][it1] + x=(tid-it0)*xy0[0]+(1-tid+it0)*xy1[0] + y=(tid-it0)*xy0[1]+(1-tid+it0)*xy1[1] + set_lightstate(lightid, "xy", [x,y]) + time.sleep(0.3) + +def checkLong0(sensorid, cg): + oldstate=None + tStart=time.time() + while True: + sensinf=getcmd(f'/sensors/{sensorid}') + sensstate=sensinf['state']['buttonevent'] + if sensstate==4003 and oldstate != 'longdown': + oldstate='longdown' + #print(f'long off press: {sensinf["state"]}') + tStart=time.time() + elif sensstate!=4003: + #print(f'no long off press: {sensinf["state"]}') + oldstate=None + if oldstate=='longdown': + t=time.time()-tStart + for lightid in cg: + tid=(t/16-(int(lightid)/3))%1 + hue=int(tid*(2**16-1)) + set_lightstate(lightid, "hue", hue) + time.sleep(0.2) + else: + time.sleep(1) + + + + +#gi=getinfo() +#pprint.pprint(gi) + +#tryouts() +#setlight(1, [0.6, 0.4]) +#setlight(2, [0.6, 0.4]) +#setlight(3, [0.6, 0.4]) + + +def main(): + global bridgeAddr + global user + global verbose + parser = argparse.ArgumentParser(description='Communicate with Phillips Hue Bridge (lamps, switches).') + #configuration + parser.add_argument('--user', + help='user (effectively password) at the bridge. If not given, read from file username') + parser.add_argument('--bridge', + help='hostname/IP of the bridge. Default: philips-hue.local') + parser.add_argument('--getuser', + help='get username (security code) from bridge (after button on bridge is pressed)') + + #actions: + parser.add_argument('--get', + help='do a GET request, show the result. Example: --get /lights') + parser.add_argument('--loop', + action='store_true', + help='change colors of the lights in a loop') + parser.add_argument('--checklong0', + help='continuously check for long 0 press on sensor id') + parser.add_argument('--hue', + help="set hue of specified lamps") + parser.add_argument('--sat', + help='set saturation of specified lamps') + parser.add_argument('--bri', + help='set brightness of specified lamps') + parser.add_argument('--xy', + help='set xy color of specified lamps. Example: --xy 0.3,0.7') + + #Others + parser.add_argument('--lamps', + help='hue,sat,bri,xy -commands work on lamps in this list. Example: --lamps 1,3') + parser.add_argument('--verbose', action='store_true') + args = parser.parse_args() + + verbose=args.verbose + #configuration + if args.bridge is not None: + bridgeAddr=args.bridge + + try: + if args.getuser is not None: + getuser(args.getuser) + + if args.user is not None: + user=args.user + else: + user=open('accesscode','r').read().strip() + + + lightsinfo=getcmd('/lights') + colorgammuts=parsecolorgamuts(lightsinfo) + + if args.lamps is not None: + lstr=args.lamps + if ',' not in lstr: #add "," if only one lamp is given (--lamps 1) + lstr=lstr+"," + lamps=tuple(str(l) for l in eval(lstr)) + else: + lamps=tuple(lightsinfo.keys()) + #print(f"lamps={lamps}") + + #actions + if args.get is not None: + answ=getcmd(args.get) + pprint.pprint(answ) + + if args.loop: + loopcolors(colorgammuts) + + if args.checklong0 is not None: + checkLong0(args.checklong0, colorgammuts) + + for lid in lamps: + if args.hue is not None: + hue=int(float(args.hue)*2**16//6) % 2**16 + #print(f"setting {lid} to hue={hue}") + set_lightstate(lid, "hue", hue) + + if args.sat is not None: + sat=int(float(args.sat)*255) + set_lightstate(lid, "sat", sat) + + if args.xy is not None: + xy=list(eval(args.xy)) + set_lightstate(lid, "xy", xy) + + if args.bri is not None: + bri=int(255*eval(args.bri)) + set_lightstate(lid, "bri", bri) + except LinkButtonNotPressed as e: + print("To get a valid user access code, the button on the bridge needs to be pressed shortly before the --getaccess command is used") + except BridgeError as e: + print(f"Error message from bridge: {e}") + if e.info['description']=='unauthorized user': + print("Access code was not accepted by bridge. Run again with the '--getaccess name' argument") + +if __name__=="__main__": + main()