Compare commits

...
244 Commits
Author SHA1 Message Date
SMKRV 0fdc3c93d3 Release v2.0.0-alpha 2024-11-26 14:02:37 +03:00
SMKRV d6e76f7805 Validate HACS 2024-11-26 13:16:10 +03:00
SMKRV b05afe1085 Release v2.0.0-alpha 2024-11-26 02:08:55 +03:00
SMKRV 2a4911f5f8 Release v2.0.0-alpha 2024-11-26 02:04:23 +03:00
SMKRV 208074d845 Release v2.0.0-alpha 2024-11-26 01:20:08 +03:00
SMKRV 616ff2c3fe 💡 Support the Project 2024-11-26 00:14:26 +03:00
SMKRV 987c939956 Support the Project 2024-11-26 00:12:43 +03:00
SMKRV 1615cc744e Support the Project 2024-11-26 00:12:10 +03:00
SMKRV 2d793a4b25 Support the Project 2024-11-26 00:11:55 +03:00
SMKRV aeeb4d5504 Support the Project 2024-11-26 00:09:46 +03:00
SMKRV daec801073 Misc 2024-11-25 23:50:33 +03:00
SMKRV e4916a7b7c Misc 2024-11-25 23:49:42 +03:00
SMKRV a08abd76e3 Misc 2024-11-25 23:48:55 +03:00
SMKRV a2be709608 Misc 2024-11-25 23:48:16 +03:00
SMKRV 69002ef926 Misc 2024-11-25 23:45:28 +03:00
SMKRV 9ade5a7194 misc 2024-11-25 23:43:05 +03:00
SMKRV 9b3f4f605b Sensor Attributes 2024-11-25 17:59:17 +03:00
SMKRV b862968d01 Release v2.0.0-alpha 2024-11-25 17:31:42 +03:00
SMKRV 29f3ae5592 Release v2.0.0 2024-11-25 17:10:38 +03:00
SMKRV 107d2a64fc Release v2.0.0 2024-11-25 17:09:39 +03:00
SMKRV e24bb884ef Release v2.0.0 2024-11-25 17:08:23 +03:00
SMKRV 107a2ef962 Release v2.0.0 2024-11-25 17:04:53 +03:00
SMKRV 9d58f2cf1e Release v2.0.0 2024-11-25 17:04:18 +03:00
SMKRV 29f6860fe1 Release v2.0.0 2024-11-25 17:03:29 +03:00
SMKRV fa89026e05 Release v2.0.0 2024-11-25 17:00:29 +03:00
SMKRV 9968452c46 Release v2.0.0 2024-11-25 16:57:14 +03:00
SMKRV 9665634013 Release v2.0.0 2024-11-25 16:55:12 +03:00
SMKRV 76d10ba8fb Release v2.0.0 2024-11-25 16:54:42 +03:00
SMKRV e9ea10203e Release v2.0.0 2024-11-25 16:54:07 +03:00
SMKRV 92a4c2da02 Release v2.0.0 2024-11-25 16:53:38 +03:00
SMKRV b6d8eb98f6 Release v2.0.0 2024-11-25 16:52:20 +03:00
SMKRV b6b01bccd7 Release v2.0.0 2024-11-25 16:51:42 +03:00
SMKRV ace2339b4f Release v2.0.0 2024-11-25 16:46:27 +03:00
SMKRV 166c1f9c9c Release v2.0.0 2024-11-25 16:45:43 +03:00
SMKRV e4039a08bc Release v2.0.0 2024-11-25 16:45:05 +03:00
SMKRV 1da5b5941d Release v2.0.0 2024-11-25 16:44:14 +03:00
SMKRV 6683f12c80 Release v2.0.0 2024-11-25 16:42:51 +03:00
SMKRV 2277f48e46 Release v2.0.0 2024-11-25 16:40:55 +03:00
SMKRV d206bde15a Release v2.0.0 2024-11-25 16:37:57 +03:00
SMKRV af16d03915 Release v2.0.0 2024-11-25 16:34:36 +03:00
SMKRV bf26cd3cfb Release v2.0.0 2024-11-25 16:25:58 +03:00
SMKRV 094062773a Release v2.0.0 2024-11-25 16:18:54 +03:00
SMKRV 4cd95813bc Release v2.0.0 2024-11-25 15:51:08 +03:00
SMKRV c2064f0b64 Release v2.0.0 2024-11-25 15:42:04 +03:00
SMKRV fafd927610 Release v2.0.0 2024-11-25 14:59:44 +03:00
SMKRV 7f46380054 Release v2.0.0 2024-11-25 02:05:13 +03:00
SMKRV 351a8b18dd Release v2.0.0 2024-11-25 02:03:29 +03:00
SMKRV 39833b333f Release v2.0.0 2024-11-25 01:52:37 +03:00
SMKRV 888a41375b Release v2.0.0 2024-11-25 01:43:06 +03:00
SMKRV 823abb22e4 Release v2.0.0 2024-11-25 01:20:44 +03:00
SMKRV a3c88309b4 Release v2.0.0 2024-11-25 01:10:32 +03:00
SMKRV beebc7e194 Release v2.0.0 2024-11-25 00:58:02 +03:00
SMKRV 3f8f22ac61 Release v2.0.0 2024-11-25 00:50:44 +03:00
SMKRV 12c95d0e92 Release v2.0.0 2024-11-25 00:08:07 +03:00
SMKRV b00f600cd9 Release v2.0.0 2024-11-25 00:05:15 +03:00
SMKRV a4925fc943 Release v2.0.0 2024-11-24 23:43:36 +03:00
SMKRV 28248ac3c4 Release v2.0.0 2024-11-24 23:23:37 +03:00
SMKRV d2b5626977 Release v2.0.0 2024-11-24 23:17:41 +03:00
SMKRV b61429b52d Release v2.0.0 2024-11-24 23:14:48 +03:00
SMKRV d05b39d8ae Release v2.0.0 2024-11-24 22:49:11 +03:00
SMKRV 6b3b0f1bd6 Release v2.0.0 2024-11-24 21:38:22 +03:00
SMKRV feb679ae77 Release v2.0.0 2024-11-24 20:17:01 +03:00
SMKRV 9779e5552d Release v2.0.0 2024-11-24 20:12:03 +03:00
SMKRV fbd187dc29 Release v2.0.0 2024-11-24 19:37:01 +03:00
SMKRV e8b6116439 Release v2.0.0 2024-11-24 19:24:30 +03:00
SMKRV fcd3e79cb7 Release v2.0.0 2024-11-24 18:36:37 +03:00
SMKRV d03078cfd4 Release v2.0.0 2024-11-24 17:55:39 +03:00
SMKRV b94d859849 Release v2.0.0 2024-11-24 17:46:39 +03:00
SMKRV dc4fcdf578 Release v2.0.0 2024-11-24 17:41:48 +03:00
SMKRV da4c40017d Release v2.0.0 2024-11-24 17:34:51 +03:00
SMKRV ac420b6495 Release v2.0.0 2024-11-24 17:27:49 +03:00
SMKRV 5791601c7e Release v2.0.0 2024-11-24 17:09:23 +03:00
SMKRV eb149184c3 Release v2.0.0 2024-11-24 16:56:39 +03:00
SMKRV d410073c64 Release v2.0.0 2024-11-24 16:45:03 +03:00
SMKRV b373f6c513 Release v2.0.0 2024-11-24 16:29:57 +03:00
SMKRV 18395a2265 Release v2.0.0 2024-11-24 13:52:27 +03:00
SMKRV 2e4c63ba7d Release v2.0.0 2024-11-24 13:51:04 +03:00
SMKRV 9fdf7c4642 Release v2.0.0 2024-11-24 03:03:01 +03:00
SMKRV 053a9050b6 Release v2.0.0 2024-11-24 02:58:04 +03:00
SMKRV 7efabdfa70 Release v2.0.0 2024-11-24 02:52:41 +03:00
SMKRV e5077969e9 Release v2.0.0 2024-11-24 02:32:08 +03:00
SMKRV 2688da5a82 Release v2.0.0 2024-11-24 02:20:29 +03:00
SMKRV 4f46d077df Release v2.0.0 2024-11-24 01:54:09 +03:00
SMKRV bde856c576 Release v2.0.0 2024-11-24 01:18:09 +03:00
SMKRV 083cb9f730 Release v2.0.0 2024-11-24 01:03:51 +03:00
SMKRV 2644d720e7 Release v2.0.0 2024-11-24 01:01:51 +03:00
SMKRV 2fe84ab801 Release v2.0.0 2024-11-24 00:56:33 +03:00
SMKRV ca1d79f848 Release v2.0.0 2024-11-24 00:29:45 +03:00
SMKRV e52572beaa Release v2.0.0 2024-11-24 00:16:27 +03:00
SMKRV 0f77a98d76 Release v2.0.0 2024-11-23 23:46:55 +03:00
SMKRV ebede3d56b Release v2.0.0 2024-11-23 23:42:33 +03:00
SMKRV 1692f5519f Release v2.0.0 2024-11-23 23:33:53 +03:00
SMKRV dad1aa1c45 Release v2.0.0 2024-11-23 23:30:39 +03:00
SMKRV 5d2244db6e Release v2.0.0 2024-11-23 23:21:32 +03:00
SMKRV 8a15cfe4b4 Release v2.0.0 2024-11-23 23:17:41 +03:00
SMKRV 500c7fbe30 Release v2.0.0 2024-11-23 21:55:34 +03:00
SMKRV 894b600b09 Release v2.0.0 2024-11-23 21:39:28 +03:00
SMKRV 97f0b30cd6 Release v2.0.0 2024-11-23 21:31:27 +03:00
SMKRV 9855e8a561 Release v2.0.0 2024-11-23 20:12:10 +03:00
SMKRV c2b259ade1 Release v2.0.0 2024-11-23 19:51:21 +03:00
SMKRV cfc185117c Release v2.0.0 2024-11-23 19:42:10 +03:00
SMKRV 9e89920e79 Release v2.0.0 2024-11-23 19:03:06 +03:00
SMKRV af190da333 Misc 2024-11-23 18:59:27 +03:00
SMKRV 3e3ec45b19 Release v2.0.0 2024-11-23 18:58:30 +03:00
SMKRV f0fe593d78 Release v2.0.0 2024-11-23 18:57:02 +03:00
SMKRV 8646118a27 Release v2.0.0 2024-11-23 18:50:48 +03:00
SMKRV 31f21b3a6b Release v2.0.0 2024-11-23 03:06:13 +03:00
SMKRV 12d87e30e1 Release v2.0.0 2024-11-23 03:02:35 +03:00
SMKRV 6ecc3f72d1 Release v2.0.0 2024-11-23 02:21:10 +03:00
SMKRV e8c40dc6b8 Release v2.0.0 2024-11-23 01:45:53 +03:00
SMKRV 0279517a42 Release v2.0.0 2024-11-23 01:34:05 +03:00
SMKRV 072eab1703 Release v2.0.0 2024-11-23 01:21:42 +03:00
SMKRV 3b38a6dd29 Release v2.0.0 2024-11-23 01:09:38 +03:00
SMKRV 7f8d8be5fb Release v2.0.0 2024-11-23 00:59:09 +03:00
SMKRV 3aa6b6a2ef Release v2.0.0 2024-11-23 00:51:57 +03:00
SMKRV d870cfbba6 Release v2.0.0 2024-11-23 00:36:58 +03:00
SMKRV 13a9e1a5d7 Release v2.0.0 2024-11-22 19:18:10 +03:00
SMKRV 06aba7e692 Release v2.0.0 2024-11-22 18:59:10 +03:00
SMKRV c73ff02bfb Release v2.0.0 2024-11-22 18:48:19 +03:00
SMKRV ed03170817 Release v2.0.0 2024-11-22 18:46:22 +03:00
SMKRV 3079994a77 Release v2.0.0 2024-11-22 18:44:38 +03:00
SMKRV 41b37f9edf Release v2.0.0 2024-11-22 18:41:40 +03:00
SMKRV 8d65d3ef4e Release v2.0.0 2024-11-22 18:39:29 +03:00
SMKRV 357c8b8be4 Release v2.0.0 2024-11-22 18:37:55 +03:00
SMKRV a47b343f93 Release v2.0.0 2024-11-22 18:16:15 +03:00
SMKRV 4388e0f2e1 Release v2.0.0 2024-11-22 17:55:46 +03:00
SMKRV f2adff1d85 Release v2.0.0 2024-11-22 17:45:41 +03:00
SMKRV ad51da7950 Release v2.0.0 2024-11-22 17:31:48 +03:00
SMKRV 7610a71829 Release v2.0.0 2024-11-22 17:20:07 +03:00
SMKRV 8800f226d2 Release v2.0.0 2024-11-22 17:19:40 +03:00
SMKRV ba16932b44 Release v2.0.0 2024-11-22 17:10:42 +03:00
SMKRV 81394345b4 Misc 2024-11-22 17:02:56 +03:00
SMKRV 72621d9d0e Release v2.0.0 2024-11-22 16:58:22 +03:00
SMKRV d35ded6502 Release v2.0.0 2024-11-22 16:46:40 +03:00
SMKRV b17e5c3db2 Release v2.0.0 2024-11-22 16:29:26 +03:00
SMKRV 365c4df2a4 Release v2.0.0 2024-11-22 16:16:58 +03:00
SMKRV ab79d05e96 Release v2.0.0 2024-11-22 16:08:33 +03:00
SMKRV a208004fe0 Release v2.0.0 2024-11-22 15:57:13 +03:00
SMKRV 51c714df25 Release v2.0.0 2024-11-22 15:49:47 +03:00
SMKRV 87b45df180 Release v2.0.0 2024-11-22 15:44:45 +03:00
SMKRV accb15a92a Release v2.0.0 2024-11-22 15:38:29 +03:00
SMKRV a421825050 Release v2.0.0 2024-11-22 15:25:01 +03:00
SMKRV c55112c8f7 Release v2.0.0 2024-11-22 15:08:45 +03:00
SMKRV accbc5635e Release v2.0.0 2024-11-22 13:51:23 +03:00
SMKRV 558ff7d141 Release v2.0.0 2024-11-22 13:39:01 +03:00
SMKRV fa70a0a4ab Release v2.0.0 2024-11-22 12:28:49 +03:00
SMKRV 50b414a904 Release v2.0.0 2024-11-22 12:23:28 +03:00
SMKRV 80c91039e1 Release v2.0.0 2024-11-22 12:17:06 +03:00
SMKRV 966e01e7e2 Release v2.0.0 2024-11-22 12:02:26 +03:00
SMKRV 82ffce1e25 Release v2.0.0 2024-11-22 11:50:47 +03:00
SMKRV c7257cc00a Release v2.0.0 2024-11-22 11:39:43 +03:00
SMKRV ab877c2e9a Release v2.0.0 2024-11-22 11:39:27 +03:00
SMKRV c6dcf307fd Release v2.0.0 2024-11-22 11:38:14 +03:00
SMKRV 30d69e7ed1 Release v2.0.0 2024-11-22 11:36:34 +03:00
SMKRV bb8195c0d1 Release v2.0.0 2024-11-22 10:16:46 +03:00
SMKRV a813302e86 Release v2.0.0 2024-11-22 02:45:31 +03:00
SMKRV 0f643664f7 Release v2.0.0 2024-11-22 02:39:40 +03:00
SMKRV 665537cb6a Release v2.0.0 2024-11-22 02:34:46 +03:00
SMKRV 766b9293cf Release v2.0.0 2024-11-22 02:34:05 +03:00
SMKRV 6e014e30d9 Release v2.0.0 2024-11-22 02:33:34 +03:00
SMKRV 30fc8ad1df Release v2.0.0 2024-11-22 02:33:07 +03:00
SMKRV bcad939a3d Release v2.0.0 2024-11-22 02:32:18 +03:00
SMKRV e153df85fb Release v2.0.0 2024-11-22 02:31:02 +03:00
SMKRV 26908b5d81 Release v2.0.0 2024-11-22 02:30:17 +03:00
SMKRV 0ddca8ff58 Release v2.0.0 2024-11-22 02:28:03 +03:00
SMKRV fd285d969e Release v2.0.0 2024-11-22 02:25:27 +03:00
SMKRV 10bebe5eb5 Release v2.0.0 2024-11-22 02:24:45 +03:00
SMKRV fef5e81033 Release v2.0.0 2024-11-22 02:23:04 +03:00
SMKRV a325ae180a Release v2.0.0 2024-11-22 02:20:10 +03:00
SMKRV df1ae9cd55 Release v2.0.0 2024-11-22 02:04:20 +03:00
SMKRV d5380b4195 Release v2.0.0 2024-11-22 02:03:24 +03:00
SMKRV 9e12ada4fa Release v2.0.0 2024-11-22 02:03:06 +03:00
SMKRV bb3240f1b3 Release v2.0.0 2024-11-22 02:00:42 +03:00
SMKRV 7780ac89ef Release v2.0.0 2024-11-22 01:57:21 +03:00
SMKRV c58eae695e Release v2.0.0 2024-11-22 01:56:36 +03:00
SMKRV 45244dcaa9 Release v2.0.0 2024-11-22 01:43:00 +03:00
SMKRV 53b15fa74c Release v2.0.0 2024-11-22 01:34:47 +03:00
SMKRV 9341b02f4b Release v2.0.0 2024-11-21 19:21:04 +03:00
SMKRV 158db522a8 Release v2.0.0 2024-11-21 19:16:21 +03:00
SMKRV f3b76c0bc1 Release v2.0.0 2024-11-21 19:04:28 +03:00
SMKRV d11f961566 Release v2.0.0 2024-11-21 18:55:11 +03:00
SMKRV 9d7f81d042 Release v2.0.0 2024-11-21 18:30:35 +03:00
SMKRV c95e1a829c Release v2.0.0 2024-11-21 18:24:39 +03:00
SMKRV 7bd06e7b88 Release v2.0.0 2024-11-21 18:17:57 +03:00
SMKRV 6e64b6feac Release v2.0.0 2024-11-21 18:12:33 +03:00
SMKRV f6c0e6265e Release v2.0.0 2024-11-21 18:05:45 +03:00
SMKRV 662ce701ca Release v2.0.0 2024-11-21 18:00:33 +03:00
SMKRV 72bbfb3f58 Release v2.0.0 2024-11-21 17:30:59 +03:00
SMKRV 9306c12fcd Release v2.0.0 2024-11-21 17:18:13 +03:00
SMKRV 9f1ea70c9f Release v2.0.0 2024-11-21 17:06:19 +03:00
SMKRV 98913b359a Release v2.0.0 2024-11-21 16:40:55 +03:00
SMKRV a4905f0778 Release v2.0.0 2024-11-21 16:17:51 +03:00
SMKRV e603231633 Release v2.0.0 2024-11-21 15:35:52 +03:00
SMKRV 345463322a Release v2.0.0 2024-11-21 15:15:50 +03:00
SMKRV c298866e3c Release v2.0.0 2024-11-21 14:55:27 +03:00
SMKRV 75e97ac652 Release v2.0.0 2024-11-21 14:28:17 +03:00
SMKRV 149ec16d57 Release v2.0.0 2024-11-21 13:56:22 +03:00
SMKRV 31e30d94aa Release v2.0.0 2024-11-20 14:29:17 +03:00
SMKRV 326f876410 Handles the VSE GPT API endpoint correctly 2024-11-20 12:23:26 +03:00
SMKRV 58a7ae4229 Resoved blocking SSL verification issue in
coordinator.py

 API endpoint handling in config_flow.py
 changes

 Added support for the custom models in const.py

 Requirements in manifest.json updated
