This document provides a comprehensive narrative of how a connection is established when a new instance of PyISYoX is created, including the sequence of REST API endpoint calls and event stream initialization.
- Overview
- Step 1: Initialization
- Step 2: Connection Testing
- Step 3: Platform Initialization
- Step 4: Event Stream Setup
- Complete Endpoint Call Sequence
- Connection Architecture
The PyISYoX connection lifecycle consists of four main phases:
- Initialization - Creating the ISY object and connection infrastructure
- Connection Testing - Validating credentials and fetching ISY configuration
- Platform Initialization - Loading entities (nodes, programs, variables, etc.)
- Event Stream Setup - Establishing real-time updates via WebSocket or TCP
from pyisyox import ISY
from pyisyox.connection import ISYConnectionInfo
# Create connection info
connection_info = ISYConnectionInfo(
"http://polisy.local:8080",
"admin",
"password"
)
# Create ISY instance
isy = ISY(connection_info, use_websocket=True)-
ISYConnectionInfo Creation (
connection.py:42-63)- Parses the URL and determines if HTTPS is used
- Creates REST URL:
{url}/rest - Creates WebSocket URL:
{rest_url.replace('http', 'ws')}/subscribe - Stores authentication credentials as
aiohttp.BasicAuth
-
ISY Class Instantiation (
isy.py:62-94)- Creates
Connectionobject with connection info - Initializes connection semaphore:
- ISY994: 2 HTTPS / 5 HTTP concurrent connections
- IoX: 20 HTTPS / 50 HTTP concurrent connections (upgraded later)
- Creates
aiohttp.ClientSessionwith connection pooling - Initializes platform classes (executed before any network calls):
Clock- ISY time/location managementNetworkResources- Network commandsVariables- ISY variables (integer and state)Programs- ISY programsNodes- Devices, groups, and foldersNodeServers- Polyglot node server definitions
- Creates
WebSocketClient(ifuse_websocket=True) or prepares for TCPEventStream - Initializes event emitters for connection and status events
- Creates
Important: At this point, no network calls have been made. The ISY object exists but is not connected.
# Must be called to actually connect
await isy.initialize()The initialize() method begins with connection validation (isy.py:96-106):
self.config = await self.conn.test_connection()Purpose: Validate connection and retrieve ISY system configuration
Called By: Connection.test_connection() → Configuration.update() (configuration.py:101-146)
Response Contains:
- ISY UUID (unique identifier)
- Firmware version
- Platform type (ISY994 vs IoX)
- Device model and name
- Installed features (Z-Wave, Networking Module, Portal Integration, etc.)
- Module capabilities (variables enabled, node definitions, etc.)
Configuration Detection:
if self.config.platform == "IoX":
self.conn.increase_available_connections()If the ISY is running on IoX hardware (eisy, Polisy), the connection limits are increased:
- From 2 → 20 HTTPS connections
- From 5 → 50 HTTP connections
This allows for faster parallel loading of large systems.
After connection validation, platform data is loaded in parallel using asyncio.gather() (isy.py:111-129).
isy_setup_tasks = []
if nodes:
isy_setup_tasks.append(self.nodes.initialize())
if clock:
isy_setup_tasks.append(self.clock.update())
if programs:
isy_setup_tasks.append(self.programs.update())
if variables:
isy_setup_tasks.append(self.variables.update())
if networking:
isy_setup_tasks.append(self.networking.update())
await asyncio.gather(*isy_setup_tasks)Each platform loads concurrently to minimize initialization time. Let's examine each platform's endpoint calls:
Method: Nodes.initialize() (nodes/__init__.py:74-92)
The Nodes platform requires two REST calls with special orchestration:
Purpose: Get current status values for all nodes
Why First: This endpoint typically takes the longest to download, so it's started as a background task while nodes are being loaded.
Response Contains:
- Node addresses
- Current status values for all properties
- Unit of measurement (UOM) codes
- Precision/formatting information
status_task = asyncio.create_task(self.update_status())Purpose: Get node definitions and metadata
Response Contains:
- Folders (organizational hierarchy)
- Nodes (individual devices):
- Address, name, type
- Device category (Insteon type, Z-Wave category, etc.)
- Node definition ID (for custom node servers)
- Parent/child relationships
- Device family (Insteon, Z-Wave, Zigbee, Node Server, etc.)
- Enabled/disabled state
- Groups (ISY scenes/controllers)
nodes_task = asyncio.create_task(self.update())Node Parsing Flow:
- Parse folders first (organizational structure)
- Parse individual nodes (devices)
- Parse groups (scenes)
- Once both tasks complete, merge status data into node objects
Result: All nodes are loaded with their current status, properties, and metadata.
Method: Programs.update() (programs/__init__.py:60-86)
Purpose: Get all ISY programs with folder hierarchy
Query Parameter: subfolders=true ensures nested folders are included
Response Contains:
- Program folders (organizational hierarchy)
- Programs:
- ID, name, parent folder
- Status (condition: true/false)
- Enabled/disabled state
- Run at startup flag
- Last run time
- Last finish time
- Running status (idle, running then, running else)
Parsing: Programs are differentiated from folders using the folder tag in the XML response.
Method: Variables.update() (variables/__init__.py:53-77)
The Variables platform makes four parallel requests to fetch both definitions and current values:
Purpose: Get integer variable definitions (names, IDs, precision)
Purpose: Get state variable definitions (names, IDs, precision)
Purpose: Get current values for all integer variables
Purpose: Get current values for all state variables
endpoints = [
[URL_VARIABLES, URL_DEFINITIONS, VAR_INTEGER], # /rest/vars/definitions/1
[URL_VARIABLES, URL_DEFINITIONS, VAR_STATE], # /rest/vars/definitions/2
[URL_VARIABLES, URL_GET, VAR_INTEGER], # /rest/vars/get/1
[URL_VARIABLES, URL_GET, VAR_STATE], # /rest/vars/get/2
]Why Separate Calls: ISY distinguishes between:
- Integer variables (type 1): General-purpose integer storage
- State variables (type 2): Program state storage
Variable Parsing:
- Check if variables are enabled in config
- Parse definitions (names, IDs, precision/scale)
- Parse current values (initial value, current value)
- Merge definitions with values to create Variable objects
Edge Cases Handled:
- No variables defined: returns empty
- Single variable: response is a dict instead of list
- Variables disabled in ISY config
Method: NetworkResources.update() (networking.py:48-71)
Purpose: Get network resource commands
Conditional: Only called if config.networking or config.portal is enabled
Response Contains:
- Network resource IDs and names
- Command type (GET, POST, etc.)
- URL or IP address
- Authentication details
- Enabled/disabled state
Note: Network resources are typically used for:
- HTTP GET/POST commands
- Integration with external systems
- Custom automation triggers
Method: Clock.update() (clock.py)
Purpose: Get ISY clock/location information
Response Contains:
- Current date/time (NTP timestamp)
- Time zone offset
- DST (Daylight Saving Time) status
- Latitude and longitude
- Sunrise time (calculated)
- Sunset time (calculated)
- Military time format preference
Special Handling: ISY uses NTP timestamps with a custom EPOCH offset (36524 days). PyISYoX converts these to Python datetime objects.
Method: NodeServers.update() (node_servers.py)
Purpose: Get Polyglot node server profile definitions
Conditional: Only called if node_servers=True in initialize()
Response Contains:
- Node server slot information
- Node type definitions (custom device types)
- Command definitions
- Status definitions
- Editor definitions (for admin console)
Use Case: Required for custom node servers (Polyglot plugins) that define non-standard device types beyond Insteon/Z-Wave.
After all platforms are initialized, real-time event updates can be enabled.
Method: WebSocketClient.start() (events/websocket.py:68-74)
Protocol Headers:
{
"Sec-WebSocket-Protocol": "ISYSUB",
"Sec-WebSocket-Version": "13",
"Origin": "com.universal-devices.websockets.isy"
}Connection Process:
- Establish WebSocket connection with ISY
- Authenticate using same credentials as REST API
- Receive stream ID from ISY
- Begin receiving real-time events as XML messages
Heartbeat Monitoring:
- ISY sends heartbeat every 30 seconds
- If heartbeat missed by 35 seconds (30 + 5 grace), connection is reset
- Auto-reconnect with exponential backoff: 0.01s, 1s, 10s, 30s, 60s
Event Types Received:
- Node status changes
- Program status changes
- Variable value changes
- System status (BUSY, IDLE, SAFE_MODE, etc.)
- Trigger events (DON, DOF, etc.)
Method: EventStream (events/tcpsocket.py)
Connection: Raw TCP socket to ISY on HTTP port
If WebSocket is disabled (use_websocket=False):
isy.auto_update = True # Starts TCP event streamProcess:
- Opens TCP connection to ISY
- Sends subscription request
- Receives chunked XML event data
- Manually parses XML messages (more complex than WebSocket)
Note: WebSocket is preferred and enabled by default. TCP is only used for older ISY994 firmware that doesn't support WebSockets.
All events (WebSocket or TCP) are processed by EventRouter (events/router.py):
class EventRouter:
def process_event(self, event_data):
match event.action:
case "_0": # Heartbeat
self.websocket.heartbeat()
case "_1": # Node changed
isy.nodes.update_received(event)
case "_2": # Variable changed
isy.variables.update_received(event)
case "_3": # Program changed
isy.programs.update_received(event)
# ... etcEvents are routed to the appropriate platform's update_received() method, which updates entity state and fires event notifications.
Here is the complete order of REST API calls when initializing PyISYoX with all options enabled:
GET /rest/config- Validate connection and get system configuration
GET /rest/status- Get all node status values (started first, longest)GET /rest/nodes- Get all node definitionsGET /rest/programs?subfolders=true- Get all programsGET /rest/vars/definitions/1- Get integer variable definitionsGET /rest/vars/definitions/2- Get state variable definitionsGET /rest/vars/get/1- Get integer variable valuesGET /rest/vars/get/2- Get state variable valuesGET /rest/networking/resources- Get network resources (if enabled)GET /rest/time- Get clock/location infoGET /rest/profiles/ns- Get node server definitions (if enabled)
- WebSocket
ws://{host}/rest/subscribe- Establish event stream
- Minimum: 1 config + 2 platform calls = 3 requests (minimal setup)
- Typical: 1 config + 10 platform calls = 11 requests (full setup)
- Maximum: 1 config + 11 platform calls = 12 requests (with node servers)
Performance: All platform calls execute in parallel via asyncio.gather(), limited only by the connection semaphore (2-20 concurrent connections depending on platform).
PyISYoX uses aiohttp.ClientSession with connection pooling for efficiency:
session = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(
limit=MAX_CONNECTIONS, # 2 or 20 for HTTPS
limit_per_host=MAX_CONNECTIONS
)
)Benefits:
- Reuses TCP connections across requests
- Reduces handshake overhead
- Improves performance for parallel loading
Every REST request includes automatic retry with exponential backoff (connection.py:128-208):
Retry Strategy:
MAX_RETRIES = 5
RETRY_BACKOFF = [0.01, 0.10, 0.25, 1, 2] # SecondsRetry Conditions:
503 Service Unavailable- ISY too busy- Timeout errors (30s default)
- Network errors (connection reset, disconnected)
Non-Retry Conditions:
401 Unauthorized- Invalid credentials (raises exception immediately)404 Not Found- Invalid endpoint (returns None or "" ifok404=True)
To prevent overwhelming the ISY, all requests are controlled by an asyncio.Semaphore:
async with self.semaphore:
async with self.req_session.get(url, ...) as response:
# Process responseLimits:
- ISY994 HTTPS: 2 concurrent connections
- ISY994 HTTP: 5 concurrent connections
- IoX HTTPS: 20 concurrent connections
- IoX HTTP: 50 concurrent connections
This ensures PyISYoX never exceeds the ISY's connection limits, which would cause request failures.
PyISYoX tracks connection state through event notifications:
class EventStreamStatus(StrEnum):
NOT_STARTED = "not_started"
INITIALIZING = "stream_initializing"
LOADED = "stream_loaded"
CONNECTED = "connected"
DISCONNECTED = "disconnected"
LOST_CONNECTION = "lost_connection"
RECONNECTING = "reconnecting"
STOP_UPDATES = "stop_updates"State Transitions:
NOT_STARTED- Initial stateINITIALIZING- WebSocket connectingLOADED- First heartbeat receivedCONNECTED- Fully connected and receiving eventsLOST_CONNECTION- Connection dropped (auto-reconnects)RECONNECTING- Attempting to reconnectDISCONNECTED- Cleanly disconnectedSTOP_UPDATES- Event stream stopped by user
The PyISYoX connection flow is designed for:
- Reliability: Automatic retries, connection pooling, heartbeat monitoring
- Performance: Parallel loading, connection semaphores, optimal request ordering
- Flexibility: Optional platform loading, WebSocket or TCP events
- Real-time: Event-driven updates via WebSocket with auto-reconnect
By understanding this flow, developers can:
- Optimize initialization for specific use cases
- Debug connection issues effectively
- Extend PyISYoX with new platforms or features
- Integrate PyISYoX into larger applications (like Home Assistant)
For more information, see: