ha-mcp-dev 6.3.1.dev137__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.
Files changed (71) hide show
  1. ha_mcp/__init__.py +51 -0
  2. ha_mcp/__main__.py +753 -0
  3. ha_mcp/_pypi_marker +0 -0
  4. ha_mcp/auth/__init__.py +10 -0
  5. ha_mcp/auth/consent_form.py +501 -0
  6. ha_mcp/auth/provider.py +786 -0
  7. ha_mcp/client/__init__.py +10 -0
  8. ha_mcp/client/rest_client.py +924 -0
  9. ha_mcp/client/websocket_client.py +656 -0
  10. ha_mcp/client/websocket_listener.py +340 -0
  11. ha_mcp/config.py +175 -0
  12. ha_mcp/errors.py +399 -0
  13. ha_mcp/py.typed +0 -0
  14. ha_mcp/resources/card_types.json +48 -0
  15. ha_mcp/resources/dashboard_guide.md +573 -0
  16. ha_mcp/server.py +189 -0
  17. ha_mcp/smoke_test.py +108 -0
  18. ha_mcp/tools/__init__.py +11 -0
  19. ha_mcp/tools/backup.py +457 -0
  20. ha_mcp/tools/device_control.py +687 -0
  21. ha_mcp/tools/enhanced.py +191 -0
  22. ha_mcp/tools/helpers.py +184 -0
  23. ha_mcp/tools/registry.py +199 -0
  24. ha_mcp/tools/smart_search.py +797 -0
  25. ha_mcp/tools/tools_addons.py +343 -0
  26. ha_mcp/tools/tools_areas.py +478 -0
  27. ha_mcp/tools/tools_blueprints.py +279 -0
  28. ha_mcp/tools/tools_bug_report.py +570 -0
  29. ha_mcp/tools/tools_calendar.py +391 -0
  30. ha_mcp/tools/tools_camera.py +149 -0
  31. ha_mcp/tools/tools_config_automations.py +508 -0
  32. ha_mcp/tools/tools_config_dashboards.py +1502 -0
  33. ha_mcp/tools/tools_config_entry_flow.py +223 -0
  34. ha_mcp/tools/tools_config_helpers.py +925 -0
  35. ha_mcp/tools/tools_config_info.py +258 -0
  36. ha_mcp/tools/tools_config_scripts.py +267 -0
  37. ha_mcp/tools/tools_entities.py +76 -0
  38. ha_mcp/tools/tools_filesystem.py +589 -0
  39. ha_mcp/tools/tools_groups.py +306 -0
  40. ha_mcp/tools/tools_hacs.py +787 -0
  41. ha_mcp/tools/tools_history.py +714 -0
  42. ha_mcp/tools/tools_integrations.py +297 -0
  43. ha_mcp/tools/tools_labels.py +713 -0
  44. ha_mcp/tools/tools_mcp_component.py +305 -0
  45. ha_mcp/tools/tools_registry.py +1115 -0
  46. ha_mcp/tools/tools_resources.py +622 -0
  47. ha_mcp/tools/tools_search.py +573 -0
  48. ha_mcp/tools/tools_service.py +211 -0
  49. ha_mcp/tools/tools_services.py +352 -0
  50. ha_mcp/tools/tools_system.py +363 -0
  51. ha_mcp/tools/tools_todo.py +530 -0
  52. ha_mcp/tools/tools_traces.py +496 -0
  53. ha_mcp/tools/tools_updates.py +476 -0
  54. ha_mcp/tools/tools_utility.py +519 -0
  55. ha_mcp/tools/tools_voice_assistant.py +345 -0
  56. ha_mcp/tools/tools_zones.py +387 -0
  57. ha_mcp/tools/util_helpers.py +199 -0
  58. ha_mcp/utils/__init__.py +30 -0
  59. ha_mcp/utils/domain_handlers.py +380 -0
  60. ha_mcp/utils/fuzzy_search.py +339 -0
  61. ha_mcp/utils/operation_manager.py +442 -0
  62. ha_mcp/utils/usage_logger.py +290 -0
  63. ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
  64. ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
  65. ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
  66. ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
  67. ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
  68. ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
  69. tests/__init__.py +1 -0
  70. tests/test_constants.py +15 -0
  71. tests/test_env_manager.py +336 -0
