72 lines
3.9 KiB
Python
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)
|