Mingcedar

Product: Mingcedar CONTROL

IO State API: authorization, exact reads, and streaming | Mingcedar

Prepare an IO State-enabled HTTPS URL, healthy Tags, and an io:manage account. Clients read collected state; the API neither polls PLCs nor provides writes, alarms, or historical trends.

Published Updated

<div class="manual-doc-article manual-doc-article-section"><h2 id="io-state-api">Developer tutorial: IO State API: authorization, exact reads, and streaming</h2>

Create a Profile and Access Key, read SignalRefs, obtain a single-use Ticket, and subscribe through WebSocket.

<div class="doc-card-list doc-tutorial">

<article class="doc-card">

<h3 id="io-state-api-item-1">Prerequisites and responsibility</h3>

<p>Prepare an IO State-enabled HTTPS URL, healthy Tags, and an io:manage account. Clients read collected state; the API neither polls PLCs nor provides writes, alarms, or historical trends.</p>

<ul>

<li>Copy hq://io/... SignalRefs from the catalog; derived bits use hq://io-bit/.... Display names are not stable identities.</li>

<li>Calculated Tags are outside this IO State V2 SignalRef scope; use the dedicated calculated-value consumer interface.</li>

<li>Access Keys are server-side client credentials. Keep them out of public HTML, logs, screenshots, and shared code. Runtime custom pages use the page SDK.</li>

</ul>

</article>

<article class="doc-card">

<h3 id="io-state-api-item-2">1. Create a Profile for one signal</h3>

<p>A Profile defines delivered signals. Start with one good-quality physical Tag’s full SignalRef.</p>

<ol>

<li>Open IO State API → Subscription Profiles and click New Profile.</li>

<li>Set Name to Equipment Status, Code to equipment_status, Membership mode to frozen, and check Enable Profile.</li>

<li>Click Add Selector and choose tagRef. Check Include and Required, then paste the complete hq://io/... URI into SignalRef.</li>

<li>For this single-signal exercise, keep the default advanced delivery, projection, and limits. Click Save draft.</li>

<li>Select the saved Profile row and click Preview in its details.</li>

</ol>

<p class="doc-step-result"><strong>Expected result</strong> Preview resolves one signal and permits publication; drafts cannot be subscribed to.</p>

<div class="doc-step-troubleshooting"><p><strong>If the result differs</strong></p><ul>

<li>Zero resolved signals or a required-match failure: check the complete SignalRef against the available catalog; do not replace UUIDs with display names.</li>

<li>Cannot publish: read the preview findings, correct the selector, save, and preview again.</li>

</ul></div>

</article>

<article class="doc-card">

<h3 id="io-profile-publish">2. Publish the Profile and confirm its Revision</h3>

<p>Publication uses the saved, previewed draft. Revision identifies the exact version a client subscribes to.</p>

<ol>

<li>Click Publish Revision below the passing preview.</li>

<li>Confirm that the dialog shows one signal, then publish.</li>

<li>In details, verify Published status and a Current Revision value. Open Revision history to inspect the new record.</li>

<li>Record the Profile code and Revision. The client query below returns its Profile ID; code and ID are not interchangeable.</li>

</ol>

<p class="doc-step-result"><strong>Expected result</strong> A Revision is published. If another user changes the draft, refresh and preview again; do not override the conflict.</p>

</article>

<article class="doc-card">

<h3 id="io-state-api-item-3">3. Generate an Access Key for the client</h3>

<p>An Access Key identifies the caller. Create the credential here, then configure V2 authorization in the next step.</p>

<ul>

<li>Use X-IO-State-Key, not a Portal administrator login Token.</li>

<li>Available Profiles include id, revision, resolvedCount, membershipHash, streamAllowed, and adHocReadAllowed. Check the mode flags.</li>

<li>Specified revision, count, and sequence fields are decimal strings. Avoid conversion to a potentially lossy JavaScript Number.</li>

</ul>

<ol>

<li>Open Access Keys and click Create Access Key. Set Client name to Equipment Reporting.</li>

<li>Set the required expiration, check Enable now and Allow ad-hoc reads, and use Concurrent stream limit 1.</li>

<li>Choose Selected scope, select Tags, enter the same target as device-code.tag-code, and click Add scope. The creation form requires at least one scope item. Do not enter the hq:// URI or choose All IO State here.</li>