ha_mcp/_pypi_marker ADDED
File without changes
@@ -0,0 +1,10 @@
1
+ """
2
+ OAuth 2.1 authentication for Home Assistant MCP Server.
3
+
4
+ This module provides OAuth 2.1 authentication with Dynamic Client Registration (DCR)
5
+ and a consent form for collecting Home Assistant credentials.
6
+ """
7
+
8
+ from .provider import HomeAssistantOAuthProvider
9
+
10
+ __all__ = ["HomeAssistantOAuthProvider"]
@@ -0,0 +1,501 @@
1
+ """
2
+ Consent form HTML template for Home Assistant OAuth authentication.
3
+
4
+ This module provides the HTML consent form where users enter their
5
+ Home Assistant URL and Long-Lived Access Token (LLAT).
6
+ """
7
+
8
+
9
+ def create_consent_html(
10
+ client_id: str,
11
+ client_name: str | None,
12
+ redirect_uri: str,
13
+ state: str,
14
+ scopes: list[str],
15
+ error_message: str | None = None,
16
+ ) -> str:
17
+ """
18
+ Generate HTML consent form for Home Assistant authentication.
19
+
20
+ Args:
21
+ client_id: OAuth client ID
22
+ client_name: Human-readable client name
23
+ redirect_uri: OAuth redirect URI
24
+ state: OAuth state parameter
25
+ scopes: Requested OAuth scopes
26
+ error_message: Optional error message to display
27
+
28
+ Returns:
29
+ HTML string for the consent form
30
+ """
31
+ display_name = client_name or client_id
32
+ scopes_display = ", ".join(scopes) if scopes else "full access"
33
+
34
+ error_html = ""
35
+ if error_message:
36
+ error_html = f"""
37
+ <div class="error-message">
38
+ <strong>Error:</strong> {error_message}
39
+ </div>
40
+ """
41
+
42
+ return f"""
43
+ <!DOCTYPE html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="UTF-8">
47
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
48
+ <title>Connect to Home Assistant</title>
49
+ <style>
50
+ :root {{
51
+ --primary-color: #03a9f4;
52
+ --primary-hover: #0288d1;
53
+ --error-color: #f44336;
54
+ --error-bg: #ffebee;
55
+ --success-color: #4caf50;
56
+ --text-color: #212121;
57
+ --text-secondary: #757575;
58
+ --border-color: #e0e0e0;
59
+ --bg-color: #fafafa;
60
+ --card-bg: #ffffff;
61
+ }}
62
+
63
+ @media (prefers-color-scheme: dark) {{
64
+ :root {{
65
+ --primary-color: #29b6f6;
66
+ --primary-hover: #4fc3f7;
67
+ --error-color: #ef5350;
68
+ --error-bg: #3e2723;
69
+ --success-color: #66bb6a;
70
+ --text-color: #e0e0e0;
71
+ --text-secondary: #9e9e9e;
72
+ --border-color: #424242;
73
+ --bg-color: #121212;
74
+ --card-bg: #1e1e1e;
75
+ }}
76
+ }}
77
+
78
+ * {{
79
+ box-sizing: border-box;
80
+ margin: 0;
81
+ padding: 0;
82
+ }}
83
+
84
+ body {{
85
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
86
+ background-color: var(--bg-color);
87
+ color: var(--text-color);
88
+ min-height: 100vh;
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ padding: 20px;
93
+ }}
94
+
95
+ .container {{
96
+ background: var(--card-bg);
97
+ border-radius: 16px;
98
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
99
+ max-width: 440px;
100
+ width: 100%;
101
+ padding: 32px;
102
+ }}
103
+
104
+ .header {{
105
+ text-align: center;
106
+ margin-bottom: 24px;
107
+ }}
108
+
109
+ .logo {{
110
+ width: 80px;
111
+ height: 80px;
112
+ margin-bottom: 16px;
113
+ }}
114
+
115
+ h1 {{
116
+ font-size: 24px;
117
+ font-weight: 600;
118
+ margin-bottom: 8px;
119
+ }}
120
+
121
+ .subtitle {{
122
+ color: var(--text-secondary);
123
+ font-size: 14px;
124
+ }}
125
+
126
+ .client-info {{
127
+ background: var(--bg-color);
128
+ border-radius: 8px;
129
+ padding: 16px;
130
+ margin-bottom: 24px;
131
+ }}
132
+
133
+ .client-info p {{
134
+ font-size: 14px;
135
+ color: var(--text-secondary);
136
+ }}
137
+
138
+ .client-info strong {{
139
+ color: var(--text-color);
140
+ }}
141
+
142
+ .scopes {{
143
+ margin-top: 8px;
144
+ font-size: 13px;
145
+ color: var(--text-secondary);
146
+ }}
147
+
148
+ .error-message {{
149
+ background: var(--error-bg);
150
+ border: 1px solid var(--error-color);
151
+ border-radius: 8px;
152
+ padding: 12px 16px;
153
+ margin-bottom: 20px;
154
+ font-size: 14px;
155
+ color: var(--error-color);
156
+ }}
157
+
158
+ .form-group {{
159
+ margin-bottom: 20px;
160
+ }}
161
+
162
+ label {{
163
+ display: block;
164
+ font-size: 14px;
165
+ font-weight: 500;
166
+ margin-bottom: 8px;
167
+ }}
168
+
169
+ input[type="text"],
170
+ input[type="url"],
171
+ input[type="password"] {{
172
+ width: 100%;
173
+ padding: 12px 16px;
174
+ font-size: 16px;
175
+ border: 1px solid var(--border-color);
176
+ border-radius: 8px;
177
+ background: var(--card-bg);
178
+ color: var(--text-color);
179
+ transition: border-color 0.2s, box-shadow 0.2s;
180
+ }}
181
+
182
+ input:focus {{
183
+ outline: none;
184
+ border-color: var(--primary-color);
185
+ box-shadow: 0 0 0 3px rgba(3, 169, 244, 0.1);
186
+ }}
187
+
188
+ input::placeholder {{
189
+ color: var(--text-secondary);
190
+ }}
191
+
192
+ .help-text {{
193
+ font-size: 12px;
194
+ color: var(--text-secondary);
195
+ margin-top: 6px;
196
+ }}
197
+
198
+ .help-text a {{
199
+ color: var(--primary-color);
200
+ text-decoration: none;
201
+ }}
202
+
203
+ .help-text a:hover {{
204
+ text-decoration: underline;
205
+ }}
206
+
207
+ .button-group {{
208
+ display: flex;
209
+ gap: 12px;
210
+ margin-top: 24px;
211
+ }}
212
+
213
+ button {{
214
+ flex: 1;
215
+ padding: 14px 24px;
216
+ font-size: 16px;
217
+ font-weight: 500;
218
+ border-radius: 8px;
219
+ cursor: pointer;
220
+ transition: all 0.2s;
221
+ }}
222
+
223
+ .btn-primary {{
224
+ background: var(--primary-color);
225
+ color: white;
226
+ border: none;
227
+ }}
228
+
229
+ .btn-primary:hover {{
230
+ background: var(--primary-hover);
231
+ }}
232
+
233
+ .btn-primary:disabled {{
234
+ background: var(--border-color);
235
+ cursor: not-allowed;
236
+ }}
237
+
238
+ .btn-secondary {{
239
+ background: transparent;
240
+ color: var(--text-color);
241
+ border: 1px solid var(--border-color);
242
+ }}
243
+
244
+ .btn-secondary:hover {{
245
+ background: var(--bg-color);
246
+ }}
247
+
248
+ .security-note {{
249
+ margin-top: 20px;
250
+ padding: 12px;
251
+ background: var(--bg-color);
252
+ border-radius: 8px;
253
+ font-size: 12px;
254
+ color: var(--text-secondary);
255
+ text-align: center;
256
+ }}
257
+
258
+ .security-note svg {{
259
+ width: 14px;
260
+ height: 14px;
261
+ vertical-align: middle;
262
+ margin-right: 4px;
263
+ }}
264
+
265
+ .loading {{
266
+ display: none;
267
+ }}
268
+
269
+ .loading.active {{
270
+ display: inline-block;
271
+ }}
272
+
273
+ @keyframes spin {{
274
+ to {{ transform: rotate(360deg); }}
275
+ }}
276
+
277
+ .spinner {{
278
+ width: 16px;
279
+ height: 16px;
280
+ border: 2px solid transparent;
281
+ border-top-color: currentColor;
282
+ border-radius: 50%;
283
+ animation: spin 0.8s linear infinite;
284
+ display: inline-block;
285
+ vertical-align: middle;
286
+ margin-right: 8px;
287
+ }}
288
+ </style>
289
+ </head>
290
+ <body>
291
+ <div class="container">
292
+ <div class="header">
293
+ <svg class="logo" viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg">
294
+ <path fill="#18BCF2" d="M120 0C53.7 0 0 53.7 0 120s53.7 120 120 120 120-53.7 120-120S186.3 0 120 0zm0 220c-55.2 0-100-44.8-100-100S64.8 20 120 20s100 44.8 100 100-44.8 100-100 100z"/>
295
+ <path fill="#18BCF2" d="M120 40c-44.1 0-80 35.9-80 80s35.9 80 80 80 80-35.9 80-80-35.9-80-80-80zm0 140c-33.1 0-60-26.9-60-60s26.9-60 60-60 60 26.9 60 60-26.9 60-60 60z"/>
296
+ <circle fill="#18BCF2" cx="120" cy="120" r="40"/>
297
+ </svg>
298
+ <h1>Connect to Home Assistant</h1>
299
+ <p class="subtitle">Authorize {display_name} to access your smart home</p>
300
+ </div>
301
+
302
+ <div class="client-info">
303
+ <p>Application: <strong>{display_name}</strong></p>
304
+ <p class="scopes">Requested access: <strong>{scopes_display}</strong></p>
305
+ </div>
306
+
307
+ {error_html}
308
+
309
+ <form method="POST" id="consent-form">
310
+ <input type="hidden" name="client_id" value="{client_id}">
311
+ <input type="hidden" name="redirect_uri" value="{redirect_uri}">
312
+ <input type="hidden" name="state" value="{state}">
313
+
314
+ <div class="form-group">
315
+ <label for="ha_url">Home Assistant URL</label>
316
+ <input
317
+ type="url"
318
+ id="ha_url"
319
+ name="ha_url"
320
+ placeholder="https://homeassistant.local:8123"
321
+ required
322
+ autocomplete="url"
323
+ >
324
+ <p class="help-text">
325
+ The URL of your Home Assistant instance (e.g., http://homeassistant.local:8123)
326
+ </p>
327
+ </div>
328
+
329
+ <div class="form-group">
330
+ <label for="ha_token">Long-Lived Access Token</label>
331
+ <input
332
+ type="password"
333
+ id="ha_token"
334
+ name="ha_token"
335
+ placeholder="Enter your access token"
336
+ required
337
+ autocomplete="off"
338
+ >
339
+ <p class="help-text">
340
+ Create a token at: Home Assistant &rarr; Profile &rarr;
341
+ <a href="https://www.home-assistant.io/docs/authentication/#your-account-profile" target="_blank" rel="noopener">
342
+ Long-Lived Access Tokens
343
+ </a>
344
+ </p>
345
+ </div>
346
+
347
+ <div class="button-group">
348
+ <button type="button" class="btn-secondary" onclick="window.close(); history.back();">
349
+ Cancel
350
+ </button>
351
+ <button type="submit" class="btn-primary" id="submit-btn">
352
+ <span class="loading" id="loading">
353
+ <span class="spinner"></span>
354
+ </span>
355
+ <span id="btn-text">Authorize</span>
356
+ </button>
357
+ </div>
358
+ </form>
359
+
360
+ <div class="security-note">
361
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
362
+ <path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
363
+ </svg>
364
+ Your credentials are validated directly with your Home Assistant instance
365
+ and stored securely for this session only.
366
+ </div>
367
+ </div>
368
+
369
+ <script>
370
+ document.getElementById('consent-form').addEventListener('submit', function(e) {{
371
+ var btn = document.getElementById('submit-btn');
372
+ var loading = document.getElementById('loading');
373
+ var btnText = document.getElementById('btn-text');
374
+
375
+ btn.disabled = true;
376
+ loading.classList.add('active');
377
+ btnText.textContent = 'Validating...';
378
+ }});
379
+
380
+ // Try to detect Home Assistant URL from common patterns
381
+ (function() {{
382
+ var savedUrl = localStorage.getItem('ha_mcp_url');
383
+ if (savedUrl) {{
384
+ document.getElementById('ha_url').value = savedUrl;
385
+ }}
386
+ }})();
387
+
388
+ // Save URL on successful form navigation
389
+ document.getElementById('ha_url').addEventListener('change', function(e) {{
390
+ if (e.target.value) {{
391
+ localStorage.setItem('ha_mcp_url', e.target.value);
392
+ }}
393
+ }});
394
+ </script>
395
+ </body>
396
+ </html>
397
+ """
398
+
399
+
400
+ def create_error_html(error: str, error_description: str) -> str:
401
+ """
402
+ Generate HTML error page for OAuth errors.
403
+
404
+ Args:
405
+ error: OAuth error code
406
+ error_description: Human-readable error description
407
+
408
+ Returns:
409
+ HTML string for the error page
410
+ """
411
+ return f"""
412
+ <!DOCTYPE html>
413
+ <html lang="en">
414
+ <head>
415
+ <meta charset="UTF-8">
416
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
417
+ <title>Authentication Error</title>
418
+ <style>
419
+ :root {{
420
+ --error-color: #f44336;
421
+ --text-color: #212121;
422
+ --text-secondary: #757575;
423
+ --bg-color: #fafafa;
424
+ --card-bg: #ffffff;
425
+ }}
426
+
427
+ @media (prefers-color-scheme: dark) {{
428
+ :root {{
429
+ --error-color: #ef5350;
430
+ --text-color: #e0e0e0;
431
+ --text-secondary: #9e9e9e;
432
+ --bg-color: #121212;
433
+ --card-bg: #1e1e1e;
434
+ }}
435
+ }}
436
+
437
+ body {{
438
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
439
+ background-color: var(--bg-color);
440
+ color: var(--text-color);
441
+ min-height: 100vh;
442
+ display: flex;
443
+ align-items: center;
444
+ justify-content: center;
445
+ padding: 20px;
446
+ margin: 0;
447
+ }}
448
+
449
+ .container {{
450
+ background: var(--card-bg);
451
+ border-radius: 16px;
452
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
453
+ max-width: 440px;
454
+ width: 100%;
455
+ padding: 32px;
456
+ text-align: center;
457
+ }}
458
+
459
+ .error-icon {{
460
+ width: 64px;
461
+ height: 64px;
462
+ margin-bottom: 16px;
463
+ color: var(--error-color);
464
+ }}
465
+
466
+ h1 {{
467
+ font-size: 24px;
468
+ font-weight: 600;
469
+ margin-bottom: 8px;
470
+ color: var(--error-color);
471
+ }}
472
+
473
+ .error-code {{
474
+ font-family: monospace;
475
+ background: var(--bg-color);
476
+ padding: 4px 8px;
477
+ border-radius: 4px;
478
+ font-size: 14px;
479
+ margin-bottom: 16px;
480
+ display: inline-block;
481
+ }}
482
+
483
+ p {{
484
+ color: var(--text-secondary);
485
+ font-size: 14px;
486
+ line-height: 1.6;
487
+ }}
488
+ </style>
489
+ </head>
490
+ <body>
491
+ <div class="container">
492
+ <svg class="error-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
493
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
494
+ </svg>
495
+ <h1>Authentication Error</h1>
496
+ <div class="error-code">{error}</div>
497
+ <p>{error_description}</p>
498
+ </div>
499
+ </body>
500
+ </html>
501
+ """