ha-mcp-dev 7.2.0.dev362__tar.gz → 7.2.0.dev363__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.
Files changed (100) hide show
  1. {ha_mcp_dev-7.2.0.dev362/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev363}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_history.py +119 -13
  4. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  5. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/LICENSE +0 -0
  6. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/MANIFEST.in +0 -0
  7. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/README.md +0 -0
  8. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/setup.cfg +0 -0
  9. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/__init__.py +0 -0
  10. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/__main__.py +0 -0
  11. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/_pypi_marker +0 -0
  12. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/auth/__init__.py +0 -0
  13. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/auth/consent_form.py +0 -0
  14. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/auth/provider.py +0 -0
  15. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/client/__init__.py +0 -0
  16. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/client/rest_client.py +0 -0
  17. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/client/websocket_client.py +0 -0
  18. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/client/websocket_listener.py +0 -0
  19. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/config.py +0 -0
  20. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/errors.py +0 -0
  21. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/py.typed +0 -0
  22. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  23. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  24. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  25. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  26. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  27. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  28. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  29. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  30. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  31. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  32. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  33. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  34. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  35. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  36. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  37. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  38. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  39. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  40. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  41. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  42. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/server.py +0 -0
  43. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/smoke_test.py +0 -0
  44. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/__init__.py +0 -0
  45. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/backup.py +0 -0
  46. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  47. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/device_control.py +0 -0
  48. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/enhanced.py +0 -0
  49. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/helpers.py +0 -0
  50. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/registry.py +0 -0
  51. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/smart_search.py +0 -0
  52. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_addons.py +0 -0
  53. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_areas.py +0 -0
  54. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  55. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  56. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_calendar.py +0 -0
  57. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_camera.py +0 -0
  58. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_categories.py +0 -0
  59. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  60. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  61. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  62. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  63. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  64. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_entities.py +0 -0
  65. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  66. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_groups.py +0 -0
  67. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_hacs.py +0 -0
  68. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_integrations.py +0 -0
  69. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_labels.py +0 -0
  70. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  71. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_registry.py +0 -0
  72. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_resources.py +0 -0
  73. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_search.py +0 -0
  74. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_service.py +0 -0
  75. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_services.py +0 -0
  76. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_system.py +0 -0
  77. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_todo.py +0 -0
  78. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_traces.py +0 -0
  79. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_updates.py +0 -0
  80. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_utility.py +0 -0
  81. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  82. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  83. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/tools_zones.py +0 -0
  84. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/tools/util_helpers.py +0 -0
  85. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/transforms/__init__.py +0 -0
  86. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/transforms/categorized_search.py +0 -0
  87. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/utils/__init__.py +0 -0
  88. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/utils/domain_handlers.py +0 -0
  89. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  90. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/utils/operation_manager.py +0 -0
  91. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/utils/python_sandbox.py +0 -0
  92. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp/utils/usage_logger.py +0 -0
  93. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  94. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  95. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  96. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  97. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  98. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/tests/__init__.py +0 -0
  99. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/tests/test_constants.py +0 -0
  100. {ha_mcp_dev-7.2.0.dev362 → ha_mcp_dev-7.2.0.dev363}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.2.0.dev362
3
+ Version: 7.2.0.dev363
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.2.0.dev362"
7
+ version = "7.2.0.dev363"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -28,6 +28,7 @@ from .helpers import (
28
28
  )
29
29
  from .util_helpers import (
30
30
  add_timezone_metadata,
31
+ build_pagination_metadata,
31
32
  coerce_int_param,
32
33
  parse_string_list_param,
33
34
  )
@@ -178,7 +179,14 @@ class HistoryTools:
178
179
  limit: Annotated[
179
180
  int | str | None,
180
181
  Field(
181
- description='Max state changes per entity. Default: 100, Max: 1000. Ignored when source="statistics"',
182
+ description='Max entries per entity. Default: 100, Max: 1000. For source="history": state changes. For source="statistics": aggregated rows. With multiple entity_ids, offset must be 0 and total rows returned can reach limit × len(entity_ids).',
183
+ default=None,
184
+ ),
185
+ ] = None,
186
+ offset: Annotated[
187
+ int | str | None,
188
+ Field(
189
+ description="Number of entries to skip per entity for pagination. Default: 0. Offset > 0 requires a single entity_id. Use with limit and has_more/next_offset in the response.",
182
190
  default=None,
183
191
  ),
184
192
  ] = None,
@@ -205,9 +213,9 @@ class HistoryTools:
205
213
  - "history" (default): Raw state changes, ~10 day retention, full resolution
206
214
  - "statistics": Pre-aggregated data, permanent retention, requires state_class
207
215
 
208
- **Shared params:** entity_ids, start_time, end_time
209
- **History params:** minimal_response, significant_changes_only, limit (ignored for statistics)
210
- **Statistics params:** period, statistic_types (ignored for history)
216
+ **Shared params:** entity_ids, start_time, end_time, limit, offset
217
+ **History params:** minimal_response, significant_changes_only
218
+ **Statistics params:** period, statistic_types
211
219
 
