Compare commits

2 Commits

15 changed files with 275 additions and 1289 deletions
-151
View File
@@ -1,151 +0,0 @@
#Requires -RunAsAdministrator
<#
.SYNOPSIS
JARVIS Agent installer for Windows.
.DESCRIPTION
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
Requires: PowerShell 5.1+, internet access, and Administrator rights.
.EXAMPLE
# Interactive install (prompts for registration key):
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
# Silent install with key:
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
#>
$ErrorActionPreference = 'Stop'
$JARVIS_URL = 'https://jarvis.orbishosting.com'
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
$SERVICE_NAME = 'JARVISAgent'
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
$CONFIG_FILE = "$INSTALL_DIR\config.json"
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
function Write-OK { param($msg) Write-Host " OK: $msg" -ForegroundColor Green }
function Write-Fail { param($msg) Write-Host " ERROR: $msg" -ForegroundColor Red; exit 1 }
Write-Host "`n========================================" -ForegroundColor Yellow
Write-Host " JARVIS Agent Installer for Windows" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Yellow
# ── Stop existing service if running ─────────────────────────────────────────
$existing = Get-Service -Name $SERVICE_NAME -ErrorAction SilentlyContinue
if ($existing) {
Write-Step "Stopping existing JARVIS Agent service..."
if ($existing.Status -eq 'Running') {
Stop-Service -Name $SERVICE_NAME -Force
Start-Sleep 2
}
try {
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
} catch {}
Write-OK "Existing service removed."
}
# ── Check / install Python ────────────────────────────────────────────────────
Write-Step "Checking Python..."
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) {
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
}
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
}
$pyVersion = & python --version 2>&1
Write-OK $pyVersion
# ── Install pywin32 ───────────────────────────────────────────────────────────
Write-Step "Checking pywin32..."
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
if ($checkWin32 -ne 'ok') {
Write-Host " Installing pywin32..." -ForegroundColor Yellow
& python -m pip install --quiet pywin32
& python -m pywin32_postinstall -install 2>$null
Write-OK "pywin32 installed."
} else {
Write-OK "pywin32 already installed."
}
# ── Create install dir ────────────────────────────────────────────────────────
Write-Step "Creating install directory..."
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
Write-OK $INSTALL_DIR
# ── Download agent script ─────────────────────────────────────────────────────
Write-Step "Downloading JARVIS agent..."
try {
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
Write-OK "Agent downloaded to $AGENT_SCRIPT"
} catch {
Write-Fail "Failed to download agent: $_"
}
# ── Get registration key ──────────────────────────────────────────────────────
$regKey = $env:JARVIS_REG_KEY
if (-not $regKey -and (Test-Path $CONFIG_FILE)) {
$existingCfg = Get-Content $CONFIG_FILE | ConvertFrom-Json
$regKey = $existingCfg.registration_key
if ($regKey) { Write-OK "Using existing registration key from config." }
}
if (-not $regKey) {
$regKey = Read-Host "`n Enter JARVIS registration key"
if (-not $regKey) { Write-Fail "Registration key required." }
}
# ── Get hostname ──────────────────────────────────────────────────────────────
$hostname = $env:COMPUTERNAME
$customHostname = $env:JARVIS_HOSTNAME
if ($customHostname) { $hostname = $customHostname }
# ── Write config ──────────────────────────────────────────────────────────────
Write-Step "Writing config..."
$cfg = @{
jarvis_url = $JARVIS_URL
registration_key = $regKey
hostname = $hostname
agent_type = 'windows'
ssl_verify = $true
poll_interval = 30
heartbeat_every = 10
update_check_hours = 24
watch_services = @('WinDefend', 'Spooler', 'wuauserv')
} | ConvertTo-Json -Depth 5
$cfg | Out-File -FilePath $CONFIG_FILE -Encoding utf8
Write-OK "Config written to $CONFIG_FILE"
# ── Install Windows Service ───────────────────────────────────────────────────
Write-Step "Installing Windows service..."
$pyPath = (Get-Command python).Source
& $pyPath "$AGENT_SCRIPT" --startup auto install
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
Write-OK "Service '$SERVICE_NAME' installed."
# ── Start service ─────────────────────────────────────────────────────────────
Write-Step "Starting service..."
Start-Service -Name $SERVICE_NAME
Start-Sleep 3
$svc = Get-Service -Name $SERVICE_NAME
if ($svc.Status -ne 'Running') { Write-Fail "Service failed to start. Check C:\ProgramData\jarvis-agent\jarvis-agent.log" }
Write-OK "Service is running."
# ── Test connectivity ─────────────────────────────────────────────────────────
Write-Step "Testing JARVIS connection..."
try {
$ping = Invoke-RestMethod -Uri "$JARVIS_URL/api/ping" -TimeoutSec 10
Write-OK "JARVIS is online: $($ping.codename)"
} catch {
Write-Host " WARNING: Could not reach JARVIS at $JARVIS_URL - check connectivity." -ForegroundColor Yellow
}
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " JARVIS Agent installed successfully!" -ForegroundColor Green
Write-Host " Hostname: $hostname" -ForegroundColor Green
Write-Host " Service: $SERVICE_NAME (auto-start at boot)" -ForegroundColor Green
Write-Host " Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log" -ForegroundColor Green
Write-Host "========================================`n" -ForegroundColor Green
-235
View File
@@ -1,235 +0,0 @@
#!/usr/bin/env python3
"""
JARVIS HA Poller — pulls entity states from Home Assistant REST API
and pushes them to JARVIS as a homeassistant-type agent.
Runs on VM211 as a systemd service (jarvis-ha-poller).
Config: /etc/jarvis-agent/ha-poller.json
"""
import json
import os
import socket
import sys
import time
import urllib.request
import urllib.error
import ssl
from datetime import datetime, timezone
from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/ha-poller.json"
STATE_PATH = "/var/lib/jarvis-agent/ha-poller-state.json"
AGENT_VERSION = "1.0"
AGENT_ID = "homeassistant_ha"
HOSTNAME = "homeassistant"
# Domains to skip — don't send to JARVIS (saves DB space, keeps UI clean)
SKIP_DOMAINS = {
'sensor', 'binary_sensor', 'button', 'update', 'select', 'number',
'device_tracker', 'event', 'image', 'person', 'zone', 'tts',
'conversation', 'assist_satellite', 'input_button', 'media_player',
'scene', 'water_heater', 'alarm_control_panel', 'automation',
'script', 'calendar', 'notify', 'weather', 'sun', 'persistent_notification',
'tag', 'system_health', 'timer', 'counter',
'camera', 'siren', 'remote', 'todo', 'lawn_mower',
}
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] {msg}", flush=True)
def load_config() -> dict:
if not os.path.exists(CONFIG_PATH):
print(f"[ERROR] Config not found at {CONFIG_PATH}", flush=True)
sys.exit(1)
with open(CONFIG_PATH) as f:
return json.load(f)
def load_state() -> dict:
if os.path.exists(STATE_PATH):
with open(STATE_PATH) as f:
return json.load(f)
return {}
def save_state(state: dict):
Path(STATE_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(STATE_PATH, "w") as f:
json.dump(state, f, indent=2)
def _ssl_ctx(verify: bool):
if not verify:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
return None
def jarvis_post(url: str, payload: dict, headers: dict, ssl_verify: bool, timeout: int = 15) -> dict:
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"error": str(e)}
def ha_get(url: str, token: str, timeout: int = 15) -> dict | list | None:
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
except Exception as e:
log(f"HA API error: {e}")
return None
def register(cfg: dict, state: dict) -> str:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
reg_key = cfg["registration_key"]
log(f"Registering HA poller with JARVIS at {jarvis_url}...")
result = jarvis_post(
f"{jarvis_url}/api/agent/register",
{
"hostname": HOSTNAME,
"version": AGENT_VERSION,
"agent_type": "homeassistant",
"ip_address": cfg.get("ha_url", "").split("//")[-1].split(":")[0],
"capabilities": ["ha_entities", "ha_state"],
"agent_id": AGENT_ID,
},
{"X-Registration-Key": reg_key},
ssl_verify,
)
if "error" in result:
log(f"Registration failed: {result['error']}")
return ""
api_key = result.get("api_key", "")
if api_key:
state["api_key"] = api_key
state["agent_id"] = AGENT_ID
save_state(state)
log(f"Registered. agent_id={AGENT_ID}")
return api_key
def push_entities(cfg: dict, api_key: str, entities: list) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
headers = {"X-Agent-Key": api_key}
# Send in batches of 200
batch_size = 200
total = len(entities)
ok = True
for i in range(0, total, batch_size):
batch = entities[i:i+batch_size]
result = jarvis_post(
f"{jarvis_url}/api/agent/ha_state",
{"entities": batch},
headers,
ssl_verify,
)
if "error" in result:
log(f"Push batch {i//batch_size+1} failed: {result['error']}")
ok = False
return ok
def heartbeat(cfg: dict, api_key: str) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
result = jarvis_post(
f"{jarvis_url}/api/agent/heartbeat",
{"version": AGENT_VERSION},
{"X-Agent-Key": api_key},
ssl_verify,
timeout=10,
)
return "error" not in result
def fetch_ha_states(cfg: dict) -> list:
ha_url = cfg["ha_url"].rstrip("/")
token = cfg["ha_token"]
states = ha_get(f"{ha_url}/api/states", token)
if not states or not isinstance(states, list):
return []
entities = []
for s in states:
entity_id = s.get("entity_id", "")
domain = entity_id.split(".")[0] if "." in entity_id else ""
if domain in SKIP_DOMAINS:
continue
attrs = s.get("attributes", {})
# Convert ISO 8601 (e.g. "2026-06-28T21:26:01.922366+00:00") to MySQL datetime
lc = s.get("last_changed", "")
try:
dt = datetime.fromisoformat(lc.replace("Z", "+00:00"))
lc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
lc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
entities.append({
"entity_id": entity_id,
"name": attrs.get("friendly_name") or entity_id,
"state": s.get("state", ""),
"attributes": attrs,
"last_changed": lc,
})
return entities
def main():
cfg = load_config()
state = load_state()
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
api_key = state.get("api_key", "")
while not api_key:
api_key = register(cfg, state)
if not api_key:
log("Could not register. Retrying in 60s...")
time.sleep(60)
headers = {"X-Agent-Key": api_key}
last_push = 0
log(f"HA Poller v{AGENT_VERSION} running. Polling HA every {poll_interval}s, heartbeat every {heartbeat_every}s.")
while True:
now = time.time()
# Heartbeat
if not heartbeat(cfg, api_key):
log("Heartbeat failed (401?) — re-registering...")
state.clear()
save_state(state)
api_key = register(cfg, state)
if not api_key:
time.sleep(60)
continue
# Push entity states every poll_interval
if now - last_push >= poll_interval:
entities = fetch_ha_states(cfg)
if entities:
ok = push_entities(cfg, api_key, entities)
if ok:
log(f"Pushed {len(entities)} HA entities to JARVIS.")
last_push = now
else:
log("No HA entities fetched (HA down or token invalid?)")
last_push = now
time.sleep(heartbeat_every)
if __name__ == "__main__":
main()
-67
View File
@@ -1,67 +0,0 @@
#!/bin/bash
# JARVIS Network Scanner — runs on PVE1, pushes nmap results to JARVIS
# Cron: */3 * * * * /usr/local/bin/jarvis-netscan.sh >/dev/null 2>&1
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST="jarvis.orbishosting.com"
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
if [ -z "$REG_KEY" ]; then
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
exit 1
fi
SUBNET="10.48.200.0/24"
TMPFILE=$(mktemp)
nmap -sn --send-ip "$SUBNET" 2>/dev/null > "$TMPFILE"
if [ ! -s "$TMPFILE" ]; then
echo "$(date): nmap produced no output" >&2
rm -f "$TMPFILE"
exit 1
fi
JSON=$(python3 - "$TMPFILE" <<'PYEOF'
import sys, re, json
with open(sys.argv[1]) as f:
data = f.read()
devices = []
cur = None
for line in data.splitlines():
line = line.strip()
m = re.match(r'Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)\)?', line)
if m:
if cur:
devices.append(cur)
hn = m.group(1) if m.group(1) and m.group(1) != m.group(2) else ''
cur = {'ip': m.group(2), 'hostname': hn, 'mac': '', 'vendor': ''}
elif cur:
m2 = re.match(r'MAC Address: ([0-9A-Fa-f:]{17}) \(([^)]+)\)', line)
if m2:
cur['mac'] = m2.group(1).lower()
cur['vendor'] = '' if m2.group(2) == 'Unknown' else m2.group(2)
if cur:
devices.append(cur)
print(json.dumps({'devices': devices}))
PYEOF
)
rm -f "$TMPFILE"
if [ -z "$JSON" ]; then
echo "$(date): JSON parse failed" >&2
exit 1
fi
RESPONSE=$(curl -sk --max-time 15 \
-X POST "$JARVIS_URL/api/netscan" \
-H "Host: $JARVIS_HOST" \
-H "Content-Type: application/json" \
-H "X-Registration-Key: $REG_KEY" \
-d "$JSON" 2>/dev/null)
echo "$(date): $RESPONSE"
-74
View File
@@ -1,74 +0,0 @@
#!/bin/bash
# JARVIS VoIP Phone Probe — runs every minute on PVE1
# Pings all Yealink phones + checks FusionPBX SIP registration (read-only)
# 200.3 is on an external FusionPBX — ping only, no SIP check
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST="jarvis.orbishosting.com"
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
if [ -z "$REG_KEY" ]; then
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
exit 1
fi
FUSION_HOST="134.209.72.226"
# IP|alias|extension(none=skip SIP check)|mac
PHONES=(
"10.48.200.2|Yealink — Myron Main (Ext 1000)|1000|80:5e:c0:35:04:77"
"10.48.200.3|Yealink — United Mirror & Glass (External SIP)|none|c4:fc:22:28:63:71"
"10.48.200.43|Yealink T48S — Tommy Main (Ext 1001)|1001|80:5e:0c:15:0c:4f"
"10.48.200.86|Yealink — Myron Vanguard WiFi (Offline During Work Hrs)|none|"
"10.48.200.65|Yealink — Myron Vanguard Work (Ext 1003)|1003|c4:fc:22:13:e1:89"
)
# Get SIP registrations from FusionPBX (read-only)
REG_OUTPUT=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
root@$FUSION_HOST "fs_cli -x 'show registrations'" 2>/dev/null || echo "")
# Collect results as TSV, delegate JSON building to python3 to avoid injection
RESULTS=""
for PHONE in "${PHONES[@]}"; do
IFS='|' read -r IP ALIAS EXT MAC <<< "$PHONE"
if ping -c 1 -W 2 "$IP" > /dev/null 2>&1; then
STATUS="online"
else
STATUS="offline"
fi
if [ "$EXT" = "none" ]; then
SIP="external"
elif [ -n "$REG_OUTPUT" ] && echo "$REG_OUTPUT" | grep -q "^${EXT},"; then
SIP="registered"
else
SIP="unregistered"
fi
RESULTS="${RESULTS}${IP}\t${ALIAS}\t${MAC}\t${STATUS}\t${SIP}\t${EXT}\n"
done
JSON=$(printf "%b" "$RESULTS" | python3 -c "
import sys, json
devices = []
for line in sys.stdin:
line = line.rstrip('\n')
if not line:
continue
parts = line.split('\t')
if len(parts) < 6:
continue
ip, alias, mac, status, sip, ext = parts[:6]
devices.append({
'ip': ip, 'alias': alias, 'mac': mac,
'vendor': 'Yealink', 'status': status,
'sip_status': sip, 'extension': ext,
})
print(json.dumps({'devices': devices}))
")
curl -sk --max-time 10 \
-X POST "$JARVIS_URL/api/netscan" \
-H "Host: $JARVIS_HOST" \
-H "Content-Type: application/json" \
-H "X-Registration-Key: $REG_KEY" \
-d "$JSON" > /dev/null 2>&1
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/env python3
"""
JARVIS Ping Probe — runs on PVE1 (10.48.200.90), which is on the LAN.
Pings devices that can't run the full agent, then calls JARVIS heartbeat
on their behalf so the dashboard shows live status.
"""
import json
import subprocess
import urllib.request
import urllib.error
import ssl
JARVIS_URL = "http://10.48.200.211"
HOST_HEADER = "jarvis.orbishosting.com"
# Devices to probe: agent_id → api_key
DEVICES = {
"fortigate_gw": "00103aea6fcbf837bc55e11b445a3620",
"yealink_t48s": "2bf8bd7ca8dd31c28fd16aa956e15f88",
"homeassistant_ha": "6f8077dee7a7b4af202bc80886f1223d",
}
# Map agent_id → IP (for ping)
IPS = {
"fortigate_gw": "10.48.200.1",
"yealink_t48s": "10.48.200.43",
"homeassistant_ha": "10.48.200.97",
}
def ping(ip: str) -> bool:
result = subprocess.run(
["ping", "-c", "1", "-W", "2", ip],
capture_output=True, timeout=5
)
return result.returncode == 0
def heartbeat(agent_id: str, api_key: str, alive: bool):
# If device is down we still send heartbeat so JARVIS updates last_seen
# and sets status based on the alive flag via the metric payload
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
payload = json.dumps({}).encode()
req = urllib.request.Request(
f"{JARVIS_URL}/api/agent/heartbeat",
data=payload, method="POST"
)
req.add_header("Content-Type", "application/json")
req.add_header("X-Agent-Key", api_key)
req.add_header("Host", HOST_HEADER)
try:
with urllib.request.urlopen(req, timeout=10, context=ctx):
pass
except Exception:
pass
def update_status(agent_id: str, api_key: str, status: str):
"""Push a minimal metric so JARVIS knows if device is up or down."""
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
payload = json.dumps({
"type": "system",
"data": {
"hostname": agent_id,
"cpu_percent": 0,
"ping_only": True,
"ping_status": status,
}
}).encode()
req = urllib.request.Request(
f"{JARVIS_URL}/api/agent/metrics",
data=payload, method="POST"
)
req.add_header("Content-Type", "application/json")
req.add_header("X-Agent-Key", api_key)
req.add_header("Host", HOST_HEADER)
try:
with urllib.request.urlopen(req, timeout=10, context=ctx):
pass
except Exception:
pass
def main():
for agent_id, api_key in DEVICES.items():
ip = IPS.get(agent_id, "")
alive = ping(ip) if ip else False
status = "online" if alive else "offline"
print(f"{agent_id} ({ip}): {status}", flush=True)
heartbeat(agent_id, api_key, alive)
update_status(agent_id, api_key, status)
if __name__ == "__main__":
main()
+1 -2
View File
@@ -111,8 +111,7 @@ switch ($agentAction) {
// ── HEARTBEAT ────────────────────────────────────────────────────────────
case 'heartbeat':
$hbStatus = in_array($data['status'] ?? '', ['online','offline']) ? $data['status'] : 'online';
update_agent_seen($agent['agent_id'], $hbStatus, trim($data['version'] ?? '') ?: null);
update_agent_seen($agent['agent_id'], 'online', trim($data['version'] ?? '') ?: null);
// Return any pending commands for this agent
$commands = JarvisDB::query(
+2 -2
View File
@@ -181,10 +181,10 @@ function collect_all(): array {
$results['sites'] = 'skipped (fresh)';
} else try {
$sites = [
"jarvis" => "http://127.0.0.1",
"jarvis" => "http://jarvis.orbishosting.com:1972",
'tomsjavajive' => 'https://tomsjavajive.com',
'epictravelexp'=> 'https://epictravelexpeditions.com',
'parkersling' => 'https://parkerslingshotrentals.com',
'parkerslingshotrentals' => 'https://parkerslingshotrentals.com',
'orbishosting' => 'https://orbishosting.com',
'orbisportal' => 'https://orbis.orbishosting.com',
'tomtomgames' => 'https://tomtomgames.com',
+1 -2
View File
@@ -83,8 +83,7 @@ if ($method === 'POST' && $action === 'service') {
// Serve entities from ha_entities table (real-time agent push data)
$skipDomains = ['sensor','binary_sensor','button','update','select','number',
'device_tracker','event','image','person','zone','tts','conversation',
'assist_satellite','input_button','media_player','scene','water_heater',
'alarm_control_panel','automation','script','calendar','notify','weather','camera','siren','remote','todo','lawn_mower'];
'assist_satellite','input_button','media_player','scene','water_heater'];
$skipKeywords = [
// HACS / system toggles
'pre_release','get_hacs','matter_server','zerotier','mariadb',
+13 -207
View File
@@ -1,9 +1,4 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: jarvis_db
-- ------------------------------------------------------
-- Server version 10.11.14-MariaDB-0ubuntu0.24.04.1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
@@ -15,11 +10,6 @@
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `agent_commands`
--
DROP TABLE IF EXISTS `agent_commands`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -38,11 +28,6 @@ CREATE TABLE `agent_commands` (
KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `agent_metrics`
--
DROP TABLE IF EXISTS `agent_metrics`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -55,13 +40,8 @@ CREATE TABLE `agent_metrics` (
PRIMARY KEY (`id`),
KEY `idx_agent_time` (`agent_id`,`recorded_at`),
KEY `idx_recorded` (`recorded_at`)
) ENGINE=InnoDB AUTO_INCREMENT=31422 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=28329 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `alerts`
--
DROP TABLE IF EXISTS `alerts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -78,13 +58,8 @@ CREATE TABLE `alerts` (
`auto_resolve` tinyint(1) DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_source_key` (`source_key`)
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `api_cache`
--
DROP TABLE IF EXISTS `api_cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -95,80 +70,6 @@ CREATE TABLE `api_cache` (
PRIMARY KEY (`cache_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `appointments`
--
DROP TABLE IF EXISTS `appointments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `appointments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`description` text DEFAULT NULL,
`category` varchar(64) DEFAULT 'personal',
`start_at` datetime NOT NULL,
`end_at` datetime DEFAULT NULL,
`location` varchar(255) DEFAULT NULL,
`all_day` tinyint(1) DEFAULT 0,
`reminder_min` int(11) DEFAULT 30,
`alerted` tinyint(1) DEFAULT 0,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_start` (`start_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `arc_jobs`
--
DROP TABLE IF EXISTS `arc_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `arc_jobs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_type` varchar(64) NOT NULL,
`payload` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`priority` int(11) DEFAULT 0,
`status` enum('queued','running','done','failed','cancelled') DEFAULT 'queued',
`result` longtext DEFAULT NULL,
`error` varchar(2000) DEFAULT NULL,
`created_by` varchar(128) DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`started_at` datetime DEFAULT NULL,
`completed_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `arc_status`
--
DROP TABLE IF EXISTS `arc_status`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `arc_status` (
`id` int(11) NOT NULL DEFAULT 1,
`version` varchar(20) DEFAULT NULL,
`started_at` datetime DEFAULT NULL,
`last_heartbeat` datetime DEFAULT NULL,
`active_jobs` int(11) DEFAULT 0,
`jobs_done` int(11) DEFAULT 0,
`jobs_failed` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `conversations`
--
DROP TABLE IF EXISTS `conversations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -182,13 +83,8 @@ CREATE TABLE `conversations` (
PRIMARY KEY (`id`),
KEY `idx_session` (`session_id`),
KEY `idx_created` (`created_at`)
) ENGINE=InnoDB AUTO_INCREMENT=335 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=325 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `ha_entities`
--
DROP TABLE IF EXISTS `ha_entities`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -206,13 +102,8 @@ CREATE TABLE `ha_entities` (
UNIQUE KEY `uk_agent_entity` (`agent_id`,`entity_id`),
KEY `idx_domain` (`domain`),
KEY `idx_updated` (`updated_at`)
) ENGINE=InnoDB AUTO_INCREMENT=77909 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_facts`
--
DROP TABLE IF EXISTS `kb_facts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -226,13 +117,8 @@ CREATE TABLE `kb_facts` (
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `unique_fact` (`category`,`fact_key`,`host`)
) ENGINE=InnoDB AUTO_INCREMENT=41478 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=26088 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_intents`
--
DROP TABLE IF EXISTS `kb_intents`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -247,13 +133,8 @@ CREATE TABLE `kb_intents` (
`active` tinyint(1) DEFAULT 1,
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_ollama_models`
--
DROP TABLE IF EXISTS `kb_ollama_models`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -266,13 +147,8 @@ CREATE TABLE `kb_ollama_models` (
`pulled_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `model_name` (`model_name`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_preferences`
--
DROP TABLE IF EXISTS `kb_preferences`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -283,13 +159,8 @@ CREATE TABLE `kb_preferences` (
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `pref_key` (`pref_key`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `known_commands`
--
DROP TABLE IF EXISTS `known_commands`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -302,11 +173,6 @@ CREATE TABLE `known_commands` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `metrics_history`
--
DROP TABLE IF EXISTS `metrics_history`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -318,13 +184,8 @@ CREATE TABLE `metrics_history` (
`recorded_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_metric_time` (`metric_name`,`recorded_at`)
) ENGINE=InnoDB AUTO_INCREMENT=34771 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=33415 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `network_devices`
--
DROP TABLE IF EXISTS `network_devices`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -340,13 +201,8 @@ CREATE TABLE `network_devices` (
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ip` (`ip`)
) ENGINE=InnoDB AUTO_INCREMENT=5556 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=409 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `registered_agents`
--
DROP TABLE IF EXISTS `registered_agents`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -354,11 +210,10 @@ CREATE TABLE `registered_agents` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`agent_id` varchar(128) NOT NULL,
`hostname` varchar(255) NOT NULL,
`agent_type` enum('linux','homeassistant','proxmox','windows','macos') NOT NULL DEFAULT 'linux',
`agent_type` enum('linux','homeassistant','proxmox') NOT NULL DEFAULT 'linux',
`ip_address` varchar(45) DEFAULT NULL,
`api_key` varchar(64) NOT NULL,
`capabilities` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`capabilities`)),
`version` varchar(32) DEFAULT NULL,
`last_seen` datetime DEFAULT NULL,
`status` enum('online','offline','unknown') NOT NULL DEFAULT 'unknown',
`created_at` datetime DEFAULT current_timestamp(),
@@ -366,56 +221,8 @@ CREATE TABLE `registered_agents` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_agent_id` (`agent_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tasks`
--
DROP TABLE IF EXISTS `tasks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tasks` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`notes` text DEFAULT NULL,
`category` varchar(64) DEFAULT 'personal',
`priority` enum('urgent','high','normal','low') DEFAULT 'normal',
`status` enum('pending','in_progress','done','cancelled') DEFAULT 'pending',
`due_date` date DEFAULT NULL,
`due_time` time DEFAULT NULL,
`completed_at` datetime DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_status_due` (`status`,`due_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `usage_patterns`
--
DROP TABLE IF EXISTS `usage_patterns`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `usage_patterns` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`intent_name` varchar(64) NOT NULL,
`hour` tinyint(2) NOT NULL,
`dow` tinyint(1) NOT NULL,
`hit_count` int(11) DEFAULT 1,
`last_seen` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_intent_time` (`intent_name`,`hour`,`dow`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
@@ -429,7 +236,7 @@ CREATE TABLE `users` (
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -441,4 +248,3 @@ CREATE TABLE `users` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2026-06-29 23:15:44
-70
View File
@@ -1,70 +0,0 @@
-- JARVIS KB Seed Data
-- Preferences
INSERT INTO kb_preferences (pref_key, pref_value) VALUES
('user_name', 'Myron'),
('user_title', 'Mr. Blair'),
('ai_model', 'llama3.1:8b'),
('timezone', 'America/Chicago')
ON DUPLICATE KEY UPDATE pref_value = VALUES(pref_value);
-- Intents: greeting, time, system, network, proxmox, ollama, tasks, HA
INSERT INTO kb_intents (intent_name, pattern, response_template, fact_category, action_type, priority, active) VALUES
-- Greetings
('greeting', '(?i)^(hello|hi|hey|good (morning|afternoon|evening)|what.?s up|howdy)\\b', 'Good {current_time}, {user_title}. All systems are online. How can I assist you?', 'system', 'response', 10, 1),
-- Time / date
('current_time', '(?i)\\b(what.?s the (time|current time)|what time is it|tell me the time)\\b', 'It is currently {current_time}, {user_title}.', NULL, 'response', 9, 1),
('current_date', '(?i)\\b(what.?s (today.?s date|the date)|what day is it|today.?s date)\\b', 'Today is {current_date}, {user_title}.', NULL, 'response', 9, 1),
-- System status
('system_status', '(?i)\\b(system (status|health)|how.?s (the system|everything)|jarvis status|all systems)\\b', 'JARVIS is fully operational, {user_title}. CPU: {cpu_usage}%, Memory: {mem_percent}% used ({mem_used_gb}GB / {mem_total_gb}GB). Disk: {disk_used} used of {disk_total}. Uptime: {uptime}. Network agents: {online_count}/{total_count} online.', 'system', 'response', 8, 1),
('cpu_status', '(?i)\\b(cpu|processor) (usage|load|status|percent|utilization)\\b', 'Current CPU usage is {cpu_usage}%, {user_title}. Load averages: {load_1m} (1m), {load_5m} (5m), {load_15m} (15m).', 'system', 'response', 8, 1),
('memory_status', '(?i)\\b(memory|ram|mem) (usage|status|free|used|available)\\b', 'Memory: {mem_used_gb}GB used of {mem_total_gb}GB ({mem_percent}% utilized), {user_title}. Free: {mem_free_gb}GB.', 'system', 'response', 8, 1),
('disk_status', '(?i)\\b(disk|storage|drive) (usage|space|status|free|used|available)\\b', 'Disk status: {disk_used} used of {disk_total} total, {disk_free} free, {user_title}.', 'system', 'response', 8, 1),
('uptime', '(?i)\\b(uptime|how long.*running|how long.*up|server uptime)\\b', 'JARVIS has been running for {uptime}, {user_title}.', 'system', 'response', 7, 1),
-- Network status
('network_status', '(?i)\\b(network (status|health|agents)|agents (online|status)|how many (agents|devices) (online|running))\\b', 'Network status: {online_count} of {total_count} agents are online, {user_title}.', 'network', 'response', 8, 1),
('network_scan', '(?i)\\b(run (a )?network scan|scan (the )?network|nmap scan|network devices)\\b', 'Initiating network scan, {user_title}.', NULL, 'action', 7, 1),
-- Proxmox
('proxmox_status', '(?i)\\b(proxmox (status|health)|vm (status|count|summary)|virtual machines|how many vms)\\b', 'Proxmox: {vm_running} of {vm_total} VMs/containers running, {user_title}. Host CPU: {pve_cpu_percent}%, Memory: {pve_mem_used_gb}GB / {pve_mem_total_gb}GB ({pve_mem_percent}%).', 'proxmox', 'response', 8, 1),
('vm_suggestions', '(?i)\\b(vm (resources|performance|usage)|check vms|resource usage)\\b', 'Checking VM resource usage, {user_title}.', 'proxmox', 'action', 7, 1),
-- Ollama / AI
('ollama_status', '(?i)\\b(ollama (status|health|models)|ai models|llm status|local (ai|models))\\b', 'Ollama is {status} with {model_count} model(s) available: {available_models}, {user_title}.', 'ollama', 'response', 7, 1),
-- Site health
('site_status', '(?i)\\b(site(s)? (status|health|up|down)|website status|are (the )?sites (up|down))\\b', 'Site health — jarvis: {jarvis}, orbishosting: {orbishosting}, tomtomgames: {tomtomgames}, tomsjavajive: {tomsjavajive}, parkerslingshotrentals: {parkersling}, epictravelexpeditions: {epictravelexp}, {user_title}.', 'sites', 'response', 7, 1),
-- Tasks / planner
('task_count', '(?i)\\b(how many tasks|pending tasks|task (count|summary)|my tasks)\\b', 'You have {pending_count} pending tasks and {overdue_count} overdue, {user_title}.', NULL, 'response', 7, 1),
('planner_briefing', '(?i)\\b((daily )?briefing|what.?s (on|happening) today|today.?s schedule|morning briefing)\\b', 'Fetching your daily briefing, {user_title}.', NULL, 'action', 8, 1),
-- Home Assistant
('ha_lights_on', '(?i)\\b(turn (on|off) (the |all )?lights?|lights? (on|off)|switch (on|off) (the )?lights?)\\b', 'Sending light command, {user_title}.', NULL, 'action', 8, 1),
('ha_scene', '(?i)\\b(activate (a |the )?scene|set (a |the )?scene|home scene)\\b', 'Activating home scene, {user_title}.', NULL, 'action', 7, 1),
-- Jellyfin
('jellyfin_now_playing', '(?i)\\b(what.?s (playing|on)|now playing|jellyfin.*playing|playing.*jellyfin)\\b', 'Checking Jellyfin now playing, {user_title}.', NULL, 'action', 7, 1),
('jellyfin_library', '(?i)\\b(jellyfin (library|media|shows?|movies?)|media library|show.*library)\\b', 'Fetching Jellyfin library, {user_title}.', NULL, 'action', 6, 1),
('jellyfin_pause', '(?i)\\b(pause (jellyfin|playback|media)|stop (playing|jellyfin))\\b', 'Pausing Jellyfin, {user_title}.', NULL, 'action', 7, 1),
-- DO server
('do_status', '(?i)\\b(do (server|status)|digital ocean (status|server)|vps status)\\b', 'Digital Ocean server is {do_status}, {user_title}.', 'do_server', 'response', 7, 1),
-- Focus / panels
('focus_mode', '(?i)\\b(focus (mode|on)|enable focus|concentration mode)\\b', 'Enabling focus mode, {user_title}.', NULL, 'action', 6, 1),
('show_panels', '(?i)\\b(show (all )?panels|expand (all|everything)|full view)\\b', 'Expanding all panels, {user_title}.', NULL, 'action', 6, 1),
-- Help
('help', '(?i)^(help|what can you do|commands|capabilities|what do you know)\\s*\\??$', 'I can help you with: system status, network status, VM/Proxmox status, Ollama AI models, site health, tasks and planner briefings, Jellyfin media, Home Assistant lights and devices, and general questions via Ollama. What would you like to know, {user_title}?', NULL, 'response', 5, 1)
ON DUPLICATE KEY UPDATE
pattern = VALUES(pattern),
response_template = VALUES(response_template),
active = 1;
SELECT COUNT(*) AS intents_seeded FROM kb_intents;
SELECT COUNT(*) AS prefs_seeded FROM kb_preferences;
+1 -3
View File
@@ -242,9 +242,7 @@ if ($action) {
$search = strtolower(trim($_GET['search'] ?? ''));
$skipDomains = ['sensor','binary_sensor','button','update','select','number',
'device_tracker','event','image','person','zone','tts','conversation',
'assist_satellite','input_button','media_player','scene','water_heater',
'alarm_control_panel','automation','script','calendar','notify',
'weather','camera','siren','remote','todo','lawn_mower'];
'assist_satellite','input_button'];
$skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone',
'_siren_on','_email_on','_manual_record','_infrared_',
'do_not_disturb','matter_server','zerotier','mariadb',
+253 -126
View File
@@ -1,151 +1,278 @@
#Requires -RunAsAdministrator
<#
.SYNOPSIS
JARVIS Agent installer for Windows.
# JARVIS Agent Installer — Windows (PowerShell)
# Registers the agent as a proper Windows Service (Win 8.1+, no open window required).
# Requires pywin32. Runs the service as LocalSystem.
#
# Run as Administrator:
# Set-ExecutionPolicy Bypass -Scope Process
# .\install-windows.ps1 -JarvisUrl https://jarvis.orbishosting.com -Key YOUR_KEY
#
# One-liner (PowerShell as Admin):
# irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
.DESCRIPTION
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
Requires: PowerShell 5.1+, internet access, and Administrator rights.
param(
[string]$JarvisUrl = "",
[string]$Key = "",
[string]$AgentName = ""
)
.EXAMPLE
# Interactive install (prompts for registration key):
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
# param() defaults don't apply when piped through iex — set here as fallback
if (-not $JarvisUrl) { $JarvisUrl = "https://jarvis.orbishosting.com" }
if (-not $Key) { $Key = "f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518" }
if (-not $AgentName) { $AgentName = $env:COMPUTERNAME.ToLower() }
# Silent install with key:
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
#>
$ErrorActionPreference = "Stop"
$InstallDir = "C:\ProgramData\jarvis-agent"
$AgentScript = "$InstallDir\jarvis-agent-windows.py"
$ConfigFile = "$InstallDir\config.json"
$ServiceName = "JARVISAgent"
$OldTaskName = "JARVIS-Agent" # legacy scheduled-task name
$ErrorActionPreference = 'Stop'
$JARVIS_URL = 'https://jarvis.orbishosting.com'
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
$SERVICE_NAME = 'JARVISAgent'
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
$CONFIG_FILE = "$INSTALL_DIR\config.json"
Write-Host ""
Write-Host " ====================================" -ForegroundColor Cyan
Write-Host " JARVIS Agent Installer v3.1 " -ForegroundColor Cyan
Write-Host " Windows Service Edition " -ForegroundColor Cyan
Write-Host " ====================================" -ForegroundColor Cyan
Write-Host ""
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
function Write-OK { param($msg) Write-Host " OK: $msg" -ForegroundColor Green }
function Write-Fail { param($msg) Write-Host " ERROR: $msg" -ForegroundColor Red; exit 1 }
Write-Host "`n========================================" -ForegroundColor Yellow
Write-Host " JARVIS Agent Installer for Windows" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Yellow
# ── Stop existing service if running ─────────────────────────────────────────
$existing = Get-Service -Name $SERVICE_NAME -ErrorAction SilentlyContinue
if ($existing) {
Write-Step "Stopping existing JARVIS Agent service..."
if ($existing.Status -eq 'Running') {
Stop-Service -Name $SERVICE_NAME -Force
Start-Sleep 2
# ── Require admin ──────────────────────────────────────────────────────────────
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Error "Run PowerShell as Administrator and try again."
}
# ── Prompt if not provided ─────────────────────────────────────────────────────
$JarvisUrl = $JarvisUrl.TrimEnd("/")
# ── Find or install Python 3 (system-wide so LocalSystem service can reach it) ─
Write-Host "[1/6] Checking for Python 3..." -ForegroundColor Cyan
$pythonPath = $null
# System-wide paths — accessible by LocalSystem service account
$systemPaths = @(
"C:\Program Files\Python313\python.exe",
"C:\Program Files\Python312\python.exe",
"C:\Program Files\Python311\python.exe",
"C:\Program Files\Python310\python.exe",
"C:\Program Files\Python39\python.exe",
"C:\Python313\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe"
)
function Install-PythonSystemWide {
# Try winget first (Win 10 1709+ / Win 11)
$wingetOk = $false
try {
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
$null = Get-Command winget -ErrorAction Stop
Write-Host " Using winget (system-wide)..." -NoNewline
winget install Python.Python.3.12 --silent --scope machine `
--accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { $wingetOk = $true; Write-Host " done." -ForegroundColor Green }
} catch {}
Write-OK "Existing service removed."
}
# ── Check / install Python ────────────────────────────────────────────────────
Write-Step "Checking Python..."
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) {
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
}
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
}
$pyVersion = & python --version 2>&1
Write-OK $pyVersion
# ── Install pywin32 ───────────────────────────────────────────────────────────
Write-Step "Checking pywin32..."
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
if ($checkWin32 -ne 'ok') {
Write-Host " Installing pywin32..." -ForegroundColor Yellow
& python -m pip install --quiet pywin32
& python -m pywin32_postinstall -install 2>$null
Write-OK "pywin32 installed."
} else {
Write-OK "pywin32 already installed."
}
# ── Create install dir ────────────────────────────────────────────────────────
Write-Step "Creating install directory..."
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
Write-OK $INSTALL_DIR
# ── Download agent script ─────────────────────────────────────────────────────
Write-Step "Downloading JARVIS agent..."
if (-not $wingetOk) {
# Direct download — works on Win 8.1 without winget
# Python 3.11 explicitly supports Win 8.1+
Write-Host " Downloading Python 3.11 (Win 8.1+ compatible)..." -NoNewline
$pyInstaller = "$env:TEMP\python-installer.exe"
$pyUrl = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe"
try {
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
Write-OK "Agent downloaded to $AGENT_SCRIPT"
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($pyUrl, $pyInstaller)
Write-Host " downloaded." -ForegroundColor Green
} catch {
Write-Fail "Failed to download agent: $_"
Write-Error "Could not download Python. Install from https://python.org choosing 'Install for all users', then re-run."
}
Write-Host " Installing system-wide (silent)..." -NoNewline
$proc = Start-Process -FilePath $pyInstaller `
-ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0" `
-Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Error "Python installer exited $($proc.ExitCode). Install manually from https://python.org then re-run."
}
Write-Host " done." -ForegroundColor Green
Remove-Item $pyInstaller -ErrorAction SilentlyContinue
}
# ── Get registration key ──────────────────────────────────────────────────────
$regKey = $env:JARVIS_REG_KEY
if (-not $regKey -and (Test-Path $CONFIG_FILE)) {
$existingCfg = Get-Content $CONFIG_FILE | ConvertFrom-Json
$regKey = $existingCfg.registration_key
if ($regKey) { Write-OK "Using existing registration key from config." }
}
if (-not $regKey) {
$regKey = Read-Host "`n Enter JARVIS registration key"
if (-not $regKey) { Write-Fail "Registration key required." }
# Refresh PATH after install
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User")
}
# ── Get hostname ──────────────────────────────────────────────────────────────
$hostname = $env:COMPUTERNAME
$customHostname = $env:JARVIS_HOSTNAME
if ($customHostname) { $hostname = $customHostname }
# ── Search for system-wide Python first ───────────────────────────────────────
foreach ($p in $systemPaths) {
if (Test-Path $p) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {}
}
}
# ── Write config ──────────────────────────────────────────────────────────────
Write-Step "Writing config..."
$cfg = @{
jarvis_url = $JARVIS_URL
registration_key = $regKey
hostname = $hostname
agent_type = 'windows'
# ── Fall back to PATH — but flag if it's per-user ─────────────────────────────
if (-not $pythonPath) {
foreach ($cmd in @("python", "python3", "py")) {
try {
$ver = & $cmd --version 2>&1
if ("$ver" -match "Python 3") {
$resolved = (Get-Command $cmd -ErrorAction SilentlyContinue)
if ($resolved) { $pythonPath = $resolved.Source; break }
}
} catch {}
}
}
# ── If Python is per-user (AppData), install system-wide so LocalSystem can use it ──
$needsSystemPython = $false
if ($pythonPath -and ($pythonPath -match "AppData")) {
Write-Host " Found per-user Python: $pythonPath" -ForegroundColor Yellow
Write-Host " LocalSystem service needs system-wide Python. Installing..." -ForegroundColor Yellow
$needsSystemPython = $true
} elseif (-not $pythonPath) {
Write-Host " Python 3 not found. Installing system-wide..." -ForegroundColor Yellow
$needsSystemPython = $true
}
if ($needsSystemPython) {
Install-PythonSystemWide
# Locate the newly installed system-wide Python
$pythonPath = $null
foreach ($p in $systemPaths) {
if (Test-Path $p) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {}
}
}
if (-not $pythonPath) {
Write-Error "System-wide Python not found after install. Open a new Admin PowerShell and re-run."
}
}
Write-Host " Python: $pythonPath" -ForegroundColor Green
# ── Install pywin32 (required for Windows service support) ────────────────────
Write-Host "[2/6] Installing pywin32..." -ForegroundColor Cyan
# pip install
$pipResult = & $pythonPath -m pip install --upgrade pywin32 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "pip install pywin32 failed (exit $LASTEXITCODE).`n$pipResult`nTry manually: $pythonPath -m pip install pywin32"
}
# postinstall registers service runner DLLs — non-fatal if it fails
try {
$postResult = & $pythonPath -c "import pywin32_postinstall; pywin32_postinstall.install()" 2>&1
Write-Host " pywin32 installed." -ForegroundColor Green
} catch {
Write-Host " pywin32 installed (postinstall skipped — service should still work)." -ForegroundColor Yellow
}
# ── Create install directory and download agent ────────────────────────────────
Write-Host "[3/6] Downloading agent..." -ForegroundColor Cyan
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "JARVIS-Installer/3.1")
$wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript)
Write-Host " Downloaded to $AgentScript" -ForegroundColor Green
} catch {
Write-Error "Download failed: $_"
}
# ── Write config ───────────────────────────────────────────────────────────────
Write-Host "[4/6] Writing config..." -ForegroundColor Cyan
$agentId = "${AgentName}_windows"
$config = [ordered]@{
jarvis_url = $JarvisUrl
host_header = ""
ssl_verify = $true
registration_key = $Key
agent_type = "windows"
hostname = $AgentName
agent_id = $agentId
poll_interval = 30
heartbeat_every = 10
update_check_hours = 24
watch_services = @('WinDefend', 'Spooler', 'wuauserv')
} | ConvertTo-Json -Depth 5
$cfg | Out-File -FilePath $CONFIG_FILE -Encoding utf8
Write-OK "Config written to $CONFIG_FILE"
watch_services = @("WinDefend", "Spooler")
} | ConvertTo-Json -Depth 3
# ── Install Windows Service ───────────────────────────────────────────────────
Write-Step "Installing Windows service..."
$pyPath = (Get-Command python).Source
& $pyPath "$AGENT_SCRIPT" --startup auto install
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
Write-OK "Service '$SERVICE_NAME' installed."
[System.IO.File]::WriteAllText($ConfigFile, $config, [System.Text.UTF8Encoding]::new($false))
Write-Host " Config: $ConfigFile" -ForegroundColor Green
# ── Start service ─────────────────────────────────────────────────────────────
Write-Step "Starting service..."
Start-Service -Name $SERVICE_NAME
Start-Sleep 3
$svc = Get-Service -Name $SERVICE_NAME
if ($svc.Status -ne 'Running') { Write-Fail "Service failed to start. Check C:\ProgramData\jarvis-agent\jarvis-agent.log" }
Write-OK "Service is running."
# ── Test connectivity ─────────────────────────────────────────────────────────
Write-Step "Testing JARVIS connection..."
# ── Remove legacy scheduled task if present ────────────────────────────────────
try {
$ping = Invoke-RestMethod -Uri "$JARVIS_URL/api/ping" -TimeoutSec 10
Write-OK "JARVIS is online: $($ping.codename)"
} catch {
Write-Host " WARNING: Could not reach JARVIS at $JARVIS_URL - check connectivity." -ForegroundColor Yellow
$oldTask = Get-ScheduledTask -TaskName $OldTaskName -ErrorAction SilentlyContinue
if ($oldTask) {
Stop-ScheduledTask -TaskName $OldTaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $OldTaskName -Confirm:$false -ErrorAction SilentlyContinue
Write-Host " Removed legacy scheduled task '$OldTaskName'." -ForegroundColor Yellow
}
} catch {}
# ── Register Windows service ───────────────────────────────────────────────────
Write-Host "[5/6] Registering Windows service '$ServiceName'..." -ForegroundColor Cyan
# Stop + remove any existing service first
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($existing) {
if ($existing.Status -eq "Running") {
Write-Host " Stopping existing service..." -NoNewline
& $pythonPath $AgentScript stop 2>&1 | Out-Null
Start-Sleep -Seconds 3
Write-Host " stopped." -ForegroundColor Yellow
}
Write-Host " Removing existing service..." -NoNewline
& $pythonPath $AgentScript remove 2>&1 | Out-Null
Start-Sleep -Seconds 2
Write-Host " removed." -ForegroundColor Yellow
}
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " JARVIS Agent installed successfully!" -ForegroundColor Green
Write-Host " Hostname: $hostname" -ForegroundColor Green
Write-Host " Service: $SERVICE_NAME (auto-start at boot)" -ForegroundColor Green
Write-Host " Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log" -ForegroundColor Green
Write-Host "========================================`n" -ForegroundColor Green
# Install the service (--startup auto = start at boot)
& $pythonPath $AgentScript --startup auto install
if ($LASTEXITCODE -ne 0) {
Write-Error "Service registration failed (exit $LASTEXITCODE). Check that pywin32 postinstall completed."
}
# Configure failure recovery: restart after 5s, 10s, 30s
sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/10000/restart/30000 | Out-Null
Write-Host " Service registered with auto-restart on failure." -ForegroundColor Green
# ── Start the service ──────────────────────────────────────────────────────────
Write-Host "[6/6] Starting service..." -ForegroundColor Cyan
& $pythonPath $AgentScript start
Start-Sleep -Seconds 4
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
$status = if ($svc) { $svc.Status } else { "NotFound" }
$color = if ($status -eq "Running") { "Green" } else { "Yellow" }
Write-Host " Service status: $status" -ForegroundColor $color
Write-Host ""
Write-Host " ====================================" -ForegroundColor Green
Write-Host " Installation complete! " -ForegroundColor Green
Write-Host " ====================================" -ForegroundColor Green
Write-Host ""
Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White
Write-Host " JARVIS : $JarvisUrl" -ForegroundColor White
Write-Host " Python : $pythonPath" -ForegroundColor White
Write-Host " Logs : $InstallDir\jarvis-agent.log" -ForegroundColor White
Write-Host ""
Write-Host " Manage the service:" -ForegroundColor Gray
Write-Host " Get-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Start-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Restart-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Get-Content '$InstallDir\jarvis-agent.log' -Tail 30 -Wait" -ForegroundColor Gray
Write-Host ""
Write-Host " To uninstall:" -ForegroundColor Gray
Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray
Write-Host " & '$pythonPath' '$AgentScript' remove" -ForegroundColor Gray
Write-Host ""
+2 -2
View File
@@ -9,7 +9,7 @@ set -e
HOSTNAME_ARG="${1:-$(hostname -s)}"
AGENT_TYPE="${2:-linux}"
JARVIS_URL="${JARVIS_URL:-https://jarvis.orbishosting.com}"
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST=""
INSTALL_DIR="/opt/jarvis-agent"
CONFIG_DIR="/etc/jarvis-agent"
@@ -50,7 +50,7 @@ else
{
"jarvis_url": "$JARVIS_URL",
"host_header": "$JARVIS_HOST",
"ssl_verify": true,
"ssl_verify": false,
"registration_key": "$REG_KEY",
"hostname": "$HOSTNAME_ARG",
"agent_type": "$AGENT_TYPE",
-236
View File
@@ -1,236 +0,0 @@
#!/usr/bin/env python3
"""
JARVIS HA Poller pulls entity states from Home Assistant REST API
and pushes them to JARVIS as a homeassistant-type agent.
Runs on VM211 as a systemd service (jarvis-ha-poller).
Config: /etc/jarvis-agent/ha-poller.json
"""
import json
import os
import socket
import sys
import time
import urllib.request
import urllib.error
import ssl
from datetime import datetime, timezone
from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/ha-poller.json"
STATE_PATH = "/var/lib/jarvis-agent/ha-poller-state.json"
AGENT_VERSION = "1.0"
AGENT_ID = "homeassistant_ha"
HOSTNAME = "homeassistant"
# Domains to skip — don't send to JARVIS (saves DB space, keeps UI clean)
SKIP_DOMAINS = {
'sensor', 'binary_sensor', 'button', 'update', 'select', 'number',
'device_tracker', 'event', 'image', 'person', 'zone', 'tts',
'conversation', 'assist_satellite', 'input_button', 'media_player',
'scene', 'water_heater', 'alarm_control_panel', 'automation',
'script', 'calendar', 'notify', 'weather', 'sun', 'persistent_notification',
'tag', 'system_health', 'timer', 'counter',
'camera', 'siren', 'remote', 'todo', 'lawn_mower',
}
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] {msg}", flush=True)
def load_config() -> dict:
if not os.path.exists(CONFIG_PATH):
print(f"[ERROR] Config not found at {CONFIG_PATH}", flush=True)
sys.exit(1)
with open(CONFIG_PATH) as f:
return json.load(f)
def load_state() -> dict:
if os.path.exists(STATE_PATH):
with open(STATE_PATH) as f:
return json.load(f)
return {}
def save_state(state: dict):
Path(STATE_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(STATE_PATH, "w") as f:
json.dump(state, f, indent=2)
def _ssl_ctx(verify: bool):
if not verify:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
return None
def jarvis_post(url: str, payload: dict, headers: dict, ssl_verify: bool, timeout: int = 15) -> dict:
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"error": str(e)}
def ha_get(url: str, token: str, timeout: int = 15) -> dict | list | None:
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
except Exception as e:
log(f"HA API error: {e}")
return None
def register(cfg: dict, state: dict) -> str:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
reg_key = cfg["registration_key"]
log(f"Registering HA poller with JARVIS at {jarvis_url}...")
result = jarvis_post(
f"{jarvis_url}/api/agent/register",
{
"hostname": HOSTNAME,
"version": AGENT_VERSION,
"agent_type": "homeassistant",
"ip_address": cfg.get("ha_url", "").split("//")[-1].split(":")[0],
"capabilities": ["ha_entities", "ha_state"],
"agent_id": AGENT_ID,
},
{"X-Registration-Key": reg_key},
ssl_verify,
)
if "error" in result:
log(f"Registration failed: {result['error']}")
return ""
api_key = result.get("api_key", "")
if api_key:
state["api_key"] = api_key
state["agent_id"] = AGENT_ID
save_state(state)
log(f"Registered. agent_id={AGENT_ID}")
return api_key
def push_entities(cfg: dict, api_key: str, entities: list) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
headers = {"X-Agent-Key": api_key}
# Send in batches of 200
batch_size = 200
total = len(entities)
ok = True
for i in range(0, total, batch_size):
batch = entities[i:i+batch_size]
result = jarvis_post(
f"{jarvis_url}/api/agent/ha_state",
{"entities": batch},
headers,
ssl_verify,
)
if "error" in result:
log(f"Push batch {i//batch_size+1} failed: {result['error']}")
ok = False
return ok
def heartbeat(cfg: dict, api_key: str) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
result = jarvis_post(
f"{jarvis_url}/api/agent/heartbeat",
{"version": AGENT_VERSION},
{"X-Agent-Key": api_key},
ssl_verify,
timeout=10,
)
return "error" not in result
def fetch_ha_states(cfg: dict) -> list:
ha_url = cfg["ha_url"].rstrip("/")
token = cfg["ha_token"]
states = ha_get(f"{ha_url}/api/states", token)
if not states or not isinstance(states, list):
return []
entities = []
for s in states:
entity_id = s.get("entity_id", "")
domain = entity_id.split(".")[0] if "." in entity_id else ""
if domain in SKIP_DOMAINS:
continue
attrs = s.get("attributes", {})
# Convert ISO 8601 (e.g. "2026-06-28T21:26:01.922366+00:00") to MySQL datetime
lc = s.get("last_changed", "")
try:
dt = datetime.fromisoformat(lc.replace("Z", "+00:00"))
lc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
lc = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
entities.append({
"entity_id": entity_id,
"name": attrs.get("friendly_name") or entity_id,
"state": s.get("state", ""),
"attributes": attrs,
"last_changed": lc,
})
return entities
def main():
cfg = load_config()
state = load_state()
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
api_key = state.get("api_key", "")
if not api_key:
api_key = register(cfg, state)
if not api_key:
log("Could not register. Retrying in 60s...")
time.sleep(60)
main()
return
headers = {"X-Agent-Key": api_key}
last_push = 0
log(f"HA Poller v{AGENT_VERSION} running. Polling HA every {poll_interval}s, heartbeat every {heartbeat_every}s.")
while True:
now = time.time()
# Heartbeat
if not heartbeat(cfg, api_key):
log("Heartbeat failed (401?) — re-registering...")
state.clear()
save_state(state)
api_key = register(cfg, state)
if not api_key:
time.sleep(60)
continue
# Push entity states every poll_interval
if now - last_push >= poll_interval:
entities = fetch_ha_states(cfg)
if entities:
ok = push_entities(cfg, api_key, entities)
if ok:
log(f"Pushed {len(entities)} HA entities to JARVIS.")
last_push = now
else:
log("No HA entities fetched (HA down or token invalid?)")
time.sleep(heartbeat_every)
if __name__ == "__main__":
main()
+2 -13
View File
@@ -50,20 +50,9 @@ if (!isset($repoMap[$repo])) {
}
$path = $repoMap[$repo];
// NovaCPX lives on a private VM — the VM polls GitHub every minute via cron
// This webhook receipt confirms GitHub delivered the push notification
if ($path === '__NOVACPX_VM__') {
$commit = $data['after'] ?? 'HEAD';
$msg = "[" . date('Y-m-d H:i:s') . "] NovaCPX push by $pusher (commit: $commit) — VM will deploy within 1 min";
file_put_contents(DEPLOY_LOG, $msg . "\n", FILE_APPEND | LOCK_EX);
echo json_encode(['ok' => true, 'queued' => 'novacpx', 'commit' => $commit]);
exit;
}
$ts = date('Y-m-d H:i:s');
file_put_contents(DEPLOY_QUEUE, $path . "\n", FILE_APPEND | LOCK_EX);
$msg = "[" . date('Y-m-d H:i:s') . "] Queued deploy: $repo by $pusher -> $path";
file_put_contents(DEPLOY_LOG, $msg . "\n", FILE_APPEND | LOCK_EX);
file_put_contents(DEPLOY_LOG, "[$ts] Queued deploy: $repo by $pusher -> $path\n", FILE_APPEND | LOCK_EX);
echo json_encode(['ok' => true, 'queued' => $repo, 'path' => $path]);