320 lines
10 KiB
Python
Executable File
320 lines
10 KiB
Python
Executable File
#!/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
|
|
|
|
(Very) warm white:
|
|
python3 hue.py --xy 0.6,0.4 --bri 1
|
|
|
|
|
|
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 (range: 0..360)")
|
|
parser.add_argument('--sat',
|
|
help='set saturation of specified lamps (range: 0..1)')
|
|
parser.add_argument('--bri',
|
|
help='set brightness of specified lamps (range: 0..1)')
|
|
parser.add_argument('--briB',
|
|
help='set brightness of specified lamps (range: 0..255)')
|
|
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//360) % 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)
|
|
|
|
bri=None
|
|
if args.bri is not None:
|
|
bri=int(255*eval(args.bri))
|
|
if args.briB is not None:
|
|
bri=int(eval(args.briB))
|
|
if bri is not None:
|
|
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()
|