Files
jarvis-home/skills/python/philips-hue/main.py
T
2026-08-29 17:02:35 +02:00

72 lines
3.9 KiB
Python

import json, os, ssl, sys
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
PROTO="jarvis.skill.invoke.v1"
def truthy(v): return str(v or '').strip().lower() in ('1','true','yes','on')
def fail(code,msg): return {"protocol":PROTO,"success":False,"error":{"code":code,"message":msg}}
def ok(data,msg="OK",mut=False): return {"protocol":PROTO,"success":True,"data":data,"message":msg,"mutated":mut}
def config():
base=os.getenv('HUE_BRIDGE_URL','').strip().rstrip('/')
key=os.getenv('HUE_APP_KEY','').strip()
if not base or not key: raise RuntimeError('HUE_BRIDGE_URL und HUE_APP_KEY müssen im Worker gesetzt sein')
if not base.startswith(('http://','https://')): base='https://'+base
return base,key,truthy(os.getenv('HUE_VERIFY_TLS','false'))
def req(method,path,body=None):
base,key,verify=config(); data=None
headers={'Accept':'application/json','hue-application-key':key}
if body is not None:
data=json.dumps(body).encode(); headers['Content-Type']='application/json'
ctx=None
if base.startswith('https://') and not verify: ctx=ssl._create_unverified_context()
r=Request(base+path,data=data,headers=headers,method=method)
try:
with urlopen(r,timeout=6,context=ctx) as resp:
raw=resp.read(); return json.loads(raw.decode()) if raw else {}
except HTTPError as e:
detail=e.read().decode(errors='replace')[:1000]; raise RuntimeError(f'Hue HTTP {e.code}: {detail}')
except URLError as e: raise RuntimeError(f'Hue nicht erreichbar: {e.reason}')
def data(path):
raw=req('GET',path); return raw.get('data',[]) if isinstance(raw,dict) else []
def n(v):
if isinstance(v,dict): return v.get('name') or v.get('value') or ''
return v or ''
def main(inv):
a=inv.get('action'); x=inv.get('input') or {}
if a=='list_lights':
out=[]
for v in data('/clip/v2/resource/light'):
out.append({'id':v.get('id',''),'name':n(v.get('metadata')),'on':(v.get('on') or {}).get('on'), 'brightness':(v.get('dimming') or {}).get('brightness'),'owner_id':(v.get('owner') or {}).get('rid','')})
return ok({'lights':out,'count':len(out)},'Hue-Lampen geladen.')
if a=='list_rooms':
out=[]
for v in data('/clip/v2/resource/room'):
gid=''
for s in v.get('services') or []:
if s.get('rtype')=='grouped_light': gid=s.get('rid',''); break
out.append({'id':v.get('id',''),'name':n(v.get('metadata')),'grouped_light_id':gid})
return ok({'rooms':out,'count':len(out)},'Hue-Räume geladen.')
if a=='list_scenes':
out=[]
for v in data('/clip/v2/resource/scene'):
out.append({'id':v.get('id',''),'name':n(v.get('metadata')),'group_id':(v.get('group') or {}).get('rid','')})
return ok({'scenes':out,'count':len(out)},'Hue-Szenen geladen.')
if a in ('set_light','set_room'):
ident=x['light_id'] if a=='set_light' else x['grouped_light_id']
body={}
if 'on' in x: body['on']={'on':bool(x['on'])}
if 'brightness' in x: body['dimming']={'brightness':max(0.0,min(100.0,float(x['brightness'])))}
if a=='set_light' and 'x' in x and 'y' in x: body['color']={'xy':{'x':float(x['x']),'y':float(x['y'])}}
if not body: return fail('INVALID_INPUT','Mindestens on, brightness oder x/y angeben')
typ='light' if a=='set_light' else 'grouped_light'
req('PUT',f'/clip/v2/resource/{typ}/{ident}',body)
return ok({'id':ident,'changed':True},'Hue-Zustand geändert.',True)
if a=='activate_scene':
ident=x['scene_id']; req('PUT',f'/clip/v2/resource/scene/{ident}',{'recall':{'action':'active'}})
return ok({'id':ident,'activated':True},'Hue-Szene aktiviert.',True)
return fail('UNKNOWN_ACTION',f'Unbekannte Aktion: {a}')
try:
inv=json.load(sys.stdin); res=main(inv)
except Exception as e: res=fail('HUE_ERROR',str(e))
json.dump(res,sys.stdout,ensure_ascii=False)