This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
JamesII is a distributed smart home automation system written in Python 3. It uses a plugin-based architecture where nodes communicate via RabbitMQ message broker using AMQP protocol. The system supports distributed operation with one master node that holds configuration and multiple client nodes that can run specific plugins.
- RabbitMQ Broker: All nodes connect to a centralized RabbitMQ server for communication
- Broadcast Channels: Uses fanout exchanges for pub/sub messaging between nodes
discovery: Node presence, ping/pong, command registrationconfig: Configuration distribution from master to clientsrequest/response: Command execution requests and responsesmsg: User messages (notifications, alerts)presence: User presence detection (home/away)dataRequest/dataResponse: Data queries between pluginsno_alarm_clock: Calendar-based alarm clock stateevents_today: Calendar events happening today
- Master Node: Has
config/config.yaml, distributes configuration, coordinates other nodes - Client Nodes: Receive configuration from master, run assigned plugins
- Passive Nodes: Special mode for external integrations (e.g., CLI-only access)
The Core class is the main orchestrator:
- Manages RabbitMQ connection and broadcast channels
- Loads and instantiates plugins based on configuration
- Processes timeouts and events in main loop (configurable sleep time)
- Handles node discovery and presence tracking
- Thread-safe operations using
core_lockRLock
Plugins are the functional units of JamesII:
Plugin Modes:
AUTOLOAD: Loads automatically on all nodes if requirements metMANAGED: Only loads if specified in config under plugin name'snodeslistMANUAL: Must be started manually (e.g., CLI, HTTP server)
Plugin Structure:
- Each plugin file must define a
descriptordict with:name,help_text,command,mode,class,detailsNames - Plugin classes inherit from
Pluginbase class - Plugins register commands via hierarchical
Commandobjects - Plugins can spawn
PluginThreadworkers for background tasks
Key Plugin Methods:
start(): Called when core is ready, register timeouts hereterminate(): Cleanup on shutdownprocess_message(message): Handle JamesMessage broadcastsprocess_presence_event(before, now): React to user presence changeshandle_request()/handle_response(): Process command requests/responsesreturn_status(): Return plugin state for monitoring
Commands are hierarchical: root → plugin_command → subcommand → subsubcommand
- Commands support
@hostname1,hostname2prefix for targeted execution - Commands can be chained with
&&separator - Command aliases defined in config for shortcuts
- broker.yaml: RabbitMQ connection settings (required on all nodes)
- config.yaml: Master configuration (only on master node)
- Plugin configurations under plugin name keys
nodeslist under each plugin specifies which hosts run that pluginlocationsmaps hostnames to location namescommand_aliasesfor shortcut commands
- Plugin-specific configs in plugin section of main config
# Create and activate virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install base requirements
pip install -r requirements.txt
# Install plugin-specific requirements (optional)
pip install -r requirements-mpd-client.txt
pip install -r requirements-jabber.txt
# ... etc for other plugins# Start master node (from src/ directory)
cd src
python3 james.py
# Start client node
cd src
python3 james.py
# Start CLI interface
cd src
python3 cli.py
# Run single CLI command and exit
cd src
python3 cli.py help
python3 cli.py sys status# Copy example configs and edit
cp config/broker.yaml.example config/broker.yaml
cp config/config.yaml.example config/config.yaml
# Edit broker.yaml with RabbitMQ server details
# Edit config.yaml with your plugins and settings (master node only)apt-get install rabbitmq-server
rabbitmqctl add_user james2 password
rabbitmqctl add_vhost james2
rabbitmqctl set_permissions -p james2 james2 ".*" ".*" ".*"- Create file in
src/james/plugin/(e.g.,my-plugin.py) - Import plugin base:
from james.plugin import * - Define plugin class inheriting from
Plugin - Define
descriptordict at module level - Register commands in
__init__ - Implement event handlers as needed
- Use
self.core.add_timeout(seconds, handler, *args)for scheduled execution - Access config via
self.config(auto-populated from core config) - Use
self.loggerfor logging (child of core logger) - Save state in
return_status(), load in__init__viaself.load_state(name, default) - Use
self.send_command(args)to trigger other plugin commands - Use
self.core.new_message(name)andmessage.send()for notifications
- Use CLI to test commands interactively
- Check logs in console output (set
debug: truein plugin config) - Use
plugin_name debug oncommand to enable debug logging at runtime - Use
allstatuscommand to see all plugin states
src/james.py- Main entry point for james daemonsrc/cli.py- CLI interface entry pointsrc/james/__init__.py- Core class implementation (1171 lines)src/james/plugin/__init__.py- Plugin base classessrc/james/plugin/*.py- Individual pluginssrc/james/command.py- Hierarchical command systemsrc/james/jamesmessage.py- Message abstraction for notificationssrc/james/broadcastchannel.py- RabbitMQ channel wrappersrc/james/presence.py- Presence trackingconfig/broker.yaml- RabbitMQ settings (required)config/config.yaml- Master node configuration~/.james_cli_history- CLI command history~/.james_presences- Saved presence state~/.james_stats- Plugin state persistence~/.james_system_messages- System messages log
- Main event loop in
Core.run()processes RabbitMQ events and timeouts - Use
core.lock_core()/core.unlock_core()for thread-safe RabbitMQ operations - Plugin threads should communicate via
core.add_timeout()to synchronize with main thread - Timeout handlers run in main thread context
- CLI/Plugin calls
send_command(args)orsend_request() - Command sent over
requestchannel - All nodes receive via
request_listener - Matching plugin's
handle_request()processes command - Plugin sends response via
send_response() - Response broadcast on
responsechannel - Requesting plugin's
handle_response()receives result
- Core catches SIGINT, SIGTERM, SIGHUP, SIGQUIT, SIGTSTP, SIGSEGV
- Signals trigger graceful shutdown via
terminate() - Plugins have 10 seconds to terminate, threads have 30 seconds to exit
- State is saved to files on shutdown
signal.SIGALRMnot available on Windows (timeout mechanism adjusted)- Some signal handlers are POSIX-only
- File paths use
os.path.join()andos.path.expanduser()for compatibility
help # List all commands
help <command> # Show subcommands
sys status # System status
nodes # Show online nodes
exit # Exit CLI
@hostname command args # Run on specific host
@host1,host2 command args # Run on multiple hosts
command1 && command2 # Execute both commands (sent sequentially, not waited)
mcp in 30s command args # Run in 30 seconds
mcp in 5m command args # Run in 5 minutes
mcp at 14:30 command args # Run at 14:30 today
mcp at 14:30:00 command args # With seconds
mcp at 14:30 2024-12-25 command # Run at specific date/time
mcp show # List scheduled commands
mcp remove <timestamp> <command> # Remove scheduled command
Multiple plugins can detect user presence (Bluetooth, calendar, etc.). Presence events trigger process_presence_event() on all plugins.
- Google Calendar plugin provides
events_today - CalDAV plugin for Nextcloud/other CalDAV servers
- Timer plugin can schedule commands based on calendar events
locationsmaps hostnames to location names (e.g., "home", "office")nodes_main_loop_sleepsets per-node event loop sleep times- Plugins configure
nodeslist to specify which hosts run them
- Plugins save state in
~/.james_statsviareturn_status() - State restored on startup via
load_state(name, default) - Timer commands persist in
~/.james_timer_store - Presence persists in
~/.james_presences