aioafero.v1.conclave package¶
Submodules¶
aioafero.v1.conclave.access module¶
Mint a short-lived Conclave access token via REST.
The Hubspace app calls POST .../accounts/{accountId}/conclaveAccess before
opening the Conclave TLS socket. The OAuth bearer is only used to mint this
short-lived token — the socket itself authenticates with tokens[0].token
from this response, not the JWT (using the JWT was the historic cause of
“hello works, login rejected”).
- class aioafero.v1.conclave.access.ConclaveAccess(host: str, port: int, ssl: bool, compression: bool, token: str, channel_id: str, expires_timestamp: int | None = None)[source]¶
Bases:
objectParsed response from
POST .../conclaveAccess.- Parameters:
host (str) – TLS host for the Conclave socket (e.g.
conclave-stream1.afero.net).port (int) – TLS port for the Conclave socket.
ssl (bool) –
Truewhen the server advertises TLS (alwaysTruein observed captures).compression (bool) –
Truewhen the server announces zlib compression on the wire (the initialhellois sent as a single zlib blob).token (str) – Short-lived single-use Conclave socket access token. Not the OAuth bearer — pass this as
login.accessToken.channel_id (str) – Conclave channel ID (the account UUID).
expires_timestamp (int | None) – Optional epoch milliseconds at which
tokenexpires; observed tokens may also be single-use, so callers should always mint a fresh token on reconnect rather than trust this clock.
- async aioafero.v1.conclave.access.request_conclave_access(bridge: AferoBridgeV1, *, user: bool = True, soft_hub: bool = False) ConclaveAccess[source]¶
Mint a fresh Conclave access token.
Always call this on first connect and on every reconnect — observed tokens are short-lived and may be single-use.
- Parameters:
bridge (AferoBridgeV1) – Initialized
AferoBridgeV1; the bearer token and account ID are read from the bridge.user (bool) – Set
Truefor end-user clients (the only mode observed in captures). AlwaysTruefor HA / app-style consumers.soft_hub (bool) – Set
Trueonly for soft-hub clients. Token shape and channel routing forsoftHub=trueare not yet captured — leaveFalseunless you have verified the response shape.
- Returns:
Parsed
ConclaveAccess.- Raises:
AferoError – If the response is missing the conclave host or an account-scoped token.
- Return type:
aioafero.v1.conclave.client module¶
TLS client for the Conclave push channel.
- class aioafero.v1.conclave.client.ConclaveStatus(*values)[source]¶
Bases:
EnumConnection status of the Conclave push client.
- DISCONNECTED = 'disconnected'¶
- CONNECTING = 'connecting'¶
- CONNECTED = 'connected'¶
- exception aioafero.v1.conclave.client.ConclavePushStaleError[source]¶
Bases:
ConnectionErrorRaised when the socket stays up but no
privatepushes arrive in time.
- class aioafero.v1.conclave.client.ConclaveConnection(reader: StreamReader, writer: StreamWriter, *, access: ConclaveAccess, read_timeout: float = 75.0)[source]¶
Bases:
objectSingle attempt at a Conclave socket session.
- Parameters:
reader (asyncio.StreamReader)
writer (asyncio.StreamWriter)
access (ConclaveAccess)
read_timeout (float)
- __init__(reader: StreamReader, writer: StreamWriter, *, access: ConclaveAccess, read_timeout: float = 75.0) None[source]¶
Bind a Conclave TLS stream to
accesscredentials.- Parameters:
reader (StreamReader)
writer (StreamWriter)
access (ConclaveAccess)
read_timeout (float)
- Return type:
None
- set_read_timeout(timeout: float) None[source]¶
Update the per-read idle timeout (seconds).
- Parameters:
timeout (float)
- Return type:
None
- async open_session() dict[source]¶
Send the opening
{}and return the handshake envelope.- Return type:
- async iter_frames() AsyncIterator[dict][source]¶
Yield frames from the connection until it closes.
- Return type:
- class aioafero.v1.conclave.client.ConclaveClient(bridge: AferoBridgeV1, *, connect: ConnectCallable | None = None, read_timeout: float = 75.0, initial_backoff: float = 1.0, max_backoff: float = 60.0, push_idle_timeout: float | None = None, reconcile_on_reconnect: bool = True)[source]¶
Bases:
objectLong-running Conclave push consumer for an
AferoBridgeV1.- Parameters:
- __init__(bridge: AferoBridgeV1, *, connect: ConnectCallable | None = None, read_timeout: float = 75.0, initial_backoff: float = 1.0, max_backoff: float = 60.0, push_idle_timeout: float | None = None, reconcile_on_reconnect: bool = True) None[source]¶
Attach a reconnecting Conclave consumer to
bridge.- Parameters:
push_idle_timeout (float | None) – When set, end the session if no
privatepush is successfully handled within this many seconds after login while the socket is still receiving heartbeats (idle clock starts atwelcome). Successfulpublicinventory handlers do not reset the clock.Nonedisables (default). Use for zombie sessions wherewelcomesucceeds but state pushes never arrive or stop.reconcile_on_reconnect (bool) – After a failed session, run one REST state poll once login succeeds again (in the background so the TLS dispatch loop can keep acking heartbeats).
bridge (AferoBridgeV1)
connect (ConnectCallable | None)
read_timeout (float)
initial_backoff (float)
max_backoff (float)
- Return type:
None
- property status: ConclaveStatus¶
Return the current Conclave connection status.
- property connected: bool¶
Truewhen the push session has completed login.Prefer
logged_infor socket-session liveness;connectedreflects the emitted lifecycle state used bybridge.events.
- property logged_in: bool¶
Trueafterwelcomeuntil the socket session ends.Cleared before
connectedduring teardown; usestatusfor lifecycle callbacks onevents.
- property seconds_since_last_push: float | None¶
Seconds since the last handled
privatepush, when logged in.
- async wait_until_logged_in(timeout: float = 60.0) bool[source]¶
Wait until a Conclave session completes login.
- async start() None[source]¶
Schedule the background reconnect loop (idempotent).
The loop is owned by this client (
stop()/bridge.close()), not tracked as a bridge ad-hoc job — otherwiseasync_block_until_done()would hang while Conclave is running.- Return type:
None
aioafero.v1.conclave.events module¶
Apply Conclave private and public events to the cached bridge state.
- aioafero.v1.conclave.events.translate_attr_change(device: AferoDevice, attribute: Mapping[str, Any]) AferoState | None[source]¶
Map one Conclave
attributeblock to anAferoState.- Parameters:
device (AferoDevice)
- Return type:
AferoState | None
- aioafero.v1.conclave.events.translate_status_change(status: Mapping[str, Any]) list[AferoState][source]¶
Map a Conclave
statusblock toAferoStaterows.- Parameters:
- Return type:
- async aioafero.v1.conclave.events.apply_attr_change(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Apply a
private/attr_changeevent to the cached device.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- async aioafero.v1.conclave.events.apply_status_change(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Apply a
private/status_changeevent to the cached device.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- async aioafero.v1.conclave.events.apply_invalidate_remove(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Apply a
public/invalidatekind=removeinventory event.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- async aioafero.v1.conclave.events.apply_invalidate_add(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Apply a
public/invalidatekind=addinventory event.target=metadevicesfetches the new metadevice over REST (Conclave only sends the id) and fans outRESOURCE_ADDED/RESOURCE_UPDATEDlike a discovery poll for that payload alone.target=devicesis ignored when the physical device is already cached — the matchingmetadevicesadd carries the semantics we need. When the physical id is unknown, a full discovery poll reconciles inventory.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- async aioafero.v1.conclave.events.apply_public_invalidate(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Dispatch a
public/invalidateinventory frame bykind.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
aioafero.v1.conclave.frame_capture module¶
Buffered NDJSON logging.Handler for Conclave application frames.
Attach to FRAME_LOGGER while Conclave runs;
call flush() (or close()) to harvest. Home Assistant
can do the same and anonymize on Generate Debug later.
- class aioafero.v1.conclave.frame_capture.ConclaveFrameFileHandler(filename: str | Path, *, capacity: int = 32, encoding: str = 'utf-8')[source]¶
Bases:
HandlerBuffer log records in memory and append them to an NDJSON file.
Each emitted record’s formatted message is one JSON object (as produced by the Conclave frame logger). When the buffer reaches
capacity, or whenflush()/close()is called, lines are appended tofilename.- __init__(filename: str | Path, *, capacity: int = 32, encoding: str = 'utf-8') None[source]¶
Create a handler that writes buffered frame lines to
filename.
- emit(record: LogRecord) None[source]¶
Buffer
record; spill to disk when the buffer is full.- Parameters:
record (LogRecord)
- Return type:
None
- aioafero.v1.conclave.frame_capture.attach_frame_capture(path: str | Path, *, buffer_size: int = 32, propagate: bool = False) ConclaveFrameFileHandler[source]¶
Add a
ConclaveFrameFileHandlertoFRAME_LOGGER.Raises the frame logger to
DEBUGand setspropagate. Caller shouldflush()to harvest, thenFRAME_LOGGER.removeHandler(handler)andhandler.close().- Parameters:
- Return type:
aioafero.v1.conclave.frames module¶
Conclave wire-frame decoder.
Server frames arrive as a zlib hello blob, then newline-delimited or
concatenated JSON objects, with bare \n heartbeats mixed in.
Pure helpers (try_parse_zlib_prefix(), index_json_object_start())
are module-level for unit tests; ConclaveFrameDecoder is the
incremental buffer used by the TLS client.
- class aioafero.v1.conclave.frames.ZlibPrefixResult(frame: dict | None, consumed: int, decided: bool)[source]¶
Bases:
objectResult of
try_parse_zlib_prefix().
- aioafero.v1.conclave.frames.index_json_object_start(data: bytes) int | None[source]¶
Return the byte offset of the first
{indata, if any.
- aioafero.v1.conclave.frames.try_parse_zlib_prefix(data: bytes) ZlibPrefixResult[source]¶
Attempt to decompress a leading zlib envelope from
data.- Parameters:
data (bytes)
- Return type:
- class aioafero.v1.conclave.frames.ConclaveFrameDecoder[source]¶
Bases:
objectIncremental decoder for the Conclave server-to-client stream.
aioafero.v1.conclave.protocol module¶
Conclave login and private/public-envelope helpers (no I/O).
- class aioafero.v1.conclave.protocol.PrivateEvent(event: str, data: dict[str, Any])[source]¶
Bases:
objectParsed
privateenvelope from a Conclave push frame.
- class aioafero.v1.conclave.protocol.PublicEvent(event: str, data: dict[str, Any])[source]¶
Bases:
objectParsed
publicenvelope from a Conclave push frame.Inventory changes arrive as
event="invalidate"with adatablock carryingkind(add/remove/update/ …) and usually atarget(metadevices/devices).
- aioafero.v1.conclave.protocol.build_login_frame(access: ConclaveAccess, *, client_type: str = 'aioafero', version: str = '1.0.0', protocol: int = 2, trace: bool = False) dict[str, Any][source]¶
Build the client
loginJSON frame for aConclaveAccess.
- aioafero.v1.conclave.protocol.server_heartbeat_seconds(handshake: dict[str, Any], welcome: dict[str, Any]) float[source]¶
Return the server-advertised heartbeat interval in seconds.
- aioafero.v1.conclave.protocol.parse_private_frame(frame: dict[str, Any]) PrivateEvent | None[source]¶
Extract a
privateevent from a decoded Conclave frame.- Returns:
PrivateEventwhenframecarries a well-formedprivateblock; otherwiseNone.- Parameters:
- Return type:
PrivateEvent | None
- aioafero.v1.conclave.protocol.parse_public_frame(frame: dict[str, Any]) PublicEvent | None[source]¶
Extract a
publicevent from a decoded Conclave frame.- Returns:
PublicEventwhenframecarries a well-formedpublicblock; otherwiseNone.- Parameters:
- Return type:
PublicEvent | None
aioafero.v1.conclave.semantics module¶
Build a per-device attribute index from metadevice semantics.
Conclave attribute.id values map to deviceValues[].key in each device’s
description.functions[] semantics — per-device-profile, never a global
enum.
This module turns the cached AferoDevice.functions into a lookup index
keyed by str(attribute.id). The decoded (functionClass, functionInstance,
value) triple feeds straight back into the existing
AferoState cache that REST polls already populate.
- class aioafero.v1.conclave.semantics.SemanticBinding(function_class: str, function_instance: str | None, value_name: str | None, device_value: str | None)[source]¶
Bases:
objectOne
deviceValues[]entry from a metadevice’s semantics.- Parameters:
function_class (str) –
functionClassfor the parent function (e.g.power,brightness,color-mode).function_instance (str | None) – Optional
functionInstance(e.g.light-power,fan-power).value_name (str | None) – Name of the
functions[].values[]entry (e.g.on,off,sequence,hanukkah-fade).device_value (str | None) – Optional encoded value from
deviceValues[].value— set for enum mappings (e.g. color modes) and blob mappings (e.g. custom effect sequences);Nonefor numeric/free-form values.
- aioafero.v1.conclave.semantics.build_attribute_index(device: AferoDevice) AttributeIndex[source]¶
Walk
device.functionsand index attribute key → binding(s).Multiple bindings per key are normal (e.g. key
1→ power on/off with distinctdeviceValue; key5→ multiple color modes).- Parameters:
device (AferoDevice) – Discovered
AferoDevicewith itsfunctionspopulated (post REST discovery).- Returns:
Dict mapping
str(attribute.id)to a list ofSemanticBindingentries in declaration order.- Return type:
AttributeIndex
- aioafero.v1.conclave.semantics.resolve_binding(index: dict[str, list[SemanticBinding]], attribute_id: object, attribute_value: object, attribute_data: object | None = None) SemanticBinding | None[source]¶
Resolve a single push attribute to one
SemanticBinding.Resolution order:
Look up bindings by
str(attribute_id); returnNoneif unknown.If exactly one binding is registered, use it (numeric / free-form attribute — value carries through unchanged).
Otherwise, prefer a binding whose
device_valuematches the attribute’s decodedvalue(enum keys likepower,color-mode).Fall back to matching
device_valueagainst the rawdatablob (used forcolor-sequence/customon key300where Conclave pushes the encoded blob and the friendly name lives in semantics).
- Parameters:
- Return type:
SemanticBinding | None
- aioafero.v1.conclave.semantics.coerce_rest_state_value(binding: SemanticBinding, attribute_value: object, attribute_data: object | None = None) object[source]¶
Translate a Conclave attribute payload into REST-shaped
AferoState.value.Conclave pushes decoded strings such as
"0"/"1"for booleans and"4000"for color temperature. REST discovery and controllers expect semantic names ("on"/"off") and suffixed units ("4000K").- Parameters:
binding (SemanticBinding)
attribute_value (object)
attribute_data (object | None)
- Return type:
Module contents¶
Conclave push-event client for the Afero IoT API.
Conclave is Afero’s account-scoped TLS push channel. After a short-lived
conclaveAccess token is minted via REST, the client opens a long-lived TLS
connection to conclave-stream1.afero.net:443 and receives private
(attr_change / status_change) and public (invalidate inventory)
frames whenever devices on the account change — regardless of which client
(app, hub, automation) triggered it.
The Conclave socket is subscribe-only: writes still go through REST.
See docs/user/conclave.rst for setup, lifecycle, and limitations.
- class aioafero.v1.conclave.ConclaveAccess(host: str, port: int, ssl: bool, compression: bool, token: str, channel_id: str, expires_timestamp: int | None = None)[source]¶
Bases:
objectParsed response from
POST .../conclaveAccess.- Parameters:
host (str) – TLS host for the Conclave socket (e.g.
conclave-stream1.afero.net).port (int) – TLS port for the Conclave socket.
ssl (bool) –
Truewhen the server advertises TLS (alwaysTruein observed captures).compression (bool) –
Truewhen the server announces zlib compression on the wire (the initialhellois sent as a single zlib blob).token (str) – Short-lived single-use Conclave socket access token. Not the OAuth bearer — pass this as
login.accessToken.channel_id (str) – Conclave channel ID (the account UUID).
expires_timestamp (int | None) – Optional epoch milliseconds at which
tokenexpires; observed tokens may also be single-use, so callers should always mint a fresh token on reconnect rather than trust this clock.
- class aioafero.v1.conclave.ConclaveClient(bridge: AferoBridgeV1, *, connect: ConnectCallable | None = None, read_timeout: float = 75.0, initial_backoff: float = 1.0, max_backoff: float = 60.0, push_idle_timeout: float | None = None, reconcile_on_reconnect: bool = True)[source]¶
Bases:
objectLong-running Conclave push consumer for an
AferoBridgeV1.- Parameters:
- __init__(bridge: AferoBridgeV1, *, connect: ConnectCallable | None = None, read_timeout: float = 75.0, initial_backoff: float = 1.0, max_backoff: float = 60.0, push_idle_timeout: float | None = None, reconcile_on_reconnect: bool = True) None[source]¶
Attach a reconnecting Conclave consumer to
bridge.- Parameters:
push_idle_timeout (float | None) – When set, end the session if no
privatepush is successfully handled within this many seconds after login while the socket is still receiving heartbeats (idle clock starts atwelcome). Successfulpublicinventory handlers do not reset the clock.Nonedisables (default). Use for zombie sessions wherewelcomesucceeds but state pushes never arrive or stop.reconcile_on_reconnect (bool) – After a failed session, run one REST state poll once login succeeds again (in the background so the TLS dispatch loop can keep acking heartbeats).
bridge (AferoBridgeV1)
connect (ConnectCallable | None)
read_timeout (float)
initial_backoff (float)
max_backoff (float)
- Return type:
None
- property status: ConclaveStatus¶
Return the current Conclave connection status.
- property connected: bool¶
Truewhen the push session has completed login.Prefer
logged_infor socket-session liveness;connectedreflects the emitted lifecycle state used bybridge.events.
- property logged_in: bool¶
Trueafterwelcomeuntil the socket session ends.Cleared before
connectedduring teardown; usestatusfor lifecycle callbacks onevents.
- property seconds_since_last_push: float | None¶
Seconds since the last handled
privatepush, when logged in.
- async wait_until_logged_in(timeout: float = 60.0) bool[source]¶
Wait until a Conclave session completes login.
- async start() None[source]¶
Schedule the background reconnect loop (idempotent).
The loop is owned by this client (
stop()/bridge.close()), not tracked as a bridge ad-hoc job — otherwiseasync_block_until_done()would hang while Conclave is running.- Return type:
None
- class aioafero.v1.conclave.ConclaveFrameDecoder[source]¶
Bases:
objectIncremental decoder for the Conclave server-to-client stream.
- class aioafero.v1.conclave.ConclaveFrameFileHandler(filename: str | Path, *, capacity: int = 32, encoding: str = 'utf-8')[source]¶
Bases:
HandlerBuffer log records in memory and append them to an NDJSON file.
Each emitted record’s formatted message is one JSON object (as produced by the Conclave frame logger). When the buffer reaches
capacity, or whenflush()/close()is called, lines are appended tofilename.- __init__(filename: str | Path, *, capacity: int = 32, encoding: str = 'utf-8') None[source]¶
Create a handler that writes buffered frame lines to
filename.
- emit(record: LogRecord) None[source]¶
Buffer
record; spill to disk when the buffer is full.- Parameters:
record (LogRecord)
- Return type:
None
- class aioafero.v1.conclave.ConclaveStatus(*values)[source]¶
Bases:
EnumConnection status of the Conclave push client.
- DISCONNECTED = 'disconnected'¶
- CONNECTING = 'connecting'¶
- CONNECTED = 'connected'¶
- async aioafero.v1.conclave.apply_attr_change(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Apply a
private/attr_changeevent to the cached device.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- async aioafero.v1.conclave.apply_invalidate_add(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Apply a
public/invalidatekind=addinventory event.target=metadevicesfetches the new metadevice over REST (Conclave only sends the id) and fans outRESOURCE_ADDED/RESOURCE_UPDATEDlike a discovery poll for that payload alone.target=devicesis ignored when the physical device is already cached — the matchingmetadevicesadd carries the semantics we need. When the physical id is unknown, a full discovery poll reconciles inventory.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- async aioafero.v1.conclave.apply_invalidate_remove(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Apply a
public/invalidatekind=removeinventory event.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- async aioafero.v1.conclave.apply_public_invalidate(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Dispatch a
public/invalidateinventory frame bykind.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- async aioafero.v1.conclave.apply_status_change(bridge: AferoBridgeV1, payload: dict) bool[source]¶
Apply a
private/status_changeevent to the cached device.- Parameters:
bridge (AferoBridgeV1)
payload (dict)
- Return type:
- aioafero.v1.conclave.attach_frame_capture(path: str | Path, *, buffer_size: int = 32, propagate: bool = False) ConclaveFrameFileHandler[source]¶
Add a
ConclaveFrameFileHandlertoFRAME_LOGGER.Raises the frame logger to
DEBUGand setspropagate. Caller shouldflush()to harvest, thenFRAME_LOGGER.removeHandler(handler)andhandler.close().- Parameters:
- Return type:
- aioafero.v1.conclave.build_attribute_index(device: AferoDevice) AttributeIndex[source]¶
Walk
device.functionsand index attribute key → binding(s).Multiple bindings per key are normal (e.g. key
1→ power on/off with distinctdeviceValue; key5→ multiple color modes).- Parameters:
device (AferoDevice) – Discovered
AferoDevicewith itsfunctionspopulated (post REST discovery).- Returns:
Dict mapping
str(attribute.id)to a list ofSemanticBindingentries in declaration order.- Return type:
AttributeIndex
- async aioafero.v1.conclave.request_conclave_access(bridge: AferoBridgeV1, *, user: bool = True, soft_hub: bool = False) ConclaveAccess[source]¶
Mint a fresh Conclave access token.
Always call this on first connect and on every reconnect — observed tokens are short-lived and may be single-use.
- Parameters:
bridge (AferoBridgeV1) – Initialized
AferoBridgeV1; the bearer token and account ID are read from the bridge.user (bool) – Set
Truefor end-user clients (the only mode observed in captures). AlwaysTruefor HA / app-style consumers.soft_hub (bool) – Set
Trueonly for soft-hub clients. Token shape and channel routing forsoftHub=trueare not yet captured — leaveFalseunless you have verified the response shape.
- Returns:
Parsed
ConclaveAccess.- Raises:
AferoError – If the response is missing the conclave host or an account-scoped token.
- Return type: