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
@@ -0,0 +1,380 @@
1
+ """
2
+ Domain-specific handlers for Home Assistant entities.
3
+
4
+ This module provides domain-specific configuration and logic for handling
5
+ different types of Home Assistant entities (lights, climate, covers, etc.).
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ # Domain-specific handler configurations
11
+ DOMAIN_HANDLERS = {
12
+ "light": {
13
+ "valid_actions": ["on", "off", "toggle", "set", "adjust"],
14
+ "parameters": [
15
+ "brightness",
16
+ "color_temp",
17
+ "rgb_color",
18
+ "effect",
19
+ "kelvin",
20
+ "hs_color",
21
+ ],
22
+ "quick_actions": ["toggle", "dim", "brighten"],
23
+ "state_attributes": ["brightness", "color_temp", "rgb_color"],
24
+ "supports_dimming": True,
25
+ "supports_color": True,
26
+ },
27
+ "climate": {
28
+ "valid_actions": ["on", "off", "set", "heat", "cool", "auto", "heat_cool"],
29
+ "parameters": [
30
+ "temperature",
31
+ "target_temp_high",
32
+ "target_temp_low",
33
+ "hvac_mode",
34
+ "fan_mode",
35
+ ],
36
+ "quick_actions": ["warmer", "cooler", "auto_mode"],
37
+ "state_attributes": ["current_temperature", "target_temperature", "hvac_mode"],
38
+ "supports_temperature": True,
39
+ "supports_hvac_modes": True,
40
+ },
41
+ "cover": {
42
+ "valid_actions": ["open", "close", "stop", "toggle", "set"],
43
+ "parameters": ["position", "tilt_position"],
44
+ "quick_actions": ["open", "close", "toggle"],
45
+ "state_attributes": ["current_position", "current_tilt_position"],
46
+ "supports_position": True,
47
+ "supports_tilt": True,
48
+ },
49
+ "switch": {
50
+ "valid_actions": ["on", "off", "toggle"],
51
+ "parameters": [],
52
+ "quick_actions": ["toggle"],
53
+ "state_attributes": ["state"],
54
+ "supports_dimming": False,
55
+ "supports_color": False,
56
+ },
57
+ "media_player": {
58
+ "valid_actions": ["play", "pause", "stop", "toggle", "set", "next", "previous"],
59
+ "parameters": [
60
+ "volume_level",
61
+ "media_content_id",
62
+ "media_content_type",
63
+ "source",
64
+ ],
65
+ "quick_actions": ["play_pause", "volume_up", "volume_down"],
66
+ "state_attributes": ["volume_level", "media_title", "source"],
67
+ "supports_volume": True,
68
+ "supports_media": True,
69
+ },
70
+ "fan": {
71
+ "valid_actions": ["on", "off", "toggle", "set"],
72
+ "parameters": ["speed", "percentage", "preset_mode", "direction"],
73
+ "quick_actions": ["toggle", "speed_up", "speed_down"],
74
+ "state_attributes": ["percentage", "preset_mode"],
75
+ "supports_speed": True,
76
+ "supports_direction": True,
77
+ },
78
+ "vacuum": {
79
+ "valid_actions": ["start", "stop", "pause", "return_to_base", "clean_spot"],
80
+ "parameters": ["fan_speed", "spot_area"],
81
+ "quick_actions": ["start_cleaning", "return_home"],
82
+ "state_attributes": ["battery_level", "status"],
83
+ "supports_zones": True,
84
+ "supports_mapping": True,
85
+ },
86
+ "lock": {
87
+ "valid_actions": ["lock", "unlock", "open"],
88
+ "parameters": ["code"],
89
+ "quick_actions": ["toggle_lock"],
90
+ "state_attributes": ["locked"],
91
+ "supports_codes": True,
92
+ "security_sensitive": True,
93
+ },
94
+ "alarm_control_panel": {
95
+ "valid_actions": ["arm_home", "arm_away", "arm_night", "disarm"],
96
+ "parameters": ["code"],
97
+ "quick_actions": ["arm", "disarm"],
98
+ "state_attributes": ["state"],
99
+ "security_sensitive": True,
100
+ "requires_code": True,
101
+ },
102
+ "water_heater": {
103
+ "valid_actions": ["on", "off", "set"],
104
+ "parameters": ["temperature", "operation_mode"],
105
+ "quick_actions": ["toggle", "temperature_up", "temperature_down"],
106
+ "state_attributes": ["current_temperature", "target_temperature"],
107
+ "supports_temperature": True,
108
+ "supports_modes": True,
109
+ },
110
+ "humidifier": {
111
+ "valid_actions": ["on", "off", "toggle", "set"],
112
+ "parameters": ["humidity", "mode"],
113
+ "quick_actions": ["toggle", "humidity_up", "humidity_down"],
114
+ "state_attributes": ["current_humidity", "target_humidity"],
115
+ "supports_humidity": True,
116
+ "supports_modes": True,
117
+ },
118
+ "camera": {
119
+ "valid_actions": ["snapshot", "record", "stream"],
120
+ "parameters": ["filename", "duration"],
121
+ "quick_actions": ["take_snapshot"],
122
+ "state_attributes": ["entity_picture", "access_token"],
123
+ "supports_streaming": True,
124
+ "supports_recording": True,
125
+ },
126
+ "scene": {
127
+ "valid_actions": ["turn_on", "activate"],
128
+ "parameters": [],
129
+ "quick_actions": ["activate"],
130
+ "state_attributes": ["state"],
131
+ "is_stateless": True,
132
+ "action_only": True,
133
+ },
134
+ "script": {
135
+ "valid_actions": ["turn_on", "turn_off", "toggle"],
136
+ "parameters": [],
137
+ "quick_actions": ["run", "stop"],
138
+ "state_attributes": ["state"],
139
+ "supports_execution": True,
140
+ "can_be_stopped": True,
141
+ },
142
+ "automation": {
143
+ "valid_actions": ["turn_on", "turn_off", "toggle", "trigger"],
144
+ "parameters": [],
145
+ "quick_actions": ["enable", "disable", "trigger"],
146
+ "state_attributes": ["state", "last_triggered"],
147
+ "supports_enable_disable": True,
148
+ "supports_manual_trigger": True,
149
+ },
150
+ # Read-only domains (sensors, etc.)
151
+ "sensor": {
152
+ "valid_actions": [],
153
+ "parameters": [],
154
+ "quick_actions": [],
155
+ "state_attributes": ["state", "unit_of_measurement"],
156
+ "read_only": True,
157
+ "provides_data": True,
158
+ },
159
+ "binary_sensor": {
160
+ "valid_actions": [],
161
+ "parameters": [],
162
+ "quick_actions": [],
163
+ "state_attributes": ["state", "device_class"],
164
+ "read_only": True,
165
+ "provides_data": True,
166
+ },
167
+ "device_tracker": {
168
+ "valid_actions": [],
169
+ "parameters": [],
170
+ "quick_actions": [],
171
+ "state_attributes": ["state", "latitude", "longitude"],
172
+ "read_only": True,
173
+ "provides_location": True,
174
+ },
175
+ }
176
+
177
+
178
+ def get_domain_handler(entity_id: str) -> dict[str, Any]:
179
+ """Get domain-specific configuration for an entity.
180
+
181
+ Args:
182
+ entity_id: Full entity ID (e.g., 'light.living_room')
183
+
184
+ Returns:
185
+ Domain handler configuration dictionary
186
+ """
187
+ if "." not in entity_id:
188
+ # Fallback for invalid entity ID format
189
+ return get_default_handler()
190
+
191
+ domain = entity_id.split(".")[0]
192
+ return DOMAIN_HANDLERS.get(domain, get_default_handler())
193
+
194
+
195
+ def get_default_handler() -> dict[str, Any]:
196
+ """Get default handler for unknown domains.
197
+
198
+ Returns:
199
+ Default handler configuration
200
+ """
201
+ return {
202
+ "valid_actions": ["on", "off", "toggle"],
203
+ "parameters": [],
204
+ "quick_actions": ["toggle"],
205
+ "state_attributes": ["state"],
206
+ "supports_basic_control": True,
207
+ "unknown_domain": True,
208
+ }
209
+
210
+
211
+ def get_domain_capabilities(domain: str) -> dict[str, Any]:
212
+ """Get capabilities for a specific domain.
213
+
214
+ Args:
215
+ domain: Domain name (e.g., 'light', 'climate')
216
+
217
+ Returns:
218
+ Dictionary of domain capabilities
219
+ """
220
+ handler = DOMAIN_HANDLERS.get(domain, get_default_handler())
221
+
222
+ capabilities = {
223
+ "domain": domain,
224
+ "controllable": len(handler.get("valid_actions", [])) > 0,
225
+ "read_only": handler.get("read_only", False),
226
+ "security_sensitive": handler.get("security_sensitive", False),
227
+ "requires_parameters": len(handler.get("parameters", [])) > 0,
228
+ "has_quick_actions": len(handler.get("quick_actions", [])) > 0,
229
+ }
230
+
231
+ # Add specific capability flags
232
+ for capability in [
233
+ "supports_dimming",
234
+ "supports_color",
235
+ "supports_temperature",
236
+ "supports_position",
237
+ "supports_volume",
238
+ "supports_speed",
239
+ ]:
240
+ if capability in handler:
241
+ capabilities[capability] = handler[capability]
242
+
243
+ return capabilities
244
+
245
+
246
+ def get_all_controllable_domains() -> list[str]:
247
+ """Get list of all controllable domains.
248
+
249
+ Returns:
250
+ List of domain names that support control actions
251
+ """
252
+ controllable = []
253
+ for domain, handler in DOMAIN_HANDLERS.items():
254
+ if handler.get("valid_actions") and not handler.get("read_only", False):
255
+ controllable.append(domain)
256
+
257
+ return sorted(controllable)
258
+
259
+
260
+ def get_all_sensor_domains() -> list[str]:
261
+ """Get list of all sensor/read-only domains.
262
+
263
+ Returns:
264
+ List of domain names that provide data but don't support control
265
+ """
266
+ sensors = []
267
+ for domain, handler in DOMAIN_HANDLERS.items():
268
+ if handler.get("read_only", False) or handler.get("provides_data", False):
269
+ sensors.append(domain)
270
+
271
+ return sorted(sensors)
272
+
273
+
274
+ def validate_action_for_domain(domain: str, action: str) -> tuple[bool, str]:
275
+ """Validate if an action is supported for a domain.
276
+
277
+ Args:
278
+ domain: Domain name
279
+ action: Action to validate
280
+
281
+ Returns:
282
+ Tuple of (is_valid, error_message_if_invalid)
283
+ """
284
+ handler = DOMAIN_HANDLERS.get(domain, get_default_handler())
285
+ valid_actions = handler.get("valid_actions", [])
286
+
287
+ if not valid_actions:
288
+ return False, f"Domain '{domain}' does not support any control actions"
289
+
290
+ if action not in valid_actions:
291
+ return (
292
+ False,
293
+ f"Action '{action}' not supported for domain '{domain}'. Valid actions: {', '.join(valid_actions)}",
294
+ )
295
+
296
+ return True, ""
297
+
298
+
299
+ def get_suggested_parameters(domain: str, action: str) -> list[str]:
300
+ """Get suggested parameters for a domain/action combination.
301
+
302
+ Args:
303
+ domain: Domain name
304
+ action: Action being performed
305
+
306
+ Returns:
307
+ List of suggested parameter names
308
+ """
309
+ handler = DOMAIN_HANDLERS.get(domain, get_default_handler())
310
+ all_params = handler.get("parameters", [])
311
+
312
+ # Action-specific parameter suggestions
313
+ action_params = {
314
+ "light": {
315
+ "set": ["brightness", "color_temp", "rgb_color"],
316
+ "on": ["brightness", "color_temp"],
317
+ "adjust": ["brightness"],
318
+ },
319
+ "climate": {
320
+ "set": ["temperature"],
321
+ "heat": ["temperature"],
322
+ "cool": ["temperature"],
323
+ },
324
+ "cover": {"set": ["position"], "open": [], "close": []},
325
+ "media_player": {
326
+ "set": ["volume_level"],
327
+ "play": ["media_content_id", "media_content_type"],
328
+ },
329
+ }
330
+
331
+ if domain in action_params and action in action_params[domain]:
332
+ return action_params[domain][action]
333
+
334
+ return list(all_params)
335
+
336
+
337
+ def is_security_sensitive_domain(domain: str) -> bool:
338
+ """Check if a domain is security sensitive.
339
+
340
+ Args:
341
+ domain: Domain name to check
342
+
343
+ Returns:
344
+ True if domain requires extra security considerations
345
+ """
346
+ handler = DOMAIN_HANDLERS.get(domain, {})
347
+ return bool(handler.get("security_sensitive", False))
348
+
349
+
350
+ def get_domain_description(domain: str) -> str:
351
+ """Get human-readable description of a domain.
352
+
353
+ Args:
354
+ domain: Domain name
355
+
356
+ Returns:
357
+ Human-readable description
358
+ """
359
+ descriptions = {
360
+ "light": "Lighting devices (bulbs, strips, switches)",
361
+ "climate": "Climate control (thermostats, HVAC systems)",
362
+ "cover": "Window coverings (blinds, curtains, garage doors)",
363
+ "switch": "Simple on/off switches and outlets",
364
+ "media_player": "Audio/video players and streaming devices",
365
+ "fan": "Fans and ventilation devices",
366
+ "vacuum": "Robotic vacuums and cleaning devices",
367
+ "lock": "Smart locks and door controls",
368
+ "alarm_control_panel": "Security system panels and alarms",
369
+ "water_heater": "Water heating systems",
370
+ "humidifier": "Humidity control devices",
371
+ "camera": "Security cameras and video devices",
372
+ "scene": "Predefined device state combinations",
373
+ "script": "Custom automation scripts",
374
+ "automation": "Automated rules and triggers",
375
+ "sensor": "Environmental and status sensors",
376
+ "binary_sensor": "On/off status sensors",
377
+ "device_tracker": "Location tracking devices",
378
+ }
379
+
380
+ return descriptions.get(domain, f"Unknown domain: {domain}")