<li>Confirm that only the intended Tag is listed. After creation, bind the Profile and add its UUID-based V2 native Scope in the next step; the dotted code in this form does not replace V2 authorization.</li>

<li>Click Generate Key. Copy the full key while it is visible and store it in the client’s credential store.</li>

</ol>

<p class="doc-step-result"><strong>Expected result</strong> The client is enabled and the full key saved; a key hint cannot authenticate requests.</p>

</article>

<article class="doc-card">

<h3 id="io-native-scope">4. Bind the Profile and add a native TagRef Scope</h3>

<p>With All IO State off, signals must be in both the bound Profile and a native Scope; Profile binding alone grants no scope.</p>

<ol>

<li>Select Equipment Reporting in the ledger, find V2 authorization, and click Edit V2 authorization.</li>

<li>Check Equipment Status under Subscription Profiles.</li>

<li>Under Native TagRef Scopes, select Tag and enter the SignalRef’s IO Domain UUID, Device UUID, and Tag UUID from the catalog. Use identities, not codes.</li>

<li>Click Add and confirm the Scope appears in the list, then click Save V2 authorization.</li>

<li>After closing the editor, verify one bound Profile and one native Scope, with identities matching the target Tag.</li>

</ol>

<p class="doc-step-result"><strong>Expected result</strong> The key has both the Profile binding and a single-Tag Scope. Changing authorization stops its existing streams; clients must reconnect.</p>

<div class="doc-step-troubleshooting"><p><strong>If the result differs</strong></p><ul>

<li>Zero native Scopes: values may have been entered without clicking Add. Add the Scope and save.</li>

<li>Profile listed but read denied: check the three Scope identities and Allow ad-hoc reads before changing the scope.</li>

</ul></div>

</article>

<article class="doc-card">

<h3 id="io-client-setup">5. Create the external client folder</h3>

<p>Keep these files on the client computer or container, outside Integration Script. The launcher prompts for the URL and key without echoing the key.</p>

<ol>

<li>Create an io-client folder on the external client and save the code below as run_io_client.py.</li>

<li>Save the next section’s read code as io_read.py in the same folder. Match filenames exactly without a .txt extension.</li>

<li>Open a terminal in that folder and run python run_io_client.py. On Windows with the py launcher, use py -3 run_io_client.py.</li>

<li>Enter the actual HTTPS root URL and full Access Key. The launcher lists this key’s Profiles, IDs, revisions, and permission flags.</li>

<li>Enter read at the Mode prompt, then paste the complete SignalRef matching the Scope.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>run_io_client.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: run_io_client.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># External client: run_io_client.py</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> getpass</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> json</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> os</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> urllib.request </span><span style="color:#D73A49">import</span><span style="color:#24292E"> Request, urlopen</span></span>

<span class="line"></span>

