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)