2024-11-20 12:07:30 +03:00
SMKRV 8f796ad9dc Retry constants 2024-11-20 01:40:29 +03:00
SMKRV 8be860007a Retry constants
MAX_RETRIES: Final = 3
  RETRY_DELAY: Final = 1.0
2024-11-20 01:39:19 +03:00
SMKRV 1985e201b4 Updated 2024-11-20 01:29:14 +03:00
SMKRV 4e4b41661b Release v2.0.0 2024-11-20 01:25:01 +03:00
SMKRV b24a99a11f Release v2.0.0 2024-11-20 01:24:44 +03:00
SMKRV 69b151f990 Release v2.0.0 2024-11-20 01:11:23 +03:00
SMKRV 2d4eb59d6c Release v2.0.0 2024-11-20 01:09:19 +03:00
SMKRV 5037622fb6 Release v2.0.0 2024-11-20 01:02:27 +03:00
SMKRV 962a089bf4 Stability improvements 2024-11-19 23:38:07 +03:00
SMKRV 4c56565b66 Stability improvements 2024-11-19 23:30:28 +03:00
SMKRV 8432038f09 Stability improvements 2024-11-19 23:21:54 +03:00
SMKRV 929d916d41 Bugfixes 2024-11-19 19:39:05 +03:00
SMKRV 675975d951 Minor changes 2024-11-19 19:31:26 +03:00
SMKRV 524ec87395 Minor changes 2024-11-19 19:30:42 +03:00
SMKRV 94f8193996 last_update_success_time > last_update_success 2024-11-19 19:28:01 +03:00
SMKRV 7c3fcf73c3 Text edits 2024-11-19 19:23:03 +03:00
SMKRV 20bbf89679 Changes 2024-11-19 19:20:52 +03:00
SMKRV 86dca52d07 Small changes 2024-11-19 19:18:16 +03:00
SMKRV 78d1561e74 Markdown 2024-11-19 19:17:04 +03:00
SMKRV 054e4af258 Quick bugfix 2024-11-19 19:15:57 +03:00
SMKRV dc03faa97e docs: update HACS installation for custom
repository

  - Change HACS badge from Default to Custom
  - Add custom repository installation steps
  - Update installation instructions
  - Revise documentation format