<span class="line"><span style="color:#24292E">os.environ[</span><span style="color:#032F62">"HQCONTROL_URL"</span><span style="color:#24292E">] </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> input</span><span style="color:#24292E">(</span><span style="color:#032F62">"Mingcedar HTTPS URL: "</span><span style="color:#24292E">).strip().rstrip(</span><span style="color:#032F62">"/"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#D73A49"> not</span><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_URL"</span><span style="color:#24292E">].startswith(</span><span style="color:#032F62">"https://"</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Enter the configured HTTPS URL"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E">os.environ[</span><span style="color:#032F62">"HQCONTROL_IO_KEY"</span><span style="color:#24292E">] </span><span style="color:#D73A49">=</span><span style="color:#24292E"> getpass.getpass(</span><span style="color:#032F62">"Access Key: "</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E">request </span><span style="color:#D73A49">=</span><span style="color:#24292E"> Request(</span></span>

<span class="line"><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_URL"</span><span style="color:#24292E">] </span><span style="color:#D73A49">+</span><span style="color:#032F62"> "/api/v2/integration/io-state/profiles"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> headers</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#032F62">"X-IO-State-Key"</span><span style="color:#24292E">: os.environ[</span><span style="color:#032F62">"HQCONTROL_IO_KEY"</span><span style="color:#24292E">]},</span></span>

<span class="line"><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49">with</span><span style="color:#24292E"> urlopen(request, </span><span style="color:#E36209">timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">10</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> response:</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(json.dumps(json.load(response), </span><span style="color:#E36209">indent</span><span style="color:#D73A49">=</span><span style="color:#005CC5">2</span><span style="color:#24292E">))</span></span>

<span class="line"></span>

<span class="line"><span style="color:#24292E">mode </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> input</span><span style="color:#24292E">(</span><span style="color:#032F62">"Mode (read/stream): "</span><span style="color:#24292E">).strip()</span></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#24292E"> mode </span><span style="color:#D73A49">==</span><span style="color:#032F62"> "read"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_SIGNAL_REF"</span><span style="color:#24292E">] </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> input</span><span style="color:#24292E">(</span><span style="color:#032F62">"SignalRef: "</span><span style="color:#24292E">).strip()</span></span>

<span class="line"><span style="color:#D73A49"> from</span><span style="color:#24292E"> io_read </span><span style="color:#D73A49">import</span><span style="color:#24292E"> main</span></span>

<span class="line"><span style="color:#D73A49">elif</span><span style="color:#24292E"> mode </span><span style="color:#D73A49">==</span><span style="color:#032F62"> "stream"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_PROFILE_ID"</span><span style="color:#24292E">] </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> input</span><span style="color:#24292E">(</span><span style="color:#032F62">"Published Profile ID: "</span><span style="color:#24292E">).strip()</span></span>

<span class="line"><span style="color:#D73A49"> from</span><span style="color:#24292E"> io_stream </span><span style="color:#D73A49">import</span><span style="color:#24292E"> main</span></span>

<span class="line"><span style="color:#D73A49">else</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Choose read or stream"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E">main()</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> The Profile list contains the Equipment Status record. The read exercise needs adHocReadAllowed; streaming needs streamAllowed. Output does not contain the full key.</p>

</article>

<article class="doc-card">

<h3 id="io-state-api-item-4">6. Save the read program and inspect the response</h3>

<p>Save io_read.py beside run_io_client.py and launch read mode. The launcher sets variables and calls main(); only the standard library is required.</p>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>io_read.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: io_read.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># External client: io_read.py (standard library only)</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> json</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> os</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> urllib.request </span><span style="color:#D73A49">import</span><span style="color:#24292E"> Request, urlopen</span></span>

<span class="line"></span>

<span class="line"><span style="color:#005CC5">BASE_URL</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_URL"</span><span style="color:#24292E">].rstrip(</span><span style="color:#032F62">"/"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#005CC5">ACCESS_KEY</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_IO_KEY"</span><span style="color:#24292E">]</span></span>

<span class="line"><span style="color:#005CC5">SIGNAL_REF</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_SIGNAL_REF"</span><span style="color:#24292E">]</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">():</span></span>

<span class="line"><span style="color:#24292E"> request </span><span style="color:#D73A49">=</span><span style="color:#24292E"> Request(</span></span>

<span class="line"><span style="color:#005CC5"> BASE_URL</span><span style="color:#D73A49"> +</span><span style="color:#032F62"> "/api/v2/integration/io-state/read"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> data</span><span style="color:#D73A49">=</span><span style="color:#24292E">json.dumps({</span><span style="color:#032F62">"signalRefs"</span><span style="color:#24292E">: [</span><span style="color:#005CC5">SIGNAL_REF</span><span style="color:#24292E">], </span><span style="color:#032F62">"include"</span><span style="color:#24292E">: [</span><span style="color:#032F62">"metadata"</span><span style="color:#24292E">]}).encode(),</span></span>

<span class="line"><span style="color:#E36209"> headers</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#032F62">"X-IO-State-Key"</span><span style="color:#24292E">: </span><span style="color:#005CC5">ACCESS_KEY</span><span style="color:#24292E">, </span><span style="color:#032F62">"Content-Type"</span><span style="color:#24292E">: </span><span style="color:#032F62">"application/json"</span><span style="color:#24292E">},</span></span>

<span class="line"><span style="color:#E36209"> method</span><span style="color:#D73A49">=</span><span style="color:#032F62">"POST"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> urlopen(request, </span><span style="color:#E36209">timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">10</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> response:</span></span>

<span class="line"><span style="color:#24292E"> result </span><span style="color:#D73A49">=</span><span style="color:#24292E"> json.load(response)</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> result.get(</span><span style="color:#032F62">"type"</span><span style="color:#24292E">) </span><span style="color:#D73A49">!=</span><span style="color:#032F62"> "io_state_read"</span><span style="color:#D73A49"> or</span><span style="color:#005CC5"> len</span><span style="color:#24292E">(result.get(</span><span style="color:#032F62">"items"</span><span style="color:#24292E">, [])) </span><span style="color:#D73A49">!=</span><span style="color:#005CC5"> 1</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> RuntimeError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Unexpected read response"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> item </span><span style="color:#D73A49">=</span><span style="color:#24292E"> result[</span><span style="color:#032F62">"items"</span><span style="color:#24292E">][</span><span style="color:#005CC5">0</span><span style="color:#24292E">]</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> item.get(</span><span style="color:#032F62">"quality"</span><span style="color:#24292E">) </span><span style="color:#D73A49">!=</span><span style="color:#032F62"> "good"</span><span style="color:#D73A49"> or</span><span style="color:#032F62"> "state"</span><span style="color:#D73A49"> not</span><span style="color:#D73A49"> in</span><span style="color:#24292E"> item:</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> RuntimeError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Signal unavailable: "</span><span style="color:#D73A49"> +</span><span style="color:#24292E"> item.get(</span><span style="color:#032F62">"qualityDetail"</span><span style="color:#24292E">, </span><span style="color:#032F62">"unknown"</span><span style="color:#24292E">))</span></span>

<span class="line"><span style="color:#24292E"> state </span><span style="color:#D73A49">=</span><span style="color:#24292E"> item[</span><span style="color:#032F62">"state"</span><span style="color:#24292E">]</span></span>

<span class="line"><span style="color:#24292E"> typed </span><span style="color:#D73A49">=</span><span style="color:#24292E"> state[</span><span style="color:#032F62">"value"</span><span style="color:#24292E">]</span></span>

<span class="line"><span style="color:#6A737D"> # Preserve int64 / uint64 / decimal strings; do not convert them to float.</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(json.dumps({</span><span style="color:#032F62">"signalRef"</span><span style="color:#24292E">: item[</span><span style="color:#032F62">"signalRef"</span><span style="color:#24292E">], </span><span style="color:#032F62">"kind"</span><span style="color:#24292E">: typed[</span><span style="color:#032F62">"kind"</span><span style="color:#24292E">],</span></span>

<span class="line"><span style="color:#032F62"> "value"</span><span style="color:#24292E">: typed[</span><span style="color:#032F62">"value"</span><span style="color:#24292E">], </span><span style="color:#032F62">"quality"</span><span style="color:#24292E">: state[</span><span style="color:#032F62">"quality"</span><span style="color:#24292E">],</span></span>

<span class="line"><span style="color:#032F62"> "observedAt"</span><span style="color:#24292E">: state[</span><span style="color:#032F62">"observedAt"</span><span style="color:#24292E">]}, </span><span style="color:#E36209">ensure_ascii</span><span style="color:#D73A49">=</span><span style="color:#005CC5">False</span><span style="color:#24292E">))</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main()</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> Output includes signalRef, kind, value, quality=good, and observedAt. Compare it with Tag Test for the same time period.</p>

