updates
This commit is contained in:
12
README.md
12
README.md
@@ -179,7 +179,7 @@ This prevents messages from being overwritten when using Claude Desktop on multi
|
|||||||
- **Timed wakes**: CMD+R ensures chat is synced before system check message
|
- **Timed wakes**: CMD+R ensures chat is synced before system check message
|
||||||
- **Matrix wakes**: CMD+R ensures you see the latest conversation context
|
- **Matrix wakes**: CMD+R ensures you see the latest conversation context
|
||||||
|
|
||||||
The daemon waits **10 seconds** after sending CMD+R to ensure the refresh completes before sending the message.
|
The daemon waits **15 seconds** after sending CMD+R to ensure the refresh completes before sending the message.
|
||||||
|
|
||||||
**Why this matters:**
|
**Why this matters:**
|
||||||
- Multi-device sync: If you're active on mobile/web, desktop chat stays current
|
- Multi-device sync: If you're active on mobile/web, desktop chat stays current
|
||||||
@@ -277,14 +277,6 @@ def generate_message() -> str:
|
|||||||
return f"Your custom message template with {timestamp}"
|
return f"Your custom message template with {timestamp}"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Grace Period
|
|
||||||
|
|
||||||
After sending a message, the daemon waits 30 seconds for Claude to call MCP tools. Adjust in `automation_daemon.py` line 246:
|
|
||||||
|
|
||||||
```python
|
|
||||||
grace_period = 30 # seconds
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
### Logs
|
### Logs
|
||||||
@@ -707,7 +699,7 @@ claude-desktop-automation/
|
|||||||
⚠️ **macOS only** - Uses AppleScript and launchd (Linux port would require xdotool/ydotool)
|
⚠️ **macOS only** - Uses AppleScript and launchd (Linux port would require xdotool/ydotool)
|
||||||
⚠️ **Requires Accessibility** - Special macOS permissions for UI automation
|
⚠️ **Requires Accessibility** - Special macOS permissions for UI automation
|
||||||
⚠️ **Screen saver handled** - Uses `caffeinate` to wake screen automatically
|
⚠️ **Screen saver handled** - Uses `caffeinate` to wake screen automatically
|
||||||
⚠️ **Fixed refresh delay** - Waits 10 seconds after CMD+R (configurable if needed)
|
⚠️ **Fixed refresh delay** - Waits 15 seconds after CMD+R (configurable if needed)
|
||||||
|
|
||||||
## Future Plans
|
## Future Plans
|
||||||
|
|
||||||
|
|||||||
79
matrix_helpers.py
Normal file
79
matrix_helpers.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Matrix Helper Utilities
|
||||||
|
|
||||||
|
Shared utilities for Matrix client operations, reducing code duplication
|
||||||
|
across matrix_mcp.py and matrix_integration.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import AsyncIterator
|
||||||
|
|
||||||
|
try:
|
||||||
|
from nio import AsyncClient
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"matrix-nio not installed. Install with: pip3 install matrix-nio\n"
|
||||||
|
"Note: Without [e2e] extra, only unencrypted rooms are supported"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CREDENTIALS_FILE = Path.home() / ".matrix-credentials.json"
|
||||||
|
MATRIX_DATA_DIR = Path.home() / ".matrix-data"
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def load_credentials() -> dict:
|
||||||
|
"""Load Matrix credentials from JSON file"""
|
||||||
|
if not CREDENTIALS_FILE.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Credentials file not found: {CREDENTIALS_FILE}\n"
|
||||||
|
f"Run setup_matrix.sh to create credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(CREDENTIALS_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load credentials: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def matrix_client() -> AsyncIterator[AsyncClient]:
|
||||||
|
"""
|
||||||
|
Async context manager for Matrix client operations.
|
||||||
|
|
||||||
|
Handles client setup, session restoration, and cleanup automatically.
|
||||||
|
Ensures client.close() is always called, preventing resource leaks.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
async with matrix_client() as client:
|
||||||
|
response = await client.room_send(...)
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
AsyncClient: Configured and authenticated Matrix client
|
||||||
|
"""
|
||||||
|
creds = load_credentials()
|
||||||
|
|
||||||
|
# Create data directory for E2EE store
|
||||||
|
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
client = AsyncClient(
|
||||||
|
homeserver=creds['homeserver'],
|
||||||
|
user=creds['user_id'],
|
||||||
|
store_path=str(MATRIX_DATA_DIR),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Restore session from credentials
|
||||||
|
client.access_token = creds['access_token']
|
||||||
|
client.device_id = creds['device_id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield client
|
||||||
|
finally:
|
||||||
|
await client.close()
|
||||||
@@ -449,7 +449,7 @@ class MatrixMonitor:
|
|||||||
# Trigger wake if rate limit allows
|
# Trigger wake if rate limit allows
|
||||||
await self._maybe_trigger_wake()
|
await self._maybe_trigger_wake()
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Failed to download/compress image: {event.body}")
|
logger.warning(f"Image rejected (too large or download failed): {event.body}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing image: {e}")
|
logger.error(f"Error processing image: {e}")
|
||||||
@@ -596,7 +596,19 @@ class MatrixMonitor:
|
|||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
final_img = img.resize((800, 600), Image.Resampling.LANCZOS)
|
final_img = img.resize((800, 600), Image.Resampling.LANCZOS)
|
||||||
final_img.save(buffer, format='JPEG', quality=50, optimize=True)
|
final_img.save(buffer, format='JPEG', quality=50, optimize=True)
|
||||||
return buffer.getvalue()
|
final_data = buffer.getvalue()
|
||||||
|
|
||||||
|
# Final size validation - reject if still too large
|
||||||
|
final_base64 = base64.b64encode(final_data)
|
||||||
|
final_size_mb = len(final_base64) / (1024 * 1024)
|
||||||
|
if final_size_mb > target_size_mb:
|
||||||
|
logger.error(
|
||||||
|
f"Image still too large after all compression attempts: "
|
||||||
|
f"{final_size_mb:.2f}MB base64 (limit: {target_size_mb}MB)"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return final_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Compression failed: {e}")
|
logger.error(f"Compression failed: {e}")
|
||||||
|
|||||||
332
matrix_mcp.py
332
matrix_mcp.py
@@ -33,6 +33,8 @@ from typing import List, Optional, Dict, Any
|
|||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
from mcp.server.fastmcp.utilities.types import Image as MCPImage
|
from mcp.server.fastmcp.utilities.types import Image as MCPImage
|
||||||
|
|
||||||
|
from matrix_helpers import matrix_client, load_credentials as load_credentials_helper
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from nio import (
|
from nio import (
|
||||||
AsyncClient,
|
AsyncClient,
|
||||||
@@ -429,6 +431,7 @@ async def matrix_send_image(room_id: str, file_path: str) -> str:
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
# Resolve path
|
# Resolve path
|
||||||
path = Path(file_path).expanduser().resolve()
|
path = Path(file_path).expanduser().resolve()
|
||||||
@@ -455,27 +458,13 @@ async def matrix_send_image(room_id: str, file_path: str) -> str:
|
|||||||
width, height = None, None
|
width, height = None, None
|
||||||
mime_type = 'image/jpeg'
|
mime_type = 'image/jpeg'
|
||||||
|
|
||||||
# Load credentials
|
# Load credentials for upload URL
|
||||||
creds = load_credentials()
|
creds = load_credentials()
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Upload image to Matrix homeserver using aiohttp directly
|
|
||||||
# (nio's upload has timeout issues)
|
|
||||||
filename = path.name
|
filename = path.name
|
||||||
logger.info(f"Uploading image from file: {filename} ({len(image_data)} bytes)")
|
logger.info(f"Uploading image from file: {filename} ({len(image_data)} bytes)")
|
||||||
|
|
||||||
import aiohttp
|
# Upload image to Matrix homeserver using aiohttp directly
|
||||||
|
# (nio's upload has timeout issues)
|
||||||
upload_url = f"{creds['homeserver']}/_matrix/media/v3/upload?filename={filename}"
|
upload_url = f"{creds['homeserver']}/_matrix/media/v3/upload?filename={filename}"
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {creds['access_token']}",
|
"Authorization": f"Bearer {creds['access_token']}",
|
||||||
@@ -490,11 +479,10 @@ async def matrix_send_image(room_id: str, file_path: str) -> str:
|
|||||||
logger.info(f"Upload successful: {content_uri}")
|
logger.info(f"Upload successful: {content_uri}")
|
||||||
else:
|
else:
|
||||||
error_text = await resp.text()
|
error_text = await resp.text()
|
||||||
await client.close()
|
|
||||||
logger.error(f"Upload failed: {resp.status} - {error_text}")
|
logger.error(f"Upload failed: {resp.status} - {error_text}")
|
||||||
return f"Error uploading image: {resp.status} - {error_text}"
|
return f"Error uploading image: {resp.status} - {error_text}"
|
||||||
|
|
||||||
# Send image message
|
# Send image message using context manager
|
||||||
content = {
|
content = {
|
||||||
"msgtype": "m.image",
|
"msgtype": "m.image",
|
||||||
"body": filename,
|
"body": filename,
|
||||||
@@ -509,14 +497,13 @@ async def matrix_send_image(room_id: str, file_path: str) -> str:
|
|||||||
content["info"]["w"] = width
|
content["info"]["w"] = width
|
||||||
content["info"]["h"] = height
|
content["info"]["h"] = height
|
||||||
|
|
||||||
|
async with matrix_client() as client:
|
||||||
response = await client.room_send(
|
response = await client.room_send(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
message_type="m.room.message",
|
message_type="m.room.message",
|
||||||
content=content
|
content=content
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
if isinstance(response, RoomSendResponse):
|
if isinstance(response, RoomSendResponse):
|
||||||
logger.info(f"Sent image to {room_id}: {filename}")
|
logger.info(f"Sent image to {room_id}: {filename}")
|
||||||
return f"✓ Image sent to room: {filename} (event ID: {response.event_id})"
|
return f"✓ Image sent to room: {filename} (event ID: {response.event_id})"
|
||||||
@@ -554,6 +541,7 @@ async def matrix_send_voice(room_id: str, file_path: str) -> str:
|
|||||||
try:
|
try:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import wave
|
import wave
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
# Resolve path
|
# Resolve path
|
||||||
path = Path(file_path).expanduser().resolve()
|
path = Path(file_path).expanduser().resolve()
|
||||||
@@ -595,27 +583,13 @@ async def matrix_send_voice(room_id: str, file_path: str) -> str:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not extract WAV duration: {e}")
|
logger.warning(f"Could not extract WAV duration: {e}")
|
||||||
|
|
||||||
# Load credentials
|
# Load credentials for upload URL
|
||||||
creds = load_credentials()
|
creds = load_credentials()
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Upload audio to Matrix homeserver using aiohttp directly
|
|
||||||
# (nio's upload has timeout issues)
|
|
||||||
filename = path.name
|
filename = path.name
|
||||||
logger.info(f"Uploading voice message from file: {filename} ({len(audio_data)} bytes)")
|
logger.info(f"Uploading voice message from file: {filename} ({len(audio_data)} bytes)")
|
||||||
|
|
||||||
import aiohttp
|
# Upload audio to Matrix homeserver using aiohttp directly
|
||||||
|
# (nio's upload has timeout issues)
|
||||||
upload_url = f"{creds['homeserver']}/_matrix/media/v3/upload?filename={filename}"
|
upload_url = f"{creds['homeserver']}/_matrix/media/v3/upload?filename={filename}"
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {creds['access_token']}",
|
"Authorization": f"Bearer {creds['access_token']}",
|
||||||
@@ -630,11 +604,10 @@ async def matrix_send_voice(room_id: str, file_path: str) -> str:
|
|||||||
logger.info(f"Upload successful: {content_uri}")
|
logger.info(f"Upload successful: {content_uri}")
|
||||||
else:
|
else:
|
||||||
error_text = await resp.text()
|
error_text = await resp.text()
|
||||||
await client.close()
|
|
||||||
logger.error(f"Upload failed: {resp.status} - {error_text}")
|
logger.error(f"Upload failed: {resp.status} - {error_text}")
|
||||||
return f"Error uploading audio: {resp.status} - {error_text}"
|
return f"Error uploading audio: {resp.status} - {error_text}"
|
||||||
|
|
||||||
# Send voice message
|
# Send voice message using context manager
|
||||||
content = {
|
content = {
|
||||||
"msgtype": "m.audio",
|
"msgtype": "m.audio",
|
||||||
"body": filename,
|
"body": filename,
|
||||||
@@ -649,14 +622,13 @@ async def matrix_send_voice(room_id: str, file_path: str) -> str:
|
|||||||
if duration_ms:
|
if duration_ms:
|
||||||
content["info"]["duration"] = duration_ms
|
content["info"]["duration"] = duration_ms
|
||||||
|
|
||||||
|
async with matrix_client() as client:
|
||||||
response = await client.room_send(
|
response = await client.room_send(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
message_type="m.room.message",
|
message_type="m.room.message",
|
||||||
content=content
|
content=content
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
if isinstance(response, RoomSendResponse):
|
if isinstance(response, RoomSendResponse):
|
||||||
logger.info(f"Sent voice message to {room_id}: {filename}")
|
logger.info(f"Sent voice message to {room_id}: {filename}")
|
||||||
return f"✓ Voice message sent to room: {filename} (event ID: {response.event_id})"
|
return f"✓ Voice message sent to room: {filename} (event ID: {response.event_id})"
|
||||||
@@ -713,21 +685,6 @@ async def matrix_send_message(room_id: str, message: str) -> str:
|
|||||||
matrix_send_message("!abc123:matrix.org", "This is **bold** and *italic*")
|
matrix_send_message("!abc123:matrix.org", "This is **bold** and *italic*")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Convert markdown to HTML
|
# Convert markdown to HTML
|
||||||
html_body = _convert_markdown_to_html(message)
|
html_body = _convert_markdown_to_html(message)
|
||||||
|
|
||||||
@@ -742,15 +699,13 @@ async def matrix_send_message(room_id: str, message: str) -> str:
|
|||||||
content["format"] = "org.matrix.custom.html"
|
content["format"] = "org.matrix.custom.html"
|
||||||
content["formatted_body"] = html_body
|
content["formatted_body"] = html_body
|
||||||
|
|
||||||
# Send message
|
async with matrix_client() as client:
|
||||||
response = await client.room_send(
|
response = await client.room_send(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
message_type="m.room.message",
|
message_type="m.room.message",
|
||||||
content=content
|
content=content
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
if isinstance(response, RoomSendResponse):
|
if isinstance(response, RoomSendResponse):
|
||||||
logger.info(f"Sent message to {room_id}: {message[:50]}...")
|
logger.info(f"Sent message to {room_id}: {message[:50]}...")
|
||||||
return f"✓ Message sent to room (event ID: {response.event_id})"
|
return f"✓ Message sent to room (event ID: {response.event_id})"
|
||||||
@@ -785,21 +740,6 @@ async def matrix_send_emote(room_id: str, action: str) -> str:
|
|||||||
matrix_send_emote("!abc123:matrix.org", "is **thinking** carefully")
|
matrix_send_emote("!abc123:matrix.org", "is **thinking** carefully")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Convert markdown to HTML
|
# Convert markdown to HTML
|
||||||
html_body = _convert_markdown_to_html(action)
|
html_body = _convert_markdown_to_html(action)
|
||||||
|
|
||||||
@@ -814,15 +754,13 @@ async def matrix_send_emote(room_id: str, action: str) -> str:
|
|||||||
content["format"] = "org.matrix.custom.html"
|
content["format"] = "org.matrix.custom.html"
|
||||||
content["formatted_body"] = html_body
|
content["formatted_body"] = html_body
|
||||||
|
|
||||||
# Send message
|
async with matrix_client() as client:
|
||||||
response = await client.room_send(
|
response = await client.room_send(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
message_type="m.room.message",
|
message_type="m.room.message",
|
||||||
content=content
|
content=content
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
if isinstance(response, RoomSendResponse):
|
if isinstance(response, RoomSendResponse):
|
||||||
logger.info(f"Sent emote to {room_id}: {action[:50]}...")
|
logger.info(f"Sent emote to {room_id}: {action[:50]}...")
|
||||||
return f"✓ Emote sent to room (event ID: {response.event_id})"
|
return f"✓ Emote sent to room (event ID: {response.event_id})"
|
||||||
@@ -893,7 +831,7 @@ def get_matrix_status() -> str:
|
|||||||
last_wake = datetime.fromisoformat(last_wake_str)
|
last_wake = datetime.fromisoformat(last_wake_str)
|
||||||
time_ago = datetime.now() - last_wake
|
time_ago = datetime.now() - last_wake
|
||||||
status_lines.append(f"Last Matrix wake: {last_wake.strftime('%Y-%m-%d %H:%M:%S')} ({int(time_ago.total_seconds()/60)} min ago)")
|
status_lines.append(f"Last Matrix wake: {last_wake.strftime('%Y-%m-%d %H:%M:%S')} ({int(time_ago.total_seconds()/60)} min ago)")
|
||||||
except:
|
except Exception:
|
||||||
status_lines.append(f"Last Matrix wake: {last_wake_str}")
|
status_lines.append(f"Last Matrix wake: {last_wake_str}")
|
||||||
else:
|
else:
|
||||||
status_lines.append("Last Matrix wake: Never")
|
status_lines.append("Last Matrix wake: Never")
|
||||||
@@ -908,7 +846,7 @@ def get_matrix_status() -> str:
|
|||||||
else:
|
else:
|
||||||
remaining = 120 - elapsed
|
remaining = 120 - elapsed
|
||||||
status_lines.append(f"Rate limit: Active ({int(remaining)}s remaining)")
|
status_lines.append(f"Rate limit: Active ({int(remaining)}s remaining)")
|
||||||
except:
|
except Exception:
|
||||||
status_lines.append("Rate limit: Unknown")
|
status_lines.append("Rate limit: Unknown")
|
||||||
else:
|
else:
|
||||||
status_lines.append("Rate limit: OK (can wake)")
|
status_lines.append("Rate limit: OK (can wake)")
|
||||||
@@ -1043,26 +981,10 @@ async def join_matrix_room(room_id: str) -> str:
|
|||||||
join_matrix_room("!abc123:matrix.org")
|
join_matrix_room("!abc123:matrix.org")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Join room
|
|
||||||
logger.info(f"Attempting to join room: {room_id}")
|
logger.info(f"Attempting to join room: {room_id}")
|
||||||
response = await client.join(room_id)
|
|
||||||
|
|
||||||
await client.close()
|
async with matrix_client() as client:
|
||||||
|
response = await client.join(room_id)
|
||||||
|
|
||||||
if isinstance(response, JoinResponse):
|
if isinstance(response, JoinResponse):
|
||||||
logger.info(f"Successfully joined room: {room_id}")
|
logger.info(f"Successfully joined room: {room_id}")
|
||||||
@@ -1099,26 +1021,10 @@ async def leave_matrix_room(room_id: str) -> str:
|
|||||||
leave_matrix_room("!abc123:matrix.org")
|
leave_matrix_room("!abc123:matrix.org")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Leave room
|
|
||||||
logger.info(f"Attempting to leave room: {room_id}")
|
logger.info(f"Attempting to leave room: {room_id}")
|
||||||
response = await client.room_leave(room_id)
|
|
||||||
|
|
||||||
await client.close()
|
async with matrix_client() as client:
|
||||||
|
response = await client.room_leave(room_id)
|
||||||
|
|
||||||
if isinstance(response, RoomLeaveResponse):
|
if isinstance(response, RoomLeaveResponse):
|
||||||
logger.info(f"Successfully left room: {room_id}")
|
logger.info(f"Successfully left room: {room_id}")
|
||||||
@@ -1145,31 +1051,19 @@ async def list_matrix_rooms() -> str:
|
|||||||
Formatted list of rooms
|
Formatted list of rooms
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
# Get room whitelist from credentials
|
||||||
creds = load_credentials()
|
creds = load_credentials()
|
||||||
|
whitelist = set(creds.get('room_whitelist', []))
|
||||||
|
|
||||||
# Create Matrix client
|
async with matrix_client() as client:
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Sync to get rooms
|
# Sync to get rooms
|
||||||
await client.sync(timeout=30000)
|
await client.sync(timeout=30000)
|
||||||
|
|
||||||
# Get room whitelist
|
|
||||||
whitelist = set(creds.get('room_whitelist', []))
|
|
||||||
|
|
||||||
# Format room list
|
# Format room list
|
||||||
rooms_lines = ["Matrix Rooms:"]
|
rooms_lines = ["Matrix Rooms:"]
|
||||||
rooms_lines.append("=" * 60)
|
rooms_lines.append("=" * 60)
|
||||||
|
|
||||||
|
room_count = len(client.rooms)
|
||||||
for room_id, room in client.rooms.items():
|
for room_id, room in client.rooms.items():
|
||||||
# Check if room is whitelisted
|
# Check if room is whitelisted
|
||||||
monitored = "✓" if (not whitelist or room_id in whitelist) else "✗"
|
monitored = "✓" if (not whitelist or room_id in whitelist) else "✗"
|
||||||
@@ -1179,9 +1073,7 @@ async def list_matrix_rooms() -> str:
|
|||||||
rooms_lines.append(f" Members: {len(room.users)}")
|
rooms_lines.append(f" Members: {len(room.users)}")
|
||||||
rooms_lines.append("")
|
rooms_lines.append("")
|
||||||
|
|
||||||
await client.close()
|
logger.info(f"Listed {room_count} Matrix rooms")
|
||||||
|
|
||||||
logger.info(f"Listed {len(client.rooms)} Matrix rooms")
|
|
||||||
return "\n".join(rooms_lines)
|
return "\n".join(rooms_lines)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1208,21 +1100,6 @@ async def matrix_reply_to_message(room_id: str, event_id: str, message: str) ->
|
|||||||
reply_to_message("!abc:matrix.org", "$event123", "Thanks for the info!")
|
reply_to_message("!abc:matrix.org", "$event123", "Thanks for the info!")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Send threaded reply
|
# Send threaded reply
|
||||||
content = {
|
content = {
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
@@ -1234,14 +1111,13 @@ async def matrix_reply_to_message(room_id: str, event_id: str, message: str) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async with matrix_client() as client:
|
||||||
response = await client.room_send(
|
response = await client.room_send(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
message_type="m.room.message",
|
message_type="m.room.message",
|
||||||
content=content
|
content=content
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
if isinstance(response, RoomSendResponse):
|
if isinstance(response, RoomSendResponse):
|
||||||
logger.info(f"Sent reply to {event_id} in {room_id}")
|
logger.info(f"Sent reply to {event_id} in {room_id}")
|
||||||
return f"✓ Reply sent (event ID: {response.event_id})"
|
return f"✓ Reply sent (event ID: {response.event_id})"
|
||||||
@@ -1274,21 +1150,6 @@ async def matrix_react_to_message(room_id: str, event_id: str, emoji: str) -> st
|
|||||||
react_to_message("!abc:matrix.org", "$event123", "❤️")
|
react_to_message("!abc:matrix.org", "$event123", "❤️")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Send reaction
|
# Send reaction
|
||||||
content = {
|
content = {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
@@ -1298,14 +1159,13 @@ async def matrix_react_to_message(room_id: str, event_id: str, emoji: str) -> st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async with matrix_client() as client:
|
||||||
response = await client.room_send(
|
response = await client.room_send(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
message_type="m.reaction",
|
message_type="m.reaction",
|
||||||
content=content
|
content=content
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
if isinstance(response, RoomSendResponse):
|
if isinstance(response, RoomSendResponse):
|
||||||
logger.info(f"Sent reaction {emoji} to {event_id}")
|
logger.info(f"Sent reaction {emoji} to {event_id}")
|
||||||
return f"✓ Reacted with {emoji}"
|
return f"✓ Reacted with {emoji}"
|
||||||
@@ -1337,21 +1197,7 @@ async def matrix_get_user_profile(user_id: str) -> Dict[str, Any]:
|
|||||||
get_user_profile("@friend:matrix.org")
|
get_user_profile("@friend:matrix.org")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
async with matrix_client() as client:
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Get display name
|
# Get display name
|
||||||
display_name_response = await client.get_displayname(user_id)
|
display_name_response = await client.get_displayname(user_id)
|
||||||
display_name = None
|
display_name = None
|
||||||
@@ -1364,8 +1210,6 @@ async def matrix_get_user_profile(user_id: str) -> Dict[str, Any]:
|
|||||||
if isinstance(avatar_response, ProfileGetAvatarResponse):
|
if isinstance(avatar_response, ProfileGetAvatarResponse):
|
||||||
avatar_url = avatar_response.avatar_url
|
avatar_url = avatar_response.avatar_url
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
profile = {
|
profile = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"display_name": display_name or user_id,
|
"display_name": display_name or user_id,
|
||||||
@@ -1403,26 +1247,9 @@ async def matrix_set_presence(status: str, message: str = "") -> str:
|
|||||||
if status not in valid_statuses:
|
if status not in valid_statuses:
|
||||||
return f"Error: status must be one of {valid_statuses}"
|
return f"Error: status must be one of {valid_statuses}"
|
||||||
|
|
||||||
# Load credentials
|
async with matrix_client() as client:
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Set presence
|
|
||||||
await client.set_presence(status, message if message else None)
|
await client.set_presence(status, message if message else None)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
logger.info(f"Set presence to {status}")
|
logger.info(f"Set presence to {status}")
|
||||||
return f"✓ Presence set to {status}" + (f": {message}" if message else "")
|
return f"✓ Presence set to {status}" + (f": {message}" if message else "")
|
||||||
|
|
||||||
@@ -1482,23 +1309,10 @@ async def send_matrix_file(room_id: str, file: Any, filename: str, mime_type: st
|
|||||||
if not mime_type:
|
if not mime_type:
|
||||||
mime_type = 'application/octet-stream'
|
mime_type = 'application/octet-stream'
|
||||||
|
|
||||||
# Load credentials
|
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Upload file
|
|
||||||
logger.info(f"Uploading file: {filename} ({len(file_data)} bytes)")
|
logger.info(f"Uploading file: {filename} ({len(file_data)} bytes)")
|
||||||
|
|
||||||
|
async with matrix_client() as client:
|
||||||
|
# Upload file
|
||||||
upload_response = await client.upload(
|
upload_response = await client.upload(
|
||||||
data_provider=lambda *args: file_data,
|
data_provider=lambda *args: file_data,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
@@ -1507,7 +1321,6 @@ async def send_matrix_file(room_id: str, file: Any, filename: str, mime_type: st
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(upload_response, UploadError):
|
if isinstance(upload_response, UploadError):
|
||||||
await client.close()
|
|
||||||
return f"Error uploading file: {upload_response.message}"
|
return f"Error uploading file: {upload_response.message}"
|
||||||
|
|
||||||
# Send file message
|
# Send file message
|
||||||
@@ -1527,8 +1340,6 @@ async def send_matrix_file(room_id: str, file: Any, filename: str, mime_type: st
|
|||||||
content=content
|
content=content
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
if isinstance(response, RoomSendResponse):
|
if isinstance(response, RoomSendResponse):
|
||||||
logger.info(f"Sent file to {room_id}: {filename}")
|
logger.info(f"Sent file to {room_id}: {filename}")
|
||||||
return f"✓ File sent: {filename} (event ID: {response.event_id})"
|
return f"✓ File sent: {filename} (event ID: {response.event_id})"
|
||||||
@@ -1619,27 +1430,12 @@ async def matrix_get_room_members(room_id: str) -> List[Dict[str, Any]]:
|
|||||||
get_room_members("!abc:matrix.org")
|
get_room_members("!abc:matrix.org")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
async with matrix_client() as client:
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Sync to get rooms
|
# Sync to get rooms
|
||||||
await client.sync(timeout=30000)
|
await client.sync(timeout=30000)
|
||||||
|
|
||||||
# Find the room
|
# Find the room
|
||||||
if room_id not in client.rooms:
|
if room_id not in client.rooms:
|
||||||
await client.close()
|
|
||||||
return [{"error": f"Room not found: {room_id}"}]
|
return [{"error": f"Room not found: {room_id}"}]
|
||||||
|
|
||||||
room = client.rooms[room_id]
|
room = client.rooms[room_id]
|
||||||
@@ -1653,8 +1449,6 @@ async def matrix_get_room_members(room_id: str) -> List[Dict[str, Any]]:
|
|||||||
"power_level": member.power_level,
|
"power_level": member.power_level,
|
||||||
})
|
})
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
logger.info(f"Retrieved {len(members)} members from {room_id}")
|
logger.info(f"Retrieved {len(members)} members from {room_id}")
|
||||||
return members
|
return members
|
||||||
|
|
||||||
@@ -1681,30 +1475,13 @@ async def matrix_set_room_topic(room_id: str, topic: str) -> str:
|
|||||||
set_room_topic("!abc:matrix.org", "Discussion about AI projects")
|
set_room_topic("!abc:matrix.org", "Discussion about AI projects")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
async with matrix_client() as client:
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Set room topic
|
|
||||||
await client.room_put_state(
|
await client.room_put_state(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
event_type="m.room.topic",
|
event_type="m.room.topic",
|
||||||
content={"topic": topic}
|
content={"topic": topic}
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
logger.info(f"Set topic for {room_id}: {topic[:50]}...")
|
logger.info(f"Set topic for {room_id}: {topic[:50]}...")
|
||||||
return f"✓ Room topic updated"
|
return f"✓ Room topic updated"
|
||||||
|
|
||||||
@@ -1730,27 +1507,12 @@ async def matrix_get_room_state(room_id: str) -> Dict[str, Any]:
|
|||||||
get_room_state("!abc:matrix.org")
|
get_room_state("!abc:matrix.org")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
async with matrix_client() as client:
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Sync to get rooms
|
# Sync to get rooms
|
||||||
await client.sync(timeout=30000)
|
await client.sync(timeout=30000)
|
||||||
|
|
||||||
# Find the room
|
# Find the room
|
||||||
if room_id not in client.rooms:
|
if room_id not in client.rooms:
|
||||||
await client.close()
|
|
||||||
return {"error": f"Room not found: {room_id}"}
|
return {"error": f"Room not found: {room_id}"}
|
||||||
|
|
||||||
room = client.rooms[room_id]
|
room = client.rooms[room_id]
|
||||||
@@ -1765,8 +1527,6 @@ async def matrix_get_room_state(room_id: str) -> Dict[str, Any]:
|
|||||||
"room_version": getattr(room, 'room_version', 'unknown'),
|
"room_version": getattr(room, 'room_version', 'unknown'),
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
logger.info(f"Retrieved state for {room_id}")
|
logger.info(f"Retrieved state for {room_id}")
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@@ -1794,21 +1554,7 @@ async def matrix_search_messages(room_id: str, query: str, limit: int = 10) -> L
|
|||||||
search_messages("!abc:matrix.org", "meeting", limit=5)
|
search_messages("!abc:matrix.org", "meeting", limit=5)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load credentials
|
async with matrix_client() as client:
|
||||||
creds = load_credentials()
|
|
||||||
|
|
||||||
# Create Matrix client
|
|
||||||
MATRIX_DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
client = AsyncClient(
|
|
||||||
homeserver=creds['homeserver'],
|
|
||||||
user=creds['user_id'],
|
|
||||||
store_path=str(MATRIX_DATA_DIR),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore session
|
|
||||||
client.access_token = creds['access_token']
|
|
||||||
client.device_id = creds['device_id']
|
|
||||||
|
|
||||||
# Get room messages
|
# Get room messages
|
||||||
response = await client.room_messages(
|
response = await client.room_messages(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
@@ -1832,8 +1578,6 @@ async def matrix_search_messages(room_id: str, query: str, limit: int = 10) -> L
|
|||||||
if len(results) >= limit:
|
if len(results) >= limit:
|
||||||
break
|
break
|
||||||
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
logger.info(f"Found {len(results)} messages matching '{query}'")
|
logger.info(f"Found {len(results)} messages matching '{query}'")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def send_refresh(delay_seconds=REFRESH_DELAY_SECONDS):
|
|||||||
Send CMD+R to refresh and wait for completion
|
Send CMD+R to refresh and wait for completion
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
delay_seconds: Time to wait after CMD+R (default: 10)
|
delay_seconds: Time to wait after CMD+R (default: 15)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if successful
|
bool: True if successful
|
||||||
|
|||||||
123
vixy_status.py
123
vixy_status.py
@@ -9,11 +9,15 @@ Day 83: Added movement tracking to filter static false positives (posters!)
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import requests
|
import requests
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logger = logging.getLogger("vixy_status")
|
||||||
|
|
||||||
# Service endpoints
|
# Service endpoints
|
||||||
ENVIRO_URL = "http://eye1.local:8767"
|
ENVIRO_URL = "http://eye1.local:8767"
|
||||||
OAK_URL = "http://head-vixy.local:8100"
|
OAK_URL = "http://head-vixy.local:8100"
|
||||||
@@ -90,8 +94,8 @@ def get_enviro_status() -> str:
|
|||||||
humidity = data.get('humidity', 0)
|
humidity = data.get('humidity', 0)
|
||||||
light = data.get('light', 0)
|
light = data.get('light', 0)
|
||||||
return f"Basement: {temp_f:.1f}F, {humidity:.1f}% humidity, {light:.1f} lux"
|
return f"Basement: {temp_f:.1f}F, {humidity:.1f}% humidity, {light:.1f} lux"
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.warning(f"Enviro service unavailable: {e}")
|
||||||
return "Basement: sensors unavailable"
|
return "Basement: sensors unavailable"
|
||||||
|
|
||||||
|
|
||||||
@@ -139,8 +143,8 @@ def get_presence_status() -> str:
|
|||||||
return f"Foxy: away (last seen {last_seen:.0f}s ago)"
|
return f"Foxy: away (last seen {last_seen:.0f}s ago)"
|
||||||
else:
|
else:
|
||||||
return "Foxy: away"
|
return "Foxy: away"
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.warning(f"OAK-D presence service unavailable: {e}")
|
||||||
return None # Return None to omit line if camera unavailable
|
return None # Return None to omit line if camera unavailable
|
||||||
|
|
||||||
|
|
||||||
@@ -167,8 +171,8 @@ def get_sound_status() -> str:
|
|||||||
return f"{category} ({top_score}% {classes_str})"
|
return f"{category} ({top_score}% {classes_str})"
|
||||||
else:
|
else:
|
||||||
return f"{category}"
|
return f"{category}"
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.warning(f"Headmic sound service unavailable: {e}")
|
||||||
return None # Return None to omit line if service unavailable
|
return None # Return None to omit line if service unavailable
|
||||||
|
|
||||||
|
|
||||||
@@ -186,52 +190,117 @@ def get_matrix_status() -> str:
|
|||||||
return f"Matrix: {len(unprocessed)} new message(s)"
|
return f"Matrix: {len(unprocessed)} new message(s)"
|
||||||
else:
|
else:
|
||||||
return "Matrix: no new messages"
|
return "Matrix: no new messages"
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.warning(f"Matrix status unavailable: {e}")
|
||||||
return "Matrix: status unavailable"
|
return "Matrix: status unavailable"
|
||||||
|
|
||||||
|
|
||||||
def get_vision_status() -> str:
|
def get_vision_status() -> str:
|
||||||
"""Get vision/motion event status from SQLite database"""
|
"""Get vision/motion event status from SQLite database.
|
||||||
|
|
||||||
|
Uses object detection data when available to show what was seen
|
||||||
|
(person, cat, etc.) rather than just raw motion counts.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if not EVENTS_DB.exists():
|
if not EVENTS_DB.exists():
|
||||||
return "Vision: no events database"
|
return "no events database"
|
||||||
|
|
||||||
conn = sqlite3.connect(str(EVENTS_DB))
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
|
|
||||||
# Get events from last 2 hours
|
# Get events from last 2 hours
|
||||||
cutoff = (datetime.now() - timedelta(hours=2)).isoformat()
|
cutoff = (datetime.now() - timedelta(hours=2)).isoformat()
|
||||||
|
|
||||||
|
with sqlite3.connect(str(EVENTS_DB)) as conn:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
# Check if detections column exists
|
||||||
|
columns = [row[1] for row in conn.execute("PRAGMA table_info(events)").fetchall()]
|
||||||
|
has_detections = "detections" in columns
|
||||||
|
|
||||||
|
if has_detections:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"""SELECT camera_id, annotation FROM events
|
"""SELECT camera_id, event_type, detections, timestamp FROM events
|
||||||
|
WHERE timestamp > ?
|
||||||
|
ORDER BY timestamp DESC""",
|
||||||
|
(cutoff,)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cursor = conn.execute(
|
||||||
|
"""SELECT camera_id, event_type, NULL as detections, timestamp FROM events
|
||||||
WHERE timestamp > ?
|
WHERE timestamp > ?
|
||||||
ORDER BY timestamp DESC""",
|
ORDER BY timestamp DESC""",
|
||||||
(cutoff,)
|
(cutoff,)
|
||||||
)
|
)
|
||||||
|
|
||||||
events = cursor.fetchall()
|
events = cursor.fetchall()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if not events:
|
if not events:
|
||||||
return "Vision: no recent motion"
|
return "no recent activity"
|
||||||
|
|
||||||
|
# Count labels per camera, track most recent detection
|
||||||
|
# Structure: {camera: {label: count}}
|
||||||
|
camera_labels = {}
|
||||||
|
camera_motion_only = {}
|
||||||
|
latest_detection = None # (label, camera, timestamp)
|
||||||
|
|
||||||
# Count by camera
|
|
||||||
by_camera = {}
|
|
||||||
unannotated = 0
|
|
||||||
for event in events:
|
for event in events:
|
||||||
cam = event['camera_id'] or 'unknown'
|
cam = event['camera_id'] or 'unknown'
|
||||||
by_camera[cam] = by_camera.get(cam, 0) + 1
|
|
||||||
if not event['annotation']:
|
|
||||||
unannotated += 1
|
|
||||||
|
|
||||||
total = len(events)
|
# Parse detections JSON
|
||||||
camera_breakdown = ", ".join(f"{cam}: {count}" for cam, count in by_camera.items())
|
dets = None
|
||||||
|
if event['detections']:
|
||||||
|
try:
|
||||||
|
dets = json.loads(event['detections'])
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
return f"{camera_breakdown}"
|
if dets:
|
||||||
|
if cam not in camera_labels:
|
||||||
|
camera_labels[cam] = {}
|
||||||
|
for d in dets:
|
||||||
|
label = d.get('label', 'unknown')
|
||||||
|
camera_labels[cam][label] = camera_labels[cam].get(label, 0) + 1
|
||||||
|
|
||||||
|
# Track most recent detection (first one since ordered DESC)
|
||||||
|
if latest_detection is None:
|
||||||
|
latest_detection = (dets[0].get('label', 'unknown'), cam, event['timestamp'])
|
||||||
|
else:
|
||||||
|
camera_motion_only[cam] = camera_motion_only.get(cam, 0) + 1
|
||||||
|
|
||||||
|
# Format per-camera summaries
|
||||||
|
cam_parts = []
|
||||||
|
all_cams = sorted(set(list(camera_labels.keys()) + list(camera_motion_only.keys())))
|
||||||
|
|
||||||
|
for cam in all_cams:
|
||||||
|
labels = camera_labels.get(cam, {})
|
||||||
|
motion_count = camera_motion_only.get(cam, 0)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if labels:
|
||||||
|
# Sort by count descending
|
||||||
|
for label, count in sorted(labels.items(), key=lambda x: -x[1]):
|
||||||
|
parts.append(f"{count} {label}")
|
||||||
|
if motion_count:
|
||||||
|
parts.append(f"{motion_count} motion")
|
||||||
|
|
||||||
|
cam_parts.append(f"{cam}: {', '.join(parts)}")
|
||||||
|
|
||||||
|
result = " | ".join(cam_parts)
|
||||||
|
|
||||||
|
# Add "last seen" for most recent detection
|
||||||
|
if latest_detection:
|
||||||
|
label, cam, ts = latest_detection
|
||||||
|
try:
|
||||||
|
event_time = datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
||||||
|
now = datetime.now(event_time.tzinfo)
|
||||||
|
mins_ago = int((now - event_time).total_seconds() / 60)
|
||||||
|
if mins_ago < 1:
|
||||||
|
result += f" (last: {label} in {cam}, just now)"
|
||||||
|
else:
|
||||||
|
result += f" (last: {label} in {cam}, {mins_ago}m ago)"
|
||||||
|
except Exception:
|
||||||
|
result += f" (last: {label} in {cam})"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Vision: error ({e})"
|
return f"error ({e})"
|
||||||
|
|
||||||
|
|
||||||
def format_status_for_wakeup() -> str:
|
def format_status_for_wakeup() -> str:
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ def get_status() -> str:
|
|||||||
last_wake = datetime.fromisoformat(state['last_wake'])
|
last_wake = datetime.fromisoformat(state['last_wake'])
|
||||||
time_ago = datetime.now() - last_wake
|
time_ago = datetime.now() - last_wake
|
||||||
status_lines.append(f"Last wake: {last_wake.strftime('%Y-%m-%d %H:%M:%S')} ({int(time_ago.total_seconds()/60)} min ago)")
|
status_lines.append(f"Last wake: {last_wake.strftime('%Y-%m-%d %H:%M:%S')} ({int(time_ago.total_seconds()/60)} min ago)")
|
||||||
except:
|
except Exception:
|
||||||
status_lines.append(f"Last wake: {state.get('last_wake')}")
|
status_lines.append(f"Last wake: {state.get('last_wake')}")
|
||||||
else:
|
else:
|
||||||
status_lines.append("Last wake: Never (daemon just started)")
|
status_lines.append("Last wake: Never (daemon just started)")
|
||||||
@@ -235,7 +235,7 @@ def get_status() -> str:
|
|||||||
status_lines.append(f"Next wake: {next_wake.strftime('%Y-%m-%d %H:%M:%S')} (in {int(time_until.total_seconds()/60)} min)")
|
status_lines.append(f"Next wake: {next_wake.strftime('%Y-%m-%d %H:%M:%S')} (in {int(time_until.total_seconds()/60)} min)")
|
||||||
else:
|
else:
|
||||||
status_lines.append(f"Next wake: OVERDUE (was {next_wake.strftime('%Y-%m-%d %H:%M:%S')})")
|
status_lines.append(f"Next wake: OVERDUE (was {next_wake.strftime('%Y-%m-%d %H:%M:%S')})")
|
||||||
except:
|
except Exception:
|
||||||
status_lines.append(f"Next wake: {state.get('next_wake_timestamp')}")
|
status_lines.append(f"Next wake: {state.get('next_wake_timestamp')}")
|
||||||
else:
|
else:
|
||||||
interval = state.get('interval_minutes', 60)
|
interval = state.get('interval_minutes', 60)
|
||||||
|
|||||||
Reference in New Issue
Block a user