ha-mcp-dev 7.4.1.dev485__py3-none-any.whl → 7.4.1.dev486__py3-none-any.whl
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.
- ha_mcp/tools/tools_integrations.py +110 -6
- {ha_mcp_dev-7.4.1.dev485.dist-info → ha_mcp_dev-7.4.1.dev486.dist-info}/METADATA +1 -1
- {ha_mcp_dev-7.4.1.dev485.dist-info → ha_mcp_dev-7.4.1.dev486.dist-info}/RECORD +7 -7
- {ha_mcp_dev-7.4.1.dev485.dist-info → ha_mcp_dev-7.4.1.dev486.dist-info}/WHEEL +0 -0
- {ha_mcp_dev-7.4.1.dev485.dist-info → ha_mcp_dev-7.4.1.dev486.dist-info}/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev485.dist-info → ha_mcp_dev-7.4.1.dev486.dist-info}/licenses/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev485.dist-info → ha_mcp_dev-7.4.1.dev486.dist-info}/top_level.txt +0 -0
|
@@ -295,6 +295,17 @@ class IntegrationTools:
|
|
|
295
295
|
try:
|
|
296
296
|
result = await self._client.get_config_entry(entry_id)
|
|
297
297
|
entry_domain = result.get("domain") if isinstance(result, dict) else None
|
|
298
|
+
|
|
299
|
+
# Surface `options` on every per-entry response (HA's REST endpoint
|
|
300
|
+
# omits the field). For entries with supports_options=True we probe
|
|
301
|
+
# via OptionsFlow — see `_fetch_entry_options`. When include_schema
|
|
302
|
+
# is also requested, `_fetch_options_schema` below populates options
|
|
303
|
+
# from the same flow init so we don't pay for two round-trips.
|
|
304
|
+
if isinstance(result, dict):
|
|
305
|
+
result.setdefault("options", {})
|
|
306
|
+
if result.get("supports_options") and not include_schema:
|
|
307
|
+
result["options"] = await self._fetch_entry_options(entry_id)
|
|
308
|
+
|
|
298
309
|
resp: dict[str, Any] = {
|
|
299
310
|
"success": True,
|
|
300
311
|
"entry_id": entry_id,
|
|
@@ -326,21 +337,95 @@ class IntegrationTools:
|
|
|
326
337
|
],
|
|
327
338
|
)
|
|
328
339
|
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _options_from_form_flow(flow: dict[str, Any]) -> dict[str, Any]:
|
|
342
|
+
"""Extract ``{field_name: current_value}`` from a form-type OptionsFlow.
|
|
343
|
+
|
|
344
|
+
Reads each ``data_schema`` entry's ``default`` key, falling back to
|
|
345
|
+
``value`` only when the ``default`` key is absent (constant-type
|
|
346
|
+
fields ship ``value`` instead of ``default``). Fields with a missing
|
|
347
|
+
or ``None`` value are skipped.
|
|
348
|
+
"""
|
|
349
|
+
out: dict[str, Any] = {}
|
|
350
|
+
for field in flow.get("data_schema") or []:
|
|
351
|
+
name = field.get("name")
|
|
352
|
+
if name is None:
|
|
353
|
+
continue
|
|
354
|
+
value = field.get("default", field.get("value"))
|
|
355
|
+
if value is not None:
|
|
356
|
+
out[name] = value
|
|
357
|
+
return out
|
|
358
|
+
|
|
359
|
+
async def _fetch_entry_options(self, entry_id: str) -> dict[str, Any]:
|
|
360
|
+
"""Read the current ``options`` for a config entry via its OptionsFlow.
|
|
361
|
+
|
|
362
|
+
Home Assistant does not expose ``ConfigEntry.options`` through any
|
|
363
|
+
read-only REST or WebSocket endpoint — ``/api/config/config_entries/entry``
|
|
364
|
+
deliberately omits the field. The closest approximation that the HA UI
|
|
365
|
+
itself uses is the ``default`` values populated into the OptionsFlow's
|
|
366
|
+
first-step ``data_schema``: integrations build that schema from the
|
|
367
|
+
existing options dict, so the defaults match the persisted state.
|
|
368
|
+
|
|
369
|
+
Starts the flow, harvests ``{name: default}`` from the first step,
|
|
370
|
+
and aborts the flow in ``finally`` so it doesn't sit half-open.
|
|
371
|
+
|
|
372
|
+
Returns ``{}`` on any failure (unsupported entry, non-form first step
|
|
373
|
+
such as a menu, init/abort errors) so callers can treat the return as
|
|
374
|
+
the canonical "options" field without further checks. Unexpected
|
|
375
|
+
exception types are logged at ``warning`` so probe breakage is
|
|
376
|
+
discoverable.
|
|
377
|
+
"""
|
|
378
|
+
flow_id: str | None = None
|
|
379
|
+
try:
|
|
380
|
+
flow = await self._client.start_options_flow(entry_id)
|
|
381
|
+
flow_id = flow.get("flow_id")
|
|
382
|
+
flow_type = flow.get("type")
|
|
383
|
+
if flow_type != "form":
|
|
384
|
+
logger.debug(
|
|
385
|
+
f"OptionsFlow for {entry_id} returned type={flow_type!r}, "
|
|
386
|
+
f"not a form — cannot extract option defaults"
|
|
387
|
+
)
|
|
388
|
+
return {}
|
|
389
|
+
return self._options_from_form_flow(flow)
|
|
390
|
+
except Exception as exc:
|
|
391
|
+
logger.warning(
|
|
392
|
+
f"Failed to fetch options for {entry_id}: "
|
|
393
|
+
f"{type(exc).__name__}: {exc}"
|
|
394
|
+
)
|
|
395
|
+
return {}
|
|
396
|
+
finally:
|
|
397
|
+
if flow_id:
|
|
398
|
+
try:
|
|
399
|
+
await self._client.abort_options_flow(flow_id)
|
|
400
|
+
except Exception as abort_err:
|
|
401
|
+
logger.warning(
|
|
402
|
+
f"Failed to abort options flow {flow_id}: "
|
|
403
|
+
f"{type(abort_err).__name__}: {abort_err}"
|
|
404
|
+
)
|
|
405
|
+
|
|
329
406
|
async def _fetch_options_schema(
|
|
330
407
|
self, entry_id: str, resp: dict[str, Any]
|
|
331
408
|
) -> None:
|
|
332
|
-
"""Start an options flow to read the schema, then abort it.
|
|
409
|
+
"""Start an options flow to read the schema, then abort it.
|
|
410
|
+
|
|
411
|
+
Also populates ``resp["entry"]["options"]`` for form-type flows from
|
|
412
|
+
the same flow result so callers requesting both schema and options
|
|
413
|
+
don't pay for two round-trips.
|
|
414
|
+
"""
|
|
333
415
|
flow_id = None
|
|
334
416
|
try:
|
|
335
417
|
flow_result = await self._client.start_options_flow(entry_id)
|
|
336
418
|
flow_id = flow_result.get("flow_id")
|
|
337
419
|
flow_type = flow_result.get("type")
|
|
420
|
+
entry = resp.get("entry") if isinstance(resp.get("entry"), dict) else None
|
|
338
421
|
if flow_type == "form":
|
|
339
422
|
resp["options_schema"] = {
|
|
340
423
|
"flow_type": "form",
|
|
341
424
|
"step_id": flow_result.get("step_id"),
|
|
342
425
|
"data_schema": flow_result.get("data_schema", []),
|
|
343
426
|
}
|
|
427
|
+
if entry is not None:
|
|
428
|
+
entry["options"] = self._options_from_form_flow(flow_result)
|
|
344
429
|
elif flow_type == "menu":
|
|
345
430
|
resp["options_schema"] = {
|
|
346
431
|
"flow_type": "menu",
|
|
@@ -348,16 +433,18 @@ class IntegrationTools:
|
|
|
348
433
|
"menu_options": flow_result.get("menu_options", []),
|
|
349
434
|
}
|
|
350
435
|
except Exception as schema_err:
|
|
351
|
-
logger.
|
|
352
|
-
f"Failed to fetch options schema for {entry_id}:
|
|
436
|
+
logger.warning(
|
|
437
|
+
f"Failed to fetch options schema for {entry_id}: "
|
|
438
|
+
f"{type(schema_err).__name__}: {schema_err}"
|
|
353
439
|
)
|
|
354
440
|
finally:
|
|
355
441
|
if flow_id:
|
|
356
442
|
try:
|
|
357
443
|
await self._client.abort_options_flow(flow_id)
|
|
358
444
|
except Exception as abort_err:
|
|
359
|
-
logger.
|
|
360
|
-
f"Failed to abort options flow {flow_id}:
|
|
445
|
+
logger.warning(
|
|
446
|
+
f"Failed to abort options flow {flow_id}: "
|
|
447
|
+
f"{type(abort_err).__name__}: {abort_err}"
|
|
361
448
|
)
|
|
362
449
|
|
|
363
450
|
async def _list_entries(
|
|
@@ -394,11 +481,28 @@ class IntegrationTools:
|
|
|
394
481
|
# Fetch current logger levels once; enrich each entry with its effective level.
|
|
395
482
|
logger_levels = await get_logger_levels(self._client)
|
|
396
483
|
|
|
397
|
-
#
|
|
484
|
+
# `_format_entry` is sync and cannot probe the OptionsFlow; options
|
|
485
|
+
# are filled in by a second async pass below for entries that
|
|
486
|
+
# advertise supports_options=True. See `_fetch_entry_options`.
|
|
398
487
|
formatted_entries = [
|
|
399
488
|
self._format_entry(entry, include_opts, logger_levels) for entry in entries
|
|
400
489
|
]
|
|
401
490
|
|
|
491
|
+
if include_opts:
|
|
492
|
+
options_targets = [
|
|
493
|
+
e for e in formatted_entries if e.get("supports_options")
|
|
494
|
+
]
|
|
495
|
+
if options_targets:
|
|
496
|
+
fetched = await asyncio.gather(
|
|
497
|
+
*(
|
|
498
|
+
self._fetch_entry_options(e["entry_id"])
|
|
499
|
+
for e in options_targets
|
|
500
|
+
),
|
|
501
|
+
return_exceptions=False,
|
|
502
|
+
)
|
|
503
|
+
for entry, opts in zip(options_targets, fetched, strict=True):
|
|
504
|
+
entry["options"] = opts
|
|
505
|
+
|
|
402
506
|
# Apply search filter if query provided
|
|
403
507
|
if query and query.strip():
|
|
404
508
|
formatted_entries = self._filter_by_query(
|
|
@@ -65,7 +65,7 @@ ha_mcp/tools/tools_filesystem.py,sha256=QJ2qmXMhLpujd7COSBP5IHrpOt7AgonZdDneqxBw
|
|
|
65
65
|
ha_mcp/tools/tools_groups.py,sha256=Z9QaGrrBUmuLk9HPXA_Wt75hTaJYpa4dU2sohA8MX80,16470
|
|
66
66
|
ha_mcp/tools/tools_hacs.py,sha256=6nk04kUz1HBKy9yphEkkmAw_qXt7KYO9N6HMWfChadY,27269
|
|
67
67
|
ha_mcp/tools/tools_history.py,sha256=4zUGAKsjIxrdzV0NHJQ0ZtFwZKaoOYIVnBBzLuoW9TQ,27740
|
|
68
|
-
ha_mcp/tools/tools_integrations.py,sha256=
|
|
68
|
+
ha_mcp/tools/tools_integrations.py,sha256=X21p0P42TZvPE3OwwUn5oJb951c5GgeMNyFhPJTR1Wo,56908
|
|
69
69
|
ha_mcp/tools/tools_labels.py,sha256=YslFRGdR3w4HQswTSZNBJrQJxpir0ORgBHtcxr36uSA,10145
|
|
70
70
|
ha_mcp/tools/tools_mcp_component.py,sha256=_jIMmuMcI9_Rj8h0OpgaN9FMIPZEay3s-O4lzVqJnvQ,12525
|
|
71
71
|
ha_mcp/tools/tools_registry.py,sha256=H5NmcQdFxEKzHhTYrAUw4qSm6Sk160yOsLO66QAOmVw,34041
|
|
@@ -93,12 +93,12 @@ ha_mcp/utils/kill_signal_diagnostics.py,sha256=PyoRIm9xJ36iiRtTaVKxfXAKlylytSx0z
|
|
|
93
93
|
ha_mcp/utils/operation_manager.py,sha256=1ETI_L2TFNhnJUUJwtuH4R0s6ZP3_rscIOfdehYSmkU,14266
|
|
94
94
|
ha_mcp/utils/python_sandbox.py,sha256=lPWqbrqTrJJb4RIQmWKaoADeUeyIRCQ4SF0tw4pXvNs,18566
|
|
95
95
|
ha_mcp/utils/usage_logger.py,sha256=YDUJqaz9oLQVOH8Lkhwki2sn8WzD63I2s6jqeLdix6c,10021
|
|
96
|
-
ha_mcp_dev-7.4.1.
|
|
96
|
+
ha_mcp_dev-7.4.1.dev486.dist-info/licenses/LICENSE,sha256=7rJXXKBJWgJF8595wk-YTxwVTEi1kQaIqyy9dh5o_oY,1062
|
|
97
97
|
tests/__init__.py,sha256=YRpec-ZFYCJ48oD_7ZcNY7dB8avoTWOrZICjaM-BYJ0,39
|
|
98
98
|
tests/test_constants.py,sha256=bnqo5-cmXcJvJPsA8RezDljGwlRSoXW4Ecwj0h1_INM,876
|
|
99
99
|
tests/test_env_manager.py,sha256=fcaFelaWA5c52vvwJ1IBvoBb6PMhDNrwv4H38x3j9Do,11948
|
|
100
|
-
ha_mcp_dev-7.4.1.
|
|
101
|
-
ha_mcp_dev-7.4.1.
|
|
102
|
-
ha_mcp_dev-7.4.1.
|
|
103
|
-
ha_mcp_dev-7.4.1.
|
|
104
|
-
ha_mcp_dev-7.4.1.
|
|
100
|
+
ha_mcp_dev-7.4.1.dev486.dist-info/METADATA,sha256=V1J-1N2gp8ditXiZyBcumFTBbIzCURQtsN-9GifUdi4,21269
|
|
101
|
+
ha_mcp_dev-7.4.1.dev486.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
102
|
+
ha_mcp_dev-7.4.1.dev486.dist-info/entry_points.txt,sha256=ckO8PIrfV4-YQEyjqgO8wIzcQiMFTTJNWKZLyRtFpms,292
|
|
103
|
+
ha_mcp_dev-7.4.1.dev486.dist-info/top_level.txt,sha256=cqJLEmgh4gQBKg_vBqj0ahS4DCg4J0qBXYgZCDQ2IWs,13
|
|
104
|
+
ha_mcp_dev-7.4.1.dev486.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|