212
220
  **Default time range:** 24h for history, 30 days for statistics
213
221
 
@@ -221,10 +229,16 @@ class HistoryTools:
221
229
  - Computing period averages ("Average living room temperature over 6 months?")
222
230
  - Entities must have state_class (measurement, total, total_increasing)
223
231
 
232
+ **WARNING:** limit and offset apply per entity (not globally across all entities).
233
+ All data is fetched from HA before slicing; limit/offset are client-side.
234
+ With multiple entity_ids, offset must be 0 — use a single entity_id for offset > 0.
235
+ Use has_more and next_offset from the response to paginate.
236
+
224
237
  **Example -- history (default):**
225
238
  ```python
226
239
  ha_get_history(entity_ids="sensor.bedroom_temperature", start_time="24h")
227
240
  ha_get_history(entity_ids=["sensor.temperature", "sensor.humidity"], start_time="7d", limit=500)
241
+ ha_get_history(entity_ids="sensor.temperature", start_time="7d", limit=100, offset=100)
228
242
  ```
229
243
 
230
244
  **Example -- statistics:**
@@ -232,12 +246,42 @@ class HistoryTools:
232
246
  ha_get_history(source="statistics", entity_ids="sensor.total_energy_kwh", start_time="30d", period="day")
233
247
  ha_get_history(source="statistics", entity_ids="sensor.living_room_temperature",
234
248
  start_time="6m", period="month", statistic_types=["mean", "min", "max"])