2024-11-19 19:03:51 +03:00
SMKRV 9a7635c2ae Release v1.1.0 2024-11-19 18:55:14 +03:00
SMKRV df3d79c20c feat: add multi-provider support,
config improvements
2024-11-19 18:53:51 +03:00
SMKRV d6144be7ed v.1.0.10 2024-11-19 17:49:48 +03:00
SMKRV 9afbb904b3 __init__.py:
added: from .coordinator import HATextAICoordinator
2024-11-19 17:48:00 +03:00
SMKRV 93558b2444 Version update 2024-11-19 17:34:27 +03:00
SMKRV 9f93f1ee18 Main changes:
Removed the validate_endpoint function
Optimized the validate_api_connection function
Simplified API connection verification
Preserved all error handling and retry logic
Improved exception handling
The integration should now correctly verify the
OpenAI API connection without false endpoint_not_available errors.
2024-11-19 17:33:27 +03:00
SMKRV 30a9b53ba1 Markdown changes 2024-11-19 17:18:23 +03:00
SMKRV 5175970d55 structure.md added 2024-11-19 17:16:30 +03:00
SMKRV f6bfbd4a07 Release v1.0.8 2024-11-19 17:02:38 +03:00
SMKRV 4ddb0dc977 Translation fixes 2024-11-19 16:59:42 +03:00
SMKRV 24dc4ac4d4 Hotfix 2024-11-19 16:53:27 +03:00
SMKRV 976f4f16a3 translation fixes 2024-11-19 16:40:56 +03:00
SMKRV 2bdef2b494 Main changes:
Created global SSL_CONTEXT at module level
Removed blocking create_default_context calls from async functions
Optimized aiohttp.ClientSession handling:
Using single connector with SSL context
Session is created once for all requests in validate_api_connection
Improved resource management:
Automatic session closure using context managers
More efficient connection handling
These changes should eliminate the blocking call warning and improve
overall code performance.
2024-11-19 16:33:30 +03:00
SMKRV 42324a793b bufix 2024-11-19 16:25:44 +03:00
SMKRV 30aa894634 Release v1.0.6 2024-11-19 15:10:06 +03:00
SMKRV 398b2550a9 Release v1.0.5 2024-11-19 14:46:12 +03:00
SMKRV f1deaa2014 Release v1.0.4 2024-11-19 14:35:45 +03:00
SMKRV f4b0ce902e Release v1.0.3 2024-11-19 14:06:32 +03:00
SMKRV d899144149 Release v1.0.3 2024-11-19 14:00:33 +03:00
SMKRV 5d49b4a40b Release v1.0.2 2024-11-19 13:16:14 +03:00
SMKRV 1a84727cf1 Release v1.0.2 2024-11-19 13:14:00 +03:00
SMKRV d3c7e25202 Release v1.0.2 2024-11-19 13:12:41 +03:00
SMKRV 5e82b9669c Release v1.0.2 2024-11-19 13:10:18 +03:00
26 changed files with 2658 additions and 618 deletions
-2
View File
@@ -32,8 +32,6 @@ jobs:
- name: 🚀 Run hassfest validation
uses: home-assistant/actions/hassfest@master
with:
strict: true
- name: ️ Print hassfest version
if: always()
+17
View File
@@ -0,0 +1,17 @@
name: Validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
+35 -4
View File
@@ -1,8 +1,39 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Home Assistant
.storage
.cloud
.google.token
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
.env
.venv
venv/
ENV/
Thumbs.db
*.psd
*.zip
+110
View File
@@ -0,0 +1,110 @@
# 🤝 Contributing Guide
We welcome contributions to the HA Text AI project! This document will help you contribute to the project's development.
## 🌟 How to Contribute
### 1. Preparation
1. Fork the Repository
- Go to the repository page on GitHub
- Click the "Fork" button in the top right corner
2. Clone Your Fork
```bash
git clone https://github.com/YOUR_USERNAME/ha-text-ai.git
cd ha-text-ai
```
3. Set Up Remote Repositories
```bash
git remote add upstream https://github.com/smkrv/ha-text-ai.git
```
### 2. Creating a Development Branch
```bash
# Update the main branch
git checkout main
git pull upstream main
# Create a new branch for your feature
git checkout -b feature/short-description-of-changes
```
### 3. Development
- Follow the project's coding standards
- Write clean and understandable code
- Add comments when necessary
- Create unit tests for new functionality
### 4. Committing Changes
```bash
# Add modified files
git add .
# Create a meaningful commit
git commit -m "Feat: Add [short feature description]"
```
### 5. Commit Message Style
Use the following prefixes:
- `Feat:` - new feature
- `Fix:` - bug fixes
- `Docs:` - documentation updates
- `Style:` - formatting changes
- `Refactor:` - code refactoring
- `Test:` - adding tests
- `Chore:` - project maintenance
### 6. Pushing Changes
```bash
# Push changes to your fork
git push origin feature/short-description-of-changes
```
### 7. Creating a Pull Request (PR)
1. Go to your fork on GitHub
2. Click "New Pull Request"
3. Select the base branch `main` of the original repository
4. Fill out the PR description:
- Brief description of changes
- Motivation for changes
- Screenshots (if applicable)
### 8. Review Process
- Project maintainers will review your PR
- There may be comments and requests for changes
- After approval, the PR will be merged
## 🛠 Code Requirements
- Follow PEP 8 for Python
- Write clear and self-documenting code
- Add type hints
- Cover code with tests
## 🐛 Found a Bug?
1. Check existing Issues
2. Create a new Issue with:
- Bug description
- Reproduction steps
- Home Assistant version
- Plugin version
## 📜 License
By contributing to the project, you agree to the [project's license](LICENSE).
## 🤔 Questions?
If you have any questions, create an Issue or contact the project maintainers.
**Thank you for your contribution!** 🎉
+1
View File
@@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+284 -85
View File
@@ -1,165 +1,364 @@
# 🤖 HA Text AI for Home Assistant
<div align="center">
<div align="center">
![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square)
![GitHub downloads](https://img.shields.io/github/downloads/smkrv/ha-text-ai/total.svg?style=flat-square)
![GitHub stars](https://img.shields.io/github/stars/smkrv/ha-text-ai.svg?style=social)
![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square) ![GitHub downloads](https://img.shields.io/github/downloads/smkrv/ha-text-ai/total.svg?style=flat-square) ![GitHub stars](https://img.shields.io/github/stars/smkrv/ha-text-ai.svg?style=social) ![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=flat-square)](https://github.com/hacs/integration)
<img src="https://github.com/smkrv/ha-text-ai/blob/3e3ec45b195c92989434fde40ae110027f4ea124/misc/icons/icon.png" alt="HA Text AI" width="140"/>
### Advanced AI Integration for Home Assistant with multi-provider support
</div>
<p align="center">
Transform your smart home experience with powerful AI assistance powered by OpenAI's GPT models
Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
</p>
---
> [!IMPORTANT]
> 🚧 ALPHA VERSION 🚧
> Expect: potential bugs, frequent changes, incomplete features.
> 🤝 Community Driven
>
> <a href="https://community.home-assistant.io/t/ha-text-ai-transforming-home-automation-with-multi-provider-language-models/799741"><img src="https://img.shields.io/badge/Community-blue?style=for-the-badge&logo=homeassistant&logoColor=white&color=03a9f4"/></a>
## 🌟 Features
- 🧠 **Advanced AI Integration**: Leverage OpenAI's powerful models (GPT-3.5, GPT-4) for smart home interactions
- 💬 **Natural Language Control**: Control your home and get information using everyday language
- 📝 **Conversation Memory**: Maintain context with conversation history tracking
-**Real-time Responses**: Get quick, contextual responses to your queries
- 🎯 **Customizable Behavior**: Fine-tune AI responses with adjustable parameters
- 🔒 **Secure Integration**: Your API key and data are handled securely
- 🎨 **Flexible Configuration**: Easy setup with multiple configuration options
- 🔄 **Automation Ready**: Integrate AI responses into your automations
- 🧠 **Multi-Provider AI Integration**:
- Support for OpenAI GPT models
- Anthropic Claude integration
- Custom API endpoints
- Flexible model selection
- 💬 **Advanced Language Processing**:
- Context-aware responses
- Multi-turn conversations
- Custom system instructions
- Natural conversation flow
- 📝 **Enhanced Memory Management**:
- Persistent conversation history
- Context-aware responses
- Customizable history limits
- Model-specific filtering
-**Performance Optimization**:
- Efficient token usage
- Smart rate limiting
- Response caching
- Request interval control
- 🎯 **Advanced Customization**:
- Per-request model selection
- Adjustable parameters
- Custom system prompts
- Temperature control
- 🔒 **Enhanced Security**:
- Secure API key storage
- Rate limiting protection
- Error handling
- Usage monitoring
- 🎨 **Improved User Experience**:
- Intuitive configuration UI
- Detailed sensor attributes
- Rich service interface
- Model selection UI
- 🔄 **Automation Integration**:
- Event-driven responses
- Conditional logic support
- Template compatibility
- Model-specific automation
## 📋 Prerequisites
- Home Assistant installation (Core, OS, Container, or Supervised)
- OpenAI API key ([Get one here](https://platform.openai.com/account/api-keys))
- Home Assistant 2023.11 or later
- Active API key from:
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
- Anthropic ([Get key](https://console.anthropic.com/))
- OpenRouter ([Get key](https://openrouter.ai/keys))
- Python 3.9 or newer
- Stable internet connection
## ⚡ Quick Start
### Configuration Options
- API Provider (OpenAI/Anthropic)
- API Key (provider-specific)
- Model Selection (flexible, provider-specific models)
- Temperature (Creativity control, 0.0-2.0)
- Max Tokens (Response length limit)
- Request Interval (API call throttling)
- Custom API Endpoint (optional)
#### ⓘ Potentially Compatible Providers
The integration is designed to be flexible and may work with other providers offering OpenAI-compatible APIs:
- Groq
- Together AI
- Perplexity AI
- Mistral AI
- Google AI
- Local AI servers (like Ollama)
- Custom OpenAI-compatible endpoints
#### Additional Notes
- Not all providers guarantee full compatibility
- Performance may vary between providers
- Check individual provider's documentation
- Ensure your API key has sufficient credits/quota
#### Provider Compatibility Requirements
To be compatible, a provider should support:
- OpenAI-like REST API structure
- JSON request/response format
- Standard authentication method
- Similar model parameter handling
## ⚡ Installation
### HACS Installation (Recommended)
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
1. Open HACS in Home Assistant
2. Click on "Integrations"
3. Click "..." in top right corner
4. Select "Custom repositories"
5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
6. Choose "Integration" as category
7. Click "Download"
8. Restart Home Assistant
### Manual Installation
1. Download the repository
2. Copy `custom_components/ha_text_ai` to your `custom_components` directory
1. Download the latest release
2. Extract and copy `custom_components/ha_text_ai` to your `custom_components` directory
3. Restart Home Assistant
4. Add configuration to `configuration.yaml`:
4. Add configuration via UI or YAML
## ⚙️ Configuration
### Via UI (Recommended)
1. Go to Settings → Devices & Services
2. Click "Add Integration"
3. Search for "HA Text AI"
4. Follow the configuration steps
### Via YAML
```yaml
ha_text_ai:
api_key: !secret openai_api_key
api_provider: openai # or anthropic
api_key: !secret ai_api_key
model: gpt-4o-mini
temperature: 0.7
max_tokens: 1000
request_interval: 1.0
api_endpoint: https://api.openai.com/v1 # optional, for custom endpoints
system_prompt: |
You are a home automation expert assistant.
Focus on practical and efficient solutions.
```
## ⚙️ Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `api_key` | string | Required | Your OpenAI API key |
| `model` | string | `gpt-3.5-turbo` | AI model to use |
| `temperature` | float | `0.7` | Response creativity (0-2) |
| `max_tokens` | integer | `1000` | Maximum response length |
| `request_interval` | float | `1.0` | Minimum seconds between requests |
| `api_endpoint` | string | OpenAI default | Custom API endpoint URL |
## 🛠️ Available Services
### ask_question
Ask the AI assistant a question:
```yaml
service: ha_text_ai.ask_question
data:
question: "What's the optimal temperature for sleeping?"
model: "gpt-4" # optional
model: "claude-3-sonnet" # optional
temperature: 0.5 # optional
max_tokens: 500 # optional
context_messages: 10 #optional, number of previous messages to include in context, default: 5
system_prompt: "You are a sleep optimization expert" # optional
```
### set_system_prompt
Configure AI behavior:
```yaml
service: ha_text_ai.set_system_prompt
data:
prompt: "You are a home automation expert focused on energy efficiency"
prompt: |
You are a home automation expert focused on:
1. Energy efficiency
2. Comfort optimization
3. Security considerations
Provide practical, actionable advice.
```
### clear_history
Reset conversation history:
```yaml
service: ha_text_ai.clear_history
```
### get_history
Retrieve conversation history:
```yaml
service: ha_text_ai.get_history
data:
limit: 5 # optional
filter_model: "gpt-4o" # optional
```
## 🔧 Practical Examples
### 🏷️ HA Text AI Sensor Naming Convention
### Smart Temperature Management
#### Sensor Name Structure
```yaml
# Always starts with 'sensor.ha_text_ai_'
# You define only the part after the underscore
sensor.ha_text_ai_YOUR_UNIQUE_SUFFIX
# Examples:
sensor.ha_text_ai_gpt # GPT-based sensor
sensor.ha_text_ai_claude # Claude-based sensor
sensor.ha_text_ai_gpt # Custom suffix
```
#### Response Retrieval
```yaml
# Use your specific sensor name
{{ state_attr('sensor.ha_text_ai_gpt', 'response') }}
```
#### Practical Usage
```yaml
automation:
trigger:
platform: time_pattern
hours: "/1"
action:
service: ha_text_ai.ask_question
data:
question: >
Current temperature is {{ states('sensor.living_room_temperature') }}°C.
Should I adjust the thermostat for optimal comfort and energy savings?
- alias: "AI Response with Custom Sensor"
action:
- service: ha_text_ai.ask_question
data:
question: "Home automation advice"
- service: notify.mobile
data:
message: >
AI Tip:
{{ state_attr('sensor.ha_text_ai_gpt', 'response') }}
```
### Smart Lighting Assistant
### 💡 Naming Rules
- Prefix is always `sensor.ha_text_ai_`
- Add your unique identifier after the underscore
- Use lowercase
- No spaces allowed
- Keep it descriptive but concise
### 🔍 HA Text AI Sensor Attributes
#### Model and Provider Information
```yaml
automation:
trigger:
platform: state
entity_id: binary_sensor.living_room_motion
to: 'on'
condition:
condition: template
value_template: "{{ states('sensor.illuminance') | float < 10 }}"
action:
service: ha_text_ai.ask_question
data:
question: >
Motion detected in living room with low light levels.
What's the best lighting scene to set based on the time of day?
# Model details
{{ state_attr('sensor.ha_text_ai_gpt', 'Model') }} # gpt-4o
{{ state_attr('sensor.ha_text_ai_gpt', 'Api provider') }} # openai
{{ state_attr('sensor.ha_text_ai_gpt', 'Last model') }} # gpt-4o
```
## ❗ Common Issues
#### System Status
```yaml
# Operational status
{{ state_attr('sensor.ha_text_ai_gpt', 'Api status') }} # ready
{{ state_attr('sensor.ha_text_ai_gpt', 'Is processing') }} # false
{{ state_attr('sensor.ha_text_ai_gpt', 'Is rate limited') }} # false
{{ state_attr('sensor.ha_text_ai_gpt', 'Endpoint status') }} # ready
```
### API Rate Limits
- Increase `request_interval` if hitting rate limits
- Consider upgrading your OpenAI plan
- Use caching for frequent queries
#### Performance Metrics
```yaml
# Request and performance statistics
{{ state_attr('sensor.ha_text_ai_gpt', 'Successful requests') }} # 0
{{ state_attr('sensor.ha_text_ai_gpt', 'Failed requests') }} # 0
{{ state_attr('sensor.ha_text_ai_gpt', 'Average latency') }} # 0
{{ state_attr('sensor.ha_text_ai_gpt', 'Max latency') }} # 0
```
### High Token Usage
- Reduce `max_tokens` parameter
- Clear conversation history regularly
- Use focused system prompts
#### Conversation and Token Usage
```yaml
# Conversation and token details
{{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0
{{ state_attr('sensor.ha_text_ai_gpt', 'Total tokens') }} # 0
{{ state_attr('sensor.ha_text_ai_gpt', 'Prompt tokens') }} # 0
{{ state_attr('sensor.ha_text_ai_gpt', 'Completion tokens') }} # 0
```
### Connection Issues
- Check internet connectivity
- Verify API key validity
- Ensure endpoint accessibility
#### Last Interaction Details
```yaml
# Last interaction information
{{ state_attr('sensor.ha_text_ai_gpt', 'Response') }} # Last AI response
{{ state_attr('sensor.ha_text_ai_gpt', 'Question') }} # Last asked question
{{ state_attr('sensor.ha_text_ai_gpt', 'Last timestamp') }} # Timestamp
```
#### System Health
```yaml
# System health and maintenance
{{ state_attr('sensor.ha_text_ai_gpt', 'Total errors') }} # 0
{{ state_attr('sensor.ha_text_ai_gpt', 'Is maintenance') }} # false
{{ state_attr('sensor.ha_text_ai_gpt', 'Uptime') }} # 547,58
```
### 💡 Pro Tips
- Always check attribute existence
- Use these attributes for monitoring and automation
- Some values might be 0 or empty initially
## 📘 FAQ
**Q: Which AI providers are supported?**
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
**Q: How can I reduce API costs?**
A: Use GPT-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
**Q: Are there limitations on the number of requests?**
A: Depends on your API provider's plan. We recommend monitoring usage and implementing request throttling via `request_interval` configuration.
**Q: Can I use custom models?**
A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration.
**Q: How do I switch between different AI providers?**
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
**Q: How can I reduce API costs?**
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
**Q: Is my data secure?**
A: Yes, your data is secure. The system operates entirely on your local machine, keeping your data under your control. API keys are stored securely and all external communications use encrypted connections.
**Q: How do context messages work?**
A: Context messages allow the AI to remember and reference previous conversation history. By default, 5 previous messages are included, but you can customize this from 1 to 20 messages to control the conversation depth and token usage.
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
2. Create feature branch (`git checkout -b feature/Enhancement`)
3. Commit changes (`git commit -m 'Add Enhancement'`)
4. Push branch (`git push origin feature/Enhancement`)
5. Open Pull Request
## 📝 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
MIT License - see [LICENSE](LICENSE) for details.
## 💡 Support the Project
The best support is:
- Sharing feedback
- Contributing ideas
- Recommending to friends
- Reporting issues
- Star the repository
If you want to say thanks financially, you can send a small token of appreciation in USDT:
**USDT Wallet (TRC10/TRC20):**
`TXC9zYHYPfWUGi4Sv4R1ctTBGScXXQk5HZ`
*Open-source is built by community passion!* 🚀
---
<div align="center">
Made with ❤️ for the Home Assistant Community
Made with ❤️ and Claude 3.5 Sonnet for the Home Assistant Community
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
</div>
+227 -120
View File
@@ -1,188 +1,295 @@
"""The HA text AI integration."""
"""The HA Text AI integration."""
from __future__ import annotations
import logging
from typing import Any
import os
import shutil
from datetime import datetime, timedelta
from typing import Any, Dict
import voluptuous as vol
from async_timeout import timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import aiohttp_client
from .coordinator import HATextAICoordinator
from .api_client import APIClient
from .const import (
DOMAIN,
PLATFORMS,
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT,
CONF_MODEL,
CONF_TEMPERATURE,
CONF_MAX_TOKENS,
CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL,
CONF_API_PROVIDER,
CONF_CONTEXT_MESSAGES,
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC,
DEFAULT_MODEL,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS,
DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT,
DEFAULT_REQUEST_INTERVAL,
DEFAULT_CONTEXT_MESSAGES,
API_TIMEOUT,
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT,
)
from .coordinator import HATextAICoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the HA text AI component."""
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Required("instance"): cv.string,
vol.Required("question"): cv.string,
vol.Optional("system_prompt"): cv.string,
vol.Optional("model"): cv.string,
vol.Optional("temperature"): cv.positive_float,
vol.Optional("max_tokens"): cv.positive_int,
vol.Optional("context_messages"): cv.positive_int,
})
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required("instance"): cv.string,
vol.Required("prompt"): cv.string,
})
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Required("instance"): cv.string,
vol.Optional("limit"): cv.positive_int,
vol.Optional("filter_model"): cv.string,
})
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
"""Get coordinator by instance name."""
if instance.startswith("sensor."):
instance = instance.replace("sensor.ha_text_ai_", "", 1)
for entry_id, coord in hass.data[DOMAIN].items():
if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower():
return coord
raise HomeAssistantError(f"Instance {instance} not found")
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
"""Set up the HA Text AI component."""
hass.data.setdefault(DOMAIN, {})
try:
source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg')
dest_dir = os.path.join(hass.config.path('www'), 'icons')
os.makedirs(dest_dir, exist_ok=True)
dest = os.path.join(dest_dir, 'icon.png')
if not os.path.exists(dest):
shutil.copyfile(source, dest)
except Exception as ex:
_LOGGER.warning("Failed to copy custom icon: %s", str(ex))
async def async_ask_question(call: ServiceCall) -> None:
"""Handle the ask_question service call."""
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
coordinator = next(iter(hass.data[DOMAIN].values()))
question = call.data["question"]
original_params = {
"model": coordinator.model,
"temperature": coordinator.temperature,
"max_tokens": coordinator.max_tokens
}
"""Handle ask_question service."""
try:
if "model" in call.data:
coordinator.model = call.data["model"]
if "temperature" in call.data:
coordinator.temperature = call.data["temperature"]
if "max_tokens" in call.data:
coordinator.max_tokens = call.data["max_tokens"]
await coordinator.async_ask_question(question)
except Exception as ex:
_LOGGER.error("Error asking question: %s", str(ex))
raise HomeAssistantError(f"Failed to ask question: {str(ex)}") from ex
finally:
coordinator.model = original_params["model"]
coordinator.temperature = original_params["temperature"]
coordinator.max_tokens = original_params["max_tokens"]
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_ask_question(
question=call.data["question"],
model=call.data.get("model"),
temperature=call.data.get("temperature"),
max_tokens=call.data.get("max_tokens"),
system_prompt=call.data.get("system_prompt"),
context_messages=call.data.get("context_messages"),
)
except Exception as err:
_LOGGER.error("Error asking question: %s", str(err))
raise HomeAssistantError(f"Failed to process question: {str(err)}")
async def async_clear_history(call: ServiceCall) -> None:
"""Handle the clear_history service call."""
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
"""Handle clear_history service."""
try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_clear_history()
except Exception as err:
_LOGGER.error("Error clearing history: %s", str(err))
raise HomeAssistantError(f"Failed to clear history: {str(err)}")
coordinator = next(iter(hass.data[DOMAIN].values()))
coordinator._responses.clear()
await coordinator.async_refresh()
async def async_get_history(call: ServiceCall) -> dict[str, list]:
"""Handle the get_history service call."""
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
coordinator = next(iter(hass.data[DOMAIN].values()))
if not coordinator._responses:
return {"history": []}
limit = call.data.get("limit", 10)
history = list(coordinator._responses.items())
limited_history = history[-limit:] if len(history) > limit else history
return {
"history": [
{"question": q, "response": r} for q, r in limited_history
]
}
async def async_get_history(call: ServiceCall) -> list:
"""Handle get_history service."""
try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
return await coordinator.async_get_history(
limit=call.data.get("limit"),
filter_model=call.data.get("filter_model")
)
except Exception as err:
_LOGGER.error("Error getting history: %s", str(err))
raise HomeAssistantError(f"Failed to get history: {str(err)}")
async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle the set_system_prompt service call."""
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
coordinator = next(iter(hass.data[DOMAIN].values()))
coordinator.system_prompt = call.data["prompt"]
"""Handle set_system_prompt service."""
try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_set_system_prompt(call.data["prompt"])
except Exception as err:
_LOGGER.error("Error setting system prompt: %s", str(err))
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
hass.services.async_register(
DOMAIN,
SERVICE_ASK_QUESTION,
async_ask_question,
schema=vol.Schema({
vol.Required("question"): cv.string,
vol.Optional("model"): cv.string,
vol.Optional("temperature"): vol.All(
vol.Coerce(float), vol.Range(min=0, max=2)
),
vol.Optional("max_tokens"): vol.All(
vol.Coerce(int), vol.Range(min=1, max=4096)
),
})
schema=SERVICE_SCHEMA_ASK_QUESTION
)
hass.services.async_register(
DOMAIN,
SERVICE_CLEAR_HISTORY,
async_clear_history,
schema=vol.Schema({})
schema=vol.Schema({vol.Required("instance"): cv.string})
)
hass.services.async_register(
DOMAIN,
SERVICE_GET_HISTORY,
async_get_history,
schema=vol.Schema({
vol.Optional("limit", default=10): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
})
schema=SERVICE_SCHEMA_GET_HISTORY
)
hass.services.async_register(
DOMAIN,
SERVICE_SET_SYSTEM_PROMPT,
async_set_system_prompt,
schema=vol.Schema({
vol.Required("prompt"): cv.string,
})
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA text AI from a config entry."""
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
"""Check API availability for different providers."""
try:
coordinator = HATextAICoordinator(
hass,
api_key=entry.data[CONF_API_KEY],
endpoint=entry.data.get(CONF_API_ENDPOINT),
model=entry.data.get(CONF_MODEL),
temperature=entry.data.get(CONF_TEMPERATURE),
max_tokens=entry.data.get(CONF_MAX_TOKENS),
request_interval=entry.data.get(CONF_REQUEST_INTERVAL),
if provider == API_PROVIDER_ANTHROPIC:
check_url = f"{endpoint}/v1/models"
else: # OpenAI
check_url = f"{endpoint}/models"
async with timeout(API_TIMEOUT):
async with session.get(check_url, headers=headers) as response:
if response.status in [200, 404]:
return True
elif response.status == 401:
raise ConfigEntryNotReady("Invalid API key")
elif response.status == 429:
_LOGGER.warning("Rate limit exceeded during API check")
return False
else:
_LOGGER.error("API check failed with status: %d", response.status)
return False
except Exception as ex:
_LOGGER.error("API check error: %s", str(ex))
return False
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA Text AI from a config entry."""
try:
if CONF_API_PROVIDER not in entry.data:
_LOGGER.error("API provider not specified")
raise ConfigEntryNotReady("API provider is required")
session = aiohttp_client.async_get_clientsession(hass)
api_provider = entry.data.get(CONF_API_PROVIDER)
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
endpoint = entry.data.get(
CONF_API_ENDPOINT,
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT
).rstrip('/')
api_key = entry.data[CONF_API_KEY]
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
if is_anthropic:
headers["x-api-key"] = api_key
headers["anthropic-version"] = "2023-06-01"
else:
headers["Authorization"] = f"Bearer {api_key}"
if not await async_check_api(session, endpoint, headers, api_provider):
raise ConfigEntryNotReady("API connection failed")
_LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
api_client = APIClient(
session=session,
endpoint=endpoint,
headers=headers,
api_provider=api_provider,
model=model,
)
coordinator = HATextAICoordinator(
hass=hass,
client=api_client,
model=model,
update_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
instance_name=instance_name,
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
is_anthropic=is_anthropic,
context_messages=entry.data.get(
CONF_CONTEXT_MESSAGES,
DEFAULT_CONTEXT_MESSAGES
),
)
coordinator.data = coordinator._initial_state.copy()
_LOGGER.debug(f"Initial state set for coordinator {instance_name}")
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
_LOGGER.info(
"Successfully set up %s instance '%s' with model %s",
api_provider,
instance_name,
model
)
return True
except Exception as ex:
raise ConfigEntryNotReady(f"Failed to setup entry: {str(ex)}") from ex
_LOGGER.exception("Setup error: %s", str(ex))
raise ConfigEntryNotReady(f"Setup error: {str(ex)}") from ex
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
try:
if entry.entry_id in hass.data[DOMAIN]:
coordinator = hass.data[DOMAIN][entry.entry_id]
if hasattr(coordinator.client, 'shutdown'):
await coordinator.client.shutdown()
if not hass.data[DOMAIN]:
services = [
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT
]
for service in services:
if service in hass.services.async_services().get(DOMAIN, {}):
hass.services.async_remove(DOMAIN, service)
await coordinator.async_shutdown()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
except Exception as ex:
_LOGGER.exception("Error unloading entry: %s", str(ex))
return False
+180
View File
@@ -0,0 +1,180 @@
"""API Client for HA Text AI."""
import logging
import asyncio
from typing import Any, Dict, List, Optional
from aiohttp import ClientSession, ClientTimeout
from async_timeout import timeout
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import (
API_TIMEOUT,
API_RETRY_COUNT,
API_PROVIDER_ANTHROPIC,
MIN_TEMPERATURE,
MAX_TEMPERATURE,
MIN_MAX_TOKENS,
MAX_MAX_TOKENS,
)
_LOGGER = logging.getLogger(__name__)
class APIClient:
"""API Client for OpenAI and Anthropic."""
def __init__(
self,
session: ClientSession,
endpoint: str,
headers: Dict[str, str],
api_provider: str,
model: str,
) -> None:
"""Initialize API client."""
self.session = session
self.endpoint = endpoint
self.headers = headers
self.api_provider = api_provider
self.model = model
self.timeout = ClientTimeout(total=API_TIMEOUT)
def _validate_parameters(
self,
temperature: float,
max_tokens: int,
) -> None:
"""Validate API parameters."""
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
raise ValueError(
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}"
)
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
raise ValueError(
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}"
)
async def _make_request(
self,
url: str,
payload: Dict[str, Any],
) -> Dict[str, Any]:
"""Make API request with retry logic."""
for attempt in range(API_RETRY_COUNT):
try:
async with timeout(API_TIMEOUT):
async with self.session.post(
url,
json=payload,
headers=self.headers,
timeout=self.timeout
) as response:
if response.status != 200:
error_data = await response.json()
raise HomeAssistantError(f"API error: {error_data}")
return await response.json()
except asyncio.TimeoutError:
if attempt == API_RETRY_COUNT - 1:
raise HomeAssistantError("API request timed out")
await asyncio.sleep(1 * (attempt + 1))
except Exception as e:
if attempt == API_RETRY_COUNT - 1:
raise
_LOGGER.warning("API request failed, retrying: %s", str(e))
await asyncio.sleep(1 * (attempt + 1))
async def create(
self,
model: str,
messages: List[Dict[str, str]],
temperature: float,
max_tokens: int,
) -> Dict[str, Any]:
"""Create completion using appropriate API."""
try:
self._validate_parameters(temperature, max_tokens)
if self.api_provider == API_PROVIDER_ANTHROPIC:
return await self._create_anthropic_completion(
model, messages, temperature, max_tokens
)
else:
return await self._create_openai_completion(
model, messages, temperature, max_tokens
)
except Exception as e:
_LOGGER.error("API request failed: %s", str(e))
raise HomeAssistantError(f"API request failed: {str(e)}")
async def _create_openai_completion(
self,
model: str,
messages: List[Dict[str, str]],
temperature: float,
max_tokens: int,
) -> Dict[str, Any]:
"""Create completion using OpenAI API."""
url = f"{self.endpoint}/chat/completions"
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
data = await self._make_request(url, payload)
return {
"choices": [
{
"message": {
"content": data["choices"][0]["message"]["content"]
}
}
],
"usage": {
"prompt_tokens": data["usage"]["prompt_tokens"],
"completion_tokens": data["usage"]["completion_tokens"],
"total_tokens": data["usage"]["total_tokens"]
}
}
async def _create_anthropic_completion(
self,
model: str,
messages: List[Dict[str, str]],
temperature: float,
max_tokens: int,
) -> Dict[str, Any]:
"""Create completion using Anthropic API."""
url = f"{self.endpoint}/v1/messages"
# Convert messages to Anthropic format
system_prompt = next(
(msg["content"] for msg in messages if msg["role"] == "system"),
None
)
conversation = [msg for msg in messages if msg["role"] != "system"]
payload = {
"model": model,
"messages": conversation,
"max_tokens": max_tokens,
"temperature": temperature,
}
if system_prompt:
payload["system"] = system_prompt
data = await self._make_request(url, payload)
return {
"choices": [
{
"message": {
"content": data["content"][0]["text"]
}
}
],
"usage": {
"prompt_tokens": data["usage"]["input_tokens"],
"completion_tokens": data["usage"]["output_tokens"],
"total_tokens": data["usage"]["input_tokens"] + data["usage"]["output_tokens"]
}
}
+195 -86
View File
@@ -1,12 +1,14 @@
"""Config flow for HA text AI integration."""
import logging
from typing import Any, Dict, Optional
import voluptuous as vol
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import callback
import openai
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import selector
from .const import (
DOMAIN,
@@ -15,124 +17,231 @@ from .const import (
CONF_MAX_TOKENS,
CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL,
CONF_API_PROVIDER,
CONF_CONTEXT_MESSAGES,
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC,
API_PROVIDERS,
DEFAULT_MODEL,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS,
DEFAULT_API_ENDPOINT,
DEFAULT_REQUEST_INTERVAL,
DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT,
DEFAULT_CONTEXT_MESSAGES,
MIN_TEMPERATURE,
MAX_TEMPERATURE,
MIN_MAX_TOKENS,
MAX_MAX_TOKENS,
MIN_REQUEST_INTERVAL,
)
STEP_USER_DATA_SCHEMA = vol.Schema({
vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Optional(
CONF_TEMPERATURE,
default=DEFAULT_TEMPERATURE
): vol.All(vol.Coerce(float), vol.Range(min=0, max=2)),
vol.Optional(
CONF_MAX_TOKENS,
default=DEFAULT_MAX_TOKENS
): vol.All(vol.Coerce(int), vol.Range(min=1, max=4096)),
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): str,
vol.Optional(
CONF_REQUEST_INTERVAL,
default=DEFAULT_REQUEST_INTERVAL
): vol.All(vol.Coerce(float), vol.Range(min=0.1)),
})
_LOGGER = logging.getLogger(__name__)
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HA text AI."""
VERSION = 1
async def async_step_user(
self,
user_input: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
def __init__(self) -> None:
"""Initialize flow."""
self._errors = {}
self._data = {}
self._provider = None
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
"""Handle the initial step."""
errors: Dict[str, str] = {}
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
selector.SelectSelectorConfig(
options=API_PROVIDERS,
translation_key="api_provider"
)
),
})
)
if user_input is not None:
try:
client = openai.OpenAI(
api_key=user_input[CONF_API_KEY],
base_url=user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT)
)
await self.hass.async_add_executor_job(
client.models.list
)
self._provider = user_input[CONF_API_PROVIDER]
return await self.async_step_provider()
async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
"""Handle provider configuration step."""
if user_input is None:
default_endpoint = (
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT
)
await self.async_set_unique_id(user_input[CONF_API_KEY])
self._abort_if_unique_id_configured()
suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}"
return self.async_create_entry(
title="HA text AI",
data=user_input
)
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
vol.Required(CONF_NAME, default=suggested_name): str,
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
vol.Optional(
CONF_CONTEXT_MESSAGES,
default=DEFAULT_CONTEXT_MESSAGES
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
),
}),
errors=self._errors
)
except openai.AuthenticationError:
errors["base"] = "invalid_auth"
except openai.APIError:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
errors["base"] = "unknown"
instance_name = user_input[CONF_NAME]
await self._async_validate_name(instance_name)
if self._errors:
return await self.async_step_provider()
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
if not await self._async_validate_api(user_input):
return await self.async_step_provider()
return await self._create_entry(user_input)
async def _async_validate_name(self, name: str) -> bool:
"""Validate that the name is unique."""
for entry in self._async_current_entries():
if entry.data.get(CONF_NAME) == name:
self._errors["name"] = "name_exists"
return False
return True
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
"""Validate API connection."""
try:
session = async_get_clientsession(self.hass)
headers = self._get_api_headers(user_input)
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
check_url = (
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
else f"{endpoint}/models"
)
async with session.get(check_url, headers=headers) as response:
if response.status == 401:
self._errors["base"] = "invalid_auth"
return False
elif response.status not in [200, 404]:
self._errors["base"] = "cannot_connect"
return False
return True
except Exception as err:
_LOGGER.error("API validation error: %s", str(err))
self._errors["base"] = "cannot_connect"
return False
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
"""Get API headers based on provider."""
api_key = user_input[CONF_API_KEY]
if self._provider == API_PROVIDER_ANTHROPIC:
return {
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json"
}
return {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
"""Create the config entry."""
instance_name = user_input[CONF_NAME]
unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_")
return self.async_create_entry(
title=instance_name,
data={
CONF_API_PROVIDER: self._provider,
CONF_NAME: instance_name,
**user_input,
"unique_id": unique_id,
CONF_CONTEXT_MESSAGES: user_input.get(
CONF_CONTEXT_MESSAGES,
DEFAULT_CONTEXT_MESSAGES)
}
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for HA text AI."""
"""Handle options flow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self,
user_input: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Handle options flow."""
async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
options_schema = vol.Schema({
vol.Optional(
CONF_TEMPERATURE,
default=self.config_entry.options.get(
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
),
description="Temperature for response generation (0-2)",
): vol.All(vol.Coerce(float), vol.Range(min=0, max=2)),
vol.Optional(
CONF_MAX_TOKENS,
default=self.config_entry.options.get(
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
),
description="Maximum tokens in response (1-4096)",
): vol.All(vol.Coerce(int), vol.Range(min=1, max=4096)),
vol.Optional(
CONF_REQUEST_INTERVAL,
default=self.config_entry.options.get(
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
),
description="Minimum time between API requests (seconds)",
): vol.All(vol.Coerce(float), vol.Range(min=0.1)),
})
current_data = {**self.config_entry.data, **self.config_entry.options}
return self.async_show_form(
step_id="init",
data_schema=options_schema,
data_schema=vol.Schema({
vol.Optional(
CONF_MODEL,
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
): str,
vol.Optional(
CONF_TEMPERATURE,
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(
CONF_MAX_TOKENS,
default=current_data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(
CONF_REQUEST_INTERVAL,
default=current_data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
vol.Optional(
CONF_CONTEXT_MESSAGES,
default=current_data.get(
CONF_CONTEXT_MESSAGES,
DEFAULT_CONTEXT_MESSAGES
)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
),
})
)
+160 -20
View File
@@ -1,24 +1,47 @@
"""Constants for the HA text AI integration."""
from typing import Final
from homeassistant.const import Platform
import voluptuous as vol
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
from homeassistant.helpers import config_validation as cv
# Domain
# Domain and platforms
DOMAIN: Final = "ha_text_ai"
PLATFORMS: Final = [Platform.SENSOR]
# Provider configuration
CONF_API_PROVIDER: Final = "api_provider"
API_PROVIDER_OPENAI: Final = "openai"
API_PROVIDER_ANTHROPIC: Final = "anthropic"
API_PROVIDERS: Final = [
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC
]
# Default endpoints
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
# Configuration constants
CONF_MODEL: Final = "model"
CONF_TEMPERATURE: Final = "temperature"
CONF_MAX_TOKENS: Final = "max_tokens"
CONF_API_ENDPOINT: Final = "api_endpoint"
CONF_REQUEST_INTERVAL: Final = "request_interval"
CONF_INSTANCE: Final = "instance"
CONF_MAX_HISTORY_SIZE: Final = "max_history_size"
CONF_IS_ANTHROPIC: Final = "is_anthropic"
CONF_CONTEXT_MESSAGES: Final = "context_messages"
# Default values
DEFAULT_MODEL: Final = "gpt-3.5-turbo"
DEFAULT_TEMPERATURE: Final = 0.7
DEFAULT_MODEL: Final = "gpt-4o-mini"
DEFAULT_TEMPERATURE: Final = 0.1
DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com/v1"
DEFAULT_REQUEST_INTERVAL: Final = 1.0
DEFAULT_TIMEOUT: Final = 30
DEFAULT_MAX_HISTORY: Final = 50
DEFAULT_NAME: Final = "HA Text AI"
DEFAULT_CONTEXT_MESSAGES: Final = 5
# Parameter constraints
MIN_TEMPERATURE: Final = 0.0
@@ -26,6 +49,11 @@ MAX_TEMPERATURE: Final = 2.0
MIN_MAX_TOKENS: Final = 1
MAX_MAX_TOKENS: Final = 4096
MIN_REQUEST_INTERVAL: Final = 0.1
MAX_REQUEST_INTERVAL: Final = 60.0
# API constants
API_TIMEOUT: Final = 30
API_RETRY_COUNT: Final = 3
# Service names
SERVICE_ASK_QUESTION: Final = "ask_question"
@@ -33,29 +61,141 @@ SERVICE_CLEAR_HISTORY: Final = "clear_history"
SERVICE_GET_HISTORY: Final = "get_history"
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
# Service descriptions
SERVICE_ASK_QUESTION_DESCRIPTION: Final = "Ask a question to the AI model"
SERVICE_CLEAR_HISTORY_DESCRIPTION: Final = "Clear conversation history"
SERVICE_GET_HISTORY_DESCRIPTION: Final = "Get conversation history"
SERVICE_SET_SYSTEM_PROMPT_DESCRIPTION: Final = "Set system prompt for AI model"
# Attribute keys
ATTR_QUESTION: Final = "question"
ATTR_RESPONSE: Final = "response"
ATTR_LAST_UPDATED: Final = "last_updated"
ATTR_INSTANCE: Final = "instance"
ATTR_MODEL: Final = "model"
ATTR_TEMPERATURE: Final = "temperature"
ATTR_MAX_TOKENS: Final = "max_tokens"
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
ATTR_API_STATUS: Final = "api_status"
ATTR_ERROR_COUNT: Final = "error_count"
ATTR_CONVERSATION_HISTORY: Final = "conversation_history"
# Sensor attributes
ATTR_TOTAL_RESPONSES: Final = "total_responses"
ATTR_TOTAL_ERRORS: Final = "total_errors"
ATTR_AVG_RESPONSE_TIME: Final = "average_response_time"
ATTR_LAST_REQUEST_TIME: Final = "last_request_time"
ATTR_LAST_ERROR: Final = "last_error"
ATTR_IS_PROCESSING: Final = "is_processing"
ATTR_IS_RATE_LIMITED: Final = "is_rate_limited"
ATTR_IS_MAINTENANCE: Final = "is_maintenance"
ATTR_API_VERSION: Final = "api_version"
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
ATTR_PERFORMANCE_METRICS: Final = "performance_metrics"
ATTR_HISTORY_SIZE: Final = "history_size"
ATTR_UPTIME: Final = "uptime"
ATTR_API_PROVIDER: Final = "api_provider"
ATTR_METRICS: Final = "metrics"
ATTR_STATE: Final = "state"
ATTR_LAST_RESPONSE: Final = "last_response"
ATTR_ERROR: Final = "error"
ATTR_TIMESTAMP: Final = "timestamp"
# Sensor metrics
METRIC_TOTAL_TOKENS: Final = "total_tokens"
METRIC_PROMPT_TOKENS: Final = "prompt_tokens"
METRIC_COMPLETION_TOKENS: Final = "completion_tokens"
METRIC_SUCCESSFUL_REQUESTS: Final = "successful_requests"
METRIC_FAILED_REQUESTS: Final = "failed_requests"
METRIC_AVERAGE_LATENCY: Final = "average_latency"
METRIC_MAX_LATENCY: Final = "max_latency"
METRIC_MIN_LATENCY: Final = "min_latency"
# Error messages
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
ERROR_CANNOT_CONNECT: Final = "cannot_connect"
ERROR_UNKNOWN: Final = "unknown_error"
# Configuration descriptions
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses"
CONF_TEMPERATURE_DESCRIPTION: Final = "Temperature for response generation (0-2)"
CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)"
CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL"
CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)"
ERROR_INVALID_MODEL: Final = "invalid_model"
ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
ERROR_API_ERROR: Final = "api_error"
ERROR_TIMEOUT: Final = "timeout_error"
ERROR_INVALID_INSTANCE: Final = "invalid_instance"
ERROR_NAME_EXISTS: Final = "name_exists"
# Entity attributes
ENTITY_NAME: Final = "HA Text AI"
ENTITY_ICON: Final = "mdi:robot"
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
# State attributes
STATE_READY: Final = "ready"
STATE_PROCESSING: Final = "processing"
STATE_ERROR: Final = "error"
STATE_INITIALIZING: Final = "initializing"
STATE_MAINTENANCE: Final = "maintenance"
STATE_RATE_LIMITED: Final = "rate_limited"
STATE_DISCONNECTED: Final = "disconnected"
# Event names
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
# Service schema constants
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Required("question"): cv.string,
vol.Optional("system_prompt"): cv.string,
vol.Optional("model"): cv.string,
vol.Optional("temperature"): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional("max_tokens"): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional("context_messages"): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
)
})
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Required("prompt"): cv.string
})
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Optional("limit", default=10): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
),
vol.Optional("filter_model"): cv.string
})
# Configuration schema
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS),
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_API_ENDPOINT): cv.string,
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
),
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100),
),
vol.Optional(CONF_CONTEXT_MESSAGES, default=DEFAULT_CONTEXT_MESSAGES): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
)
})
}, extra=vol.ALLOW_EXTRA)
+342 -99
View File
@@ -1,134 +1,377 @@
"""The HA Text AI integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, PLATFORMS
from .coordinator import HATextAICoordinator
"""The HA Text AI coordinator."""
from __future__ import annotations
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA Text AI from a config entry."""
try:
coordinator = HATextAICoordinator(
hass,
api_key=entry.data["api_key"],
endpoint=entry.data.get("api_endpoint", "https://api.openai.com/v1"),
model=entry.data.get("model", "gpt-3.5-turbo"),
temperature=entry.data.get("temperature", 0.7),
max_tokens=entry.data.get("max_tokens", 1000),
request_interval=entry.data.get("request_interval", 1.0),
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as ex:
raise ConfigEntryNotReady(f"Failed to setup entry: {str(ex)}") from ex
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
"""Data coordinator for HA text AI."""
import asyncio
import logging
from datetime import timedelta
from typing import Any, Dict, Optional
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
import openai
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.util import dt as dt_util
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
from .const import (
DOMAIN,
STATE_READY,
STATE_PROCESSING,
STATE_ERROR,
STATE_RATE_LIMITED,
STATE_MAINTENANCE,
DEFAULT_MAX_TOKENS,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_HISTORY,
DEFAULT_CONTEXT_MESSAGES,
)
_LOGGER = logging.getLogger(__name__)
class HATextAICoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__(
self,
hass: HomeAssistant,
api_key: str,
endpoint: str,
client: Any,
model: str,
temperature: float,
max_tokens: int,
request_interval: float,
update_interval: int,
instance_name: str,
max_tokens: int = DEFAULT_MAX_TOKENS,
temperature: float = DEFAULT_TEMPERATURE,
max_history_size: int = DEFAULT_MAX_HISTORY,
context_messages: int = DEFAULT_CONTEXT_MESSAGES,
is_anthropic: bool = False,
) -> None:
"""Initialize."""
"""Initialize coordinator."""
self.instance_name = instance_name
self.hass = hass
self.client = client
self.model = model
self.temperature = temperature
self.max_tokens = max_tokens
self.max_history_size = max_history_size
self.is_anthropic = is_anthropic
# Initialize with default state
self._initial_state = {
"state": STATE_READY,
"metrics": {
"total_tokens": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
"successful_requests": 0,
"failed_requests": 0,
"total_errors": 0,
"average_latency": 0,
"max_latency": 0,
"min_latency": float('inf'),
},
"last_response": {
"timestamp": dt_util.utcnow().isoformat(),
"question": "",
"response": "",
"model": model,
"instance": instance_name,
"error": None
},
"is_processing": False,
"is_rate_limited": False,
"is_maintenance": False,
"endpoint_status": "ready",
"uptime": 0,
"system_prompt": None,
"history_size": 0,
"conversation_history": [],
}
update_interval_td = timedelta(seconds=update_interval)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=request_interval),
name=instance_name,
update_interval=update_interval_td,
)
if not api_key:
raise ValueError("API key is required")
if not isinstance(temperature, (int, float)) or not 0 <= temperature <= 2:
raise ValueError("Temperature must be between 0 and 2")
if not isinstance(max_tokens, int) or max_tokens < 1:
raise ValueError("Max tokens must be a positive integer")
# Register instance
self.hass.data.setdefault(DOMAIN, {})
self.hass.data[DOMAIN][instance_name] = self
self.context_messages = context_messages
self.api_key = api_key
self.endpoint = endpoint or "https://api.openai.com/v1"
self.model = model or "gpt-3.5-turbo"
self.temperature = float(temperature)
self.max_tokens = int(max_tokens)
self._question_queue = asyncio.Queue()
self._responses: Dict[str, Any] = {}
self.system_prompt: Optional[str] = None
self._system_prompt = None
self._conversation_history = []
self._performance_metrics = self._initial_state["metrics"].copy()
self._is_processing = False
self._is_rate_limited = False
self._is_maintenance = False
self.endpoint_status = "ready"
self.last_response = self._initial_state["last_response"].copy()
self._start_time = dt_util.utcnow()
self.client = openai.OpenAI(
api_key=self.api_key,
base_url=self.endpoint
)
_LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}")
async def _async_update_data(self) -> Dict[str, Any]:
"""Update data via OpenAI API."""
if self._question_queue.empty():
return self._responses
"""Update data via library."""
try:
question = await self._question_queue.get()
response_content = await self.hass.async_add_executor_job(
self._make_api_call, question
)
self._responses[question] = {
"question": question,
"response": response_content
current_state = self._get_current_state()
_LOGGER.debug(f"Updating data for {self.instance_name}, current state: {current_state}")
data = {
"state": current_state,
"metrics": self._performance_metrics,
"last_response": self.last_response,
"is_processing": self._is_processing,
"is_rate_limited": self._is_rate_limited,
"is_maintenance": self._is_maintenance,
"endpoint_status": self.endpoint_status,
"uptime": (dt_util.utcnow() - self._start_time).total_seconds(),
"system_prompt": self._system_prompt,
"history_size": len(self._conversation_history),
"conversation_history": self._conversation_history,
}
_LOGGER.debug("Response from API: %s", response_content)
return self._responses
except openai.AuthenticationError as err:
raise ConfigEntryAuthFailed from err
# Validate data
if not isinstance(data, dict):
raise ValueError("Invalid data format")
_LOGGER.debug(f"Updated data for {self.instance_name}: {data}")
return data
except Exception as err:
_LOGGER.error("Error communicating with API: %s", err)
return self._responses
_LOGGER.error(f"Error updating data for {self.instance_name}: {err}")
return self._initial_state
def _make_api_call(self, question: str) -> str:
"""Make API call to OpenAI."""
async def async_update_ha_state(self) -> None:
"""Update Home Assistant state."""
try:
_LOGGER.debug(f"Requesting state update for {self.instance_name}")
await self.async_request_refresh()
# Force update of all entities
for entity_id in self.hass.states.async_entity_ids():
if entity_id.startswith(f"sensor.ha_text_ai_{self.instance_name}"):
self.hass.states.async_set(entity_id, self._get_current_state())
except Exception as err:
_LOGGER.error(f"Error updating HA state for {self.instance_name}: {err}")
def _get_current_state(self) -> str:
"""Get current state based on internal flags."""
if self._is_processing:
return STATE_PROCESSING
elif self._is_rate_limited:
return STATE_RATE_LIMITED
elif self._is_maintenance:
return STATE_MAINTENANCE
elif self.last_response.get("error"):
return STATE_ERROR
return STATE_READY
def _calculate_context_tokens(self, messages: List[Dict[str, str]]) -> int:
try:
if self.is_anthropic and hasattr(self.client, 'count_tokens'):
return sum(self.client.count_tokens(msg['content']) for msg in messages)
return sum(len(msg['content']) // 4 for msg in messages)
except Exception as e:
_LOGGER.warning(f"Error calculating context tokens: {e}")
return 0
async def async_ask_question(
self,
question: str,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None,
context_messages: Optional[int] = None,
) -> dict:
"""Process a question with optional parameters."""
return await self.async_process_question(
question, model, temperature, max_tokens, system_prompt, context_messages
)
async def async_process_question(
self,
question: str,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None,
context_messages: Optional[int] = None,
) -> dict:
temp_context_messages = context_messages or self.context_messages
if not question:
raise ValueError("Question cannot be empty")
_LOGGER.debug(f"Processing question for instance {self.instance_name}")
try:
self._is_processing = True
await self.async_update_ha_state()
temp_model = model or self.model
temp_temperature = temperature or self.temperature
temp_max_tokens = max_tokens or self.max_tokens
temp_system_prompt = system_prompt or self._system_prompt
start_time = dt_util.utcnow()
messages = []
if self.system_prompt:
messages.append({"role": "system", "content": self.system_prompt})
if temp_system_prompt:
if self.is_anthropic:
system_content = f"\n\nHuman: {temp_system_prompt}\n\nAssistant: I understand and will follow these instructions."
messages.append({"role": "user", "content": system_content})
else:
messages.append({"role": "system", "content": temp_system_prompt})
# Add conversation history
context_history = self._conversation_history[-temp_context_messages:]
for entry in context_history:
messages.append({"role": "user", "content": entry["question"]})
messages.append({"role": "assistant", "content": entry["response"]})
messages.append({"role": "user", "content": question})
completion = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=self.temperature,
max_tokens=self.max_tokens,
)
return completion.choices[0].message.content
kwargs = {
"model": temp_model,
"temperature": temp_temperature,
"max_tokens": temp_max_tokens,
"messages": messages,
}
response = await self.async_process_message(question, **kwargs)
# Update metrics
end_time = dt_util.utcnow()
latency = (end_time - start_time).total_seconds()
self._update_metrics(latency, response)
# Update history
self._update_history(question, response)
return response
except Exception as err:
_LOGGER.error("Error in API call: %s", err)
self._handle_error(err)
raise HomeAssistantError(f"Failed to process question: {err}")
finally:
self._is_processing = False
await self.async_update_ha_state()
async def async_process_message(self, question: str, **kwargs) -> dict:
"""Process message using the AI client."""
try:
if self.is_anthropic:
response = await self._process_anthropic_message(question, **kwargs)
else:
response = await self._process_openai_message(question, **kwargs)
self.last_response = {
"timestamp": dt_util.utcnow().isoformat(),
"question": question,
"response": response["content"],
"model": kwargs.get("model", self.model),
"instance": self.instance_name,
"error": None
}
return response
except Exception as err:
self._handle_error(err)
raise
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
"""Process message using Anthropic API."""
response = await self.client.messages.create(
model=kwargs["model"],
max_tokens=kwargs["max_tokens"],
messages=kwargs["messages"],
temperature=kwargs["temperature"],
)
return {
"content": response.content[0].text,
"tokens": {
"prompt": response.usage.input_tokens,
"completion": response.usage.output_tokens,
"total": response.usage.input_tokens + response.usage.output_tokens
}
}
async def _process_openai_message(self, question: str, **kwargs) -> dict:
"""Process message using OpenAI API."""
try:
response = await self.client.create(
model=kwargs["model"],
messages=kwargs["messages"],
temperature=kwargs["temperature"],
max_tokens=kwargs["max_tokens"],
)
return {
"content": response["choices"][0]["message"]["content"],
"tokens": {
"prompt": response["usage"]["prompt_tokens"],
"completion": response["usage"]["completion_tokens"],
"total": response["usage"]["total_tokens"]
}
}
except Exception as e:
_LOGGER.error(f"Error in OpenAI API call: {str(e)}")
raise
def _update_metrics(self, latency: float, response: dict) -> None:
"""Update performance metrics."""
metrics = self._performance_metrics
tokens = response.get("tokens", {})
metrics["total_tokens"] += tokens.get("total", 0)
metrics["prompt_tokens"] += tokens.get("prompt", 0)
metrics["completion_tokens"] += tokens.get("completion", 0)
metrics["successful_requests"] += 1
metrics["average_latency"] = (
(metrics["average_latency"] * (metrics["successful_requests"] - 1) + latency)
/ metrics["successful_requests"]
)
metrics["max_latency"] = max(metrics["max_latency"], latency)
metrics["min_latency"] = min(metrics["min_latency"], latency)
def _update_history(self, question: str, response: dict) -> None:
"""Update conversation history."""
self._conversation_history.append({
"timestamp": dt_util.utcnow().isoformat(),
"question": question,
"response": response["content"]
})
while len(self._conversation_history) > self.max_history_size:
self._conversation_history.pop(0)
def _handle_error(self, error: Exception) -> None:
"""Handle error and update metrics."""
self._performance_metrics["total_errors"] += 1
self._performance_metrics["failed_requests"] += 1
self.last_response = {
"timestamp": dt_util.utcnow().isoformat(),
"question": "",
"response": "",
"model": self.model,
"instance": self.instance_name,
"error": str(error)
}
async def async_clear_history(self) -> None:
"""Clear conversation history."""
self._conversation_history = []
await self.async_update_ha_state()
async def async_get_history(self) -> List[Dict[str, str]]:
"""Get conversation history."""
return self._conversation_history
async def async_set_system_prompt(self, prompt: str) -> None:
"""Set system prompt."""
self._system_prompt = prompt
await self.async_update_ha_state()
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+21 -7
View File
@@ -1,14 +1,28 @@
{
"domain": "ha_text_ai",
"name": "HA Text AI",
"after_dependencies": ["http"],
"bluetooth": [],
"codeowners": ["@smkrv"],
"config_flow": true,
"documentation": "https://github.com/smkrv/ha-text-ai/wiki",
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"requirements": ["openai>=1.0.0"],
"ssdp": [],
"zeroconf": [],
"dependencies": [],
"version": "1.0.1c",
"documentation": "https://github.com/smkrv/ha-text-ai",
"integration_type": "service",
"iot_class": "cloud_polling",
"codeowners": ["@smkrv"]
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"loggers": ["custom_components.ha_text_ai"],
"mqtt": [],
"quality_scale": "silver",
"requirements": [
"openai>=1.12.0",
"anthropic>=0.8.0",
"aiohttp>=3.8.0",
"async-timeout>=4.0.0",
"certifi>=2024.2.2"
],
"single_config_entry": false,
"ssdp": [],
"usb": [],
"version": "2.0.0-alpha",
"zeroconf": []
}
+249 -49
View File
@@ -1,20 +1,65 @@
"""Sensor platform for HA text AI."""
from datetime import datetime
"""Sensor platform for HA Text AI."""
import logging
from typing import Any, Callable, Dict, Optional
import math
from typing import Any, Dict
from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
SensorDeviceClass,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from homeassistant.util import slugify
from .const import (
DOMAIN,
CONF_MODEL,
CONF_API_PROVIDER,
ATTR_TOTAL_RESPONSES,
ATTR_TOTAL_ERRORS,
ATTR_AVG_RESPONSE_TIME,
ATTR_LAST_REQUEST_TIME,
ATTR_LAST_ERROR,
ATTR_IS_PROCESSING,
ATTR_IS_RATE_LIMITED,
ATTR_IS_MAINTENANCE,
ATTR_API_VERSION,
ATTR_ENDPOINT_STATUS,
ATTR_PERFORMANCE_METRICS,
ATTR_HISTORY_SIZE,
ATTR_UPTIME,
ATTR_API_PROVIDER,
ATTR_MODEL,
ATTR_SYSTEM_PROMPT,
ATTR_API_STATUS,
ATTR_RESPONSE,
ATTR_QUESTION,
ATTR_CONVERSATION_HISTORY,
METRIC_TOTAL_TOKENS,
METRIC_PROMPT_TOKENS,
METRIC_COMPLETION_TOKENS,
METRIC_SUCCESSFUL_REQUESTS,
METRIC_FAILED_REQUESTS,
METRIC_AVERAGE_LATENCY,
METRIC_MAX_LATENCY,
METRIC_MIN_LATENCY,
STATE_READY,
STATE_PROCESSING,
STATE_ERROR,
STATE_INITIALIZING,
STATE_MAINTENANCE,
STATE_RATE_LIMITED,
STATE_DISCONNECTED,
ENTITY_ICON,
ENTITY_ICON_ERROR,
ENTITY_ICON_PROCESSING,
)
from .const import DOMAIN, ATTR_QUESTION, ATTR_RESPONSE, ATTR_LAST_UPDATED
from .coordinator import HATextAICoordinator
_LOGGER = logging.getLogger(__name__)
@@ -24,17 +69,19 @@ async def async_setup_entry(
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the HA text AI sensor."""
"""Set up the HA Text AI sensor."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([HATextAISensor(coordinator, entry)], True)
instance_name = coordinator.instance_name
_LOGGER.debug(f"Setting up sensor with instance: {instance_name}")
sensor = HATextAISensor(coordinator, entry)
async_add_entities([sensor], True)
class HATextAISensor(CoordinatorEntity, SensorEntity):
"""HA text AI Sensor."""
"""HA Text AI Sensor."""
_attr_has_entity_name = True
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_icon = "mdi:robot"
coordinator: HATextAICoordinator
def __init__(
self,
@@ -43,52 +90,205 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._config_entry = config_entry
self._instance_name = coordinator.instance_name
self._conversation_history = []
self._system_prompt = None
self._attr_name = f"HA Text AI {self._instance_name}"
self.entity_id = f"sensor.ha_text_ai_{slugify(self._instance_name)}"
self._attr_unique_id = f"{config_entry.entry_id}"
self._attr_name = "Last Response"
@property
def state(self) -> StateType:
"""Return the state of the sensor."""
if not self.coordinator.data:
return None
return self.coordinator.last_update_success_time
self.entity_description = SensorEntityDescription(
key=f"ha_text_ai_{self._instance_name}",
entity_registry_enabled_default=True,
)
@property
def extra_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return entity specific state attributes."""
if not self.coordinator.data:
return None
self._current_state = STATE_INITIALIZING
self._error_count = 0
self._last_error = None
self._last_update = None
self._is_processing = False
self._last_response = {}
self._metrics = {}
try:
model = config_entry.data.get(CONF_MODEL, "Unknown")
api_provider = config_entry.data.get(CONF_API_PROVIDER, "Unknown")
history = list(self.coordinator.data.items())
if not history:
return None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=self._attr_name, # Используем имя сенсора
manufacturer="Community",
model=f"{model} ({api_provider} provider)",
sw_version="1.0.0",
)
last_question, last_data = history[-1]
if isinstance(last_data, dict):
last_response = last_data.get("response", "")
else:
last_response = str(last_data)
return {
ATTR_QUESTION: last_question,
ATTR_RESPONSE: last_response,
ATTR_LAST_UPDATED: self.coordinator.last_update_success_time,
}
except (IndexError, KeyError, AttributeError) as err:
_LOGGER.warning("Error getting attributes: %s", err)
return None
_LOGGER.debug(f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}")
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success
return (
self.coordinator.last_update_success
and self.coordinator.data is not None
and self._current_state != STATE_DISCONNECTED
)
def _sanitize_value(self, value: Any) -> Any:
"""Sanitize values for JSON serialization."""
if isinstance(value, float):
if math.isinf(value) or math.isnan(value):
return None
return value
def _sanitize_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
"""Sanitize all attributes for JSON serialization."""
return {
key: self._sanitize_value(value)
for key, value in attributes.items()
if value is not None
}
@property
def should_poll(self) -> bool:
"""No need to poll. Coordinator notifies entity of updates."""
return False
def native_value(self) -> StateType:
"""Return the native value of the sensor."""
if not self.coordinator.last_update_success or not self.coordinator.data:
self._current_state = STATE_DISCONNECTED
return self._current_state
status = self.coordinator.data.get("state", STATE_READY)
self._current_state = status
return status
@property
def icon(self) -> str:
"""Return the icon based on the current state."""
if self._current_state == STATE_ERROR:
return ENTITY_ICON_ERROR
elif self._current_state == STATE_PROCESSING:
return ENTITY_ICON_PROCESSING
return ENTITY_ICON
@property
def extra_state_attributes(self) -> Dict[str, Any]:
"""Return entity specific state attributes."""
if not self.coordinator.data:
return {}
try:
data = self.coordinator.data
attributes = {
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
ATTR_API_PROVIDER: self._config_entry.data.get(CONF_API_PROVIDER, "Unknown"),
ATTR_API_STATUS: self._current_state,
ATTR_TOTAL_ERRORS: self._error_count,
ATTR_LAST_ERROR: self._last_error,
"instance_name": self._instance_name,
ATTR_SYSTEM_PROMPT: data.get("system_prompt"),
ATTR_IS_PROCESSING: data.get("is_processing", False),
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
ATTR_IS_MAINTENANCE: data.get("is_maintenance", False),
ATTR_ENDPOINT_STATUS: data.get("endpoint_status", "unknown"),
ATTR_UPTIME: data.get("uptime", 0),
ATTR_HISTORY_SIZE: data.get("history_size", 0),
ATTR_CONVERSATION_HISTORY: data.get("conversation_history", []),
}
# Add metrics
metrics = data.get("metrics", {})
if isinstance(metrics, dict):
self._metrics = metrics
attributes.update({
METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0),
METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0),
METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0),
METRIC_SUCCESSFUL_REQUESTS: metrics.get("successful_requests", 0),
METRIC_FAILED_REQUESTS: metrics.get("failed_requests", 0),
METRIC_AVERAGE_LATENCY: metrics.get("average_latency", 0),
METRIC_MAX_LATENCY: metrics.get("max_latency", 0),
METRIC_MIN_LATENCY: metrics.get("min_latency", float("inf")),
})
# Add last response
last_response = data.get("last_response", {})
if isinstance(last_response, dict):
self._last_response = last_response
attributes.update({
ATTR_RESPONSE: last_response.get("response", ""),
ATTR_QUESTION: last_response.get("question", ""),
"last_model": last_response.get("model", ""),
"last_timestamp": last_response.get("timestamp", ""),
"last_error": last_response.get("error"),
})
# Add performance metrics if available
if ATTR_PERFORMANCE_METRICS in data:
attributes[ATTR_PERFORMANCE_METRICS] = data[ATTR_PERFORMANCE_METRICS]
# Add API version if available
if ATTR_API_VERSION in data:
attributes[ATTR_API_VERSION] = data[ATTR_API_VERSION]
return self._sanitize_attributes(attributes)
except Exception as err:
_LOGGER.error("Error preparing attributes: %s", err, exc_info=True)
return {}
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()
_LOGGER.debug(f"Entity {self.entity_id} added to Home Assistant")
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
try:
data = self.coordinator.data
if not self.coordinator.last_update_success or not data:
self._current_state = STATE_DISCONNECTED
_LOGGER.warning(f"No data available for {self.entity_id}")
self.async_write_ha_state()
return
self._is_processing = data.get("is_processing", False)
# Update conversation history and system prompt
self._conversation_history = data.get("conversation_history", [])
self._system_prompt = data.get("system_prompt")
# Update state based on conditions
if self._is_processing:
self._current_state = STATE_PROCESSING
elif data.get("is_rate_limited"):
self._current_state = STATE_RATE_LIMITED
elif data.get("is_maintenance"):
self._current_state = STATE_MAINTENANCE
elif data.get("error"):
self._current_state = STATE_ERROR
self._last_error = data["error"]
self._error_count += 1
else:
self._current_state = data.get("state", STATE_READY)
# Update last update timestamp
self._last_update = dt_util.utcnow()
_LOGGER.debug(
f"Updated {self.entity_id} state to: {self._current_state} "
f"(available: {self.available})"
)
except Exception as err:
self._current_state = STATE_ERROR
self._last_error = str(err)
self._error_count += 1
_LOGGER.error(
"Error handling update for %s: %s",
self.entity_id,
err,
exc_info=True
)
self.async_write_ha_state()
+38 -66
View File
@@ -1,42 +1,58 @@
ask_question:
name: Ask Question
description: Send a question to the AI model and receive a detailed response
description: >-
Send a question to the AI model and receive a detailed response.
The response will be stored in the conversation history and can be retrieved later.
fields:
instance:
name: Instance
description: Name of the HA Text AI instance to use
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
question:
name: Question
description: Your question or prompt for the AI assistant
required: true
example: "What automations would you recommend for a smart kitchen?"
selector:
text:
multiline: true
type: text
system_prompt:
name: System Prompt
description: Optional system prompt to set context for this specific question
required: false
selector:
text:
multiline: true
context_messages:
name: Context Messages
description: Number of previous messages to include in context (1-20)
required: false
default: 5
selector:
number:
min: 1
max: 20
step: 1
mode: box
model:
name: Model
description: Select an AI model to use (optional, overrides default setting)
description: "Select AI model to use (optional, overrides default setting)"
required: false
example: "gpt-3.5-turbo"
default: "gpt-3.5-turbo"
selector:
select:
options:
- label: "GPT-3.5 Turbo"
value: "gpt-3.5-turbo"
icon: "mdi:rocket-launch"
- label: "GPT-4"
value: "gpt-4"
icon: "mdi:brain"
- label: "GPT-4 32K"
value: "gpt-4-32k"
icon: "mdi:brain-circuit"
selector:
text:
multiline: false
temperature:
name: Temperature
description: >-
Controls response creativity (0-2):
Lower values (0-0.7) for focused, consistent responses
Higher values (0.7-2.0) for more creative, varied responses
description: Controls response creativity (0.0-2.0)
required: false
default: 0.7
selector:
@@ -45,11 +61,10 @@ ask_question:
max: 2.0
step: 0.1
mode: slider
unit_of_measurement: ""
max_tokens:
name: Max Tokens
description: Maximum length of the response (longer responses use more tokens)
description: Maximum length of the response (1-4096 tokens)
required: false
default: 1000
selector:
@@ -58,46 +73,3 @@ ask_question:
max: 4096
step: 1
mode: box
clear_history:
name: Clear History
description: Delete all stored questions and responses from the conversation history
fields: {}
get_history:
name: Get History
description: Retrieve recent conversation history between you and the AI
fields:
limit:
name: Limit
description: Number of most recent conversations to return
required: false
default: 10
selector:
number:
min: 1
max: 100
step: 1
mode: box
set_system_prompt:
name: Set System Prompt
description: >-
Configure the AI's behavior by setting a system prompt that will be used
for all future conversations until changed
fields:
prompt:
name: System Prompt
description: >-
Instructions that define how the AI should behave and respond.
This affects all future conversations.
required: true
example: >-
You are a home automation expert assistant. Provide practical advice
focused on smart home technology and automation. Use clear, concise
language and include specific product recommendations when relevant.
selector:
text:
multiline: true
type: text
rows: 4
@@ -0,0 +1,261 @@
{
"config": {
"step": {
"provider": {
"title": "KI-Anbieter auswählen",
"description": "Wählen Sie den KI-Dienst-Anbieter für diese Instanz",
"data": {
"api_provider": "API-Anbieter",
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
}
},
"user": {
"title": "HA Text AI Instanz konfigurieren",
"description": "Richten Sie eine neue KI-Assistenten-Instanz mit Ihrem ausgewählten Anbieter ein",
"data": {
"name": "Instanzname (z.B. 'GPT Assistent', 'Claude Helfer')",
"api_key": "API-Schlüssel für Authentifizierung",
"model": "Zu verwendendes KI-Modell",
"temperature": "Antwort-Kreativität (0-2, niedriger = fokussierter)",
"max_tokens": "Maximale Antwortlänge (1-4096 Token)",
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
}
}
},
"error": {
"name_exists": "Eine Instanz mit diesem Namen existiert bereits",
"invalid_name": "Ungültiger Instanzname",
"invalid_auth": "Authentifizierung fehlgeschlagen - überprüfen Sie Ihren API-Schlüssel",
"invalid_api_key": "Ungültiger API-Schlüssel - bitte überprüfen Sie Ihre Anmeldedaten",
"cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen",
"invalid_model": "Ausgewähltes Modell ist nicht verfügbar",
"rate_limit": "Anfragelimit überschritten",
"context_length": "Kontextlänge überschritten",
"rate_limit_exceeded": "API-Anfragelimit überschritten",
"maintenance": "Dienst ist in Wartung",
"invalid_response": "Ungültige API-Antwort erhalten",
"api_error": "API-Dienst-Fehler aufgetreten",
"timeout": "Anfrage-Zeitüberschreitung",
"invalid_instance": "Ungültige Instanz angegeben",
"unknown": "Unerwarteter Fehler aufgetreten"
}
},
"options": {
"step": {
"init": {
"title": "Instanzeinstellungen aktualisieren",
"description": "Einstellungen für diese KI-Assistenten-Instanz ändern",
"data": {
"model": "KI-Modell",
"temperature": "Antwort-Kreativität (0-2)",
"max_tokens": "Maximale Antwortlänge (1-4096)",
"request_interval": "Minimales Anfragen-Intervall (0,1-60 Sekunden)",
"context_messages": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
}
}
}
},
"services": {
"ask_question": {
"name": "Frage stellen (HA Text AI)",
"description": "Senden Sie eine Frage an das KI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird in der Gesprächshistorie gespeichert und kann später abgerufen werden.",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der zu verwendenden HA Text AI Instanz"
},
"question": {
"name": "Frage",
"description": "Ihre Frage oder Eingabeaufforderung für den KI-Assistenten"
},
"context_messages": {
"name": "Kontextnachrichten",
"description": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
},
"system_prompt": {
"name": "Systemaufforderung",
"description": "Optionale Systemaufforderung zur Kontexteinstellung für diese spezifische Frage"
},
"model": {
"name": "Modell",
"description": "Wählen Sie das zu verwendende KI-Modell (optional, überschreibt Standardeinstellung)"
},
"temperature": {
"name": "Temperatur",
"description": "Steuert die Antwort-Kreativität (0,0-2,0)"
},
"max_tokens": {
"name": "Max. Token",
"description": "Maximale Länge der Antwort (1-4096 Token)"
}
}
},
"clear_history": {
"name": "Verlauf löschen",
"description": "Alle gespeicherten Fragen und Antworten aus dem Gesprächsverlauf löschen",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI Instanz, für die der Verlauf gelöscht werden soll"
}
}
},
"get_history": {
"name": "Verlauf abrufen",
"description": "Gesprächsverlauf mit optionaler Filterung und Sortierung abrufen",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI Instanz, aus der der Verlauf abgerufen werden soll"
},
"limit": {
"name": "Limit",
"description": "Anzahl der zurückzugebenden Gespräche (1-100)"
},
"filter_model": {
"name": "Modell filtern",
"description": "Gespräche nach bestimmtem KI-Modell filtern"
},
"start_date": {
"name": "Startdatum",
"description": "Gespräche ab diesem Datum/Zeitpunkt filtern"
},
"include_metadata": {
"name": "Metadaten einbeziehen",
"description": "Zusätzliche Informationen wie verwendete Token, Antwortzeit usw. einbeziehen"
},
"sort_order": {
"name": "Sortierreihenfolge",
"description": "Sortierreihenfolge der Ergebnisse (neueste oder älteste zuerst)"
}
}
},
"set_system_prompt": {
"name": "Systemaufforderung festlegen",
"description": "Standardmäßige Systemverhaltensinstruktionen für alle zukünftigen Gespräche festlegen",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI Instanz, für die die Systemaufforderung festgelegt werden soll"
},
"prompt": {
"name": "Systemaufforderung",
"description": "Anweisungen, die definieren, wie sich die KI verhalten und antworten soll"
}
}
}
},
"entity": {
"sensor": {
"ha_text_ai": {
"name": "{name}",
"state": {
"ready": "Bereit",
"processing": "Verarbeitung",
"error": "Fehler",
"disconnected": "Getrennt",
"rate_limited": "Anfragelimit",
"maintenance": "Wartung",
"initializing": "Initialisierung",
"retrying": "Wiederholung",
"queued": "In Warteschlange"
},
"state_attributes": {
"question": {
"name": "Letzte Frage"
},
"response": {
"name": "Letzte Antwort"
},
"model": {
"name": "Aktuelles Modell"
},
"temperature": {
"name": "Temperatur"
},
"max_tokens": {
"name": "Max. Token"
},
"system_prompt": {
"name": "Systemaufforderung"
},
"response_time": {
"name": "Letzte Antwortzeit"
},
"total_responses": {
"name": "Gesamte Antworten"
},
"error_count": {
"name": "Fehleranzahl"
},
"last_error": {
"name": "Letzter Fehler"
},
"api_status": {
"name": "API-Status"
},
"tokens_used": {
"name": "Insgesamt verwendete Token"
},
"average_response_time": {
"name": "Durchschnittliche Antwortzeit"
},
"last_request_time": {
"name": "Letzte Anforderungszeit"
},
"is_processing": {
"name": "Verarbeitungsstatus"
},
"is_rate_limited": {
"name": "Status Anfragelimit"
},
"is_maintenance": {
"name": "Wartungsstatus"
},
"api_version": {
"name": "API-Version"
},
"endpoint_status": {
"name": "Endpunktstatus"
},
"performance_metrics": {
"name": "Leistungsmetriken"
},
"history_size": {
"name": "Verlaufsgröße"
},
"uptime": {
"name": "Betriebszeit"
},
"total_tokens": {
"name": "Gesamte Token"
},
"prompt_tokens": {
"name": "Prompt-Token"
},
"completion_tokens": {
"name": "Abschluss-Token"
},
"successful_requests": {
"name": "Erfolgreiche Anfragen"
},
"failed_requests": {
"name": "Fehlgeschlagene Anfragen"
},
"average_latency": {
"name": "Durchschnittliche Latenz"
},
"max_latency": {
"name": "Maximale Latenz"
},
"min_latency": {
"name": "Minimale Latenz"
}
}
}
}
}
}
+252 -72
View File
@@ -1,80 +1,260 @@
{
"config": {
"step": {
"user": {
"title": "Set up HA text AI",
"description": "Configure your OpenAI integration for smart home interactions",
"data": {
"api_key": {
"name": "API Key",
"description": "Your OpenAI API key (starts with 'sk-')"
},
"model": {
"name": "AI Model",
"description": "Select the AI model to use (e.g., gpt-3.5-turbo)"
},
"temperature": {
"name": "Temperature",
"description": "Response creativity (0-2): lower for focused, higher for creative responses"
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum response length (1-4096 tokens)"
},
"api_endpoint": {
"name": "API Endpoint",
"description": "OpenAI API endpoint URL (leave default if unsure)"
},
"request_interval": {
"name": "Request Interval",
"description": "Minimum time between API requests in seconds (0.1 or higher)"
}
"config": {
"step": {
"provider": {
"title": "Select AI Provider",
"description": "Choose which AI service provider to use for this instance",
"data": {
"api_provider": "API Provider",
"context_messages": "Number of context messages to retain (1-20)"
}
},
"user": {
"title": "Configure HA Text AI Instance",
"description": "Set up a new AI assistant instance with your selected provider",
"data": {
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
"api_key": "API key for authentication",
"model": "AI model to use",
"temperature": "Response creativity (0-2, lower = more focused)",
"max_tokens": "Maximum response length (1-4096 tokens)",
"api_endpoint": "Custom API endpoint URL (optional)",
"request_interval": "Minimum time between requests (0.1-60 seconds)",
"context_messages": "Number of context messages to retain (1-20)"
}
}
},
"error": {
"name_exists": "An instance with this name already exists",
"invalid_name": "Invalid instance name",
"invalid_auth": "Authentication failed - check your API key",
"invalid_api_key": "Invalid API key - please verify your credentials",
"cannot_connect": "Failed to connect to API service",
"invalid_model": "Selected model is not available",
"rate_limit": "Rate limit exceeded",
"context_length": "Context length exceeded",
"rate_limit_exceeded": "API rate limit exceeded",
"maintenance": "Service is under maintenance",
"invalid_response": "Invalid API response received",
"api_error": "API service error occurred",
"timeout": "Request timed out",
"invalid_instance": "Invalid instance specified",
"unknown": "Unexpected error occurred"
}
}
},
"error": {
"invalid_auth": "Invalid API key. Please check your OpenAI API key and try again.",
"cannot_connect": "Failed to connect to API. Please check your internet connection and API endpoint.",
"unknown": "Unexpected error occurred. Please check the logs for more details.",
"already_exists": "This API key is already configured in another integration."
"options": {
"step": {
"init": {
"title": "Update Instance Settings",
"description": "Modify settings for this AI assistant instance",
"data": {
"model": "AI model",
"temperature": "Response creativity (0-2)",
"max_tokens": "Maximum response length (1-4096)",
"request_interval": "Minimum request interval (0.1-60 seconds)",
"context_messages": "Number of previous messages to include in context (1-20)"
}
}
}
},
"abort": {
"already_configured": "This OpenAI integration is already configured",
"auth_failed": "Authentication failed. Please verify your API key."
}
},
"options": {
"step": {
"init": {
"title": "HA text AI Options",
"description": "Adjust your OpenAI integration settings",
"data": {
"temperature": {
"name": "Temperature",
"description": "Controls response creativity (0-2): lower values for focused responses, higher for more creative ones"
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum length of responses (1-4096 tokens)"
},
"request_interval": {
"name": "Request Interval",
"description": "Minimum time between API requests in seconds (0.1 or higher)"
}
"services": {
"ask_question": {
"name": "Ask Question (HA Text AI)",
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.",
"fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to use"
},
"question": {
"name": "Question",
"description": "Your question or prompt for the AI assistant"
},
"context_messages": {
"name": "Context Messages",
"description": "Number of previous messages to include in context (1-20)"
},
"system_prompt": {
"name": "System Prompt",
"description": "Optional system prompt to set context for this specific question"
},
"model": {
"name": "Model",
"description": "Select AI model to use (optional, overrides default setting)"
},
"temperature": {
"name": "Temperature",
"description": "Controls response creativity (0.0-2.0)"
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum length of the response (1-4096 tokens)"
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Delete all stored questions and responses from the conversation history",
"fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to clear history for"
}
}
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history with optional filtering and sorting",
"fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to get history from"
},
"limit": {
"name": "Limit",
"description": "Number of conversations to return (1-100)"
},
"filter_model": {
"name": "Filter Model",
"description": "Filter conversations by specific AI model"
},
"start_date": {
"name": "Start Date",
"description": "Filter conversations starting from this date/time"
},
"include_metadata": {
"name": "Include Metadata",
"description": "Include additional information like tokens used, response time, etc."
},
"sort_order": {
"name": "Sort Order",
"description": "Sort order for results (newest or oldest first)"
}
}
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Set default system behavior instructions for all future conversations",
"fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to set system prompt for"
},
"prompt": {
"name": "System Prompt",
"description": "Instructions that define how the AI should behave and respond"
}
}
}
}
}
},
"entity": {
"sensor": {
"last_response": {
"name": "Last Response",
"state_attributes": {
"question": "Last Question",
"response": "AI Response",
"last_updated": "Last Updated"
},
"entity": {
"sensor": {
"ha_text_ai": {
"name": "{name}",
"state": {
"ready": "Ready",
"processing": "Processing",
"error": "Error",
"disconnected": "Disconnected",
"rate_limited": "Rate Limited",
"maintenance": "Maintenance",
"initializing": "Initializing",
"retrying": "Retrying",
"queued": "Queued"
},
"state_attributes": {
"question": {
"name": "Last Question"
},
"response": {
"name": "Last Response"
},
"model": {
"name": "Current Model"
},
"temperature": {
"name": "Temperature"
},
"max_tokens": {
"name": "Max Tokens"
},
"system_prompt": {
"name": "System Prompt"
},
"response_time": {
"name": "Last Response Time"
},
"total_responses": {
"name": "Total Responses"
},
"error_count": {
"name": "Error Count"
},
"last_error": {
"name": "Last Error"
},
"api_status": {
"name": "API Status"
},
"tokens_used": {
"name": "Total Tokens Used"
},
"average_response_time": {
"name": "Average Response Time"
},
"last_request_time": {
"name": "Last Request Time"
},
"is_processing": {
"name": "Processing Status"
},
"is_rate_limited": {
"name": "Rate Limited Status"
},
"is_maintenance": {
"name": "Maintenance Status"
},
"api_version": {
"name": "API Version"
},
"endpoint_status": {
"name": "Endpoint Status"
},
"performance_metrics": {
"name": "Performance Metrics"
},
"history_size": {
"name": "History Size"
},
"uptime": {
"name": "Uptime"
},
"total_tokens": {
"name": "Total Tokens"
},
"prompt_tokens": {
"name": "Prompt Tokens"
},
"completion_tokens": {
"name": "Completion Tokens"
},
"successful_requests": {
"name": "Successful Requests"
},
"failed_requests": {
"name": "Failed Requests"
},
"average_latency": {
"name": "Average Latency"
},
"max_latency": {
"name": "Maximum Latency"
},
"min_latency": {
"name": "Minimum Latency"
}
}
}
}
}
}
}
}
@@ -0,0 +1,260 @@
{
"config": {
"step": {
"provider": {
"title": "Выбор провайдера ИИ",
"description": "Выберите сервис ИИ для этого экземпляра",
"data": {
"api_provider": "Провайдер API",
"context_messages": "Количество сообщений в контексте (1-20)"
}
},
"user": {
"title": "Настройка экземпляра HA Text AI",
"description": "Настройте нового помощника ИИ с выбранным провайдером",
"data": {
"name": "Название экземпляра (например, 'GPT Помощник', 'Claude Ассистент')",
"api_key": "API-ключ для аутентификации",
"model": "Модель ИИ для использования",
"temperature": "Креативность ответов (0-2, меньше = более сфокусированно)",
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
"api_endpoint": "Пользовательский URL-адрес API (необязательно)",
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)"
}
}
},
"error": {
"name_exists": "Экземпляр с таким именем уже существует",
"invalid_name": "Некорректное имя экземпляра",
"invalid_auth": "Ошибка аутентификации - проверьте API-ключ",
"invalid_api_key": "Неверный API-ключ - проверьте учетные данные",
"cannot_connect": "Не удалось подключиться к сервису API",
"invalid_model": "Выбранная модель недоступна",
"rate_limit": "Превышен лимит запросов",
"context_length": "Превышена длина контекста",
"rate_limit_exceeded": "Превышен лимит API",
"maintenance": "Сервис на техническом обслуживании",
"invalid_response": "Получен некорректный ответ API",
"api_error": "Произошла ошибка сервиса API",
"timeout": "Время ожидания истекло",
"invalid_instance": "Указан неверный экземпляр",
"unknown": "Произошла непредвиденная ошибка"
}
},
"options": {
"step": {
"init": {
"title": "Обновление настроек экземпляра",
"description": "Измените настройки для этого помощника ИИ",
"data": {
"model": "Модель ИИ",
"temperature": "Креативность ответов (0-2)",
"max_tokens": "Максимальная длина ответа (1-4096)",
"request_interval": "Минимальный интервал запросов (0.1-60 секунд)",
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)"
}
}
}
},
"services": {
"ask_question": {
"name": "Задать вопрос (HA Text AI)",
"description": "Отправьте вопрос модели ИИ и получите подробный ответ. Ответ будет сохранен в истории беседы и может быть извлечен позже.",
"fields": {
"instance": {
"name": "Экземпляр",
"description": "Название экземпляра HA Text AI для использования"
},
"question": {
"name": "Вопрос",
"description": "Ваш вопрос или запрос помощнику ИИ"
},
"context_messages": {
"name": "Контекстные сообщения",
"description": "Количество предыдущих сообщений для включения в контекст (1-20)"
},
"system_prompt": {
"name": "Системный промпт",
"description": "Необязательный системный промпт для установки контекста для этого конкретного вопроса"
},
"model": {
"name": "Модель",
"description": "Выберите модель ИИ (необязательно, переопределяет настройки по умолчанию)"
},
"temperature": {
"name": "Температура",
"description": "Управляет креативностью ответа (0.0-2.0)"
},
"max_tokens": {
"name": "Максимальное количество токенов",
"description": "Максимальная длина ответа (1-4096 токенов)"
}
}
},
"clear_history": {
"name": "Очистить историю",
"description": "Удалить все сохраненные вопросы и ответы из истории беседы",
"fields": {
"instance": {
"name": "Экземпляр",
"description": "Название экземпляра HA Text AI для очистки истории"
}
}
},
"get_history": {
"name": "Получить историю",
"description": "Получить историю беседы с дополнительной фильтрацией и сортировкой",
"fields": {
"instance": {
"name": "Экземпляр",
"description": "Название экземпляра HA Text AI для получения истории"
},
"limit": {
"name": "Лимит",
"description": "Количество возвращаемых бесед (1-100)"
},
"filter_model": {
"name": "Фильтр модели",
"description": "Фильтрация бесед по конкретной модели ИИ"
},
"start_date": {
"name": "Начальная дата",
"description": "Фильтрация бесед, начиная с указанной даты/времени"
},
"include_metadata": {
"name": "Включить метаданные",
"description": "Включить дополнительную информацию, например, использованные токены, время ответа и т.д."
},
"sort_order": {
"name": "Порядок сортировки",
"description": "Порядок результатов (сначала новые или старые)"
}
}
},
"set_system_prompt": {
"name": "Установить системный промпт",
"description": "Установить инструкции по умолчанию для поведения ИИ во всех будущих разговорах",
"fields": {
"instance": {
"name": "Экземпляр",
"description": "Название экземпляра HA Text AI для установки системного промпта"
},
"prompt": {
"name": "Системный промпт",
"description": "Инструкции, определяющие, как ИИ должен вести себя и отвечать"
}
}
}
},
"entity": {
"sensor": {
"ha_text_ai": {
"name": "{name}",
"state": {
"ready": "Готов",
"processing": "Обработка",
"error": "Ошибка",
"disconnected": "Отключен",
"rate_limited": "Лимит запросов",
"maintenance": "Техническое обслуживание",
"initializing": "Инициализация",
"retrying": "Повторная попытка",
"queued": "В очереди"
},
"state_attributes": {
"question": {
"name": "Последний вопрос"
},
"response": {
"name": "Последний ответ"
},
"model": {
"name": "Текущая модель"
},
"temperature": {
"name": "Температура"
},
"max_tokens": {
"name": "Максимальное количество токенов"
},
"system_prompt": {
"name": "Системный промпт"
},
"response_time": {
"name": "Время последнего ответа"
},
"total_responses": {
"name": "Всего ответов"
},
"error_count": {
"name": "Количество ошибок"
},
"last_error": {
"name": "Последняя ошибка"
},
"api_status": {
"name": "Статус API"
},
"tokens_used": {
"name": "Всего использовано токенов"
},
"average_response_time": {
"name": "Среднее время ответа"
},
"last_request_time": {
"name": "Время последнего запроса"
},
"is_processing": {
"name": "Статус обработки"
},
"is_rate_limited": {
"name": "Статус лимита запросов"
},
"is_maintenance": {
"name": "Статус обслуживания"
},
"api_version": {
"name": "Версия API"
},
"endpoint_status": {
"name": "Статус эндпоинта"
},
"performance_metrics": {
"name": "Показатели производительности"
},
"history_size": {
"name": "Размер истории"
},
"uptime": {
"name": "Время работы"
},
"total_tokens": {
"name": "Всего токенов"
},
"prompt_tokens": {
"name": "Токены промпта"
},
"completion_tokens": {
"name": "Токены завершения"
},
"successful_requests": {
"name": "Успешные запросы"
},
"failed_requests": {
"name": "Неудачные запросы"
},
"average_latency": {
"name": "Средняя задержка"
},
"max_latency": {
"name": "Максимальная задержка"
},
"min_latency": {
"name": "Минимальная задержка"
}
}
}
}
}
}
BIN
View File
Binary file not shown.
+1 -5
View File
@@ -1,9 +1,5 @@
{
"name": "HA text AI",
"render_readme": true,
"domains": ["sensor"],
"homeassistant": "2024.11.0",
"icon": "mdi:brain",
"version": "1.0.1b",
"documentation": "https://github.com/smkrv/ha-text-ai"
"homeassistant": "2024.11.0"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

-3
View File
@@ -1,3 +0,0 @@
pytest
pytest-asyncio
homeassistant
+25
View File
@@ -0,0 +1,25 @@
```
ha-text-ai/
├── custom_components/
├── ha_text_ai/
│ ├── __init__.py
│ ├── config_flow.py
│ ├── coordinator.py
│ ├── manifest.json
│ ├── sensor.py
│ ├── services.yaml
│ ├── const.py
│ └── api_client.py
├── translations/
│ ├── en.json
│ ├── de.json
│ └── ru.json
└── icons/
├── icon.png
├── icon@2x.png
├── logo.png
└── logo@2x.png
```