</article>

<article class="doc-card">

<h3 id="io-state-api-item-5">Request and response fields</h3>

<p>POST /api/v2/integration/io-state/read takes 1–1000 signalRefs and optional include:[&quot;metadata&quot;]. The response type is io_state_read; items preserves input order. HTTP success does not imply every signal is good.</p>

<ul>

<li>Each item contains signalRef, quality, and qualityDetail. state can be absent when no value is available.</li>

<li>state.value is {kind, value}, not a raw number. Preserve string representations for int64, uint64, and decimal precision.</li>

<li>Inspect quality, observation/server timestamps, version, and source. Do not turn missing, stale, pending, or unavailable into zero.</li>

<li>Expect the console SignalRef, type, value, good quality, and observation time to agree with workbench diagnostics.</li>

</ul>

</article>

<article class="doc-card">

<h3 id="io-state-api-item-6">7. Install the client dependency and observe the stream</h3>

<p>Complete the read exercise first. This program observes frames without maintaining a production cache or embedding the Access Key in code.</p>

<ol>

<li>In the same Python environment used by the client, run python -m pip install &quot;websocket-client==&lt;approved-version&gt;&quot; after replacing the version. Use the earlier Wheel preparation procedure for an offline client.</li>

<li>Save the code below as io_stream.py beside the other client files.</li>

