Update auf Skills

This commit is contained in:
2026-08-29 17:02:35 +02:00
parent d6a50d8a0d
commit 7aec6c39a1
60 changed files with 9937 additions and 50 deletions
+62
View File
@@ -0,0 +1,62 @@
import json, os, re, subprocess, sys
from pathlib import Path
PROTO='jarvis.skill.invoke.v1'
NAMES=('compose.yaml','compose.yml','docker-compose.yaml','docker-compose.yml')
def fail(c,m): return {'protocol':PROTO,'success':False,'error':{'code':c,'message':m}}
def ok(d,m='OK',mut=False): return {'protocol':PROTO,'success':True,'data':d,'message':m,'mutated':mut}
def root():
p=Path(os.getenv('DOCKGE_STACKS_DIR','/opt/stacks')).resolve()
if not p.exists() or not p.is_dir(): raise RuntimeError(f'Dockge-Stacks-Verzeichnis fehlt: {p}')
return p
def stack_dir(name):
if not re.fullmatch(r'[A-Za-z0-9_.-]+',name or ''): raise RuntimeError('Ungültiger Stack-Name')
p=(root()/name).resolve(); r=root()
if p.parent!=r or not p.is_dir(): raise RuntimeError('Stack nicht gefunden')
if not any((p/n).is_file() for n in NAMES): raise RuntimeError('Keine Compose-Datei im Stack gefunden')
return p
def allowed(name):
vals=[x.strip() for x in os.getenv('DOCKGE_ALLOWED_STACKS','').split(',') if x.strip()]
return '*' in vals or name in vals
def require_allowed(name):
if not allowed(name): raise RuntimeError('Mutation verweigert: Stack ist nicht in DOCKGE_ALLOWED_STACKS freigegeben')
def compose_cmd():
# Prefer modern plugin; fall back to legacy docker-compose.
try:
subprocess.run(['docker','compose','version'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,check=True,timeout=5)
return ['docker','compose']
except Exception:
return ['docker-compose']
def run(name,args,timeout=90):
p=stack_dir(name); cmd=compose_cmd()+args
cp=subprocess.run(cmd,cwd=p,text=True,capture_output=True,timeout=timeout)
if cp.returncode!=0: raise RuntimeError((cp.stderr or cp.stdout or f'Exit {cp.returncode}')[-1800:])
return (cp.stdout or '').strip()
def main(inv):
a=inv.get('action'); x=inv.get('input') or {}
if a=='list_stacks':
out=[]
for p in sorted(root().iterdir()):
if not p.is_dir(): continue
cf=next((n for n in NAMES if (p/n).is_file()),None)
if cf: out.append({'name':p.name,'compose_file':cf,'mutation_allowed':allowed(p.name)})
return ok({'stacks':out,'count':len(out)},'Dockge-Stacks geladen.')
name=x['stack']
if a=='stack_status':
raw=run(name,['ps','--format','json'],30); items=[]
for line in raw.splitlines():
try:
v=json.loads(line)
items.extend(v if isinstance(v,list) else [v])
except Exception: pass
return ok({'stack':name,'services':items,'count':len(items)},'Stack-Status geladen.')
require_allowed(name)
if a=='start_stack': run(name,['up','-d','--remove-orphans']); return ok({'stack':name,'action':'start'},'Stack gestartet.',True)
if a=='stop_stack': run(name,['stop']); return ok({'stack':name,'action':'stop'},'Stack gestoppt.',True)
if a=='restart_stack': run(name,['restart']); return ok({'stack':name,'action':'restart'},'Stack neu gestartet.',True)
if a=='update_stack':
run(name,['pull'],120); run(name,['up','-d','--remove-orphans'],120); return ok({'stack':name,'action':'update'},'Stack aktualisiert.',True)
return fail('UNKNOWN_ACTION',f'Unbekannte Aktion: {a}')
try: res=main(json.load(sys.stdin))
except subprocess.TimeoutExpired: res=fail('DOCKGE_TIMEOUT','Docker-Compose-Aktion hat das Zeitlimit überschritten')
except Exception as e: res=fail('DOCKGE_ERROR',str(e))
json.dump(res,sys.stdout,ensure_ascii=False)
+155
View File
@@ -0,0 +1,155 @@
{
"protocol": "jarvis.skill.v1",
"id": "dockge.compose",
"name": "Dockge Stack Control",
"version": "1.0.0",
"description": "Steuert Dockge-verwaltete Compose-Stacks direkt über deren Stack-Verzeichnis und Docker Compose. Nutzt bewusst nicht Dockges instabile interne Socket-API.",
"runtime": {
"type": "process",
"command": "python3",
"args": [
"main.py"
],
"timeout_ms": 120000,
"env_from": [
"DOCKGE_STACKS_DIR",
"DOCKGE_ALLOWED_STACKS"
]
},
"permissions": {
"system_exec": true
},
"actions": [
{
"name": "list_stacks",
"description": "Listet Dockge-Stack-Verzeichnisse und deren Compose-Dateien.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"stacks": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"stacks",
"count"
],
"additionalProperties": false
}
},
{
"name": "stack_status",
"description": "Liest den Docker-Compose-Status eines Dockge-Stacks.",
"input_schema": {
"type": "object",
"properties": {
"stack": {
"type": "string"
}
},
"required": [
"stack"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "start_stack",
"description": "Startet/deployed einen erlaubten Dockge-Stack mit docker compose up -d.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"stack": {
"type": "string"
}
},
"required": [
"stack"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "stop_stack",
"description": "Stoppt einen erlaubten Dockge-Stack.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"stack": {
"type": "string"
}
},
"required": [
"stack"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "restart_stack",
"description": "Startet einen erlaubten Dockge-Stack neu.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"stack": {
"type": "string"
}
},
"required": [
"stack"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "update_stack",
"description": "Pullt Images eines erlaubten Dockge-Stacks und deployed ihn anschließend neu.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"stack": {
"type": "string"
}
},
"required": [
"stack"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
import json, sys
req=json.load(sys.stdin)
text=str(req.get("input",{}).get("text",""))
json.dump({"protocol":"jarvis.skill.invoke.v1","success":True,"data":{"text":text.upper()},"message":"Text verarbeitet."},sys.stdout)
+31
View File
@@ -0,0 +1,31 @@
{
"protocol": "jarvis.skill.v1",
"id": "example.python.text",
"name": "Python Text Example",
"version": "1.0.0",
"description": "Beispiel für einen Remote-Python-Skill im Skill Mesh.",
"runtime": {
"type": "process",
"command": "python3",
"args": ["main.py"],
"timeout_ms": 5000
},
"permissions": {"system_exec": true},
"actions": [{
"name": "uppercase",
"description": "Wandelt Text in Großbuchstaben um.",
"triggers": ["großbuchstaben", "uppercase"],
"input_schema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
"additionalProperties": false
}
}]
}
+34
View File
@@ -0,0 +1,34 @@
import json, os, socket, ssl, sys, time
from urllib.request import Request, urlopen
from urllib.error import HTTPError
PROTO='jarvis.skill.invoke.v1'
def fail(c,m): return {'protocol':PROTO,'success':False,'error':{'code':c,'message':m}}
def ok(d,m='OK',mut=False): return {'protocol':PROTO,'success':True,'data':d,'message':m,'mutated':mut}
def main(inv):
a=inv.get('action'); x=inv.get('input') or {}
if a=='tcp_check':
t=time.perf_counter(); reachable=True
try:
with socket.create_connection((x['host'],int(x['port'])),timeout=float(x.get('timeout_seconds',3))): pass
except OSError: reachable=False
ms=(time.perf_counter()-t)*1000; return ok({'reachable':reachable,'latency_ms':round(ms,2)},'TCP-Prüfung abgeschlossen.')
if a=='http_check':
t=time.perf_counter(); status=0; reachable=False; verify=bool(x.get('verify_tls',True)); ctx=None
if x['url'].startswith('https://') and not verify: ctx=ssl._create_unverified_context()
try:
with urlopen(Request(x['url'],headers={'User-Agent':'JARVIS-Skill/1'}),timeout=float(x.get('timeout_seconds',5)),context=ctx) as r: status=int(r.status); reachable=True
except HTTPError as e: status=int(e.code); reachable=True
except Exception: pass
ms=(time.perf_counter()-t)*1000; return ok({'reachable':reachable,'status':status,'latency_ms':round(ms,2)},'HTTP-Prüfung abgeschlossen.')
if a=='dns_lookup':
vals=sorted({i[4][0] for i in socket.getaddrinfo(x['host'],None)}); return ok({'addresses':vals,'count':len(vals)},'DNS aufgelöst.')
if a=='wake_on_lan':
mac=''.join(c for c in x['mac'] if c.isalnum())
if len(mac)!=12: return fail('INVALID_MAC','MAC-Adresse ungültig')
packet=b'\xff'*6+bytes.fromhex(mac)*16; broad=x.get('broadcast') or os.getenv('WOL_BROADCAST','255.255.255.255'); port=int(x.get('port') or os.getenv('WOL_PORT','9'))
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1); s.sendto(packet,(broad,port)); s.close()
return ok({'sent':True,'broadcast':broad,'port':port},'Wake-on-LAN gesendet.',True)
return fail('UNKNOWN_ACTION',f'Unbekannte Aktion: {a}')
try: res=main(json.load(sys.stdin))
except Exception as e: res=fail('NETWORK_TOOL_ERROR',str(e))
json.dump(res,sys.stdout,ensure_ascii=False)
+184
View File
@@ -0,0 +1,184 @@
{
"protocol": "jarvis.skill.v1",
"id": "home.network-tools",
"name": "Home Network Tools",
"version": "1.0.0",
"description": "Kleine Netzwerk-Werkzeuge für Erreichbarkeit, DNS und Wake-on-LAN.",
"runtime": {
"type": "process",
"command": "python3",
"args": [
"main.py"
],
"timeout_ms": 10000,
"env_from": [
"WOL_BROADCAST",
"WOL_PORT"
]
},
"permissions": {
"network": true,
"system_exec": true
},
"actions": [
{
"name": "tcp_check",
"description": "Prüft, ob ein TCP-Port erreichbar ist.",
"input_schema": {
"type": "object",
"properties": {
"host": {
"type": "string"
},
"port": {
"type": "integer"
},
"timeout_seconds": {
"type": "number"
}
},
"required": [
"host",
"port"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"reachable": {
"type": "boolean"
},
"latency_ms": {
"type": "number"
}
},
"required": [
"reachable",
"latency_ms"
],
"additionalProperties": false
}
},
{
"name": "http_check",
"description": "Prüft einen HTTP/HTTPS-Endpunkt und misst die Antwortzeit.",
"input_schema": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"timeout_seconds": {
"type": "number"
},
"verify_tls": {
"type": "boolean"
}
},
"required": [
"url"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"reachable": {
"type": "boolean"
},
"status": {
"type": "integer"
},
"latency_ms": {
"type": "number"
}
},
"required": [
"reachable",
"status",
"latency_ms"
],
"additionalProperties": false
}
},
{
"name": "dns_lookup",
"description": "Löst einen Hostnamen in IP-Adressen auf.",
"input_schema": {
"type": "object",
"properties": {
"host": {
"type": "string"
}
},
"required": [
"host"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"addresses": {
"type": "array",
"items": {
"type": "string"
}
},
"count": {
"type": "integer"
}
},
"required": [
"addresses",
"count"
],
"additionalProperties": false
}
},
{
"name": "wake_on_lan",
"description": "Sendet ein Wake-on-LAN Magic Packet an ein Gerät.",
"mutates": true,
"input_schema": {
"type": "object",
"properties": {
"mac": {
"type": "string"
},
"broadcast": {
"type": "string"
},
"port": {
"type": "integer"
}
},
"required": [
"mac"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"sent": {
"type": "boolean"
},
"broadcast": {
"type": "string"
},
"port": {
"type": "integer"
}
},
"required": [
"sent",
"broadcast",
"port"
],
"additionalProperties": false
}
}
]
}
+24
View File
@@ -0,0 +1,24 @@
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(c,m): return {'protocol':PROTO,'success':False,'error':{'code':c,'message':m}}
def ok(d,m='OK',mut=False): return {'protocol':PROTO,'success':True,'data':d,'message':m,'mutated':mut}
def main(inv):
x=inv.get('input') or {}; base=os.getenv('NTFY_BASE_URL','https://ntfy.sh').strip().rstrip('/'); topic=(x.get('topic') or os.getenv('NTFY_DEFAULT_TOPIC','')).strip()
if not topic: return fail('CONFIG_MISSING','NTFY_DEFAULT_TOPIC oder input.topic fehlt')
h={'Content-Type':'text/plain; charset=utf-8'}; token=os.getenv('NTFY_TOKEN','').strip()
if token: h['Authorization']='Bearer '+token
if x.get('title'): h['Title']=x['title']
if x.get('priority'): h['Priority']=x['priority']
if x.get('tags'): h['Tags']=x['tags']
ctx=ssl._create_unverified_context() if base.startswith('https://') and not truthy(os.getenv('NTFY_VERIFY_TLS','true')) else None
try:
with urlopen(Request(base+'/'+topic,data=x['message'].encode(),headers=h,method='POST'),timeout=6,context=ctx) as r: r.read()
except HTTPError as e: raise RuntimeError(f'ntfy HTTP {e.code}: {e.read().decode(errors="replace")[:500]}')
except URLError as e: raise RuntimeError(f'ntfy nicht erreichbar: {e.reason}')
return ok({'sent':True,'topic':topic},'Benachrichtigung gesendet.',True)
try: res=main(json.load(sys.stdin))
except Exception as e: res=fail('NTFY_ERROR',str(e))
json.dump(res,sys.stdout,ensure_ascii=False)
+72
View File
@@ -0,0 +1,72 @@
{
"protocol": "jarvis.skill.v1",
"id": "notify.ntfy",
"name": "ntfy Notifications",
"version": "1.0.0",
"description": "Sendet Benachrichtigungen an einen selbstgehosteten oder öffentlichen ntfy-Server.",
"runtime": {
"type": "process",
"command": "python3",
"args": [
"main.py"
],
"timeout_ms": 8000,
"env_from": [
"NTFY_BASE_URL",
"NTFY_TOKEN",
"NTFY_DEFAULT_TOPIC",
"NTFY_VERIFY_TLS"
]
},
"permissions": {
"network": true,
"system_exec": true
},
"actions": [
{
"name": "send",
"description": "Sendet eine ntfy-Benachrichtigung.",
"mutates": true,
"input_schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"title": {
"type": "string"
},
"topic": {
"type": "string"
},
"priority": {
"type": "string"
},
"tags": {
"type": "string"
}
},
"required": [
"message"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"sent": {
"type": "boolean"
},
"topic": {
"type": "string"
}
},
"required": [
"sent",
"topic"
],
"additionalProperties": false
}
}
]
}
+71
View File
@@ -0,0 +1,71 @@
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)
+232
View File
@@ -0,0 +1,232 @@
{
"protocol": "jarvis.skill.v1",
"id": "philips.hue",
"name": "Philips Hue",
"version": "1.0.0",
"description": "Lokale Philips-Hue-Bridge-Steuerung über die CLIP API v2.",
"runtime": {
"type": "process",
"command": "python3",
"args": [
"main.py"
],
"timeout_ms": 8000,
"env_from": [
"HUE_BRIDGE_URL",
"HUE_APP_KEY",
"HUE_VERIFY_TLS"
]
},
"permissions": {
"network": true,
"system_exec": true
},
"actions": [
{
"name": "list_lights",
"description": "Listet Hue-Lampen mit ID, Name, Ein/Aus und Helligkeit.",
"triggers": [
"hue lampen",
"lichter",
"beleuchtung"
],
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"lights": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"lights",
"count"
],
"additionalProperties": false
}
},
{
"name": "list_rooms",
"description": "Listet Hue-Räume und die zugehörige grouped_light-ID zur Raumsteuerung.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"rooms": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"rooms",
"count"
],
"additionalProperties": false
}
},
{
"name": "list_scenes",
"description": "Listet Hue-Szenen mit ID und Name.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"scenes": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"scenes",
"count"
],
"additionalProperties": false
}
},
{
"name": "set_light",
"description": "Schaltet eine Hue-Lampe oder setzt Helligkeit/Farbe.",
"mutates": true,
"input_schema": {
"type": "object",
"properties": {
"light_id": {
"type": "string"
},
"on": {
"type": "boolean"
},
"brightness": {
"type": "number"
},
"x": {
"type": "number"
},
"y": {
"type": "number"
}
},
"required": [
"light_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"changed": {
"type": "boolean"
}
},
"required": [
"id",
"changed"
],
"additionalProperties": false
}
},
{
"name": "set_room",
"description": "Schaltet einen Hue-Raum über seine grouped_light-ID oder setzt die Helligkeit.",
"mutates": true,
"input_schema": {
"type": "object",
"properties": {
"grouped_light_id": {
"type": "string"
},
"on": {
"type": "boolean"
},
"brightness": {
"type": "number"
}
},
"required": [
"grouped_light_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"changed": {
"type": "boolean"
}
},
"required": [
"id",
"changed"
],
"additionalProperties": false
}
},
{
"name": "activate_scene",
"description": "Aktiviert eine vorhandene Hue-Szene anhand ihrer ID.",
"mutates": true,
"input_schema": {
"type": "object",
"properties": {
"scene_id": {
"type": "string"
}
},
"required": [
"scene_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"activated": {
"type": "boolean"
}
},
"required": [
"id",
"activated"
],
"additionalProperties": false
}
}
]
}
+57
View File
@@ -0,0 +1,57 @@
import json, os, ssl, sys
from urllib.parse import urlencode
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(c,m): return {'protocol':PROTO,'success':False,'error':{'code':c,'message':m}}
def ok(d,m='OK',mut=False): return {'protocol':PROTO,'success':True,'data':d,'message':m,'mutated':mut}
def cfg():
b=os.getenv('PROXMOX_BASE_URL','').strip().rstrip('/'); tid=os.getenv('PROXMOX_TOKEN_ID','').strip(); sec=os.getenv('PROXMOX_TOKEN_SECRET','').strip()
if not b or not tid or not sec: raise RuntimeError('PROXMOX_BASE_URL, PROXMOX_TOKEN_ID und PROXMOX_TOKEN_SECRET müssen gesetzt sein')
if not b.startswith(('http://','https://')): b='https://'+b
return b,tid,sec,truthy(os.getenv('PROXMOX_VERIFY_TLS','false'))
def request(method,path,form=None,query=None):
b,tid,sec,verify=cfg(); url=b+'/api2/json'+path
if query: url+='?'+urlencode({k:v for k,v in query.items() if v not in (None,'')})
data=None; h={'Accept':'application/json','Authorization':f'PVEAPIToken={tid}={sec}'}
if form is not None: data=urlencode(form).encode(); h['Content-Type']='application/x-www-form-urlencoded'
elif method=='POST': data=b''
ctx=ssl._create_unverified_context() if url.startswith('https://') and not verify else None
try:
with urlopen(Request(url,data=data,headers=h,method=method),timeout=12,context=ctx) as r:
raw=r.read(); obj=json.loads(raw.decode()) if raw else {}; return obj.get('data') if isinstance(obj,dict) and 'data' in obj else obj
except HTTPError as e: raise RuntimeError(f'Proxmox HTTP {e.code}: {e.read().decode(errors="replace")[:1000]}')
except URLError as e: raise RuntimeError(f'Proxmox nicht erreichbar: {e.reason}')
def guest_path(x):
typ=x['guest_type'];
if typ not in ('qemu','lxc'): raise RuntimeError('guest_type muss qemu oder lxc sein')
return f"/nodes/{x['node']}/{typ}/{int(x['vmid'])}"
def main(inv):
a=inv.get('action'); x=inv.get('input') or {}
if a=='version': return ok(request('GET','/version'),'Proxmox API erreichbar.')
if a=='list_nodes':
vals=request('GET','/nodes') or []; out=[{'node':v.get('node',''),'status':v.get('status',''),'cpu':v.get('cpu'),'maxcpu':v.get('maxcpu'),'mem':v.get('mem'),'maxmem':v.get('maxmem'),'uptime':v.get('uptime')} for v in vals]
return ok({'nodes':out,'count':len(out)},'Proxmox-Nodes geladen.')
if a=='list_guests':
vals=request('GET','/cluster/resources',query={'type':'vm'}) or []; out=[]
for v in vals:
typ=v.get('type','')
if typ not in ('qemu','lxc'): continue
if x.get('node') and v.get('node')!=x['node']: continue
if x.get('status') and v.get('status')!=x['status']: continue
out.append({'vmid':v.get('vmid'),'name':v.get('name',''),'type':typ,'node':v.get('node',''),'status':v.get('status',''),'cpu':v.get('cpu'),'mem':v.get('mem'),'maxmem':v.get('maxmem'),'uptime':v.get('uptime')})
return ok({'guests':out,'count':len(out)},'Proxmox-Guests geladen.')
if a=='guest_status': return ok(request('GET',guest_path(x)+'/status/current') or {},'Guest-Status geladen.')
if a in ('start_guest','shutdown_guest','reboot_guest'):
action={'start_guest':'start','shutdown_guest':'shutdown','reboot_guest':'reboot'}[a]; data=request('POST',guest_path(x)+'/status/'+action)
return ok({'upid':data or '', 'action':action},f'Guest-Aktion {action} ausgelöst.',True)
if a=='snapshot_guest':
form={'snapname':x['name']}
if x.get('description'): form['description']=x['description']
if x.get('include_ram'): form['vmstate']='1'
data=request('POST',guest_path(x)+'/snapshot',form=form); return ok({'upid':data or '', 'snapshot':x['name']},'Snapshot angelegt.',True)
return fail('UNKNOWN_ACTION',f'Unbekannte Aktion: {a}')
try: res=main(json.load(sys.stdin))
except Exception as e: res=fail('PROXMOX_ERROR',str(e))
json.dump(res,sys.stdout,ensure_ascii=False)
+275
View File
@@ -0,0 +1,275 @@
{
"protocol": "jarvis.skill.v1",
"id": "proxmox.ve",
"name": "Proxmox VE",
"version": "1.0.0",
"description": "Proxmox-VE-Cluster- und Guest-Steuerung über die REST API.",
"runtime": {
"type": "process",
"command": "python3",
"args": [
"main.py"
],
"timeout_ms": 15000,
"env_from": [
"PROXMOX_BASE_URL",
"PROXMOX_TOKEN_ID",
"PROXMOX_TOKEN_SECRET",
"PROXMOX_VERIFY_TLS"
]
},
"permissions": {
"network": true,
"system_exec": true
},
"actions": [
{
"name": "version",
"description": "Prüft die Proxmox-API und liefert die Version.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "list_nodes",
"description": "Listet Proxmox-Nodes samt Status und Ressourcen.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"nodes",
"count"
],
"additionalProperties": false
}
},
{
"name": "list_guests",
"description": "Listet QEMU-VMs und LXC-Container clusterweit.",
"input_schema": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"status": {
"type": "string"
}
},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"guests": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"guests",
"count"
],
"additionalProperties": false
}
},
{
"name": "guest_status",
"description": "Liest den aktuellen Status einer QEMU-VM oder eines LXC-Containers.",
"input_schema": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"guest_type": {
"type": "string",
"enum": [
"qemu",
"lxc"
]
},
"vmid": {
"type": "integer"
}
},
"required": [
"node",
"guest_type",
"vmid"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "start_guest",
"description": "Startet eine QEMU-VM oder einen LXC-Container.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"guest_type": {
"type": "string",
"enum": [
"qemu",
"lxc"
]
},
"vmid": {
"type": "integer"
}
},
"required": [
"node",
"guest_type",
"vmid"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "shutdown_guest",
"description": "Fährt eine QEMU-VM oder einen LXC-Container sauber herunter.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"guest_type": {
"type": "string",
"enum": [
"qemu",
"lxc"
]
},
"vmid": {
"type": "integer"
}
},
"required": [
"node",
"guest_type",
"vmid"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "reboot_guest",
"description": "Startet eine QEMU-VM oder einen LXC-Container neu.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"guest_type": {
"type": "string",
"enum": [
"qemu",
"lxc"
]
},
"vmid": {
"type": "integer"
}
},
"required": [
"node",
"guest_type",
"vmid"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "snapshot_guest",
"description": "Erstellt einen Proxmox-Snapshot eines Guests.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"guest_type": {
"type": "string",
"enum": [
"qemu",
"lxc"
]
},
"vmid": {
"type": "integer"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"include_ram": {
"type": "boolean"
}
},
"required": [
"node",
"guest_type",
"vmid",
"name"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
}
]
}
+59
View File
@@ -0,0 +1,59 @@
import json, os, ssl, sys
from urllib.parse import urlencode
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(c,m): return {'protocol':PROTO,'success':False,'error':{'code':c,'message':m}}
def ok(d,m='OK',mut=False): return {'protocol':PROTO,'success':True,'data':d,'message':m,'mutated':mut}
def cfg():
b=os.getenv('UNIFI_NETWORK_URL','').strip().rstrip('/'); k=os.getenv('UNIFI_API_KEY','').strip()
if not b or not k: raise RuntimeError('UNIFI_NETWORK_URL und UNIFI_API_KEY müssen gesetzt sein')
if not b.startswith(('http://','https://')): b='https://'+b
return b,k,truthy(os.getenv('UNIFI_VERIFY_TLS','false'))
def request(method,path,body=None,query=None):
b,k,verify=cfg(); url=b+path
if query:
q={a:v for a,v in query.items() if v not in (None,'')};
if q: url+='?'+urlencode(q)
data=None; h={'Accept':'application/json','X-API-Key':k}
if body is not None: data=json.dumps(body).encode(); h['Content-Type']='application/json'
ctx=None
if url.startswith('https://') and not verify: ctx=ssl._create_unverified_context()
try:
with urlopen(Request(url,data=data,headers=h,method=method),timeout=8,context=ctx) as r:
raw=r.read(); return json.loads(raw.decode()) if raw else {}
except HTTPError as e: raise RuntimeError(f'UniFi Network HTTP {e.code}: {e.read().decode(errors="replace")[:1000]}')
except URLError as e: raise RuntimeError(f'UniFi Network nicht erreichbar: {e.reason}')
def items(raw):
if isinstance(raw,list): return raw
if isinstance(raw,dict):
d=raw.get('data'); return d if isinstance(d,list) else []
return []
def normalize_site(v): return {'id':v.get('id') or v.get('siteId') or '', 'name':v.get('name') or (v.get('meta') or {}).get('name') or '', 'description':v.get('description') or (v.get('meta') or {}).get('desc') or ''}
def normalize_device(v): return {'id':v.get('id',''),'name':v.get('name',''),'model':v.get('model',''),'state':v.get('state',''),'ip':v.get('ipAddress') or v.get('ip',''),'mac':v.get('macAddress') or v.get('mac',''),'firmware':v.get('firmwareVersion',''),'updatable':v.get('firmwareUpdatable')}
def normalize_client(v):
return {'id':v.get('id',''),'name':v.get('name') or v.get('hostname') or v.get('displayName') or '', 'type':v.get('type',''),'ip':v.get('ipAddress') or v.get('ip',''),'mac':v.get('macAddress') or v.get('mac',''),'connected_at':v.get('connectedAt'),'access':v.get('access')}
def main(inv):
a=inv.get('action'); x=inv.get('input') or {}
if a=='info': return ok(request('GET','/v1/info'),'UniFi Network API erreichbar.')
if a=='list_sites':
vals=[normalize_site(v) for v in items(request('GET','/v1/sites'))]; return ok({'sites':vals,'count':len(vals)},'UniFi-Sites geladen.')
if a in ('list_devices','list_clients'):
limit=max(1,min(200,int(x.get('limit',50)))); q={'limit':limit,'filter':x.get('filter','')}
path=f"/v1/sites/{x['site_id']}/"+('devices' if a=='list_devices' else 'clients')
raw=items(request('GET',path,query=q)); vals=[(normalize_device(v) if a=='list_devices' else normalize_client(v)) for v in raw]
key='devices' if a=='list_devices' else 'clients'; return ok({key:vals,'count':len(vals)},'UniFi-Daten geladen.')
if a=='restart_device':
request('POST',f"/v1/sites/{x['site_id']}/devices/{x['device_id']}/actions",{'action':'RESTART'}); return ok({'accepted':True},'Geräte-Neustart ausgelöst.',True)
if a=='power_cycle_port':
request('POST',f"/v1/sites/{x['site_id']}/devices/{x['device_id']}/interfaces/ports/{int(x['port_idx'])}/actions",{'action':'POWER_CYCLE'}); return ok({'accepted':True},'PoE Power-Cycle ausgelöst.',True)
if a in ('authorize_guest','unauthorize_guest'):
body={'action':'AUTHORIZE_GUEST_ACCESS' if a=='authorize_guest' else 'UNAUTHORIZE_GUEST_ACCESS'}
if a=='authorize_guest' and x.get('time_limit_minutes'): body['timeLimitMinutes']=int(x['time_limit_minutes'])
raw=request('POST',f"/v1/sites/{x['site_id']}/clients/{x['client_id']}/actions",body)
return ok(raw if isinstance(raw,dict) else {'result':raw},'Gastzugang geändert.',True)
return fail('UNKNOWN_ACTION',f'Unbekannte Aktion: {a}')
try: res=main(json.load(sys.stdin))
except Exception as e: res=fail('UNIFI_NETWORK_ERROR',str(e))
json.dump(res,sys.stdout,ensure_ascii=False)
+273
View File
@@ -0,0 +1,273 @@
{
"protocol": "jarvis.skill.v1",
"id": "unifi.network",
"name": "UniFi Network",
"version": "1.0.0",
"description": "Lokale UniFi-Network-Integration über die offizielle Integration API.",
"runtime": {
"type": "process",
"command": "python3",
"args": [
"main.py"
],
"timeout_ms": 10000,
"env_from": [
"UNIFI_NETWORK_URL",
"UNIFI_API_KEY",
"UNIFI_VERIFY_TLS"
]
},
"permissions": {
"network": true,
"system_exec": true
},
"actions": [
{
"name": "info",
"description": "Prüft die UniFi-Network-API und liefert Versionsinformationen.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "list_sites",
"description": "Listet lokale UniFi-Network-Sites.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"sites": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"sites",
"count"
],
"additionalProperties": false
}
},
{
"name": "list_devices",
"description": "Listet adoptierte UniFi-Geräte einer Site.",
"input_schema": {
"type": "object",
"properties": {
"site_id": {
"type": "string"
},
"filter": {
"type": "string"
},
"limit": {
"type": "integer"
}
},
"required": [
"site_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"devices": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"devices",
"count"
],
"additionalProperties": false
}
},
{
"name": "list_clients",
"description": "Listet verbundene Clients einer UniFi-Site; kann zur Anwesenheitserkennung genutzt werden.",
"input_schema": {
"type": "object",
"properties": {
"site_id": {
"type": "string"
},
"filter": {
"type": "string"
},
"limit": {
"type": "integer"
}
},
"required": [
"site_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"clients": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"clients",
"count"
],
"additionalProperties": false
}
},
{
"name": "restart_device",
"description": "Startet ein adoptiertes UniFi-Gerät neu.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"site_id": {
"type": "string"
},
"device_id": {
"type": "string"
}
},
"required": [
"site_id",
"device_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"accepted": {
"type": "boolean"
}
},
"required": [
"accepted"
],
"additionalProperties": false
}
},
{
"name": "power_cycle_port",
"description": "Führt einen PoE Power-Cycle auf einem Switch-Port aus.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"site_id": {
"type": "string"
},
"device_id": {
"type": "string"
},
"port_idx": {
"type": "integer"
}
},
"required": [
"site_id",
"device_id",
"port_idx"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"accepted": {
"type": "boolean"
}
},
"required": [
"accepted"
],
"additionalProperties": false
}
},
{
"name": "authorize_guest",
"description": "Autorisiert einen UniFi-Gastclient optional zeitlich begrenzt.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"site_id": {
"type": "string"
},
"client_id": {
"type": "string"
},
"time_limit_minutes": {
"type": "integer"
}
},
"required": [
"site_id",
"client_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "unauthorize_guest",
"description": "Entzieht einem UniFi-Gastclient den Netzwerkzugang.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"site_id": {
"type": "string"
},
"client_id": {
"type": "string"
}
},
"required": [
"site_id",
"client_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
}
]
}
+50
View File
@@ -0,0 +1,50 @@
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(c,m): return {'protocol':PROTO,'success':False,'error':{'code':c,'message':m}}
def ok(d,m='OK',mut=False): return {'protocol':PROTO,'success':True,'data':d,'message':m,'mutated':mut}
def cfg():
b=os.getenv('UNIFI_PROTECT_URL','').strip().rstrip('/'); k=os.getenv('UNIFI_API_KEY','').strip()
if not b or not k: raise RuntimeError('UNIFI_PROTECT_URL und UNIFI_API_KEY müssen gesetzt sein')
if not b.startswith(('http://','https://')): b='https://'+b
return b,k,truthy(os.getenv('UNIFI_VERIFY_TLS','false'))
def request(method,path,body=None):
b,k,verify=cfg(); data=None; h={'Accept':'application/json','X-API-Key':k}
if body is not None: data=json.dumps(body).encode(); h['Content-Type']='application/json'
ctx=ssl._create_unverified_context() if b.startswith('https://') and not verify else None
try:
with urlopen(Request(b+path,data=data,headers=h,method=method),timeout=8,context=ctx) as r:
raw=r.read(); return json.loads(raw.decode()) if raw else {}
except HTTPError as e: raise RuntimeError(f'UniFi Protect HTTP {e.code}: {e.read().decode(errors="replace")[:1000]}')
except URLError as e: raise RuntimeError(f'UniFi Protect nicht erreichbar: {e.reason}')
def name(v):
n=v.get('name','') if isinstance(v,dict) else ''
if isinstance(n,dict): return n.get('name') or n.get('value') or str(n)
return n or ''
def list_norm(path,kind):
raw=request('GET',path); vals=raw if isinstance(raw,list) else raw.get('data',[]) if isinstance(raw,dict) else []
out=[]
for v in vals:
base={'id':v.get('id',''),'name':name(v),'model':v.get('modelKey',''),'state':v.get('state',''),'mac':v.get('mac','')}
if kind=='camera':
f=v.get('featureFlags') or {}; base.update({'video_mode':v.get('videoMode',''),'hdr':v.get('hdrType',''),'has_mic':f.get('hasMic'),'has_speaker':f.get('hasSpeaker'),'smart_detect_types':f.get('smartDetectTypes') or []})
out.append(base)
return out
def main(inv):
a=inv.get('action'); x=inv.get('input') or {}
if a=='info': return ok(request('GET','/v1/meta/info'),'Protect API erreichbar.')
if a in ('list_cameras','list_sensors','list_lights'):
typ={'list_cameras':('cameras','camera'),'list_sensors':('sensors','sensor'),'list_lights':('lights','light')}[a]
vals=list_norm('/v1/'+typ[0],typ[1]); return ok({typ[0]:vals,'count':len(vals)},'Protect-Geräte geladen.')
if a=='get_camera':
raw=request('GET',f"/v1/cameras/{x['camera_id']}"); return ok(raw if isinstance(raw,dict) else {'camera':raw},'Kamera geladen.')
if a=='ptz_goto': request('POST',f"/v1/cameras/{x['camera_id']}/ptz/goto/{int(x['slot'])}"); return ok({'accepted':True},'PTZ-Preset angefahren.',True)
if a=='ptz_patrol_start': request('POST',f"/v1/cameras/{x['camera_id']}/ptz/patrol/start/{int(x['slot'])}"); return ok({'accepted':True},'PTZ-Patrouille gestartet.',True)
if a=='ptz_patrol_stop': request('POST',f"/v1/cameras/{x['camera_id']}/ptz/patrol/stop"); return ok({'accepted':True},'PTZ-Patrouille gestoppt.',True)
if a=='trigger_alarm_webhook': request('POST',f"/v1/alarm-manager/webhook/{x['trigger_id']}"); return ok({'accepted':True},'Protect-Alarm-Webhook ausgelöst.',True)
return fail('UNKNOWN_ACTION',f'Unbekannte Aktion: {a}')
try: res=main(json.load(sys.stdin))
except Exception as e: res=fail('UNIFI_PROTECT_ERROR',str(e))
json.dump(res,sys.stdout,ensure_ascii=False)
+266
View File
@@ -0,0 +1,266 @@
{
"protocol": "jarvis.skill.v1",
"id": "unifi.protect",
"name": "UniFi Protect",
"version": "1.0.0",
"description": "Lokale UniFi-Protect-Integration über die offizielle Integration API.",
"runtime": {
"type": "process",
"command": "python3",
"args": [
"main.py"
],
"timeout_ms": 10000,
"env_from": [
"UNIFI_PROTECT_URL",
"UNIFI_API_KEY",
"UNIFI_VERIFY_TLS"
]
},
"permissions": {
"network": true,
"system_exec": true
},
"actions": [
{
"name": "info",
"description": "Prüft die Protect-API und liefert Versionsinformationen.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "list_cameras",
"description": "Listet Protect-Kameras mit Status und Fähigkeiten.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"cameras": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"cameras",
"count"
],
"additionalProperties": false
}
},
{
"name": "get_camera",
"description": "Liest Details einer Protect-Kamera.",
"input_schema": {
"type": "object",
"properties": {
"camera_id": {
"type": "string"
}
},
"required": [
"camera_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object"
}
},
{
"name": "list_sensors",
"description": "Listet Protect-Sensoren.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"sensors": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"sensors",
"count"
],
"additionalProperties": false
}
},
{
"name": "list_lights",
"description": "Listet Protect-Lights/Floodlights.",
"input_schema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"lights": {
"type": "array",
"items": {
"type": "object"
}
},
"count": {
"type": "integer"
}
},
"required": [
"lights",
"count"
],
"additionalProperties": false
}
},
{
"name": "ptz_goto",
"description": "Fährt eine PTZ-Kamera auf ein vorhandenes Preset.",
"mutates": true,
"input_schema": {
"type": "object",
"properties": {
"camera_id": {
"type": "string"
},
"slot": {
"type": "integer"
}
},
"required": [
"camera_id",
"slot"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"accepted": {
"type": "boolean"
}
},
"required": [
"accepted"
],
"additionalProperties": false
}
},
{
"name": "ptz_patrol_start",
"description": "Startet eine konfigurierte PTZ-Patrouille.",
"mutates": true,
"input_schema": {
"type": "object",
"properties": {
"camera_id": {
"type": "string"
},
"slot": {
"type": "integer"
}
},
"required": [
"camera_id",
"slot"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"accepted": {
"type": "boolean"
}
},
"required": [
"accepted"
],
"additionalProperties": false
}
},
{
"name": "ptz_patrol_stop",
"description": "Stoppt die aktive PTZ-Patrouille.",
"mutates": true,
"input_schema": {
"type": "object",
"properties": {
"camera_id": {
"type": "string"
}
},
"required": [
"camera_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"accepted": {
"type": "boolean"
}
},
"required": [
"accepted"
],
"additionalProperties": false
}
},
{
"name": "trigger_alarm_webhook",
"description": "Triggert einen in Protect konfigurierten Alarm-Manager-Webhook.",
"mutates": true,
"requires_confirmation": true,
"input_schema": {
"type": "object",
"properties": {
"trigger_id": {
"type": "string"
}
},
"required": [
"trigger_id"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"accepted": {
"type": "boolean"
}
},
"required": [
"accepted"
],
"additionalProperties": false
}
}
]
}