hyperagents 1.2.3rc5.post3__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 (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
@@ -0,0 +1,528 @@
1
+ """
2
+ Smart Home & IoT Connectors.
3
+ Integration with Philips Hue, Home Assistant, Sonos, Spotify, MQTT, SmartThings.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+ import httpx
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class Device:
20
+ """Smart home device."""
21
+ id: str
22
+ name: str
23
+ type: str
24
+ state: dict = field(default_factory=dict)
25
+ raw: dict = field(default_factory=dict)
26
+
27
+
28
+ class SmartHomeConnector(ABC):
29
+ """Abstract base for smart home connectors."""
30
+
31
+ name: str = "base"
32
+
33
+ @abstractmethod
34
+ async def get_devices(self) -> list[Device]:
35
+ """Get all devices."""
36
+ pass
37
+
38
+ @abstractmethod
39
+ async def control(self, device_id: str, command: str, **kwargs) -> dict:
40
+ """Control a device."""
41
+ pass
42
+
43
+
44
+ # ═══════════════════════════════════════════════════════════════════════════════
45
+ # PHILIPS HUE
46
+ # ═══════════════════════════════════════════════════════════════════════════════
47
+
48
+ class PhilipsHueConnector(SmartHomeConnector):
49
+ """Philips Hue smart lighting connector."""
50
+
51
+ name = "hue"
52
+
53
+ def __init__(
54
+ self,
55
+ bridge_ip: str | None = None,
56
+ username: str | None = None,
57
+ api_key: str | None = None,
58
+ ):
59
+ self.bridge_ip = bridge_ip or os.environ.get("HUE_BRIDGE_IP")
60
+ self.username = username or api_key or os.environ.get("HUE_USERNAME")
61
+ self.base_url = f"http://{self.bridge_ip}/api/{self.username}"
62
+
63
+ async def get_devices(self) -> list[Device]:
64
+ async with httpx.AsyncClient() as client:
65
+ response = await client.get(f"{self.base_url}/lights", timeout=10.0)
66
+ response.raise_for_status()
67
+ data = response.json()
68
+
69
+ return [
70
+ Device(
71
+ id=light_id,
72
+ name=light["name"],
73
+ type="light",
74
+ state={
75
+ "on": light["state"]["on"],
76
+ "brightness": light["state"].get("bri", 0),
77
+ "hue": light["state"].get("hue"),
78
+ "saturation": light["state"].get("sat"),
79
+ },
80
+ raw=light,
81
+ )
82
+ for light_id, light in data.items()
83
+ ]
84
+
85
+ async def control(self, device_id: str, command: str, **kwargs) -> dict:
86
+ state: dict[str, Any] = {}
87
+
88
+ if command == "on":
89
+ state["on"] = True
90
+ elif command == "off":
91
+ state["on"] = False
92
+ elif command == "brightness":
93
+ state["bri"] = int(kwargs.get("value", 254))
94
+ elif command == "color":
95
+ state["hue"] = int(kwargs.get("hue", 0))
96
+ state["sat"] = int(kwargs.get("saturation", 254))
97
+ elif command == "scene":
98
+ # Activate a scene
99
+ scene_id = kwargs.get("scene_id")
100
+ async with httpx.AsyncClient() as client:
101
+ response = await client.put(
102
+ f"{self.base_url}/groups/0/action",
103
+ json={"scene": scene_id},
104
+ timeout=10.0,
105
+ )
106
+ response.raise_for_status()
107
+ return response.json()
108
+
109
+ async with httpx.AsyncClient() as client:
110
+ response = await client.put(
111
+ f"{self.base_url}/lights/{device_id}/state",
112
+ json=state,
113
+ timeout=10.0,
114
+ )
115
+ response.raise_for_status()
116
+ return response.json()
117
+
118
+ async def get_scenes(self) -> list[dict]:
119
+ """Get all scenes."""
120
+ async with httpx.AsyncClient() as client:
121
+ response = await client.get(f"{self.base_url}/scenes", timeout=10.0)
122
+ response.raise_for_status()
123
+ data = response.json()
124
+ return [{"id": sid, **scene} for sid, scene in data.items()]
125
+
126
+
127
+ # ═══════════════════════════════════════════════════════════════════════════════
128
+ # HOME ASSISTANT
129
+ # ═══════════════════════════════════════════════════════════════════════════════
130
+
131
+ class HomeAssistantConnector(SmartHomeConnector):
132
+ """Home Assistant connector."""
133
+
134
+ name = "homeassistant"
135
+
136
+ def __init__(
137
+ self,
138
+ url: str | None = None,
139
+ token: str | None = None,
140
+ ):
141
+ self.url = (url or os.environ.get("HOMEASSISTANT_URL", "http://localhost:8123")).rstrip("/")
142
+ self.token = token or os.environ.get("HOMEASSISTANT_TOKEN")
143
+
144
+ async def get_devices(self) -> list[Device]:
145
+ async with httpx.AsyncClient() as client:
146
+ response = await client.get(
147
+ f"{self.url}/api/states",
148
+ headers={"Authorization": f"Bearer {self.token}"},
149
+ timeout=30.0,
150
+ )
151
+ response.raise_for_status()
152
+ states = response.json()
153
+
154
+ return [
155
+ Device(
156
+ id=entity["entity_id"],
157
+ name=entity["attributes"].get("friendly_name", entity["entity_id"]),
158
+ type=entity["entity_id"].split(".")[0],
159
+ state={"state": entity["state"], **entity["attributes"]},
160
+ raw=entity,
161
+ )
162
+ for entity in states
163
+ ]
164
+
165
+ async def control(self, device_id: str, command: str, **kwargs) -> dict:
166
+ domain = device_id.split(".")[0]
167
+
168
+ service_data = {"entity_id": device_id, **kwargs}
169
+
170
+ async with httpx.AsyncClient() as client:
171
+ response = await client.post(
172
+ f"{self.url}/api/services/{domain}/{command}",
173
+ headers={"Authorization": f"Bearer {self.token}"},
174
+ json=service_data,
175
+ timeout=30.0,
176
+ )
177
+ response.raise_for_status()
178
+ return response.json()
179
+
180
+ async def call_service(self, domain: str, service: str, **kwargs) -> dict:
181
+ """Call any Home Assistant service."""
182
+ async with httpx.AsyncClient() as client:
183
+ response = await client.post(
184
+ f"{self.url}/api/services/{domain}/{service}",
185
+ headers={"Authorization": f"Bearer {self.token}"},
186
+ json=kwargs,
187
+ timeout=30.0,
188
+ )
189
+ response.raise_for_status()
190
+ return response.json()
191
+
192
+
193
+ # ═══════════════════════════════════════════════════════════════════════════════
194
+ # SONOS
195
+ # ═══════════════════════════════════════════════════════════════════════════════
196
+
197
+ class SonosConnector(SmartHomeConnector):
198
+ """Sonos speaker connector (via local API or Sonos Cloud)."""
199
+
200
+ name = "sonos"
201
+
202
+ def __init__(
203
+ self,
204
+ api_key: str | None = None,
205
+ household_id: str | None = None,
206
+ ):
207
+ self.api_key = api_key or os.environ.get("SONOS_API_KEY")
208
+ self.household_id = household_id or os.environ.get("SONOS_HOUSEHOLD_ID")
209
+ self.base_url = "https://api.ws.sonos.com/control/api/v1"
210
+
211
+ async def get_devices(self) -> list[Device]:
212
+ async with httpx.AsyncClient() as client:
213
+ response = await client.get(
214
+ f"{self.base_url}/households/{self.household_id}/groups",
215
+ headers={"Authorization": f"Bearer {self.api_key}"},
216
+ timeout=30.0,
217
+ )
218
+ response.raise_for_status()
219
+ data = response.json()
220
+
221
+ devices = []
222
+ for group in data.get("groups", []):
223
+ for player in group.get("players", []):
224
+ devices.append(
225
+ Device(
226
+ id=player["id"],
227
+ name=player["name"],
228
+ type="speaker",
229
+ state={"zone": group["name"]},
230
+ raw=player,
231
+ )
232
+ )
233
+ return devices
234
+
235
+ async def control(self, device_id: str, command: str, **kwargs) -> dict:
236
+ async with httpx.AsyncClient() as client:
237
+ if command == "play":
238
+ endpoint = f"{self.base_url}/groups/{device_id}/playback/play"
239
+ elif command == "pause":
240
+ endpoint = f"{self.base_url}/groups/{device_id}/playback/pause"
241
+ elif command == "volume":
242
+ endpoint = f"{self.base_url}/players/{device_id}/playerVolume"
243
+ response = await client.post(
244
+ endpoint,
245
+ headers={"Authorization": f"Bearer {self.api_key}"},
246
+ json={"volume": kwargs.get("value", 50)},
247
+ timeout=30.0,
248
+ )
249
+ response.raise_for_status()
250
+ return response.json()
251
+ else:
252
+ raise ValueError(f"Unknown command: {command}")
253
+
254
+ response = await client.post(
255
+ endpoint,
256
+ headers={"Authorization": f"Bearer {self.api_key}"},
257
+ timeout=30.0,
258
+ )
259
+ response.raise_for_status()
260
+ return response.json()
261
+
262
+
263
+ # ═══════════════════════════════════════════════════════════════════════════════
264
+ # SPOTIFY
265
+ # ═══════════════════════════════════════════════════════════════════════════════
266
+
267
+ class SpotifyConnector(SmartHomeConnector):
268
+ """Spotify playback connector."""
269
+
270
+ name = "spotify"
271
+
272
+ def __init__(self, access_token: str | None = None):
273
+ self.access_token = access_token or os.environ.get("SPOTIFY_ACCESS_TOKEN")
274
+ self.base_url = "https://api.spotify.com/v1"
275
+
276
+ async def get_devices(self) -> list[Device]:
277
+ async with httpx.AsyncClient() as client:
278
+ response = await client.get(
279
+ f"{self.base_url}/me/player/devices",
280
+ headers={"Authorization": f"Bearer {self.access_token}"},
281
+ timeout=30.0,
282
+ )
283
+ response.raise_for_status()
284
+ data = response.json()
285
+
286
+ return [
287
+ Device(
288
+ id=device["id"],
289
+ name=device["name"],
290
+ type=device["type"],
291
+ state={
292
+ "active": device["is_active"],
293
+ "volume": device["volume_percent"],
294
+ },
295
+ raw=device,
296
+ )
297
+ for device in data.get("devices", [])
298
+ ]
299
+
300
+ async def control(self, device_id: str, command: str, **kwargs) -> dict:
301
+ async with httpx.AsyncClient() as client:
302
+ if command == "play":
303
+ uri = kwargs.get("uri")
304
+ body = {"device_id": device_id}
305
+ if uri:
306
+ if "track" in uri:
307
+ body["uris"] = [uri]
308
+ else:
309
+ body["context_uri"] = uri
310
+
311
+ response = await client.put(
312
+ f"{self.base_url}/me/player/play",
313
+ headers={"Authorization": f"Bearer {self.access_token}"},
314
+ params={"device_id": device_id},
315
+ json=body if uri else None,
316
+ timeout=30.0,
317
+ )
318
+ elif command == "pause":
319
+ response = await client.put(
320
+ f"{self.base_url}/me/player/pause",
321
+ headers={"Authorization": f"Bearer {self.access_token}"},
322
+ params={"device_id": device_id},
323
+ timeout=30.0,
324
+ )
325
+ elif command == "next":
326
+ response = await client.post(
327
+ f"{self.base_url}/me/player/next",
328
+ headers={"Authorization": f"Bearer {self.access_token}"},
329
+ params={"device_id": device_id},
330
+ timeout=30.0,
331
+ )
332
+ elif command == "previous":
333
+ response = await client.post(
334
+ f"{self.base_url}/me/player/previous",
335
+ headers={"Authorization": f"Bearer {self.access_token}"},
336
+ params={"device_id": device_id},
337
+ timeout=30.0,
338
+ )
339
+ elif command == "volume":
340
+ response = await client.put(
341
+ f"{self.base_url}/me/player/volume",
342
+ headers={"Authorization": f"Bearer {self.access_token}"},
343
+ params={
344
+ "device_id": device_id,
345
+ "volume_percent": kwargs.get("value", 50),
346
+ },
347
+ timeout=30.0,
348
+ )
349
+ else:
350
+ raise ValueError(f"Unknown command: {command}")
351
+
352
+ if response.status_code == 204:
353
+ return {"status": "ok"}
354
+ response.raise_for_status()
355
+ return response.json() if response.content else {"status": "ok"}
356
+
357
+ async def search(self, query: str, types: str = "track", limit: int = 10) -> dict:
358
+ """Search Spotify."""
359
+ async with httpx.AsyncClient() as client:
360
+ response = await client.get(
361
+ f"{self.base_url}/search",
362
+ headers={"Authorization": f"Bearer {self.access_token}"},
363
+ params={"q": query, "type": types, "limit": limit},
364
+ timeout=30.0,
365
+ )
366
+ response.raise_for_status()
367
+ return response.json()
368
+
369
+
370
+ # ═══════════════════════════════════════════════════════════════════════════════
371
+ # MQTT
372
+ # ═══════════════════════════════════════════════════════════════════════════════
373
+
374
+ class MQTTConnector(SmartHomeConnector):
375
+ """MQTT IoT protocol connector."""
376
+
377
+ name = "mqtt"
378
+
379
+ def __init__(
380
+ self,
381
+ broker: str | None = None,
382
+ port: int = 1883,
383
+ username: str | None = None,
384
+ password: str | None = None,
385
+ ):
386
+ self.broker = broker or os.environ.get("MQTT_BROKER", "localhost")
387
+ self.port = int(os.environ.get("MQTT_PORT", port))
388
+ self.username = username or os.environ.get("MQTT_USERNAME")
389
+ self.password = password or os.environ.get("MQTT_PASSWORD")
390
+ self._client = None
391
+
392
+ def _get_client(self):
393
+ """Get or create MQTT client."""
394
+ try:
395
+ import paho.mqtt.client as mqtt
396
+ except ImportError:
397
+ raise ImportError("paho-mqtt required: pip install paho-mqtt")
398
+
399
+ if self._client is None:
400
+ self._client = mqtt.Client()
401
+ if self.username:
402
+ self._client.username_pw_set(self.username, self.password)
403
+ self._client.connect(self.broker, self.port)
404
+ self._client.loop_start()
405
+
406
+ return self._client
407
+
408
+ async def get_devices(self) -> list[Device]:
409
+ # MQTT doesn't have a standard device discovery
410
+ # Return empty list - devices are discovered via subscription
411
+ return []
412
+
413
+ async def control(self, device_id: str, command: str, **kwargs) -> dict:
414
+ """Publish to MQTT topic."""
415
+ import json
416
+
417
+ client = self._get_client()
418
+ topic = device_id
419
+ payload = json.dumps({"command": command, **kwargs})
420
+
421
+ result = client.publish(topic, payload)
422
+ result.wait_for_publish()
423
+
424
+ return {"topic": topic, "payload": payload, "published": result.is_published()}
425
+
426
+ async def subscribe(self, topic: str, callback) -> None:
427
+ """Subscribe to MQTT topic."""
428
+ client = self._get_client()
429
+ client.subscribe(topic)
430
+ client.message_callback_add(topic, callback)
431
+
432
+ async def publish(self, topic: str, payload: Any) -> dict:
433
+ """Publish to MQTT topic."""
434
+ import json
435
+
436
+ client = self._get_client()
437
+ if isinstance(payload, (dict, list)):
438
+ payload = json.dumps(payload)
439
+
440
+ result = client.publish(topic, payload)
441
+ result.wait_for_publish()
442
+
443
+ return {"topic": topic, "published": result.is_published()}
444
+
445
+
446
+ # ═══════════════════════════════════════════════════════════════════════════════
447
+ # SAMSUNG SMARTTHINGS
448
+ # ═══════════════════════════════════════════════════════════════════════════════
449
+
450
+ class SmartThingsConnector(SmartHomeConnector):
451
+ """Samsung SmartThings connector."""
452
+
453
+ name = "smartthings"
454
+
455
+ def __init__(self, token: str | None = None):
456
+ self.token = token or os.environ.get("SMARTTHINGS_TOKEN")
457
+ self.base_url = "https://api.smartthings.com/v1"
458
+
459
+ async def get_devices(self) -> list[Device]:
460
+ async with httpx.AsyncClient() as client:
461
+ response = await client.get(
462
+ f"{self.base_url}/devices",
463
+ headers={"Authorization": f"Bearer {self.token}"},
464
+ timeout=30.0,
465
+ )
466
+ response.raise_for_status()
467
+ data = response.json()
468
+
469
+ return [
470
+ Device(
471
+ id=device["deviceId"],
472
+ name=device["label"] or device["name"],
473
+ type=device["deviceTypeName"],
474
+ state={},
475
+ raw=device,
476
+ )
477
+ for device in data.get("items", [])
478
+ ]
479
+
480
+ async def control(self, device_id: str, command: str, **kwargs) -> dict:
481
+ component = kwargs.get("component", "main")
482
+ capability = kwargs.get("capability", "switch")
483
+
484
+ async with httpx.AsyncClient() as client:
485
+ response = await client.post(
486
+ f"{self.base_url}/devices/{device_id}/commands",
487
+ headers={"Authorization": f"Bearer {self.token}"},
488
+ json={
489
+ "commands": [
490
+ {
491
+ "component": component,
492
+ "capability": capability,
493
+ "command": command,
494
+ "arguments": kwargs.get("arguments", []),
495
+ }
496
+ ]
497
+ },
498
+ timeout=30.0,
499
+ )
500
+ response.raise_for_status()
501
+ return response.json()
502
+
503
+
504
+ # ═══════════════════════════════════════════════════════════════════════════════
505
+ # PROVIDER FACTORY
506
+ # ═══════════════════════════════════════════════════════════════════════════════
507
+
508
+ SMARTHOME_CONNECTORS: dict[str, type[SmartHomeConnector]] = {
509
+ "hue": PhilipsHueConnector,
510
+ "philips_hue": PhilipsHueConnector,
511
+ "homeassistant": HomeAssistantConnector,
512
+ "hass": HomeAssistantConnector,
513
+ "sonos": SonosConnector,
514
+ "spotify": SpotifyConnector,
515
+ "mqtt": MQTTConnector,
516
+ "smartthings": SmartThingsConnector,
517
+ }
518
+
519
+
520
+ def get_smarthome_connector(name: str, **kwargs) -> SmartHomeConnector:
521
+ """Get a smart home connector by name."""
522
+ name = name.lower()
523
+
524
+ if name not in SMARTHOME_CONNECTORS:
525
+ available = ", ".join(sorted(SMARTHOME_CONNECTORS.keys()))
526
+ raise ValueError(f"Unknown smart home connector: {name}. Available: {available}")
527
+
528
+ return SMARTHOME_CONNECTORS[name](**kwargs)