<li>Run python run_io_client.py again, enter the URL and key, and note the target Profile id and revision in its response.</li>

<li>Enter stream for Mode and the returned id for Published Profile ID, not the code equipment_status.</li>

<li>Observe subscription_ready, snapshot_begin, snapshot_chunk, snapshot_end, then heartbeat or io_state_delta. Stop with Ctrl+C.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>io_stream.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: io_stream.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># External client: io_stream.py. Install the approved websocket-client package.</span></span>

<span class="line"><span style="color:#6A737D"># This diagnostic prints the stream lifecycle; it is not a production state cache.</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> json</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> os</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> urllib.parse </span><span style="color:#D73A49">import</span><span style="color:#24292E"> urlencode</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> urllib.request </span><span style="color:#D73A49">import</span><span style="color:#24292E"> Request, urlopen</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> websocket</span></span>

<span class="line"></span>

<span class="line"><span style="color:#005CC5">BASE</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_URL"</span><span style="color:#24292E">].rstrip(</span><span style="color:#032F62">"/"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#005CC5">KEY</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_IO_KEY"</span><span style="color:#24292E">]</span></span>

<span class="line"><span style="color:#005CC5">PROFILE</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> os.environ[</span><span style="color:#032F62">"HQCONTROL_PROFILE_ID"</span><span style="color:#24292E">]</span></span>