249
+ ha_get_history(source="statistics", entity_ids="sensor.energy_kwh",
250
+ start_time="30d", period="5minute", limit=100, offset=200)
235
251
  ```
236
252
  """
237
253
  try:
238
254
  # Parse entity_ids
239
255
  entity_id_list = _parse_entity_ids(entity_ids)
240
256
 
257
+ # Offset > 0 is only supported for single-entity requests.
258
+ # build_pagination_metadata applies per entity — limit=100 across
259
+ # 5 entities returns up to 500 rows with no top-level has_more signal.
260
+ # Coerce and validate offset before the multi-entity guard so that
261
+ # invalid strings (e.g. "garbage") produce VALIDATION_INVALID_PARAMETER
262
+ # instead of a bare ValueError swallowed by the outer except.
263
+ try:
264
+ _effective_offset_check = coerce_int_param(
265
+ offset,
266
+ param_name="offset",
267
+ default=0,
268
+ min_value=0,
269
+ )
270
+ except ValueError as e:
271
+ raise_tool_error(create_error_response(
272
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
273
+ str(e),
274
+ context={"parameter": "offset"},
275
+ suggestions=["Provide offset as a non-negative integer (e.g., 0)"],
276
+ ))
277
+ if _effective_offset_check > 0 and len(entity_id_list) > 1:
278
+ raise_tool_error(create_error_response(
279
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
280
+ "offset > 0 requires a single entity_id",
281
+ context={"offset": offset, "entity_count": len(entity_id_list)},
282
+ suggestions=["Use a single entity_id when offset > 0, or use offset=0 for multi-entity requests."],
283
+ ))
284
+
241
285
  # Source-dependent default hours
242
286
  default_hours = _DEFAULT_START_HOURS_BY_SOURCE[source]
243
287
 
@@ -259,12 +303,13 @@ class HistoryTools:
259
303
  return await _fetch_statistics(
260
304
  ws_client, self._client, entity_id_list,
261
305
  start_dt, end_dt, period, statistic_types,
306
+ limit, offset,
262
307
  )
263
308
  else:
264
309
  return await _fetch_history(
265
310
  ws_client, self._client, entity_id_list,
266
311
  start_dt, end_dt, minimal_response,
267
- significant_changes_only, limit,
312
+ significant_changes_only, limit, offset,
268
313
  _DEFAULT_HISTORY_LIMIT, _MAX_HISTORY_LIMIT,
269
314
  )
270
315
  finally:
@@ -371,6 +416,7 @@ async def _fetch_history(
371
416
  minimal_response: bool,
372
417
  significant_changes_only: bool,
373
418
  limit: int | str | None,
419
+ offset: int | str | None,
374
420
  default_limit: int,
375
421
  max_limit: int,
376
422
  ) -> dict[str, Any]:
@@ -382,7 +428,7 @@ async def _fetch_history(
382
428
  default=default_limit,
383
429
  min_value=1,
384
430
  max_value=max_limit,
385
- ) or default_limit
431
+ )
386
432
  except ValueError as e:
387
433
  raise_tool_error(create_error_response(
388
434
  ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -391,6 +437,21 @@ async def _fetch_history(
391
437
  suggestions=["Provide limit as an integer (e.g., 100)"],
392
438
  ))
393
439
 
440
+ try:
441
+ effective_offset = coerce_int_param(
442
+ offset,
443
+ param_name="offset",
444
+ default=0,
445
+ min_value=0,
446
+ )
447
+ except ValueError as e:
448
+ raise_tool_error(create_error_response(
449
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
450
+ str(e),
451
+ context={"parameter": "offset"},
452
+ suggestions=["Provide offset as a non-negative integer (e.g., 0)"],
453
+ ))
454
+
394
455
  command_params = {
395
456
  "start_time": start_dt.isoformat(),
396
457
  "end_time": end_dt.isoformat(),
@@ -422,10 +483,10 @@ async def _fetch_history(
422
483
 
423
484
  for entity_id in entity_id_list:
424
485
  entity_states = result_data.get(entity_id, [])
425
- limited_states = entity_states[:effective_limit]
486
+ paged_states = entity_states[effective_offset : effective_offset + effective_limit]
426
487
 
427
488
  formatted_states = []
428
- for state in limited_states:
489
+ for state in paged_states:
429
490
  last_updated_raw = state.get("lu", state.get("last_updated"))
430
491
  last_changed_raw = state.get("lc", state.get("last_changed"))
431
492
  if last_changed_raw is None and last_updated_raw is not None:
@@ -440,6 +501,12 @@ async def _fetch_history(
440
501
  state_entry["attributes"] = state.get("a", state.get("attributes", {}))
441
502
  formatted_states.append(state_entry)
442
503
 
504
+ pagination = build_pagination_metadata(
505
+ total_count=len(entity_states),
506
+ offset=effective_offset,
507
+ limit=effective_limit,
508
+ count=len(formatted_states),
509
+ )
443
510
  entities_history.append({
444
511
  "entity_id": entity_id,
445
512
  "period": {
@@ -447,9 +514,7 @@ async def _fetch_history(
447
514
  "end": end_dt.isoformat(),
448
515
  },
449
516
  "states": formatted_states,
450
- "count": len(formatted_states),
451
- "total_available": len(entity_states),
452
- "truncated": len(entity_states) > effective_limit,
517
+ **pagination,
453
518
  })
454
519
 
455
520
  history_data = {
@@ -464,6 +529,7 @@ async def _fetch_history(
464
529
  "minimal_response": minimal_response,
465
530
  "significant_changes_only": significant_changes_only,
466
531
  "limit": effective_limit,
532
+ "offset": effective_offset,
467
533
  },
468
534
  }
469
535
 
@@ -478,8 +544,41 @@ async def _fetch_statistics(
478
544
  end_dt: datetime,
479
545
  period: str,
480
546
  statistic_types: str | list[str] | None,
547
+ limit: int | str | None,
548
+ offset: int | str | None,
481
549
  ) -> dict[str, Any]:
482
550
  """Execute the recorder/statistics_during_period WebSocket call."""
551
+ try:
552
+ effective_limit = coerce_int_param(
553
+ limit,
554
+ param_name="limit",
555
+ default=_DEFAULT_HISTORY_LIMIT,
556
+ min_value=1,
557
+ max_value=_MAX_HISTORY_LIMIT,
558
+ )
559
+ except ValueError as e:
560
+ raise_tool_error(create_error_response(
561
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
562
+ str(e),
563
+ context={"parameter": "limit"},
564
+ suggestions=["Provide limit as an integer (e.g., 100)"],
565
+ ))
566
+
567
+ try:
568
+ effective_offset = coerce_int_param(
569
+ offset,
570
+ param_name="offset",
571
+ default=0,
572
+ min_value=0,
573
+ )
574
+ except ValueError as e:
575
+ raise_tool_error(create_error_response(
576
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
577
+ str(e),
578
+ context={"parameter": "offset"},
579
+ suggestions=["Provide offset as a non-negative integer (e.g., 0)"],
580
+ ))
581
+
483
582
  # Validate period
484
583
  valid_periods = ["5minute", "hour", "day", "week", "month"]
485
584
  if period not in valid_periods:
@@ -547,10 +646,11 @@ async def _fetch_statistics(
547
646
 
548
647
  for entity_id in entity_id_list:
549
648
  entity_stats = result_data.get(entity_id, [])
649
+ paged_stats = entity_stats[effective_offset : effective_offset + effective_limit]
550
650
  formatted_stats = []
551
651
  unit = None
552
652
 
553
- for stat in entity_stats:
653
+ for stat in paged_stats:
554
654
  stat_entry: dict[str, Any] = {"start": stat.get("start")}
555
655
  for stat_type in all_stat_types:
556
656
  if stat_type in stat:
@@ -559,12 +659,18 @@ async def _fetch_statistics(
559
659
  unit = stat["unit_of_measurement"]
560
660
  formatted_stats.append(stat_entry)
561
661
 
662
+ pagination = build_pagination_metadata(
663
+ total_count=len(entity_stats),
664
+ offset=effective_offset,
665
+ limit=effective_limit,
666
+ count=len(formatted_stats),
667
+ )
562
668
  entities_statistics.append({
563
669
  "entity_id": entity_id,
564
670
  "period": period,
565
671
  "statistics": formatted_stats,
566
- "count": len(formatted_stats),
567
672
  "unit_of_measurement": unit,
673
+ **pagination,
568
674
  })
569
675
 
570
676
  empty_entities: list[str] = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.2.0.dev362
3
+ Version: 7.2.0.dev363
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT