dosync 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dosync-0.4.2 → dosync-0.5.0}/PKG-INFO +114 -39
- {dosync-0.4.2 → dosync-0.5.0}/README.md +111 -37
- {dosync-0.4.2 → dosync-0.5.0}/dosync/__init__.py +1 -1
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/__init__.py +26 -20
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/homeassistant.py +27 -21
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/matter.py +10 -10
- dosync-0.5.0/dosync/adapters/notifications.py +241 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/shelly.py +11 -11
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/wiz.py +24 -20
- {dosync-0.4.2 → dosync-0.5.0}/dosync/auth.py +17 -17
- {dosync-0.4.2 → dosync-0.5.0}/dosync/certify.py +2 -2
- {dosync-0.4.2 → dosync-0.5.0}/dosync/cli.py +18 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/dashboard.html +50 -11
- {dosync-0.4.2 → dosync-0.5.0}/dosync/db.py +34 -34
- {dosync-0.4.2 → dosync-0.5.0}/dosync/declarative.py +2 -1
- dosync-0.5.0/dosync/discoverers.py +99 -0
- dosync-0.5.0/dosync/discoverers_mdns.py +181 -0
- dosync-0.5.0/dosync/discoverers_ssdp.py +294 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/discovery.py +25 -13
- {dosync-0.4.2 → dosync-0.5.0}/dosync/executor.py +15 -3
- {dosync-0.4.2 → dosync-0.5.0}/dosync/hub.py +216 -23
- {dosync-0.4.2 → dosync-0.5.0}/dosync/manage.py +1 -1
- {dosync-0.4.2 → dosync-0.5.0}/dosync/mcp_server.py +74 -65
- {dosync-0.4.2 → dosync-0.5.0}/dosync/models.py +33 -5
- {dosync-0.4.2 → dosync-0.5.0}/dosync/operation_guards.py +1 -1
- dosync-0.5.0/dosync/paths.py +224 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/policy_config.py +3 -1
- {dosync-0.4.2 → dosync-0.5.0}/dosync/security.py +33 -32
- {dosync-0.4.2 → dosync-0.5.0}/dosync/server.py +226 -35
- {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/PKG-INFO +114 -39
- {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/SOURCES.txt +16 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/requires.txt +2 -1
- {dosync-0.4.2 → dosync-0.5.0}/pyproject.toml +13 -1
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_adapters.py +2 -2
- dosync-0.5.0/tests/test_dashboard_tells_the_truth.py +68 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_deployment_env_contract.py +21 -0
- dosync-0.5.0/tests/test_deployment_layout.py +221 -0
- dosync-0.5.0/tests/test_env_loading_is_explicit.py +68 -0
- dosync-0.5.0/tests/test_evaluation_metrics.py +157 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_explain_consistency.py +34 -3
- dosync-0.5.0/tests/test_explain_resolve_parity.py +232 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_ha_bridge_hygiene.py +1 -1
- dosync-0.5.0/tests/test_no_operator_data.py +265 -0
- dosync-0.5.0/tests/test_notification_templates.py +82 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_policy_config.py +7 -1
- dosync-0.5.0/tests/test_public_claims.py +96 -0
- dosync-0.5.0/tests/test_quickstart_is_runnable.py +142 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_recall_benchmark_postpolicy.py +6 -6
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_resolver_scoring.py +15 -1
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_sensor_kind.py +5 -5
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_server.py +42 -0
- dosync-0.5.0/tests/test_simulation_is_declared.py +295 -0
- dosync-0.5.0/tests/test_transport_discoverers.py +468 -0
- dosync-0.5.0/tests/test_universal_intent_contract.py +202 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_validation_integration.py +5 -1
- dosync-0.4.2/dosync/adapters/notifications.py +0 -166
- {dosync-0.4.2 → dosync-0.5.0}/LICENSE +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/ble.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/declarative.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/mavlink.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/mqtt.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/audit_backup.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/auth_fastapi.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/cert_signing.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/composite_operations.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/config_reference.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/device_arbiter.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/ed25519_pure.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/__init__.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/3d-printer.yaml +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/air-conditioner.yaml +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/building-lighting.json +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/industrial-conveyor.yaml +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/light-generic.yaml +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/television.yaml +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/geo.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/hub_monitor.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/lightweight.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/metrics.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/operation_supervisor.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/operations.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/plugins.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/policies.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/py.typed +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/reconciler.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/route_composer.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/spec_coverage.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync/validation.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/dependency_links.txt +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/entry_points.txt +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/top_level.txt +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/setup.cfg +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_audit_archive.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_audit_backup.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_audit_chain_integrity.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_audit_provenance.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_auth.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_ble_adapter.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_certification_honesty.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_claim_state_machine.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_composite_operations.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_composite_orchestration.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_composition_kind_db.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_composition_kind_endpoint.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_composition_routing.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_db.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_declarative_adapters.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_declarative_quarantine.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_device_health.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_device_heartbeat.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_direct_action_governance.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_discovery_adoption.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_drone_policies.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_ed25519_pure.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_emergency_preemption.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_event_loop_migration.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_geo.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_hub_monitor.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_idempotency.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_independent_observation.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_integration_suite.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_lightweight_heartbeat.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_adapter.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_channels.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_listener.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_return_home.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_single_reader.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_telemetry_closure.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_mcp_dynamic_intents.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_mcp_partial_progress.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_metrics.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_models.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_multihub_endpoints.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_operation_guards.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_operation_supervisor.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_operations.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_operations_endpoints.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_operations_persistence.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_operations_wiring.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_panel_polish_2026_07_21.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_policies.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_pushed_verification.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_reachability_cause.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_reconciler.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_resolution_wiring.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_resolver_semantics.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_route_composer.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_telemetry_bridge.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_third_party_adapters.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_validation.py +0 -0
- {dosync-0.4.2 → dosync-0.5.0}/tests/test_wiring_audit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dosync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: The semantic layer between AI agents and physical devices
|
|
5
5
|
Author-email: Rodrigo Giuliani <rgiuliani@dosync.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -24,10 +24,11 @@ Requires-Python: >=3.10
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: fastapi<1.0,>=0.115.0
|
|
27
|
-
Requires-Dist: uvicorn<1.0,>=0.23.0
|
|
27
|
+
Requires-Dist: uvicorn[standard]<1.0,>=0.23.0
|
|
28
28
|
Requires-Dist: pydantic<3.0,>=2.7.0
|
|
29
29
|
Requires-Dist: jsonschema<5.0,>=4.18
|
|
30
30
|
Requires-Dist: bleak>=0.21
|
|
31
|
+
Requires-Dist: zeroconf>=0.130
|
|
31
32
|
Requires-Dist: pyyaml>=6.0.1
|
|
32
33
|
Requires-Dist: aiohttp>=3.9.0
|
|
33
34
|
Requires-Dist: paho-mqtt>=2.0.0
|
|
@@ -210,7 +211,7 @@ DoSync earns its place in specific situations — and honestly gets in the way i
|
|
|
210
211
|
```
|
|
211
212
|
User / AI says: "there is an emergency at home"
|
|
212
213
|
│
|
|
213
|
-
|
|
214
|
+
DoSync Hub
|
|
214
215
|
│
|
|
215
216
|
┌───────────────┼───────────────┐
|
|
216
217
|
▼ ▼ ▼
|
|
@@ -257,6 +258,50 @@ pipx install dosync # recommended
|
|
|
257
258
|
dosync-hub
|
|
258
259
|
```
|
|
259
260
|
|
|
261
|
+
<details>
|
|
262
|
+
<summary><b>On Windows</b> — pipx is not installed with Python there</summary>
|
|
263
|
+
|
|
264
|
+
Every step below was found by running it on a clean Windows machine. None of it
|
|
265
|
+
is exotic; it is simply what Windows needs and what this page used to omit.
|
|
266
|
+
|
|
267
|
+
```powershell
|
|
268
|
+
python -m pip install --user pipx
|
|
269
|
+
python -m pipx ensurepath
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Close PowerShell and open it again.** `ensurepath` edits your PATH and the
|
|
273
|
+
session you are in cannot see the change. Then:
|
|
274
|
+
|
|
275
|
+
```powershell
|
|
276
|
+
pipx install dosync
|
|
277
|
+
dosync-hub
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Two things the rest of this page assumes that PowerShell does not provide:
|
|
281
|
+
`export VAR=value` is `$env:VAR = "value"`, and `curl` is an alias for
|
|
282
|
+
`Invoke-WebRequest`, which is a different program with a different syntax — use
|
|
283
|
+
`curl.exe`, or the PowerShell examples further down. `setup_pki.sh` is a shell
|
|
284
|
+
script and needs Git Bash or WSL.
|
|
285
|
+
|
|
286
|
+
</details>
|
|
287
|
+
|
|
288
|
+
### Then open the dashboard
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
http://localhost:47200
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Paste the API key the hub printed on startup, press **Scan**, and the hub
|
|
295
|
+
searches every transport it can reach — WiFi broadcast, mDNS, SSDP, Bluetooth —
|
|
296
|
+
and shows what answered. Name anything you want to keep and it is adopted.
|
|
297
|
+
|
|
298
|
+
**Start here.** Discovering and adopting a device needs no terminal and no
|
|
299
|
+
JSON: a 3D printer, a television and a Bluetooth sensor were adopted this way
|
|
300
|
+
on the reference deployment without a line being typed. The API calls below are
|
|
301
|
+
for building against DoSync, not for setting it up — they came first on this
|
|
302
|
+
page for a long time, which told people who do not write code that the project
|
|
303
|
+
was not for them, while a button that did the same job sat one section lower.
|
|
304
|
+
|
|
260
305
|
<details>
|
|
261
306
|
<summary><b>"error: externally-managed-environment"?</b> — Raspberry Pi OS, Debian 12+, Ubuntu 23.04+</summary>
|
|
262
307
|
|
|
@@ -294,11 +339,28 @@ That is a working hub on `http://127.0.0.1:47200`. It starts with a simulated
|
|
|
294
339
|
executor, so you can drive the whole protocol — register devices, fire intents,
|
|
295
340
|
read the audit chain — before you own a single smart device.
|
|
296
341
|
|
|
342
|
+
### Or drive it from the API
|
|
343
|
+
|
|
344
|
+
Everything the dashboard does is an HTTP call, and this is the part to read if
|
|
345
|
+
you are building against DoSync rather than setting it up.
|
|
346
|
+
|
|
347
|
+
The hub prints an API key on that first start and stores only a hash of it, so
|
|
348
|
+
it is shown once. Save it — every command below needs it:
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
export DOSYNC_TOKEN=<the key printed on first start>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```powershell
|
|
355
|
+
$env:DOSYNC_TOKEN = "<the key printed on first start>"
|
|
356
|
+
```
|
|
357
|
+
|
|
297
358
|
Register something and give it a goal:
|
|
298
359
|
|
|
299
360
|
```bash
|
|
300
361
|
# 1. A device declares what it CAN DO (not what commands it takes)
|
|
301
362
|
curl -X POST http://127.0.0.1:47200/v1/devices/register \
|
|
363
|
+
-H "Authorization: Bearer $DOSYNC_TOKEN" \
|
|
302
364
|
-H 'Content-Type: application/json' -d '{
|
|
303
365
|
"device_id": "siren-hall", "device_name": "Hall Siren",
|
|
304
366
|
"manufacturer": "acme", "model": "S1", "firmware": "1.0",
|
|
@@ -309,14 +371,50 @@ curl -X POST http://127.0.0.1:47200/v1/devices/register \
|
|
|
309
371
|
|
|
310
372
|
# 2. An AI expresses a GOAL — not a command, and it names no device
|
|
311
373
|
curl -X POST http://127.0.0.1:47200/v1/intent/async \
|
|
374
|
+
-H "Authorization: Bearer $DOSYNC_TOKEN" \
|
|
312
375
|
-H 'Content-Type: application/json' \
|
|
313
376
|
-d '{"intent": "ensure_safety", "urgency": "emergency", "context": {}}'
|
|
314
377
|
|
|
315
378
|
# 3. Ask WHY those devices were chosen
|
|
316
|
-
curl http://127.0.0.1:47200/v1/intents/ensure_safety/explain
|
|
379
|
+
curl http://127.0.0.1:47200/v1/intents/ensure_safety/explain \
|
|
380
|
+
-H "Authorization: Bearer $DOSYNC_TOKEN"
|
|
317
381
|
|
|
318
382
|
# 4. Read the tamper-evident record of what happened
|
|
319
|
-
curl http://127.0.0.1:47200/v1/audit?limit=10
|
|
383
|
+
curl "http://127.0.0.1:47200/v1/audit?limit=10" \
|
|
384
|
+
-H "Authorization: Bearer $DOSYNC_TOKEN"
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
<details>
|
|
388
|
+
<summary><b>The same calls in PowerShell</b></summary>
|
|
389
|
+
|
|
390
|
+
`curl` in PowerShell is an alias for `Invoke-WebRequest`, and quoting JSON for
|
|
391
|
+
`curl.exe` is impractical — a clean Windows machine following the commands above
|
|
392
|
+
verbatim gets `JSON decode error`, because PowerShell passes the escaped quotes
|
|
393
|
+
through literally. Build the body as an object instead:
|
|
394
|
+
|
|
395
|
+
```powershell
|
|
396
|
+
$body = @{
|
|
397
|
+
device_id = "siren-hall"; device_name = "Hall Siren"
|
|
398
|
+
manufacturer = "acme"; model = "S1"; firmware = "1.0"
|
|
399
|
+
category = "actuator"; tags = @("alarm","emergency")
|
|
400
|
+
emergency_capable = $true; cert_tier = "basic"
|
|
401
|
+
capabilities = @{ sensors = @(); actuators = @(@{ id="alarm"; type="alarm"; description="sound" }) }
|
|
402
|
+
} | ConvertTo-Json -Depth 6
|
|
403
|
+
|
|
404
|
+
$headers = @{ Authorization = "Bearer $env:DOSYNC_TOKEN" }
|
|
405
|
+
|
|
406
|
+
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:47200/v1/devices/register `
|
|
407
|
+
-Headers $headers -ContentType "application/json" -Body $body
|
|
408
|
+
|
|
409
|
+
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:47200/v1/intent/async `
|
|
410
|
+
-Headers $headers -ContentType "application/json" `
|
|
411
|
+
-Body (@{ intent = "ensure_safety"; urgency = "emergency"; context = @{} } | ConvertTo-Json)
|
|
412
|
+
|
|
413
|
+
Invoke-RestMethod -Uri http://127.0.0.1:47200/v1/intents/ensure_safety/explain -Headers $headers
|
|
414
|
+
Invoke-RestMethod -Uri "http://127.0.0.1:47200/v1/audit?limit=10" -Headers $headers
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
</details>
|
|
320
418
|
```
|
|
321
419
|
|
|
322
420
|
Step 3 is the one worth pausing on: the hub tells you which devices it
|
|
@@ -338,7 +436,13 @@ Matter, BLE, MAVLink, and the Home Assistant bridge, which is the widest door of
|
|
|
338
436
|
all: anything HA already integrates, DoSync can reach. **Reference** adapters
|
|
339
437
|
(WiZ, Shelly) implement one vendor's product and ship as worked examples of how
|
|
340
438
|
an adapter is written — not as endorsement, partnership, or a promise to track
|
|
341
|
-
anyone's firmware. **
|
|
439
|
+
anyone's firmware. **They register only when their vendor library is installed,
|
|
440
|
+
and say nothing when it is not.** A hub whose operator owns nothing from that
|
|
441
|
+
vendor should never be told to install anything: the protocol does not presume
|
|
442
|
+
your hardware, and a reference adapter that nagged for `pip install pywizlight`
|
|
443
|
+
on every start was presuming it. If you do register a device that names an
|
|
444
|
+
adapter the hub cannot load, the startup check names that device and tells you
|
|
445
|
+
its actions will be simulated. **Infrastructure** is notifications.
|
|
342
446
|
|
|
343
447
|
**If your device is not covered, describe it in a file.** A declarative adapter
|
|
344
448
|
is YAML or JSON — no code, no release of DoSync to wait for:
|
|
@@ -419,35 +523,6 @@ pip install 'dosync[mqtt]' # MQTT devices
|
|
|
419
523
|
pip install 'dosync[all]' # everything
|
|
420
524
|
```
|
|
421
525
|
|
|
422
|
-
### Access: token, your own password, or none
|
|
423
|
-
|
|
424
|
-
The hub prints an API token the first time it starts and stores only a hash of
|
|
425
|
-
it, so it cannot show you that one again. Three ways to deal with that,
|
|
426
|
-
depending on who you are:
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
# 1. Choose your own, like a password — for a person who has to type it
|
|
430
|
-
dosync-manage keys create --token "my-house-2026-kitchen" --label dashboard
|
|
431
|
-
|
|
432
|
-
# 2. Let it generate one — for a program that will store it
|
|
433
|
-
dosync-manage keys create --label my-integration
|
|
434
|
-
|
|
435
|
-
# 3. Turn authentication off entirely
|
|
436
|
-
DOSYNC_AUTH=false dosync-hub
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
**Option 3 is legitimate and not a trap door.** On a home network, behind a
|
|
440
|
-
router, with no port forwarding, requiring a token protects against nobody who
|
|
441
|
-
is not already inside your house. It is the wrong default for a clinic and a
|
|
442
|
-
reasonable choice for a workshop, so DoSync provides it plainly instead of
|
|
443
|
-
pretending everyone has the same threat model. What it is not suitable for is
|
|
444
|
-
any hub reachable from outside its own network.
|
|
445
|
-
|
|
446
|
-
Tokens are checked without rate limiting or lockout, so a chosen one must be at
|
|
447
|
-
least 12 characters and a passphrase of several words is better than a short
|
|
448
|
-
clever string. Existing keys: `dosync-manage keys list` (previews only — they
|
|
449
|
-
are hashed), `dosync-manage keys revoke <preview>`, `dosync-manage keys reset`.
|
|
450
|
-
|
|
451
526
|
### Access: a password you choose, or none at all
|
|
452
527
|
|
|
453
528
|
The hub prints a token on first start and stores only a hash of it, so it cannot
|
|
@@ -575,7 +650,7 @@ git clone https://github.com/giulianireg-spec/dosync-protocol
|
|
|
575
650
|
cd dosync-protocol
|
|
576
651
|
python3 -m venv venv && source venv/bin/activate
|
|
577
652
|
pip install -e '.[dev]'
|
|
578
|
-
pytest #
|
|
653
|
+
pytest # runs the full suite
|
|
579
654
|
dosync-hub --reload
|
|
580
655
|
```
|
|
581
656
|
|
|
@@ -585,7 +660,7 @@ dosync-hub --reload
|
|
|
585
660
|
|
|
586
661
|
| Component | Status |
|
|
587
662
|
|---|---|
|
|
588
|
-
| REST API (
|
|
663
|
+
| REST API (40+ endpoints) | ✅ |
|
|
589
664
|
| WebSocket real-time events | ✅ |
|
|
590
665
|
| Web dashboard | ✅ |
|
|
591
666
|
| API key authentication + SHA-256 audit log | ✅ |
|
|
@@ -644,7 +719,7 @@ python3 certify.py --host <hub-ip> --port 47200 --tier standard
|
|
|
644
719
|
| Language | Location | Author | Certification |
|
|
645
720
|
|---|---|---|---|
|
|
646
721
|
| Python (reference) | `server.py` | this project | Standard 33/33 · Emergency 44/44 ✅ |
|
|
647
|
-
| Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | 33/33
|
|
722
|
+
| Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | Standard 33/33, against the v0.3 suite — re-validation against the current 56-test suite pending |
|
|
648
723
|
|
|
649
724
|
The Node.js implementation is a **companion** port that validates the protocol
|
|
650
725
|
is implementable in a second language against the same certification suite —
|
|
@@ -692,4 +767,4 @@ Apache 2.0 — free to implement, free to extend, no royalties.
|
|
|
692
767
|
|
|
693
768
|
---
|
|
694
769
|
|
|
695
|
-
*DoSync Protocol
|
|
770
|
+
*DoSync Protocol · © 2026 Rodrigo Giuliani · rgiuliani@dosync.dev*
|
|
@@ -149,7 +149,7 @@ DoSync earns its place in specific situations — and honestly gets in the way i
|
|
|
149
149
|
```
|
|
150
150
|
User / AI says: "there is an emergency at home"
|
|
151
151
|
│
|
|
152
|
-
|
|
152
|
+
DoSync Hub
|
|
153
153
|
│
|
|
154
154
|
┌───────────────┼───────────────┐
|
|
155
155
|
▼ ▼ ▼
|
|
@@ -196,6 +196,50 @@ pipx install dosync # recommended
|
|
|
196
196
|
dosync-hub
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
<details>
|
|
200
|
+
<summary><b>On Windows</b> — pipx is not installed with Python there</summary>
|
|
201
|
+
|
|
202
|
+
Every step below was found by running it on a clean Windows machine. None of it
|
|
203
|
+
is exotic; it is simply what Windows needs and what this page used to omit.
|
|
204
|
+
|
|
205
|
+
```powershell
|
|
206
|
+
python -m pip install --user pipx
|
|
207
|
+
python -m pipx ensurepath
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Close PowerShell and open it again.** `ensurepath` edits your PATH and the
|
|
211
|
+
session you are in cannot see the change. Then:
|
|
212
|
+
|
|
213
|
+
```powershell
|
|
214
|
+
pipx install dosync
|
|
215
|
+
dosync-hub
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Two things the rest of this page assumes that PowerShell does not provide:
|
|
219
|
+
`export VAR=value` is `$env:VAR = "value"`, and `curl` is an alias for
|
|
220
|
+
`Invoke-WebRequest`, which is a different program with a different syntax — use
|
|
221
|
+
`curl.exe`, or the PowerShell examples further down. `setup_pki.sh` is a shell
|
|
222
|
+
script and needs Git Bash or WSL.
|
|
223
|
+
|
|
224
|
+
</details>
|
|
225
|
+
|
|
226
|
+
### Then open the dashboard
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
http://localhost:47200
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Paste the API key the hub printed on startup, press **Scan**, and the hub
|
|
233
|
+
searches every transport it can reach — WiFi broadcast, mDNS, SSDP, Bluetooth —
|
|
234
|
+
and shows what answered. Name anything you want to keep and it is adopted.
|
|
235
|
+
|
|
236
|
+
**Start here.** Discovering and adopting a device needs no terminal and no
|
|
237
|
+
JSON: a 3D printer, a television and a Bluetooth sensor were adopted this way
|
|
238
|
+
on the reference deployment without a line being typed. The API calls below are
|
|
239
|
+
for building against DoSync, not for setting it up — they came first on this
|
|
240
|
+
page for a long time, which told people who do not write code that the project
|
|
241
|
+
was not for them, while a button that did the same job sat one section lower.
|
|
242
|
+
|
|
199
243
|
<details>
|
|
200
244
|
<summary><b>"error: externally-managed-environment"?</b> — Raspberry Pi OS, Debian 12+, Ubuntu 23.04+</summary>
|
|
201
245
|
|
|
@@ -233,11 +277,28 @@ That is a working hub on `http://127.0.0.1:47200`. It starts with a simulated
|
|
|
233
277
|
executor, so you can drive the whole protocol — register devices, fire intents,
|
|
234
278
|
read the audit chain — before you own a single smart device.
|
|
235
279
|
|
|
280
|
+
### Or drive it from the API
|
|
281
|
+
|
|
282
|
+
Everything the dashboard does is an HTTP call, and this is the part to read if
|
|
283
|
+
you are building against DoSync rather than setting it up.
|
|
284
|
+
|
|
285
|
+
The hub prints an API key on that first start and stores only a hash of it, so
|
|
286
|
+
it is shown once. Save it — every command below needs it:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
export DOSYNC_TOKEN=<the key printed on first start>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```powershell
|
|
293
|
+
$env:DOSYNC_TOKEN = "<the key printed on first start>"
|
|
294
|
+
```
|
|
295
|
+
|
|
236
296
|
Register something and give it a goal:
|
|
237
297
|
|
|
238
298
|
```bash
|
|
239
299
|
# 1. A device declares what it CAN DO (not what commands it takes)
|
|
240
300
|
curl -X POST http://127.0.0.1:47200/v1/devices/register \
|
|
301
|
+
-H "Authorization: Bearer $DOSYNC_TOKEN" \
|
|
241
302
|
-H 'Content-Type: application/json' -d '{
|
|
242
303
|
"device_id": "siren-hall", "device_name": "Hall Siren",
|
|
243
304
|
"manufacturer": "acme", "model": "S1", "firmware": "1.0",
|
|
@@ -248,14 +309,50 @@ curl -X POST http://127.0.0.1:47200/v1/devices/register \
|
|
|
248
309
|
|
|
249
310
|
# 2. An AI expresses a GOAL — not a command, and it names no device
|
|
250
311
|
curl -X POST http://127.0.0.1:47200/v1/intent/async \
|
|
312
|
+
-H "Authorization: Bearer $DOSYNC_TOKEN" \
|
|
251
313
|
-H 'Content-Type: application/json' \
|
|
252
314
|
-d '{"intent": "ensure_safety", "urgency": "emergency", "context": {}}'
|
|
253
315
|
|
|
254
316
|
# 3. Ask WHY those devices were chosen
|
|
255
|
-
curl http://127.0.0.1:47200/v1/intents/ensure_safety/explain
|
|
317
|
+
curl http://127.0.0.1:47200/v1/intents/ensure_safety/explain \
|
|
318
|
+
-H "Authorization: Bearer $DOSYNC_TOKEN"
|
|
256
319
|
|
|
257
320
|
# 4. Read the tamper-evident record of what happened
|
|
258
|
-
curl http://127.0.0.1:47200/v1/audit?limit=10
|
|
321
|
+
curl "http://127.0.0.1:47200/v1/audit?limit=10" \
|
|
322
|
+
-H "Authorization: Bearer $DOSYNC_TOKEN"
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
<details>
|
|
326
|
+
<summary><b>The same calls in PowerShell</b></summary>
|
|
327
|
+
|
|
328
|
+
`curl` in PowerShell is an alias for `Invoke-WebRequest`, and quoting JSON for
|
|
329
|
+
`curl.exe` is impractical — a clean Windows machine following the commands above
|
|
330
|
+
verbatim gets `JSON decode error`, because PowerShell passes the escaped quotes
|
|
331
|
+
through literally. Build the body as an object instead:
|
|
332
|
+
|
|
333
|
+
```powershell
|
|
334
|
+
$body = @{
|
|
335
|
+
device_id = "siren-hall"; device_name = "Hall Siren"
|
|
336
|
+
manufacturer = "acme"; model = "S1"; firmware = "1.0"
|
|
337
|
+
category = "actuator"; tags = @("alarm","emergency")
|
|
338
|
+
emergency_capable = $true; cert_tier = "basic"
|
|
339
|
+
capabilities = @{ sensors = @(); actuators = @(@{ id="alarm"; type="alarm"; description="sound" }) }
|
|
340
|
+
} | ConvertTo-Json -Depth 6
|
|
341
|
+
|
|
342
|
+
$headers = @{ Authorization = "Bearer $env:DOSYNC_TOKEN" }
|
|
343
|
+
|
|
344
|
+
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:47200/v1/devices/register `
|
|
345
|
+
-Headers $headers -ContentType "application/json" -Body $body
|
|
346
|
+
|
|
347
|
+
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:47200/v1/intent/async `
|
|
348
|
+
-Headers $headers -ContentType "application/json" `
|
|
349
|
+
-Body (@{ intent = "ensure_safety"; urgency = "emergency"; context = @{} } | ConvertTo-Json)
|
|
350
|
+
|
|
351
|
+
Invoke-RestMethod -Uri http://127.0.0.1:47200/v1/intents/ensure_safety/explain -Headers $headers
|
|
352
|
+
Invoke-RestMethod -Uri "http://127.0.0.1:47200/v1/audit?limit=10" -Headers $headers
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
</details>
|
|
259
356
|
```
|
|
260
357
|
|
|
261
358
|
Step 3 is the one worth pausing on: the hub tells you which devices it
|
|
@@ -277,7 +374,13 @@ Matter, BLE, MAVLink, and the Home Assistant bridge, which is the widest door of
|
|
|
277
374
|
all: anything HA already integrates, DoSync can reach. **Reference** adapters
|
|
278
375
|
(WiZ, Shelly) implement one vendor's product and ship as worked examples of how
|
|
279
376
|
an adapter is written — not as endorsement, partnership, or a promise to track
|
|
280
|
-
anyone's firmware. **
|
|
377
|
+
anyone's firmware. **They register only when their vendor library is installed,
|
|
378
|
+
and say nothing when it is not.** A hub whose operator owns nothing from that
|
|
379
|
+
vendor should never be told to install anything: the protocol does not presume
|
|
380
|
+
your hardware, and a reference adapter that nagged for `pip install pywizlight`
|
|
381
|
+
on every start was presuming it. If you do register a device that names an
|
|
382
|
+
adapter the hub cannot load, the startup check names that device and tells you
|
|
383
|
+
its actions will be simulated. **Infrastructure** is notifications.
|
|
281
384
|
|
|
282
385
|
**If your device is not covered, describe it in a file.** A declarative adapter
|
|
283
386
|
is YAML or JSON — no code, no release of DoSync to wait for:
|
|
@@ -358,35 +461,6 @@ pip install 'dosync[mqtt]' # MQTT devices
|
|
|
358
461
|
pip install 'dosync[all]' # everything
|
|
359
462
|
```
|
|
360
463
|
|
|
361
|
-
### Access: token, your own password, or none
|
|
362
|
-
|
|
363
|
-
The hub prints an API token the first time it starts and stores only a hash of
|
|
364
|
-
it, so it cannot show you that one again. Three ways to deal with that,
|
|
365
|
-
depending on who you are:
|
|
366
|
-
|
|
367
|
-
```bash
|
|
368
|
-
# 1. Choose your own, like a password — for a person who has to type it
|
|
369
|
-
dosync-manage keys create --token "my-house-2026-kitchen" --label dashboard
|
|
370
|
-
|
|
371
|
-
# 2. Let it generate one — for a program that will store it
|
|
372
|
-
dosync-manage keys create --label my-integration
|
|
373
|
-
|
|
374
|
-
# 3. Turn authentication off entirely
|
|
375
|
-
DOSYNC_AUTH=false dosync-hub
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
**Option 3 is legitimate and not a trap door.** On a home network, behind a
|
|
379
|
-
router, with no port forwarding, requiring a token protects against nobody who
|
|
380
|
-
is not already inside your house. It is the wrong default for a clinic and a
|
|
381
|
-
reasonable choice for a workshop, so DoSync provides it plainly instead of
|
|
382
|
-
pretending everyone has the same threat model. What it is not suitable for is
|
|
383
|
-
any hub reachable from outside its own network.
|
|
384
|
-
|
|
385
|
-
Tokens are checked without rate limiting or lockout, so a chosen one must be at
|
|
386
|
-
least 12 characters and a passphrase of several words is better than a short
|
|
387
|
-
clever string. Existing keys: `dosync-manage keys list` (previews only — they
|
|
388
|
-
are hashed), `dosync-manage keys revoke <preview>`, `dosync-manage keys reset`.
|
|
389
|
-
|
|
390
464
|
### Access: a password you choose, or none at all
|
|
391
465
|
|
|
392
466
|
The hub prints a token on first start and stores only a hash of it, so it cannot
|
|
@@ -514,7 +588,7 @@ git clone https://github.com/giulianireg-spec/dosync-protocol
|
|
|
514
588
|
cd dosync-protocol
|
|
515
589
|
python3 -m venv venv && source venv/bin/activate
|
|
516
590
|
pip install -e '.[dev]'
|
|
517
|
-
pytest #
|
|
591
|
+
pytest # runs the full suite
|
|
518
592
|
dosync-hub --reload
|
|
519
593
|
```
|
|
520
594
|
|
|
@@ -524,7 +598,7 @@ dosync-hub --reload
|
|
|
524
598
|
|
|
525
599
|
| Component | Status |
|
|
526
600
|
|---|---|
|
|
527
|
-
| REST API (
|
|
601
|
+
| REST API (40+ endpoints) | ✅ |
|
|
528
602
|
| WebSocket real-time events | ✅ |
|
|
529
603
|
| Web dashboard | ✅ |
|
|
530
604
|
| API key authentication + SHA-256 audit log | ✅ |
|
|
@@ -583,7 +657,7 @@ python3 certify.py --host <hub-ip> --port 47200 --tier standard
|
|
|
583
657
|
| Language | Location | Author | Certification |
|
|
584
658
|
|---|---|---|---|
|
|
585
659
|
| Python (reference) | `server.py` | this project | Standard 33/33 · Emergency 44/44 ✅ |
|
|
586
|
-
| Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | 33/33
|
|
660
|
+
| Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | Standard 33/33, against the v0.3 suite — re-validation against the current 56-test suite pending |
|
|
587
661
|
|
|
588
662
|
The Node.js implementation is a **companion** port that validates the protocol
|
|
589
663
|
is implementable in a second language against the same certification suite —
|
|
@@ -631,4 +705,4 @@ Apache 2.0 — free to implement, free to extend, no royalties.
|
|
|
631
705
|
|
|
632
706
|
---
|
|
633
707
|
|
|
634
|
-
*DoSync Protocol
|
|
708
|
+
*DoSync Protocol · © 2026 Rodrigo Giuliani · rgiuliani@dosync.dev*
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
2
|
DoSync — Adapter Layer
|
|
3
3
|
======================
|
|
4
|
-
|
|
4
|
+
Translation layer between the DoSync protocol and real physical devices.
|
|
5
5
|
|
|
6
6
|
Modelo:
|
|
7
7
|
DoSync Hub → AdapterExecutor → [WiZAdapter | GPIOAdapter | ShellyAdapter | ...]
|
|
8
8
|
|
|
9
9
|
Para agregar un nuevo dispositivo:
|
|
10
10
|
1. Crear adapters/mi_marca.py implementando DoSyncAdapter
|
|
11
|
-
2.
|
|
12
|
-
3.
|
|
11
|
+
2. Register the device with adapter="my_brand" in its CapabilityManifest
|
|
12
|
+
3. The hub treats it like any other device — no core changes needed
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Publishing third-party adapters:
|
|
15
15
|
pip install dosync-adapter-philipshue
|
|
16
16
|
pip install dosync-adapter-shelly
|
|
17
17
|
pip install dosync-adapter-matter
|
|
@@ -27,11 +27,11 @@ from ..models import ActionResult, DeviceAction, Urgency
|
|
|
27
27
|
log = logging.getLogger("dosync.adapters")
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
# ──
|
|
30
|
+
# ── Base interface every adapter must implement ───────────────────────────────
|
|
31
31
|
|
|
32
32
|
class DoSyncAdapter(ABC):
|
|
33
33
|
"""
|
|
34
|
-
|
|
34
|
+
Base interface for physical device adapters.
|
|
35
35
|
|
|
36
36
|
Cada adapter traduce acciones DoSync al protocolo nativo
|
|
37
37
|
del dispositivo (UDP, HTTP, GPIO, BLE, etc.).
|
|
@@ -160,16 +160,16 @@ class DoSyncAdapter(ABC):
|
|
|
160
160
|
class AdapterExecutor:
|
|
161
161
|
"""
|
|
162
162
|
Ejecutor central que delega acciones al adapter correcto
|
|
163
|
-
|
|
163
|
+
based on the 'adapter' field of the device's CapabilityManifest.
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
A device with no registered adapter falls back to the SimulatedExecutor.
|
|
166
166
|
|
|
167
167
|
Uso:
|
|
168
168
|
executor = AdapterExecutor(hub)
|
|
169
169
|
executor.register(WiZAdapter())
|
|
170
170
|
executor.register(GPIOAdapter())
|
|
171
171
|
|
|
172
|
-
#
|
|
172
|
+
# The hub uses this executor instead of the SimulatedExecutor
|
|
173
173
|
result = await hub.execute_intent(intent, executor)
|
|
174
174
|
"""
|
|
175
175
|
|
|
@@ -191,7 +191,7 @@ class AdapterExecutor:
|
|
|
191
191
|
self._simulated = None
|
|
192
192
|
|
|
193
193
|
def register(self, adapter: DoSyncAdapter) -> None:
|
|
194
|
-
"""
|
|
194
|
+
"""Register an adapter under its name."""
|
|
195
195
|
self._adapters[adapter.adapter_name] = adapter
|
|
196
196
|
log.info("Adapter registered: %s", adapter.adapter_name)
|
|
197
197
|
|
|
@@ -206,8 +206,8 @@ class AdapterExecutor:
|
|
|
206
206
|
|
|
207
207
|
async def execute(self, action: DeviceAction, urgency: Urgency) -> ActionResult:
|
|
208
208
|
"""
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
Execute an action, looking up the right adapter for the device.
|
|
210
|
+
Falls back to the SimulatedExecutor when the device has no adapter.
|
|
211
211
|
"""
|
|
212
212
|
device = self._hub.registry.get(action.device_id)
|
|
213
213
|
|
|
@@ -250,7 +250,7 @@ class AdapterExecutor:
|
|
|
250
250
|
result = await self._adapters[adapter_name].execute(action, urgency)
|
|
251
251
|
if result.success:
|
|
252
252
|
self._update_resolver_state(action)
|
|
253
|
-
# Device Health Monitor —
|
|
253
|
+
# Device Health Monitor — record the outcome
|
|
254
254
|
self._record_health(action, result)
|
|
255
255
|
return result
|
|
256
256
|
except Exception as e:
|
|
@@ -267,13 +267,19 @@ class AdapterExecutor:
|
|
|
267
267
|
self._record_health(action, err_result)
|
|
268
268
|
return err_result
|
|
269
269
|
|
|
270
|
-
# Fallback
|
|
270
|
+
# Fallback. WARNING, not INFO, and the result says it was simulated:
|
|
271
|
+
# this branch is how a device that declares actuators nobody can execute
|
|
272
|
+
# still reports success. The reference deployment ran an SMS notifier
|
|
273
|
+
# here for an unknown length of time, and every log line read like an
|
|
274
|
+
# execution.
|
|
271
275
|
if self._simulated:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
276
|
+
reason = ("no_adapter_declared" if not adapter_name
|
|
277
|
+
else "adapter_unavailable")
|
|
278
|
+
log.warning(
|
|
279
|
+
"Simulating %s.%s — %s (adapter=%s). Nothing was sent to the device.",
|
|
280
|
+
action.device_id, action.action, reason, adapter_name or "none",
|
|
275
281
|
)
|
|
276
|
-
return await self._simulated.execute(action, urgency)
|
|
282
|
+
return await self._simulated.execute(action, urgency, reason=reason)
|
|
277
283
|
|
|
278
284
|
return ActionResult(
|
|
279
285
|
device_id=action.device_id,
|
|
@@ -283,7 +289,7 @@ class AdapterExecutor:
|
|
|
283
289
|
)
|
|
284
290
|
|
|
285
291
|
def _record_health(self, action: DeviceAction, result) -> None:
|
|
286
|
-
"""
|
|
292
|
+
"""Record the outcome in the Device Health Monitor."""
|
|
287
293
|
try:
|
|
288
294
|
db = getattr(self._hub, 'db', None)
|
|
289
295
|
if db:
|
|
@@ -298,7 +304,7 @@ class AdapterExecutor:
|
|
|
298
304
|
action.device_id, _e)
|
|
299
305
|
|
|
300
306
|
def _update_resolver_state(self, action: DeviceAction) -> None:
|
|
301
|
-
"""
|
|
307
|
+
"""Tell the StateAwareResolver the new state after a successful action."""
|
|
302
308
|
from ..hub import StateAwareResolver
|
|
303
309
|
resolver = getattr(self._hub, 'resolver', None)
|
|
304
310
|
if not isinstance(resolver, StateAwareResolver):
|