<span class="line"><span style="color:#005CC5">USER_AGENT</span><span style="color:#D73A49"> =</span><span style="color:#032F62"> "Mingcedar-Tutorial/1.0"</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> request_json</span><span style="color:#24292E">(path, body</span><span style="color:#D73A49">=</span><span style="color:#005CC5">None</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#24292E"> headers </span><span style="color:#D73A49">=</span><span style="color:#24292E"> {</span><span style="color:#032F62">"X-IO-State-Key"</span><span style="color:#24292E">: </span><span style="color:#005CC5">KEY</span><span style="color:#24292E">, </span><span style="color:#032F62">"User-Agent"</span><span style="color:#24292E">: </span><span style="color:#005CC5">USER_AGENT</span><span style="color:#24292E">}</span></span>

<span class="line"><span style="color:#24292E"> data </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> None</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> body </span><span style="color:#D73A49">is</span><span style="color:#D73A49"> not</span><span style="color:#005CC5"> None</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> headers[</span><span style="color:#032F62">"Content-Type"</span><span style="color:#24292E">] </span><span style="color:#D73A49">=</span><span style="color:#032F62"> "application/json"</span></span>

<span class="line"><span style="color:#24292E"> data </span><span style="color:#D73A49">=</span><span style="color:#24292E"> json.dumps(body).encode()</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> urlopen(Request(</span><span style="color:#005CC5">BASE</span><span style="color:#D73A49"> +</span><span style="color:#24292E"> path, </span><span style="color:#E36209">data</span><span style="color:#D73A49">=</span><span style="color:#24292E">data, </span><span style="color:#E36209">headers</span><span style="color:#D73A49">=</span><span style="color:#24292E">headers), </span><span style="color:#E36209">timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">10</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> response:</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> json.load(response)</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">():</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#D73A49"> not</span><span style="color:#005CC5"> BASE</span><span style="color:#24292E">.startswith(</span><span style="color:#032F62">"https://"</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Use the configured HTTPS server URL"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> profiles </span><span style="color:#D73A49">=</span><span style="color:#24292E"> request_json(</span><span style="color:#032F62">"/api/v2/integration/io-state/profiles"</span><span style="color:#24292E">)[</span><span style="color:#032F62">"items"</span><span style="color:#24292E">]</span></span>

<span class="line"><span style="color:#24292E"> profile </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> next</span><span style="color:#24292E">(item </span><span style="color:#D73A49">for</span><span style="color:#24292E"> item </span><span style="color:#D73A49">in</span><span style="color:#24292E"> profiles </span><span style="color:#D73A49">if</span><span style="color:#24292E"> item[</span><span style="color:#032F62">"id"</span><span style="color:#24292E">] </span><span style="color:#D73A49">==</span><span style="color:#005CC5"> PROFILE</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> ticket </span><span style="color:#D73A49">=</span><span style="color:#24292E"> request_json(</span><span style="color:#032F62">"/api/v2/integration/io-state/stream-tickets"</span><span style="color:#24292E">, {</span></span>

<span class="line"><span style="color:#032F62"> "profileId"</span><span style="color:#24292E">: profile[</span><span style="color:#032F62">"id"</span><span style="color:#24292E">], </span><span style="color:#032F62">"expectedRevision"</span><span style="color:#24292E">: profile[</span><span style="color:#032F62">"revision"</span><span style="color:#24292E">],</span></span>

<span class="line"><span style="color:#032F62"> "clientCapabilities"</span><span style="color:#24292E">: {</span><span style="color:#032F62">"maxSignals"</span><span style="color:#24292E">: </span><span style="color:#005CC5">1000</span><span style="color:#24292E">, </span><span style="color:#032F62">"maxFrameBytes"</span><span style="color:#24292E">: </span><span style="color:#005CC5">1048576</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "maxUpdatesPerSecond"</span><span style="color:#24292E">: </span><span style="color:#005CC5">1000</span><span style="color:#24292E">},</span></span>

<span class="line"><span style="color:#24292E"> })</span></span>

<span class="line"><span style="color:#24292E"> address </span><span style="color:#D73A49">=</span><span style="color:#032F62"> "wss://"</span><span style="color:#D73A49"> +</span><span style="color:#005CC5"> BASE</span><span style="color:#24292E">[</span><span style="color:#005CC5">len</span><span style="color:#24292E">(</span><span style="color:#032F62">"https://"</span><span style="color:#24292E">):] </span><span style="color:#D73A49">+</span><span style="color:#032F62"> "/api/v2/integration/io-state/subscribe?"</span><span style="color:#D73A49"> +</span><span style="color:#24292E"> urlencode({</span><span style="color:#032F62">"ticket"</span><span style="color:#24292E">: ticket[</span><span style="color:#032F62">"ticket"</span><span style="color:#24292E">]})</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> websocket.create_connection(address, </span><span style="color:#E36209">timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">30</span><span style="color:#24292E">, </span><span style="color:#E36209">suppress_origin</span><span style="color:#D73A49">=</span><span style="color:#005CC5">True</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> header</span><span style="color:#D73A49">=</span><span style="color:#24292E">[</span><span style="color:#032F62">"User-Agent: "</span><span style="color:#D73A49"> +</span><span style="color:#005CC5"> USER_AGENT</span><span style="color:#24292E">])</span></span>

<span class="line"><span style="color:#24292E"> stream </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> None</span></span>

<span class="line"><span style="color:#24292E"> previous </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> None</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> while</span><span style="color:#005CC5"> True</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> frame </span><span style="color:#D73A49">=</span><span style="color:#24292E"> json.loads(connection.recv())</span></span>

<span class="line"><span style="color:#24292E"> identity </span><span style="color:#D73A49">=</span><span style="color:#24292E"> (frame[</span><span style="color:#032F62">"streamId"</span><span style="color:#24292E">], frame[</span><span style="color:#032F62">"streamEpoch"</span><span style="color:#24292E">])</span></span>

<span class="line"><span style="color:#24292E"> sequence </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> int</span><span style="color:#24292E">(frame[</span><span style="color:#032F62">"frameSeq"</span><span style="color:#24292E">])</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> stream </span><span style="color:#D73A49">is</span><span style="color:#D73A49"> not</span><span style="color:#005CC5"> None</span><span style="color:#D73A49"> and</span><span style="color:#24292E"> (identity </span><span style="color:#D73A49">!=</span><span style="color:#24292E"> stream </span><span style="color:#D73A49">or</span><span style="color:#24292E"> sequence </span><span style="color:#D73A49">!=</span><span style="color:#24292E"> previous </span><span style="color:#D73A49">+</span><span style="color:#005CC5"> 1</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> RuntimeError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Stream continuity lost; obtain a new ticket and snapshot"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> stream, previous </span><span style="color:#D73A49">=</span><span style="color:#24292E"> identity, sequence</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(frame[</span><span style="color:#032F62">"type"</span><span style="color:#24292E">], frame[</span><span style="color:#032F62">"frameSeq"</span><span style="color:#24292E">])</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> frame[</span><span style="color:#032F62">"type"</span><span style="color:#24292E">] </span><span style="color:#D73A49">in</span><span style="color:#24292E"> (</span><span style="color:#032F62">"reset_required"</span><span style="color:#24292E">, </span><span style="color:#032F62">"error"</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> RuntimeError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Stream reset; review profile and authorization before reconnecting"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main()</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> Expect a complete snapshot and continuing heartbeats; no delta is normal when values are unchanged.</p>

<div class="doc-step-troubleshooting"><p><strong>If the result differs</strong></p><ul>

<li>StopIteration: the entered ID is absent from the client-visible Profiles; check binding, publication, and the entered ID.</li>

<li>ModuleNotFoundError: websocket: install websocket-client using the same interpreter as the launcher.</li>

<li>Ticket failure or disconnect: end the diagnostic, inspect revision, authorization, and error code, then restart to request a new ticket.</li>

</ul></div>

</article>

<article class="doc-card">

<h3 id="io-state-api-item-7">Processing rules for realtime subscriptions</h3>

<p>Frame order: subscription_ready → snapshot_begin → snapshot_chunk (one or more) → snapshot_end → io_state_delta / heartbeat. The example displays frames; production clients must maintain a cache as described below.</p>

<ul>

<li>Ticket requests require profileId, expectedRevision, and clientCapabilities with maxSignals, maxFrameBytes, and maxUpdatesPerSecond. Match capacity to the actual Profile.</li>

<li>Tickets are single-use, expiring, and bound to IP/User-Agent. Do not log the Ticket URL; obtain a new Ticket after a failed connection.</li>

<li>A production client stages a snapshot by snapshotId, verifies chunkIndex order, chunks, items, complete, revision, and membershipHash, and replaces its active cache only after completion.</li>

<li>Verify streamId, streamEpoch, and contiguous frameSeq. Signal versions use domainEpoch/changeSeq to identify state generations.</li>

<li>Apply upsert/remove operations in io_state_delta. A heartbeat describes connection availability, not a new measurement. Latest-state delivery is not a lossless event log.</li>

<li>On reset_required, error, sequence gaps, membership changes, or disconnect, mark the old cache unavailable, close the connection, reconcile authorization/version, and use backoff before obtaining a new Ticket and complete snapshot.</li>

</ul>

</article>

<article class="doc-card">

<h3 id="io-state-api-item-8">Connection troubleshooting and recovery</h3>

<p>Check authorization, signal scope, Profile revision, capacity, connectivity, and data quality in that order.</p>

<ul>

<li>401: Key or Ticket missing/invalid/expired/consumed. Do not print credentials in error logs.</li>

<li>403: insufficient read/stream scope. 409: Profile revision changed; refresh published state.</li>

<li>413: reduce request or Profile/frame capacity. 429: follow rate-limit guidance. 503/504: inspect service and source availability.</li>

<li>TLS: configure the correct CA trust and certificate-matching hostname; keep verification enabled.</li>

<li>For missing data, source loss, Key revocation, or Profile changes, show data as unavailable and handle reconnection. Old cached values must not remain marked live and good.</li>

</ul>

</article>

</div>

<div class="docs-note-grid manual-doc-image-grid">

<figure class="docs-note-card manual-doc-image-card" id="io-state-api-image-1" data-after-card-anchor="io-state-api-item-2">

<img src="/WEB_img/HQserver/current-20261025/io-profile-editor.jpg" alt="IO State API Profile editor and Selector signal scope" loading="lazy" decoding="async" />

<figcaption>

<strong>Figure 1: Configure a subscription Profile</strong>

<p>Set the Profile name, code, and membership mode, then use Selectors to limit the shared signals. Preview the resolved signals before publishing.</p>

</figcaption>

</figure>

</div>

</div>