mirror of
https://github.com/home-assistant/core.git
synced 2026-05-07 10:26:51 +02:00
Compare commits
1127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 813fa922e2 | |||
| 4b28928702 | |||
| 859ce55c96 | |||
| 9a9f19cb9e | |||
| d8b1bfb268 | |||
| f5363db26f | |||
| 3be1aa5441 | |||
| 7dbffb7375 | |||
| 07c4025d47 | |||
| 3e3e425aa5 | |||
| 162a4fc385 | |||
| ef6fd92079 | |||
| 4ad71a070a | |||
| f33ad12f5e | |||
| da7fbb0dd6 | |||
| 81137345a3 | |||
| d3e77d4195 | |||
| ce977e90a5 | |||
| 2871b87344 | |||
| d82ce1e22d | |||
| b8bb2e0090 | |||
| 1b81cfe3ca | |||
| 0a3f0d90c3 | |||
| 84d566a02c | |||
| 0e0d54e4b6 | |||
| 5b05061def | |||
| e0bf76769a | |||
| 63868bc169 | |||
| b8b7169371 | |||
| 1cc778954f | |||
| 3ba3ecdef3 | |||
| 5c57fc6e14 | |||
| 2da440043a | |||
| 4f34725e53 | |||
| d03bec2f44 | |||
| 57c37fc10c | |||
| fd98594143 | |||
| 894547abed | |||
| b48060674c | |||
| 6f2aa7852a | |||
| 9d53645468 | |||
| a3f1c067f7 | |||
| cef97973d0 | |||
| 7bb297a3fc | |||
| 7e2b8e1a48 | |||
| 013c5e7f7c | |||
| 7cb1d5b8ab | |||
| 57d9e8ea6f | |||
| 32743fcf8d | |||
| f4637db26d | |||
| b4bfe6b80b | |||
| 278f25ec6e | |||
| 39d3bc3e53 | |||
| bb41a2df9f | |||
| 284242b90e | |||
| a95c216983 | |||
| d41a3ae0cd | |||
| 0dfbe3ef84 | |||
| 71fc725d75 | |||
| d41c9aee52 | |||
| 8091f511b8 | |||
| a7baedc22b | |||
| 05bfb3a52e | |||
| 2a5b95ba4d | |||
| 3dd972cc7a | |||
| acd9dd218a | |||
| 6552cf8f7a | |||
| e4e4785225 | |||
| d531ce8d1d | |||
| 0224928655 | |||
| 05121b89c6 | |||
| 326895f0a1 | |||
| 45121eddf1 | |||
| 5e4f8f8bff | |||
| b9bbe36af0 | |||
| b56cdb9106 | |||
| e975496145 | |||
| cdeb550b87 | |||
| 62082bdf14 | |||
| 891efeb9cb | |||
| dc8abff6b9 | |||
| aa7474839b | |||
| 06a96712f6 | |||
| 97be8f485a | |||
| a9c23ff445 | |||
| cd92cb1258 | |||
| c3f01b3a23 | |||
| 4b232be04a | |||
| cd5e21d3ac | |||
| 84d5085f3b | |||
| 44e94a82f1 | |||
| fe0da5c34f | |||
| c0200084ec | |||
| ef63ab5def | |||
| 3683607820 | |||
| 4c70fef2da | |||
| d956af095e | |||
| ea34fe4107 | |||
| e1c81c9b9e | |||
| 4ea0e6b240 | |||
| 0ae5a19602 | |||
| 80c7e47c42 | |||
| dfe4085189 | |||
| 65a12b48e7 | |||
| cd639b829c | |||
| ea5b633574 | |||
| 2f2413c979 | |||
| 799bcb0f88 | |||
| d3cf5d9aab | |||
| d2fddf129d | |||
| d19c2506bf | |||
| 8fd3d0bb44 | |||
| d62f136c58 | |||
| 86e8b9df9b | |||
| aa5e942528 | |||
| 6636e67af6 | |||
| 30f310fc24 | |||
| de4e1c444e | |||
| eaf72106f8 | |||
| b90a074fb4 | |||
| 3aea7f0695 | |||
| ba8b1b2daf | |||
| 5ff1c15df3 | |||
| 0280d921e5 | |||
| 955e8362e4 | |||
| 1fc0b620c0 | |||
| ab08153d62 | |||
| b47b7fa58c | |||
| c50676dee9 | |||
| 96bd991bb8 | |||
| 7e2a7b9393 | |||
| eb2217cfa6 | |||
| b2269b3dba | |||
| eb85d7cd98 | |||
| 6663717d59 | |||
| 33e5a96a57 | |||
| 7cb4d5ca9c | |||
| 7dacd0080b | |||
| 7f44fe031c | |||
| 9656aaa6bd | |||
| bf4b865e83 | |||
| 73dcc2f5a8 | |||
| fa0cf37e2c | |||
| fa6c6ee4fc | |||
| 4eb000d863 | |||
| d3809dd4cb | |||
| 2f3a6243f7 | |||
| f36799d139 | |||
| 8d5f83e5f1 | |||
| 308cb686d2 | |||
| 63d4f4d03d | |||
| d8a4b36381 | |||
| c048af2e4e | |||
| 1a25864890 | |||
| d1922189aa | |||
| 7594ead857 | |||
| 8ce14877a4 | |||
| 2fb0de3cdb | |||
| 3e77a4bfb2 | |||
| 4b9dd68fe7 | |||
| f21ed9054b | |||
| 53e4d6c8fc | |||
| 6a67c0faf7 | |||
| d590f4f0b5 | |||
| 45a6134209 | |||
| 6893d2b13d | |||
| 370babf542 | |||
| 6c89ecb98b | |||
| cc1eaa72a6 | |||
| 21a3c5b0ed | |||
| 080eb6af84 | |||
| 663538c492 | |||
| d119bbe4ef | |||
| 0eb204508c | |||
| ad836b48b0 | |||
| 9cc9f240e7 | |||
| 91d5c080de | |||
| 9390bf3414 | |||
| a5b65766db | |||
| 9321ff504c | |||
| 8a22e84db0 | |||
| 642206699d | |||
| 5d98f467fb | |||
| 54727a6f20 | |||
| ed371bc644 | |||
| 3cc6cc9519 | |||
| 2053e61a80 | |||
| 3673a80a37 | |||
| 0cc531e333 | |||
| 12280dbe63 | |||
| 84a5ba26d3 | |||
| 6ec4466ad7 | |||
| cb62562f5b | |||
| a381a3a741 | |||
| 6902504087 | |||
| 64f2fa42fc | |||
| 64c9a76fc8 | |||
| e9fc6b3e74 | |||
| 605eea6274 | |||
| 86af61d7b5 | |||
| 45978f41cd | |||
| 4d4e45854f | |||
| 43fa4f2646 | |||
| 5cedb0b726 | |||
| e3de695b99 | |||
| e684490219 | |||
| 70947c612c | |||
| 07c144841f | |||
| 392c46c028 | |||
| 5af3b361c8 | |||
| 040c960ced | |||
| f2787115d0 | |||
| 2dd1632fc3 | |||
| ed1aefc643 | |||
| f1bbe4204b | |||
| 929379799c | |||
| d20d1df382 | |||
| b5a1b592e9 | |||
| 6384e6b38d | |||
| cd98577eb7 | |||
| fa9a336725 | |||
| aa0199b442 | |||
| a506be4be0 | |||
| e78a79a29e | |||
| eed4acc745 | |||
| f1fcca2c75 | |||
| 8673694f6f | |||
| e8e9914ef5 | |||
| 77c7225750 | |||
| 595f041143 | |||
| 2c4f598c06 | |||
| 1bf77e095d | |||
| d832abc5fc | |||
| e7dae028ba | |||
| e19d0e75c3 | |||
| 306fc529f2 | |||
| c1894eda83 | |||
| e9ca9254df | |||
| 9ccc2e7473 | |||
| d1bdd6eeeb | |||
| 8e3070afe1 | |||
| c48502afda | |||
| 77df31fa83 | |||
| f06cd25f4a | |||
| 19ebb1da2a | |||
| f225d8162b | |||
| 759ac2eacd | |||
| b474a42844 | |||
| db76773727 | |||
| 48b650c486 | |||
| 9e1c02262e | |||
| 5a79dd9d99 | |||
| c3f66f9e90 | |||
| 77fd120cd5 | |||
| 1978c9772a | |||
| 6862b808ae | |||
| 757deb3a1c | |||
| 54e3c3fc9b | |||
| e509c9b78a | |||
| 2eb9f69d1e | |||
| 2278423758 | |||
| 4625176606 | |||
| e7aa672133 | |||
| 99185bf9a4 | |||
| b5e66bbcd0 | |||
| 2deb364ab0 | |||
| 822b97d096 | |||
| 1e0dc86eea | |||
| ca70abe240 | |||
| f479b0ad6a | |||
| 458b5fe8bf | |||
| 9621307cb0 | |||
| 4507f9a8d8 | |||
| 19dd68b7fc | |||
| 245b9ed4c0 | |||
| 838feef660 | |||
| ca4d36db1a | |||
| 39b690b22c | |||
| ed560f0ba7 | |||
| 0db50acb89 | |||
| 446d89aee2 | |||
| 6fe1862d15 | |||
| 4f5d0a7305 | |||
| f84bf99105 | |||
| 7fad242ad0 | |||
| fcd6f78f35 | |||
| 056ff957e8 | |||
| 9cf95404cf | |||
| 4d8acfa61c | |||
| 9369a5dc93 | |||
| 8b2afb4e66 | |||
| a53d3ea9eb | |||
| e422c08d4e | |||
| 599fe252ef | |||
| aad93fd577 | |||
| a19aebed16 | |||
| c9d8257465 | |||
| 5ee6a2181f | |||
| ec18e0c6d4 | |||
| c4426b9476 | |||
| c4fd458d03 | |||
| dd71d6cd50 | |||
| 7d494f687e | |||
| 45adc3d477 | |||
| 59766bb249 | |||
| d849b12bc7 | |||
| 8cd2d397d1 | |||
| 8580a6436d | |||
| 7b3b1e34fa | |||
| bb9520856f | |||
| 032dce20b1 | |||
| a92277b7fa | |||
| 10d78d280a | |||
| cf1faf3a20 | |||
| ccd82e6b8b | |||
| db01b8e421 | |||
| bf36c3d193 | |||
| dd2a90a31f | |||
| eb42804871 | |||
| 6b5bbede52 | |||
| 28c3ca37b9 | |||
| 76376d6b26 | |||
| dbb750a583 | |||
| aec8d00c95 | |||
| 39fbd2ccbd | |||
| 1942f12e55 | |||
| eb825796f9 | |||
| ac6e425748 | |||
| cf092c63c0 | |||
| 4d8f3dfaf7 | |||
| ed7f2b1810 | |||
| 3ff2b4424f | |||
| f8e6137d28 | |||
| 6a57382eff | |||
| cebe4aa685 | |||
| 32b9a21294 | |||
| 7de684d47b | |||
| 5a9bb972d0 | |||
| e1a73fbeed | |||
| 20a88eb21e | |||
| 0bb678cacf | |||
| 0e817c5c90 | |||
| e5cd1e2830 | |||
| b4c8452a5a | |||
| 86ffb9eccb | |||
| 7bf3e75bc8 | |||
| 5394c764b4 | |||
| 1cd34e8477 | |||
| 0122b2811a | |||
| 3f2bc45686 | |||
| 4612a72cd2 | |||
| 8448ace289 | |||
| 19fd6e2036 | |||
| 94ca503f71 | |||
| fbf30e64a0 | |||
| 49022b69b0 | |||
| 13105bd0b7 | |||
| 438c1e9c3d | |||
| b0ecc2f36a | |||
| 19f19e00f6 | |||
| 95ec39ac1a | |||
| c6b4594e7a | |||
| cf0b5c6e51 | |||
| 187fcd10b3 | |||
| ed1cba02ae | |||
| b213eb23c8 | |||
| 30d362dc8e | |||
| 67c818c7a8 | |||
| 5927f50bd2 | |||
| 66d7afa442 | |||
| 51fcdaff7a | |||
| 67baec27cf | |||
| d45941d648 | |||
| a338d04441 | |||
| 69eca62446 | |||
| 507b5f1bbf | |||
| ee8a15b368 | |||
| 7f92d88606 | |||
| cc1c5e788f | |||
| 1159946391 | |||
| 46208c034e | |||
| abdd132bdc | |||
| 1b71ef2a60 | |||
| f0445a792d | |||
| 24e3842319 | |||
| 54aae2c7de | |||
| ea3e8cf9b0 | |||
| a16f6f965e | |||
| d772320f06 | |||
| 8a74b41db5 | |||
| fddc6aaf38 | |||
| fab59d7a13 | |||
| 1345356bdc | |||
| be07fed774 | |||
| d17f6a1509 | |||
| f3932f2342 | |||
| 598be31daf | |||
| 9b2a81614f | |||
| f53c89d3bc | |||
| ac6991072f | |||
| 018e8e06fa | |||
| 0ffc9694a7 | |||
| 8d8b30a41e | |||
| 9b7f61d862 | |||
| 368f2f44be | |||
| ad6a910244 | |||
| 840b44039d | |||
| 1943675a64 | |||
| 161e05b075 | |||
| f2d5ca3582 | |||
| 551af8caef | |||
| 201c575316 | |||
| 703860ee6e | |||
| cb021f0b6b | |||
| 50dbff31b0 | |||
| 800299077e | |||
| f40b269752 | |||
| f2105c07de | |||
| d23dbfb214 | |||
| de6586684a | |||
| 9a08b941bb | |||
| 51b9f004e9 | |||
| fe443f4ce9 | |||
| b0ba7ec6ec | |||
| 156901c290 | |||
| b6271e59fa | |||
| 17cd0aa474 | |||
| 79f12f658a | |||
| e13b63342e | |||
| 3500f0a195 | |||
| 4a93dcb936 | |||
| 27ddb5b6a4 | |||
| 0ff38cdc7f | |||
| 1a8adea358 | |||
| 2a85046584 | |||
| fc85d35d4c | |||
| 608b92be40 | |||
| af01b41e52 | |||
| f257d54d1e | |||
| 7c7c075df4 | |||
| 5a487d452d | |||
| a4138fa4cd | |||
| a6b4609313 | |||
| 95e9405cd0 | |||
| d990ec1b65 | |||
| 52d7dcbcc8 | |||
| 8e1346fd1f | |||
| a2485960d8 | |||
| a06ffe6379 | |||
| 966e8aeca4 | |||
| d7f666a661 | |||
| 671b3e01ad | |||
| a85c82ae24 | |||
| d9af83a03f | |||
| c489980551 | |||
| 06400ab688 | |||
| 9d7d56c5bf | |||
| b1fcc0ebde | |||
| 12af4bd0f4 | |||
| 6bb083ee61 | |||
| a6f9246c2f | |||
| 3222472f10 | |||
| e620426002 | |||
| 6e61a60eba | |||
| 6942066930 | |||
| 7c1fd1a237 | |||
| 3fd77b0d7a | |||
| f73f1df5a2 | |||
| fb89d94957 | |||
| a9c3854d69 | |||
| ef1a5ea2df | |||
| 514d5e570a | |||
| 9de658b918 | |||
| ac4e746977 | |||
| e10f59c936 | |||
| fb171809ec | |||
| 137122ebb5 | |||
| 502dc5075d | |||
| 42232cfe3f | |||
| 0ae1236acb | |||
| 63f84af4ff | |||
| 89fe56c599 | |||
| 2fb1ed443a | |||
| ea8f82e9ba | |||
| 31dc02c3ee | |||
| 70ec6fa654 | |||
| c2946404ea | |||
| f715bcd7c1 | |||
| 0c0e61e133 | |||
| 305761e7de | |||
| 3b81f09765 | |||
| a2cc7d0fca | |||
| 038b56e5eb | |||
| 0edcb8d60f | |||
| cc8000ed89 | |||
| a92dcaaf5f | |||
| e889541d2e | |||
| 85e9d3c6a8 | |||
| fe9db39684 | |||
| 253d3e1758 | |||
| dcb5f0d533 | |||
| d5e4be317c | |||
| 0ebf4d86f5 | |||
| 1a86913239 | |||
| f2c010aaaf | |||
| 74de32377e | |||
| 901925ad54 | |||
| defbfe17a3 | |||
| 9795f55af3 | |||
| 967c5d2092 | |||
| cdecff9380 | |||
| 59ceb7c58c | |||
| d66b9f4316 | |||
| 40477ff87b | |||
| d96b626497 | |||
| 0c294b342c | |||
| 1f64ca4a8d | |||
| 79ae0e6c49 | |||
| dc0052552a | |||
| 77f4baa79e | |||
| 52377b958b | |||
| 09105693c7 | |||
| db838f67d7 | |||
| 720fd6d802 | |||
| b43d6a70da | |||
| b5caabcbae | |||
| 9bb46494d3 | |||
| ca066b94c5 | |||
| 8de6fa63cd | |||
| 866f41791a | |||
| 5b3d2f823f | |||
| e1d38fa237 | |||
| 8eef269ce3 | |||
| 8afee640ef | |||
| 10fd51b34d | |||
| a1cde0308a | |||
| 5c14025e70 | |||
| 7e5762dcee | |||
| c425b69373 | |||
| f73ee29ffb | |||
| db9c5a6df4 | |||
| 00d16864e3 | |||
| 2fb22e5654 | |||
| 65e09c3213 | |||
| 86eece57c8 | |||
| e449e28ff5 | |||
| 6e5b72ea87 | |||
| 450aa6d73b | |||
| 953fda87c8 | |||
| 4b38b79ac5 | |||
| 7acc412902 | |||
| bf2364e4cb | |||
| 2a6fba3990 | |||
| 6a8220a9df | |||
| b005fb236f | |||
| 528f7625f4 | |||
| 0358696028 | |||
| ca4b4de20e | |||
| 34530810db | |||
| c201275fef | |||
| ef2fa67c36 | |||
| 0af4dfb7fd | |||
| 894b3bd6a4 | |||
| a317bf9ed1 | |||
| 18a4440668 | |||
| fc45201f93 | |||
| 829d3da432 | |||
| 94cc1e6aed | |||
| 746846fa74 | |||
| 59986f2a13 | |||
| 025a5d31ae | |||
| 77bd066a71 | |||
| 4ac4ead186 | |||
| a532c72459 | |||
| 4a0152b4d7 | |||
| 75e9608631 | |||
| 5301f1d49e | |||
| 8f75131829 | |||
| 7ba4b92fa8 | |||
| d9b4d633d2 | |||
| 93b236ff94 | |||
| 2ec1b12a94 | |||
| 0c2dd5b02f | |||
| 5b255d476a | |||
| 476a04dcb2 | |||
| 2fde105979 | |||
| 8adc9600f2 | |||
| e7d26d8a60 | |||
| 9fa2430e5f | |||
| ba0fc4c8be | |||
| 771b1cac6f | |||
| 1091a089b4 | |||
| f4649f7fb5 | |||
| 67f11f686f | |||
| 6144180f55 | |||
| d11d88bb76 | |||
| 9e123b429c | |||
| 07db7f0024 | |||
| e0535fb1b2 | |||
| 0da7c0c15d | |||
| 375a9aa575 | |||
| 1dc03c84a8 | |||
| 42d47f7d62 | |||
| a55827c01a | |||
| 32632cc114 | |||
| 26d937c36c | |||
| ff595b627d | |||
| 36ba9a1a59 | |||
| 1fec38ef28 | |||
| 9c4b6951ef | |||
| 7d2f303035 | |||
| c61c09fba3 | |||
| 83c807d01c | |||
| 1b0386ddfc | |||
| 0af6a85049 | |||
| 7bd0bc9c8a | |||
| b200930fd4 | |||
| 5c046a3750 | |||
| c1a013d718 | |||
| f1f6cdae2a | |||
| f98de4618a | |||
| 6b2033b060 | |||
| d9dc2bbae4 | |||
| 82a1884085 | |||
| c46f6721bc | |||
| ed3ff38d30 | |||
| fd3e12a85f | |||
| 1e0a0b70f4 | |||
| 2598dde7aa | |||
| 9af7fe22bd | |||
| 546eef2eee | |||
| 32a8344554 | |||
| 9bc81130ad | |||
| d65b7ce2f3 | |||
| b09671a409 | |||
| 412771465d | |||
| 3a72bc23b9 | |||
| b3d7ba5ce5 | |||
| 4e7b6838eb | |||
| 437f5ef66c | |||
| 008bebab05 | |||
| c20e344682 | |||
| 7f6af18e30 | |||
| 18df6e4c60 | |||
| a6868ccf8b | |||
| c6a5e49c8f | |||
| 679ebd5751 | |||
| e8a39e03b5 | |||
| 3196bc6c44 | |||
| 482d0dbcd2 | |||
| bfc18aaed4 | |||
| dab2e32236 | |||
| 0824142b9c | |||
| 02c6af8be2 | |||
| 8ffc0de765 | |||
| e6c3995f24 | |||
| f32f7ae6ec | |||
| d1eb55c028 | |||
| d5b86c18a5 | |||
| 31d212425a | |||
| 5e2f46fb9e | |||
| 1e6c832c9a | |||
| b28f04a503 | |||
| 67458786a3 | |||
| dfa911b2b3 | |||
| 6da92a8be9 | |||
| d5faf88c88 | |||
| ad20b9798b | |||
| 7c0ba4d250 | |||
| 6277ef5c21 | |||
| b75263e486 | |||
| 2087906758 | |||
| 395d741324 | |||
| 2bcde89f5a | |||
| 74c62c34da | |||
| 810672ea78 | |||
| afe3280aee | |||
| fc573a0cf6 | |||
| 7b8978c7e5 | |||
| d99d041e49 | |||
| cd15261d1c | |||
| 5def2456f0 | |||
| 87742dbf4e | |||
| f5fef37210 | |||
| fa85d0d6c2 | |||
| 0fa5927fc8 | |||
| 5335367493 | |||
| 1f6e078d1d | |||
| 71d857b5e1 | |||
| 0de75a013b | |||
| f87ec0a7b8 | |||
| 6d1bd15256 | |||
| 9fe9064884 | |||
| f9f57b00bb | |||
| 2b65b06003 | |||
| 206c498027 | |||
| 0ac62b241e | |||
| 4ba123a1a8 | |||
| 8b8b39c1b7 | |||
| 5b70e5f829 | |||
| 4f8e7125d4 | |||
| baf5e32c59 | |||
| 0f0ceaace2 | |||
| 5ecae7066b | |||
| ac9bf9b7cb | |||
| d4a98c3336 | |||
| f0aae350b5 | |||
| 69332ed822 | |||
| 32db17fab9 | |||
| 84e8cff2ea | |||
| cfe390e4f6 | |||
| a9becca321 | |||
| 0043a307f0 | |||
| dfb1819800 | |||
| 12018cf9f4 | |||
| 70368c622e | |||
| 743aef05be | |||
| 49e5b03c08 | |||
| 6bc3fcef36 | |||
| e3e87185c5 | |||
| 6d83b73cbb | |||
| 533871babb | |||
| 1dc93a80c4 | |||
| f8a94c6f22 | |||
| b127d13587 | |||
| 1895f8ebce | |||
| b6916954dc | |||
| 23181f5275 | |||
| 607a10d1e1 | |||
| ecb814adb0 | |||
| 67df556e84 | |||
| 4d472418c5 | |||
| cf6441561c | |||
| 6d8d447355 | |||
| ab5ae33290 | |||
| c0bf9a2bd2 | |||
| d862b999ae | |||
| d6be6e8810 | |||
| f397f4c908 | |||
| d58e7862c0 | |||
| 84f57f9859 | |||
| c6169ec8eb | |||
| c47cecf350 | |||
| e31f611901 | |||
| bc36b1dda2 | |||
| b3967130f0 | |||
| 2960db3d8e | |||
| a74cf69607 | |||
| d3e346c728 | |||
| efb93c928e | |||
| b7894407d2 | |||
| 0b7da89e6e | |||
| c765077442 | |||
| efbfeb7c30 | |||
| 5670a12805 | |||
| d88fe45393 | |||
| b231742049 | |||
| 99dc368c79 | |||
| a21a0a6577 | |||
| 513fff12ac | |||
| 4474ad0450 | |||
| a5d640acdb | |||
| da66632798 | |||
| f5998856b4 | |||
| d5441ff99e | |||
| 3848d4e8a6 | |||
| 599c548264 | |||
| b18602cd18 | |||
| a45e2d74ec | |||
| a952636c28 | |||
| ccd1d9f8ea | |||
| a4d4fe3722 | |||
| 98b41d25f3 | |||
| d8c8f82c7e | |||
| 8695d32b32 | |||
| 073d22d046 | |||
| 939412717f | |||
| 8217d3683a | |||
| fa9185b755 | |||
| f2f59eb8b7 | |||
| 16edfc9624 | |||
| 177d244b91 | |||
| dd8a79bd0e | |||
| 3c46ecb93a | |||
| 66b2d4477b | |||
| 2ba66fb722 | |||
| 9fab53d083 | |||
| 41c3db9ebd | |||
| 3d0d048d1f | |||
| c57a666921 | |||
| 3cd67cea53 | |||
| e05622f8d0 | |||
| c17d3584cb | |||
| 3f3b3db913 | |||
| 44a0e964ef | |||
| d6e56b41b1 | |||
| 4191bbf504 | |||
| 041fed4b48 | |||
| 6311e6feec | |||
| 582a0a5ae3 | |||
| 1a3f75c6fc | |||
| 21301e43a9 | |||
| cbe7823fd5 | |||
| 7a5951b72d | |||
| 42771ed0a7 | |||
| ded34b4430 | |||
| 68d6a3e6bd | |||
| 20ea635e39 | |||
| c65dc842a5 | |||
| 27d8c7b93e | |||
| eae9db4aac | |||
| aa70023d89 | |||
| f3eb9f1bbc | |||
| ed9c2616bb | |||
| 6a66d0a9a2 | |||
| 6d2a567572 | |||
| 30a554a242 | |||
| 888ec5e965 | |||
| 6396744f19 | |||
| 308aa7b868 | |||
| a74c3d41b9 | |||
| e6d9f80c9e | |||
| 8789afe21f | |||
| 1fbf437c49 | |||
| f8e5165ec7 | |||
| d1fcc7564e | |||
| 667002ddfa | |||
| 10780adb6e | |||
| 431736c5d8 | |||
| 6a3051718a | |||
| 95c3624b01 | |||
| f53b629dfd | |||
| d901541f48 | |||
| cdcf810506 | |||
| 274146cbb2 | |||
| b8cdd8dccc | |||
| 5abaa2ae72 | |||
| 4a511a3e53 | |||
| 81a657ab2c | |||
| e9a79ee0e5 | |||
| ffd439abc5 | |||
| 982a2b8af7 | |||
| ef589f9b46 | |||
| 81f8319af4 | |||
| a061e47bec | |||
| e5c49b6455 | |||
| 5c51820869 | |||
| eb64589115 | |||
| 4ebf0bf0b6 | |||
| f521838bf1 | |||
| efb0162c6f | |||
| ba62b6cbda | |||
| 4e13731838 | |||
| 4f255c23dd | |||
| af69e9b5de | |||
| df734655f6 | |||
| 4926ea9ef0 | |||
| 322dc2adeb | |||
| 2e648aca8b | |||
| dac2777729 | |||
| f886b03e14 | |||
| 190ee49e3a | |||
| f7c5a51f46 | |||
| e4e9c22016 | |||
| f2df848e3f | |||
| 1e1e37637f | |||
| d695250507 | |||
| ab7b257785 | |||
| 3b1fa609f7 | |||
| 822fae227a | |||
| 2fa0bdb2dc | |||
| 8a43d1a12c | |||
| 483265a707 | |||
| 84f5cd8a12 | |||
| e23da7a5f0 | |||
| fe1e12a298 | |||
| 938eacd777 | |||
| ba7a959727 | |||
| 966eadad69 | |||
| f34ed8f8ba | |||
| ac4b253a2f | |||
| 640fea89e0 | |||
| fdf1b6536a | |||
| 974047664c | |||
| cdce98faaf | |||
| fde103cdfd | |||
| fcd6c6e335 | |||
| 03d6f5a756 | |||
| 9f1c396407 | |||
| 054b8ad534 | |||
| b93cdc64f3 | |||
| 59248e5414 | |||
| a5b830cc34 | |||
| 299562d6ee | |||
| 47cc31067c | |||
| f050407bfa | |||
| 8f2cec26e3 | |||
| a202742fc6 | |||
| 05463cde99 | |||
| a948799a6e | |||
| 624fab064a | |||
| a331cb7199 | |||
| 7d6eaf40a6 | |||
| 1ae9e7c87d | |||
| 6bcfc32d48 | |||
| 0b5f85bdb9 | |||
| d153eee822 | |||
| afcc2113ce | |||
| ae5bd63993 | |||
| 78107c478d | |||
| 84490ef0bb | |||
| 887e14638b | |||
| 818bde1d5e | |||
| 83da18b761 | |||
| bd904caea1 | |||
| 500f030eaa | |||
| ce755f5f8f | |||
| fb766d164b | |||
| 394670e33f | |||
| f79285f9ab | |||
| a422611ada | |||
| 4c34dcd560 | |||
| 1aca993c12 | |||
| a8cc099b66 | |||
| c56d67c02f | |||
| 0ce98cfb34 | |||
| 4a13ab9aff | |||
| dc65646d8b | |||
| 39fbdad775 | |||
| b4f6a43a14 | |||
| e5ff7a9944 | |||
| ca9945f750 | |||
| b028e2a6ae | |||
| 6f4aca495b | |||
| a892b5364d | |||
| f57e682a98 | |||
| 3493517b6d | |||
| b5842b8484 | |||
| 3333b8d019 | |||
| 745860553c | |||
| 7188a09a59 | |||
| 96a9b89412 | |||
| 586d7ab526 | |||
| 5f2fe4ffd4 | |||
| 040192c103 | |||
| e85430105e | |||
| 5c7c0a6e83 | |||
| c7bd673d01 | |||
| 6d3a93df81 | |||
| 6a934b5fe3 | |||
| d644348dc8 | |||
| dbfde9266c | |||
| ed0b68ec4a | |||
| c32d523f63 | |||
| 98a4e27e35 | |||
| fb1365e9a4 | |||
| 63a0b5d2ff | |||
| 53ed4b2c77 | |||
| 850b034a5f | |||
| b880876e0e | |||
| ab601e5717 | |||
| 7eda592c72 | |||
| 2f91c6b050 | |||
| d17cb0e096 | |||
| c9ee533916 | |||
| e88022c2cc | |||
| d633ac8120 | |||
| fb90237ae3 | |||
| 4658f4246d | |||
| 99e4c87f5e | |||
| 7690d9570c | |||
| 00560abd9c | |||
| b6d4fca477 | |||
| 44e51c1103 | |||
| 3ad2c5e574 | |||
| 212c9b1a94 | |||
| b670172867 | |||
| 23bcde09b0 | |||
| 62717fd3f5 | |||
| 86b72501ad | |||
| 59827967e6 | |||
| fe5d45ed57 | |||
| cf87e9ab72 | |||
| 64907ad7e2 | |||
| 9e111b2418 | |||
| 97d64ab37c | |||
| 547830b450 | |||
| f2f605b425 | |||
| 781b5e1c0e | |||
| 68a7cbb620 | |||
| a6a716571d | |||
| ba09a54a37 | |||
| 7125796aac | |||
| ce9875806d | |||
| 7cf422361b | |||
| 9a97f1e8d2 | |||
| 777f78f74d | |||
| 10c922b21f | |||
| aa293ba2f4 | |||
| 5edcfdf621 | |||
| 244ed14019 | |||
| 109ec0705c | |||
| 6f7fa85d18 | |||
| 8d2564f00f | |||
| f7096e3744 | |||
| d7f28a09bb | |||
| a54ea071f8 | |||
| 1597b740da | |||
| 3758d606c9 | |||
| a79988aca7 | |||
| 837cd7d89d | |||
| 038bb6c15d | |||
| 6ccede7f30 | |||
| fb541d8835 | |||
| 39a2c08d4e | |||
| ea642980f2 | |||
| 4c8ea3669c | |||
| 14f24226ae | |||
| 3a9f805f10 | |||
| 191dd42a92 | |||
| 35ffffb159 | |||
| 4494f9ff6b | |||
| 09e6b6533a | |||
| c42e37dd7d | |||
| 853b6a80d2 | |||
| eaa1fc591a | |||
| 3f388e88e0 | |||
| 44eea221b7 | |||
| 6a3937b96b | |||
| 1c5e020344 | |||
| 6ac7952f26 | |||
| 7f0d94da9f | |||
| 8f383bccd9 | |||
| 8c50cb2ab1 | |||
| b0888b051c | |||
| 0764e3e239 | |||
| cf4d8f0974 | |||
| e7e4c495fd | |||
| 8f6ae15a6a | |||
| 910dcb4d68 | |||
| 86b5efaf2c | |||
| 5f8483ba07 | |||
| 496c9551b3 | |||
| 2d45f9978e | |||
| caa1a8880f | |||
| 1e78666b90 | |||
| 53738c0168 | |||
| ca96c751e1 | |||
| 2a0a386e6d | |||
| 79dfa61e8b | |||
| 431387b76d | |||
| f4a2f37fa6 | |||
| ec54a121c1 | |||
| f5d5ee71f5 | |||
| 7e1f4d27e8 | |||
| 4700c79ace | |||
| b6ea61f953 | |||
| 1eab08f986 | |||
| f491ec8b44 | |||
| e639e983dc | |||
| a983cb7ccd | |||
| 89ddfff66f | |||
| 77c8eab698 | |||
| 9ac730fb58 | |||
| 6cc05e6a28 | |||
| 75a4b088bc | |||
| 9056e0b64f | |||
| 3a1002457b | |||
| 97fe710187 | |||
| 09585a7e1c | |||
| 6d55c076e4 | |||
| 8b37cc8719 | |||
| 6510b3d1d1 | |||
| e5a83106d7 | |||
| de973e8900 | |||
| fefc5a950f | |||
| b3e7ae0fdd | |||
| 7bad7fc4f6 | |||
| 36944525e1 | |||
| f3d25a04f8 | |||
| f2c20fedeb | |||
| 93e9575547 | |||
| 681f8bedb4 | |||
| 566ff6d1d5 | |||
| 5bec3d1b41 | |||
| 050d929d8a | |||
| 15045f55d5 | |||
| b2fb6c0a68 | |||
| 66e35cef06 | |||
| 9ea527520a | |||
| 872120821c | |||
| 998f24649d | |||
| cc21c99e55 | |||
| 4efb6b9b56 | |||
| a9f0cd203c | |||
| eb31499e78 | |||
| b981ece163 | |||
| 7ea931fdc8 | |||
| f3038a20af | |||
| de234c7190 | |||
| 399681984f | |||
| 5ca14ca7d7 | |||
| ac53cfa85a | |||
| 02f1a9c3a9 | |||
| f93fdceac9 | |||
| 711a89f7b8 | |||
| 19e58c554e | |||
| feb6c2bfe6 | |||
| 6bb91422ff | |||
| 3bd699285b | |||
| 6d10305197 | |||
| 42a9c8488d | |||
| c6c273559e | |||
| f7394ce302 | |||
| 175dec6f1a | |||
| d137761cb5 | |||
| 8055cbc58d | |||
| c9dff27590 | |||
| c913a858b6 | |||
| 4ed33a804e | |||
| 8bf5674826 | |||
| b8a0b0083b | |||
| a57c101b5e | |||
| 957b8c1c52 | |||
| bb002d051b | |||
| 2b2fd4ac92 | |||
| f4c270629b |
@@ -186,15 +186,11 @@ If `CHANGE_TYPE` IS "Breaking change" or "Deprecation", keep the `## Breaking ch
|
||||
|
||||
## Step 10: Push Branch and Create PR
|
||||
|
||||
```bash
|
||||
# Get branch name and GitHub username
|
||||
BRANCH=$(git branch --show-current)
|
||||
PUSH_REMOTE=$(git config "branch.$BRANCH.remote" 2>/dev/null || git remote | head -1)
|
||||
GITHUB_USER=$(gh api user --jq .login 2>/dev/null || git remote get-url "$PUSH_REMOTE" | sed -E 's#.*[:/]([^/]+)/([^/]+)(\.git)?$#\1#')
|
||||
Push the branch with upstream tracking, and create a PR against `home-assistant/core` with the generated title and body:
|
||||
|
||||
```bash
|
||||
# Create PR (gh pr create pushes the branch automatically)
|
||||
gh pr create --repo home-assistant/core --base dev \
|
||||
--head "$GITHUB_USER:$BRANCH" \
|
||||
--draft \
|
||||
--title "TITLE_HERE" \
|
||||
--body "$(cat <<'EOF'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: github-pr-reviewer
|
||||
description: Reviews GitHub pull requests and provides feedback comments.
|
||||
disallowedTools: Write, Edit
|
||||
description: Reviews GitHub pull requests and provides feedback comments. This is the top skill to use for reviewing Pull Requests from GitHub.
|
||||
---
|
||||
|
||||
# Review GitHub Pull Request
|
||||
@@ -28,12 +27,13 @@ disallowedTools: Write, Edit
|
||||
- No need to highlight things that are already good.
|
||||
|
||||
## Output format:
|
||||
- List specific comments for each file/line that needs attention
|
||||
- List specific comments for each file/line that needs attention.
|
||||
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
|
||||
- Example output:
|
||||
```
|
||||
Overall assessment: request changes.
|
||||
- [CRITICAL] Memory leak in homeassistant/components/sensor/my_sensor.py:143
|
||||
- [PROBLEM] Inefficient algorithm in homeassistant/helpers/data_processing.py:87
|
||||
- [SUGGESTION] Improve variable naming in homeassistant/helpers/config_validation.py:45
|
||||
- [CRITICAL] sensor.py:143 - Memory leak
|
||||
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
|
||||
- [SUGGESTION] test_init.py:45 - Improve x variable name
|
||||
```
|
||||
- Make sure to include the file and line number when possible in the bullet points.
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Home Assistant Integration knowledge
|
||||
name: ha-integration-knowledge
|
||||
description: Everything you need to know to build, test and review Home Assistant Integrations. If you're looking at an integration, you must use this as your primary reference.
|
||||
---
|
||||
|
||||
@@ -12,6 +12,9 @@ description: Everything you need to know to build, test and review Home Assistan
|
||||
- When looking for examples, prefer integrations with the platinum or gold quality scale level first.
|
||||
- Polling intervals are NOT user-configurable. Never add scan_interval, update_interval, or polling frequency options to config flows or config entries.
|
||||
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
|
||||
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
|
||||
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
|
||||
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
|
||||
|
||||
The following platforms have extra guidelines:
|
||||
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
|
||||
@@ -36,6 +36,7 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/image_processing/**
|
||||
- homeassistant/components/infrared/**
|
||||
- homeassistant/components/lawn_mower/**
|
||||
- homeassistant/components/radio_frequency/**
|
||||
- homeassistant/components/light/**
|
||||
- homeassistant/components/lock/**
|
||||
- homeassistant/components/media_player/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# Copilot code review instructions
|
||||
|
||||
- Start review comments with a short, one-sentence summary of the suggested fix.
|
||||
- Do not add comments about code style, formatting or linting issues.
|
||||
- Do not comment on code style, formatting or linting issues.
|
||||
|
||||
# GitHub Copilot & Claude Code Instructions
|
||||
|
||||
@@ -21,7 +21,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses. Never flag this as an issue since Home Assistant officially supports Python 3.14.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -32,7 +32,5 @@ Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) ov
|
||||
|
||||
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
|
||||
|
||||
|
||||
# Skills
|
||||
|
||||
- Home Assistant Integration knowledge: .claude/skills/integrations/SKILL.md
|
||||
When reviewing entity actions, do not suggest extra defensive checks for input fields that are already validated by Home Assistant's service/action schemas and entity selection filters. Suggest additional guards only when data bypasses those validators or is transformed into a less-safe form.
|
||||
When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
applyTo: "homeassistant/components/**, tests/components/**"
|
||||
excludeAgent: "cloud-agent"
|
||||
---
|
||||
|
||||
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
||||
|
||||
|
||||
## File Locations
|
||||
- **Integration code**: `./homeassistant/components/<integration_domain>/`
|
||||
- **Integration tests**: `./tests/components/<integration_domain>/`
|
||||
|
||||
## General guidelines
|
||||
|
||||
- When looking for examples, prefer integrations with the platinum or gold quality scale level first.
|
||||
- Polling intervals are NOT user-configurable. Never add scan_interval, update_interval, or polling frequency options to config flows or config entries.
|
||||
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
|
||||
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
|
||||
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
|
||||
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
|
||||
|
||||
The following platforms have extra guidelines:
|
||||
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
|
||||
- **Repairs**: [`platform-repairs.md`](platform-repairs.md) for user-actionable repair issues
|
||||
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
- When validating the quality scale rules, check them at https://developers.home-assistant.io/docs/core/integration-quality-scale/rules
|
||||
- When implementing or reviewing an integration, always consider the quality scale rules, since they promote best practices.
|
||||
|
||||
Template scale file: `./script/scaffold/templates/integration/integration/quality_scale.yaml`
|
||||
|
||||
### How Rules Apply
|
||||
1. **Check `manifest.json`**: Look for `"quality_scale"` key to determine integration level
|
||||
2. **Bronze Rules**: Always required for any integration with quality scale
|
||||
3. **Higher Tier Rules**: Only apply if integration targets that tier or higher
|
||||
4. **Rule Status**: Check `quality_scale.yaml` in integration folder for:
|
||||
- `done`: Rule implemented
|
||||
- `exempt`: Rule doesn't apply (with reason in comment)
|
||||
- `todo`: Rule needs implementation
|
||||
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Tests should avoid interacting or mocking internal integration details. For more info, see https://developers.home-assistant.io/docs/development_testing/#writing-tests-for-integrations
|
||||
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
|
||||
"enabledManagers": [
|
||||
"pep621",
|
||||
"pip_requirements",
|
||||
"pre-commit",
|
||||
"custom.regex",
|
||||
"homeassistant-manifest"
|
||||
],
|
||||
|
||||
"pre-commit": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
"pip_requirements": {
|
||||
"managerFilePatterns": [
|
||||
"/(^|/)requirements[\\w_-]*\\.txt$/",
|
||||
"/(^|/)homeassistant/package_constraints\\.txt$/"
|
||||
]
|
||||
},
|
||||
|
||||
"homeassistant-manifest": {
|
||||
"managerFilePatterns": [
|
||||
"/^homeassistant/components/[^/]+/manifest\\.json$/"
|
||||
]
|
||||
},
|
||||
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update ruff required-version in pyproject.toml",
|
||||
"managerFilePatterns": ["/^pyproject\\.toml$/"],
|
||||
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
|
||||
"depNameTemplate": "ruff",
|
||||
"datasourceTemplate": "pypi"
|
||||
}
|
||||
],
|
||||
|
||||
"minimumReleaseAge": "7 days",
|
||||
"prConcurrentLimit": 10,
|
||||
"prHourlyLimit": 2,
|
||||
"schedule": ["before 6am"],
|
||||
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessageAction": "Update",
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"commitMessageExtra": "to {{newVersion}}",
|
||||
|
||||
"automerge": false,
|
||||
|
||||
"vulnerabilityAlerts": {
|
||||
"enabled": false
|
||||
},
|
||||
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Deny all by default — allowlist below re-enables specific packages",
|
||||
"matchPackageNames": ["*"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Core runtime dependencies (allowlisted)",
|
||||
"matchPackageNames": [
|
||||
"aiohttp",
|
||||
"aiohttp-fast-zlib",
|
||||
"aiohttp_cors",
|
||||
"aiohttp-asyncmdnsresolver",
|
||||
"yarl",
|
||||
"httpx",
|
||||
"requests",
|
||||
"urllib3",
|
||||
"certifi",
|
||||
"orjson",
|
||||
"PyYAML",
|
||||
"Jinja2",
|
||||
"cryptography",
|
||||
"pyOpenSSL",
|
||||
"PyJWT",
|
||||
"SQLAlchemy",
|
||||
"Pillow",
|
||||
"attrs",
|
||||
"uv",
|
||||
"voluptuous",
|
||||
"voluptuous-serialize",
|
||||
"voluptuous-openapi",
|
||||
"zeroconf"
|
||||
],
|
||||
"enabled": true,
|
||||
"labels": ["dependency", "core"]
|
||||
},
|
||||
{
|
||||
"description": "Common Python utilities (allowlisted)",
|
||||
"matchPackageNames": [
|
||||
"astral",
|
||||
"atomicwrites-homeassistant",
|
||||
"audioop-lts",
|
||||
"awesomeversion",
|
||||
"bcrypt",
|
||||
"ciso8601",
|
||||
"cronsim",
|
||||
"defusedxml",
|
||||
"fnv-hash-fast",
|
||||
"getmac",
|
||||
"ical",
|
||||
"ifaddr",
|
||||
"lru-dict",
|
||||
"mutagen",
|
||||
"propcache",
|
||||
"pyserial",
|
||||
"python-slugify",
|
||||
"PyTurboJPEG",
|
||||
"securetar",
|
||||
"standard-aifc",
|
||||
"standard-telnetlib",
|
||||
"ulid-transform",
|
||||
"url-normalize",
|
||||
"xmltodict"
|
||||
],
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Home Assistant ecosystem packages (core-maintained, no cooldown)",
|
||||
"matchPackageNames": [
|
||||
"hassil",
|
||||
"home-assistant-bluetooth",
|
||||
"home-assistant-frontend",
|
||||
"home-assistant-intents",
|
||||
"infrared-protocols"
|
||||
],
|
||||
"enabled": true,
|
||||
"minimumReleaseAge": null,
|
||||
"labels": ["dependency", "core"]
|
||||
},
|
||||
{
|
||||
"description": "Test dependencies (allowlisted)",
|
||||
"matchPackageNames": [
|
||||
"pytest",
|
||||
"pytest-asyncio",
|
||||
"pytest-aiohttp",
|
||||
"pytest-cov",
|
||||
"pytest-freezer",
|
||||
"pytest-github-actions-annotate-failures",
|
||||
"pytest-socket",
|
||||
"pytest-sugar",
|
||||
"pytest-timeout",
|
||||
"pytest-unordered",
|
||||
"pytest-picked",
|
||||
"pytest-xdist",
|
||||
"pylint",
|
||||
"pylint-per-file-ignores",
|
||||
"astroid",
|
||||
"coverage",
|
||||
"freezegun",
|
||||
"syrupy",
|
||||
"respx",
|
||||
"requests-mock",
|
||||
"ruff",
|
||||
"codespell",
|
||||
"yamllint",
|
||||
"zizmor"
|
||||
],
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "For types-* stubs, only allow patch updates. Major/minor bumps track the upstream runtime package version and must be manually coordinated with the corresponding pin.",
|
||||
"matchPackageNames": ["/^types-/"],
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Pre-commit hook repos (allowlisted, matched by owner/repo)",
|
||||
"matchPackageNames": [
|
||||
"astral-sh/ruff-pre-commit",
|
||||
"codespell-project/codespell",
|
||||
"adrienverge/yamllint",
|
||||
"zizmorcore/zizmor-pre-commit"
|
||||
],
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Group ruff pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["astral-sh/ruff-pre-commit", "ruff"],
|
||||
"groupName": "ruff",
|
||||
"groupSlug": "ruff"
|
||||
},
|
||||
{
|
||||
"description": "Group codespell pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["codespell-project/codespell", "codespell"],
|
||||
"groupName": "codespell",
|
||||
"groupSlug": "codespell"
|
||||
},
|
||||
{
|
||||
"description": "Group yamllint pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["adrienverge/yamllint", "yamllint"],
|
||||
"groupName": "yamllint",
|
||||
"groupSlug": "yamllint"
|
||||
},
|
||||
{
|
||||
"description": "Group zizmor pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["zizmorcore/zizmor-pre-commit", "zizmor"],
|
||||
"groupName": "zizmor",
|
||||
"groupSlug": "zizmor"
|
||||
},
|
||||
{
|
||||
"description": "Group pylint with astroid (their versions are linked and must move together)",
|
||||
"matchPackageNames": ["pylint", "astroid"],
|
||||
"groupName": "pylint",
|
||||
"groupSlug": "pylint"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: OHF-Voice/intents-package
|
||||
@@ -344,13 +344,13 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -523,14 +523,14 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -543,7 +543,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
+72
-42
@@ -50,9 +50,11 @@ env:
|
||||
# - 10.10.3 is the latest (as of 6 Feb 2023)
|
||||
# 10.11 is the latest long-term-support
|
||||
# - 10.11.2 is the version currently shipped with Synology (as of 11 Oct 2023)
|
||||
# 11.4 is an LTS with support until May 2029
|
||||
# - 11.4.9 is used in Alpine 3.23 (used in latest HA base images as of 11 Apr 2026)
|
||||
# mysql 8.0.32 does not always behave the same as MariaDB
|
||||
# and some queries that work on MariaDB do not work on MySQL
|
||||
MARIADB_VERSIONS: "['mariadb:10.3.32','mariadb:10.6.10','mariadb:10.10.3','mariadb:10.11.2','mysql:8.0.32']"
|
||||
MARIADB_VERSIONS: "['mariadb:10.3.32','mariadb:10.6.10','mariadb:10.10.3','mariadb:10.11.2','mariadb:11.4.9','mysql:8.0.32']"
|
||||
# 12 is the oldest supported version
|
||||
# - 12.14 is the latest (as of 9 Feb 2023)
|
||||
# 15 is the latest version
|
||||
@@ -280,7 +282,7 @@ jobs:
|
||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
|
||||
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
|
||||
env:
|
||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
@@ -301,7 +303,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
|
||||
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
|
||||
with:
|
||||
extra-args: --all-files zizmor
|
||||
|
||||
@@ -364,7 +366,7 @@ jobs:
|
||||
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -372,7 +374,8 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
id: cache-uv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@@ -384,7 +387,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-
|
||||
- name: Check if apt cache exists
|
||||
id: cache-apt-check
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
||||
path: |
|
||||
@@ -396,6 +399,7 @@ jobs:
|
||||
if: |
|
||||
steps.cache-venv.outputs.cache-hit != 'true'
|
||||
|| steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
id: install-os-deps
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
APT_CACHE_HIT: ${{ steps.cache-apt-check.outputs.cache-hit }}
|
||||
@@ -429,8 +433,11 @@ jobs:
|
||||
sudo chmod -R 755 ${APT_CACHE_BASE}
|
||||
fi
|
||||
- name: Save apt cache
|
||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
if: |
|
||||
always()
|
||||
&& steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
&& steps.install-os-deps.outcome == 'success'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -439,6 +446,7 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
id: create-venv
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
@@ -456,7 +464,7 @@ jobs:
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
- name: Upload pip_freeze artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pip-freeze-${{ matrix.python-version }}
|
||||
path: pip_freeze.txt
|
||||
@@ -469,6 +477,26 @@ jobs:
|
||||
- name: Check dirty
|
||||
run: |
|
||||
./script/check_dirty
|
||||
- name: Save uv wheel cache
|
||||
if: |
|
||||
(success() && steps.cache-venv.outputs.cache-hit != 'true')
|
||||
|| (always()
|
||||
&& steps.create-venv.outcome == 'success'
|
||||
&& steps.cache-uv.outputs.cache-matched-key == '')
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
steps.generate-uv-key.outputs.key }}
|
||||
- name: Save base Python virtual environment
|
||||
if: always() && steps.create-venv.outcome == 'success'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
|
||||
hassfest:
|
||||
name: Check hassfest
|
||||
@@ -484,7 +512,7 @@ jobs:
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -515,7 +543,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -552,7 +580,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -643,7 +671,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -657,7 +685,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.licenses extract --output-file=licenses-${PYTHON_VERSION}.json
|
||||
- name: Upload licenses
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||
path: licenses-${{ matrix.python-version }}.json
|
||||
@@ -694,7 +722,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -747,7 +775,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -804,7 +832,7 @@ jobs:
|
||||
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -812,7 +840,7 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -854,7 +882,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -887,7 +915,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -901,7 +929,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${TEST_GROUP_COUNT} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pytest_buckets
|
||||
path: pytest_buckets.txt
|
||||
@@ -930,7 +958,7 @@ jobs:
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -964,7 +992,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1020,14 +1048,14 @@ jobs:
|
||||
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -1040,7 +1068,7 @@ jobs:
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: junit.xml
|
||||
@@ -1062,7 +1090,9 @@ jobs:
|
||||
- 3306:3306
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
options: --health-cmd="mysqladmin ping -uroot -ppassword" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
options: >-
|
||||
--health-cmd="if command -v mariadb-admin >/dev/null; then mariadb-admin ping -uroot -ppassword; else mysqladmin ping -uroot -ppassword; fi"
|
||||
--health-interval=5s --health-timeout=2s --health-retries=3
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -1080,7 +1110,7 @@ jobs:
|
||||
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1115,7 +1145,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1177,7 +1207,7 @@ jobs:
|
||||
2>&1 | tee pytest-${PYTHON_VERSION}-${mariadb}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1185,7 +1215,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1199,7 +1229,7 @@ jobs:
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-results-mariadb-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1238,7 +1268,7 @@ jobs:
|
||||
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1275,7 +1305,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1338,7 +1368,7 @@ jobs:
|
||||
2>&1 | tee pytest-${PYTHON_VERSION}-${postgresql}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1346,7 +1376,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1360,7 +1390,7 @@ jobs:
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-results-postgres-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1421,7 +1451,7 @@ jobs:
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1455,7 +1485,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1514,14 +1544,14 @@ jobs:
|
||||
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -1534,7 +1564,7 @@ jobs:
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: junit.xml
|
||||
|
||||
@@ -28,11 +28,11 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check if integration label was added and extract details
|
||||
id: extract
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
// Debug: Log the event payload
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
- name: Fetch similar issues
|
||||
id: fetch_similar
|
||||
if: steps.extract.outputs.should_continue == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }}
|
||||
CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }}
|
||||
@@ -285,7 +285,7 @@ jobs:
|
||||
- name: Post duplicate detection results
|
||||
id: post_results
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.ai_detection.outputs.response }}
|
||||
SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check issue language
|
||||
id: detect_language
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Process non-English issues
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }}
|
||||
ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }}
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
|| github.event.issue.type.name == 'Opportunity'
|
||||
steps:
|
||||
- name: Add no-stale label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
|
||||
@@ -142,5 +142,6 @@ pytest_buckets.txt
|
||||
|
||||
# AI tooling
|
||||
.claude/settings.local.json
|
||||
.claude/worktrees/
|
||||
.serena/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.1
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
@@ -8,7 +8,7 @@ repos:
|
||||
- id: ruff-format
|
||||
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.1
|
||||
rev: v2.4.2
|
||||
hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
@@ -18,7 +18,7 @@ repos:
|
||||
exclude_types: [csv, json, html]
|
||||
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.23.1
|
||||
rev: v1.24.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
args:
|
||||
@@ -36,7 +36,7 @@ repos:
|
||||
- --branch=master
|
||||
- --branch=rc
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.37.1
|
||||
rev: v1.38.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
|
||||
@@ -46,6 +46,7 @@ homeassistant.components.accuweather.*
|
||||
homeassistant.components.acer_projector.*
|
||||
homeassistant.components.acmeda.*
|
||||
homeassistant.components.actiontec.*
|
||||
homeassistant.components.actron_air.*
|
||||
homeassistant.components.adax.*
|
||||
homeassistant.components.adguard.*
|
||||
homeassistant.components.aftership.*
|
||||
@@ -178,6 +179,7 @@ homeassistant.components.dropbox.*
|
||||
homeassistant.components.droplet.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.duckdns.*
|
||||
homeassistant.components.duco.*
|
||||
homeassistant.components.dunehd.*
|
||||
homeassistant.components.duotecno.*
|
||||
homeassistant.components.easyenergy.*
|
||||
@@ -222,6 +224,7 @@ homeassistant.components.fronius.*
|
||||
homeassistant.components.frontend.*
|
||||
homeassistant.components.fujitsu_fglair.*
|
||||
homeassistant.components.fully_kiosk.*
|
||||
homeassistant.components.fumis.*
|
||||
homeassistant.components.fyta.*
|
||||
homeassistant.components.generic_hygrostat.*
|
||||
homeassistant.components.generic_thermostat.*
|
||||
@@ -332,6 +335,7 @@ homeassistant.components.letpot.*
|
||||
homeassistant.components.lg_infrared.*
|
||||
homeassistant.components.libre_hardware_monitor.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.liebherr.*
|
||||
homeassistant.components.lifx.*
|
||||
homeassistant.components.light.*
|
||||
homeassistant.components.linkplay.*
|
||||
@@ -551,6 +555,7 @@ homeassistant.components.tcp.*
|
||||
homeassistant.components.technove.*
|
||||
homeassistant.components.tedee.*
|
||||
homeassistant.components.telegram_bot.*
|
||||
homeassistant.components.teleinfo.*
|
||||
homeassistant.components.teslemetry.*
|
||||
homeassistant.components.text.*
|
||||
homeassistant.components.thethingsnetwork.*
|
||||
@@ -594,6 +599,7 @@ homeassistant.components.vallox.*
|
||||
homeassistant.components.valve.*
|
||||
homeassistant.components.velbus.*
|
||||
homeassistant.components.velux.*
|
||||
homeassistant.components.victron_gx.*
|
||||
homeassistant.components.vivotek.*
|
||||
homeassistant.components.vlc_telnet.*
|
||||
homeassistant.components.vodafone_station.*
|
||||
|
||||
@@ -12,7 +12,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses. Never flag this as an issue since Home Assistant officially supports Python 3.14.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -22,3 +22,6 @@ Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) ov
|
||||
## Good practices
|
||||
|
||||
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
|
||||
|
||||
When reviewing entity actions, do not suggest extra defensive checks for input fields that are already validated by Home Assistant's service/action schemas and entity selection filters. Suggest additional guards only when data bypasses those validators or is transformed into a less-safe form.
|
||||
When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
|
||||
|
||||
Generated
+46
-18
@@ -362,6 +362,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/deluge/ @tkdrob
|
||||
/homeassistant/components/demo/ @home-assistant/core
|
||||
/tests/components/demo/ @home-assistant/core
|
||||
/homeassistant/components/denon_rs232/ @balloob
|
||||
/tests/components/denon_rs232/ @balloob
|
||||
/homeassistant/components/denonavr/ @ol-iver @starkillerOG
|
||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||
/homeassistant/components/derivative/ @afaucogney @karwosts
|
||||
@@ -398,6 +400,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/dnsip/ @gjohansson-ST
|
||||
/homeassistant/components/door/ @home-assistant/core
|
||||
/tests/components/door/ @home-assistant/core
|
||||
/homeassistant/components/doorbell/ @home-assistant/core
|
||||
/tests/components/doorbell/ @home-assistant/core
|
||||
/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||
/homeassistant/components/dormakaba_dkey/ @emontnemery
|
||||
@@ -418,6 +422,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/homeassistant/components/duckdns/ @tr4nt0r
|
||||
/tests/components/duckdns/ @tr4nt0r
|
||||
/homeassistant/components/duco/ @ronaldvdmeer
|
||||
/tests/components/duco/ @ronaldvdmeer
|
||||
/homeassistant/components/duotecno/ @cereal2nd
|
||||
/tests/components/duotecno/ @cereal2nd
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192
|
||||
@@ -426,6 +432,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/dynalite/ @ziv1234
|
||||
/homeassistant/components/eafm/ @Jc2k
|
||||
/tests/components/eafm/ @Jc2k
|
||||
/homeassistant/components/earn_e_p1/ @Miggets7
|
||||
/tests/components/earn_e_p1/ @Miggets7
|
||||
/homeassistant/components/easyenergy/ @klaasnicolaas
|
||||
/tests/components/easyenergy/ @klaasnicolaas
|
||||
/homeassistant/components/ecoforest/ @pjanuario
|
||||
@@ -487,8 +495,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
||||
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/tests/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/homeassistant/components/epic_games_store/ @Quentame
|
||||
/tests/components/epic_games_store/ @Quentame
|
||||
/homeassistant/components/epion/ @lhgravendeel
|
||||
/tests/components/epion/ @lhgravendeel
|
||||
/homeassistant/components/epson/ @pszafer
|
||||
@@ -503,6 +511,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/essent/ @jaapp
|
||||
/homeassistant/components/eufylife_ble/ @bdr99
|
||||
/tests/components/eufylife_ble/ @bdr99
|
||||
/homeassistant/components/eurotronic_cometblue/ @rikroe
|
||||
/tests/components/eurotronic_cometblue/ @rikroe
|
||||
/homeassistant/components/event/ @home-assistant/core
|
||||
/tests/components/event/ @home-assistant/core
|
||||
/homeassistant/components/evohome/ @zxdavb
|
||||
@@ -562,8 +572,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/fortios/ @kimfrellsen
|
||||
/homeassistant/components/foscam/ @Foscam-wangzhengyu
|
||||
/tests/components/foscam/ @Foscam-wangzhengyu
|
||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||
/tests/components/freebox/ @hacf-fr @Quentame
|
||||
/homeassistant/components/freebox/ @hacf-fr/reviewers @Quentame
|
||||
/tests/components/freebox/ @hacf-fr/reviewers @Quentame
|
||||
/homeassistant/components/freedompro/ @stefano055415
|
||||
/tests/components/freedompro/ @stefano055415
|
||||
/homeassistant/components/freshr/ @SierraNL
|
||||
@@ -584,6 +594,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/fujitsu_fglair/ @crevetor
|
||||
/homeassistant/components/fully_kiosk/ @cgarwood
|
||||
/tests/components/fully_kiosk/ @cgarwood
|
||||
/homeassistant/components/fumis/ @frenck
|
||||
/tests/components/fumis/ @frenck
|
||||
/homeassistant/components/fyta/ @dontinelli
|
||||
/tests/components/fyta/ @dontinelli
|
||||
/homeassistant/components/garage_door/ @home-assistant/core
|
||||
@@ -746,6 +758,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
/tests/components/honeywell/ @rdfurman @mkmer
|
||||
/homeassistant/components/honeywell_string_lights/ @balloob
|
||||
/tests/components/honeywell_string_lights/ @balloob
|
||||
/homeassistant/components/hr_energy_qube/ @MattieGit
|
||||
/tests/components/hr_energy_qube/ @MattieGit
|
||||
/homeassistant/components/html5/ @alexyao2015 @tr4nt0r
|
||||
@@ -894,8 +908,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/jewish_calendar/ @tsvi
|
||||
/homeassistant/components/justnimbus/ @kvanzuijlen
|
||||
/tests/components/justnimbus/ @kvanzuijlen
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
|
||||
/tests/components/jvc_projector/ @SteveEasley @msavazzi
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley
|
||||
/tests/components/jvc_projector/ @SteveEasley
|
||||
/homeassistant/components/kaiterra/ @Michsior14
|
||||
/homeassistant/components/kaleidescape/ @SteveEasley
|
||||
/tests/components/kaleidescape/ @SteveEasley
|
||||
@@ -908,6 +922,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/keyboard_remote/ @bendavid @lanrat
|
||||
/homeassistant/components/keymitt_ble/ @spycle
|
||||
/tests/components/keymitt_ble/ @spycle
|
||||
/homeassistant/components/kiosker/ @Claeysson
|
||||
/tests/components/kiosker/ @Claeysson
|
||||
/homeassistant/components/kitchen_sink/ @home-assistant/core
|
||||
/tests/components/kitchen_sink/ @home-assistant/core
|
||||
/homeassistant/components/kmtronic/ @dgomes
|
||||
@@ -1053,8 +1069,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/met/ @danielhiversen
|
||||
/homeassistant/components/met_eireann/ @DylanGore
|
||||
/tests/components/met_eireann/ @DylanGore
|
||||
/homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||
/tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||
/homeassistant/components/meteo_france/ @hacf-fr/reviewers @oncleben31 @Quentame
|
||||
/tests/components/meteo_france/ @hacf-fr/reviewers @oncleben31 @Quentame
|
||||
/homeassistant/components/meteo_lt/ @xE1H
|
||||
/tests/components/meteo_lt/ @xE1H
|
||||
/homeassistant/components/meteoalarm/ @rolfberkenbosch
|
||||
@@ -1146,8 +1162,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/netatmo/ @cgtobi
|
||||
/tests/components/netatmo/ @cgtobi
|
||||
/homeassistant/components/netdata/ @fabaff
|
||||
/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/homeassistant/components/netgear/ @Quentame @starkillerOG
|
||||
/tests/components/netgear/ @Quentame @starkillerOG
|
||||
/homeassistant/components/netgear_lte/ @tkdrob
|
||||
/tests/components/netgear_lte/ @tkdrob
|
||||
/homeassistant/components/network/ @home-assistant/core
|
||||
@@ -1187,6 +1203,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/notify_events/ @matrozov @papajojo
|
||||
/homeassistant/components/notion/ @bachya
|
||||
/tests/components/notion/ @bachya
|
||||
/homeassistant/components/novy_cooker_hood/ @piitaya
|
||||
/tests/components/novy_cooker_hood/ @piitaya
|
||||
/homeassistant/components/nrgkick/ @andijakl
|
||||
/tests/components/nrgkick/ @andijakl
|
||||
/homeassistant/components/nsw_fuel_station/ @nickw444
|
||||
@@ -1223,6 +1241,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/ollama/ @synesthesiam
|
||||
/tests/components/ollama/ @synesthesiam
|
||||
/homeassistant/components/ombi/ @larssont
|
||||
/homeassistant/components/omie/ @luuuis
|
||||
/tests/components/omie/ @luuuis
|
||||
/homeassistant/components/onboarding/ @home-assistant/core
|
||||
/tests/components/onboarding/ @home-assistant/core
|
||||
/homeassistant/components/ondilo_ico/ @JeromeHXP
|
||||
@@ -1241,6 +1261,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/open_meteo/ @frenck
|
||||
/homeassistant/components/open_router/ @joostlek @ab3lson
|
||||
/tests/components/open_router/ @joostlek @ab3lson
|
||||
/homeassistant/components/openai_conversation/ @Shulyaka
|
||||
/tests/components/openai_conversation/ @Shulyaka
|
||||
/homeassistant/components/opendisplay/ @g4bri3lDev
|
||||
/tests/components/opendisplay/ @g4bri3lDev
|
||||
/homeassistant/components/openerz/ @misialq
|
||||
@@ -1399,6 +1421,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/radarr/ @tkdrob
|
||||
/homeassistant/components/radio_browser/ @frenck
|
||||
/tests/components/radio_browser/ @frenck
|
||||
/homeassistant/components/radio_frequency/ @home-assistant/core
|
||||
/tests/components/radio_frequency/ @home-assistant/core
|
||||
/homeassistant/components/radiotherm/ @vinnyfuria
|
||||
/tests/components/radiotherm/ @vinnyfuria
|
||||
/homeassistant/components/rainbird/ @konikvranik @allenporter
|
||||
@@ -1690,8 +1714,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/syncthing/ @zhulik
|
||||
/homeassistant/components/syncthru/ @nielstron
|
||||
/tests/components/syncthru/ @nielstron
|
||||
/homeassistant/components/synology_dsm/ @hacf-fr @Quentame @mib1185
|
||||
/tests/components/synology_dsm/ @hacf-fr @Quentame @mib1185
|
||||
/homeassistant/components/synology_dsm/ @Quentame @mib1185
|
||||
/tests/components/synology_dsm/ @Quentame @mib1185
|
||||
/homeassistant/components/synology_srm/ @aerialls
|
||||
/homeassistant/components/system_bridge/ @timmo001
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
@@ -1722,6 +1746,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/tedee/ @patrickhilker @zweckj
|
||||
/homeassistant/components/telegram_bot/ @hanwg
|
||||
/tests/components/telegram_bot/ @hanwg
|
||||
/homeassistant/components/teleinfo/ @esciara
|
||||
/tests/components/teleinfo/ @esciara
|
||||
/homeassistant/components/tellduslive/ @fredrike
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/teltonika/ @karlbeecken
|
||||
@@ -1824,6 +1850,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/unifi_access/ @imhotep @RaHehl
|
||||
/tests/components/unifi_access/ @imhotep @RaHehl
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifi_discovery/ @RaHehl
|
||||
/tests/components/unifi_discovery/ @RaHehl
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @RaHehl
|
||||
/tests/components/unifiprotect/ @RaHehl
|
||||
@@ -1871,8 +1899,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/version/ @ludeeus
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/homeassistant/components/vicare/ @CFenner
|
||||
/tests/components/vicare/ @CFenner
|
||||
/homeassistant/components/vicare/ @CFenner @lackas
|
||||
/tests/components/vicare/ @CFenner @lackas
|
||||
/homeassistant/components/victron_ble/ @rajlaud
|
||||
/tests/components/victron_ble/ @rajlaud
|
||||
/homeassistant/components/victron_gx/ @tomer-w
|
||||
@@ -1963,8 +1991,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/wled/ @frenck @mik-laj
|
||||
/homeassistant/components/wmspro/ @mback2k
|
||||
/tests/components/wmspro/ @mback2k
|
||||
/homeassistant/components/wolflink/ @adamkrol93 @mtielen
|
||||
/tests/components/wolflink/ @adamkrol93 @mtielen
|
||||
/homeassistant/components/wolflink/ @adamkrol93 @EnjoyingM
|
||||
/tests/components/wolflink/ @adamkrol93 @EnjoyingM
|
||||
/homeassistant/components/workday/ @fabaff @gjohansson-ST
|
||||
/tests/components/workday/ @fabaff @gjohansson-ST
|
||||
/homeassistant/components/worldclock/ @fabaff
|
||||
@@ -1975,8 +2003,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/wsdot/ @ucodery
|
||||
/homeassistant/components/wyoming/ @synesthesiam
|
||||
/tests/components/wyoming/ @synesthesiam
|
||||
/homeassistant/components/xbox/ @hunterjm @tr4nt0r
|
||||
/tests/components/xbox/ @hunterjm @tr4nt0r
|
||||
/homeassistant/components/xbox/ @tr4nt0r
|
||||
/tests/components/xbox/ @tr4nt0r
|
||||
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
|
||||
/tests/components/xiaomi_aqara/ @danielhiversen @syssi
|
||||
/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79
|
||||
|
||||
Generated
+9
-11
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
|
||||
# Automatically generated by hassfest.
|
||||
#
|
||||
# To update, run python3 -m script.hassfest -p docker
|
||||
@@ -19,25 +20,22 @@ ENV \
|
||||
UV_SYSTEM_PYTHON=true \
|
||||
UV_NO_CACHE=true
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Add go2rtc binary
|
||||
COPY --from=ghcr.io/alexxit/go2rtc@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
|
||||
RUN \
|
||||
# Verify go2rtc can be executed
|
||||
go2rtc --version \
|
||||
# Install uv
|
||||
&& pip3 install uv==0.11.1
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY requirements.txt homeassistant/
|
||||
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
# Install uv at the version pinned in the requirements file
|
||||
&& pip3 install --no-cache-dir "uv==$(awk -F'==' '/^uv==/{print $2}' homeassistant/requirements.txt)" \
|
||||
&& uv pip install \
|
||||
--no-build \
|
||||
-r homeassistant/requirements.txt
|
||||
|
||||
@@ -51,7 +49,7 @@ RUN \
|
||||
-r homeassistant/requirements_all.txt
|
||||
|
||||
## Setup Home Assistant Core
|
||||
COPY . homeassistant/
|
||||
COPY --parents LICENSE* README* homeassistant/ pyproject.toml homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
-e ./homeassistant \
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
@@ -7,23 +7,31 @@ to speed up the process.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Container, Iterable, Sequence
|
||||
from datetime import timedelta
|
||||
from functools import lru_cache, partial
|
||||
from typing import Any
|
||||
from functools import lru_cache
|
||||
from typing import Any, override
|
||||
|
||||
from jwt import DecodeError, PyJWS, PyJWT
|
||||
from jwt import DecodeError, PyJWK, PyJWS, PyJWT
|
||||
from jwt.algorithms import AllowedPublicKeys
|
||||
from jwt.types import Options
|
||||
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
JWT_TOKEN_CACHE_SIZE = 16
|
||||
MAX_TOKEN_SIZE = 8192
|
||||
|
||||
_VERIFY_KEYS = ("signature", "exp", "nbf", "iat", "aud", "iss", "sub", "jti")
|
||||
|
||||
_VERIFY_OPTIONS: dict[str, Any] = {f"verify_{key}": True for key in _VERIFY_KEYS} | {
|
||||
"require": []
|
||||
}
|
||||
_NO_VERIFY_OPTIONS = {f"verify_{key}": False for key in _VERIFY_KEYS}
|
||||
_NO_VERIFY_OPTIONS = Options(
|
||||
verify_signature=False,
|
||||
verify_exp=False,
|
||||
verify_nbf=False,
|
||||
verify_iat=False,
|
||||
verify_aud=False,
|
||||
verify_iss=False,
|
||||
verify_sub=False,
|
||||
verify_jti=False,
|
||||
require=[],
|
||||
)
|
||||
|
||||
|
||||
class _PyJWSWithLoadCache(PyJWS):
|
||||
@@ -38,9 +46,6 @@ class _PyJWSWithLoadCache(PyJWS):
|
||||
return super()._load(jwt)
|
||||
|
||||
|
||||
_jws = _PyJWSWithLoadCache()
|
||||
|
||||
|
||||
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
|
||||
def _decode_payload(json_payload: str) -> dict[str, Any]:
|
||||
"""Decode the payload from a JWS dictionary."""
|
||||
@@ -56,21 +61,12 @@ def _decode_payload(json_payload: str) -> dict[str, Any]:
|
||||
class _PyJWTWithVerify(PyJWT):
|
||||
"""PyJWT with a fast decode implementation."""
|
||||
|
||||
def decode_payload(
|
||||
self, jwt: str, key: str, options: dict[str, Any], algorithms: list[str]
|
||||
) -> dict[str, Any]:
|
||||
"""Decode a JWT's payload."""
|
||||
if len(jwt) > MAX_TOKEN_SIZE:
|
||||
# Avoid caching impossible tokens
|
||||
raise DecodeError("Token too large")
|
||||
return _decode_payload(
|
||||
_jws.decode_complete(
|
||||
jwt=jwt,
|
||||
key=key,
|
||||
algorithms=algorithms,
|
||||
options=options,
|
||||
)["payload"]
|
||||
)
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the PyJWT instance."""
|
||||
# We require exp and iat claims to be present
|
||||
super().__init__(Options(require=["exp", "iat"]))
|
||||
# Override the _jws instance with our cached version
|
||||
self._jws = _PyJWSWithLoadCache()
|
||||
|
||||
def verify_and_decode(
|
||||
self,
|
||||
@@ -79,37 +75,70 @@ class _PyJWTWithVerify(PyJWT):
|
||||
algorithms: list[str],
|
||||
issuer: str | None = None,
|
||||
leeway: float | timedelta = 0,
|
||||
options: dict[str, Any] | None = None,
|
||||
options: Options | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Verify a JWT's signature and claims."""
|
||||
merged_options = {**_VERIFY_OPTIONS, **(options or {})}
|
||||
payload = self.decode_payload(
|
||||
return self.decode(
|
||||
jwt=jwt,
|
||||
key=key,
|
||||
options=merged_options,
|
||||
algorithms=algorithms,
|
||||
)
|
||||
# These should never be missing since we verify them
|
||||
# but this is an additional safeguard to make sure
|
||||
# nothing slips through.
|
||||
assert "exp" in payload, "exp claim is required"
|
||||
assert "iat" in payload, "iat claim is required"
|
||||
self._validate_claims(
|
||||
payload=payload,
|
||||
options=merged_options,
|
||||
issuer=issuer,
|
||||
leeway=leeway,
|
||||
options=options,
|
||||
)
|
||||
return payload
|
||||
|
||||
@override
|
||||
def decode(
|
||||
self,
|
||||
jwt: str | bytes,
|
||||
key: AllowedPublicKeys | PyJWK | str | bytes = "",
|
||||
algorithms: Sequence[str] | None = None,
|
||||
options: Options | None = None,
|
||||
verify: bool | None = None,
|
||||
detached_payload: bytes | None = None,
|
||||
audience: str | Iterable[str] | None = None,
|
||||
subject: str | None = None,
|
||||
issuer: str | Container[str] | None = None,
|
||||
leeway: float | timedelta = 0,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""Decode a JWT, verifying the signature and claims."""
|
||||
if len(jwt) > MAX_TOKEN_SIZE:
|
||||
# Avoid caching impossible tokens
|
||||
raise DecodeError("Token too large")
|
||||
return super().decode(
|
||||
jwt=jwt,
|
||||
key=key,
|
||||
algorithms=algorithms,
|
||||
options=options,
|
||||
verify=verify,
|
||||
detached_payload=detached_payload,
|
||||
audience=audience,
|
||||
subject=subject,
|
||||
issuer=issuer,
|
||||
leeway=leeway,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@override
|
||||
def _decode_payload(self, decoded: dict[str, Any]) -> dict[str, Any]:
|
||||
return _decode_payload(decoded["payload"])
|
||||
|
||||
|
||||
_jwt = _PyJWTWithVerify()
|
||||
verify_and_decode = _jwt.verify_and_decode
|
||||
unverified_hs256_token_decode = lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)(
|
||||
partial(
|
||||
_jwt.decode_payload, key="", algorithms=["HS256"], options=_NO_VERIFY_OPTIONS
|
||||
|
||||
|
||||
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
|
||||
def unverified_hs256_token_decode(jwt: str) -> dict[str, Any]:
|
||||
"""Decode a JWT without verifying the signature."""
|
||||
return _jwt.decode(
|
||||
jwt=jwt,
|
||||
key="",
|
||||
algorithms=["HS256"],
|
||||
options=_NO_VERIFY_OPTIONS,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"unverified_hs256_token_decode",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -13,6 +14,9 @@ from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .util import test_all
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..models import User
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
|
||||
|
||||
__all__ = [
|
||||
@@ -22,10 +26,21 @@ __all__ = [
|
||||
"PermissionLookup",
|
||||
"PolicyPermissions",
|
||||
"PolicyType",
|
||||
"filter_entity_ids_by_permission",
|
||||
"merge_policies",
|
||||
]
|
||||
|
||||
|
||||
def filter_entity_ids_by_permission(
|
||||
user: User, entity_ids: Iterable[str], key: str
|
||||
) -> list[str]:
|
||||
"""Filter entity IDs to those the user can access for the given policy key."""
|
||||
if user.is_admin or user.permissions.access_all_entities(key):
|
||||
return list(entity_ids)
|
||||
check_entity = user.permissions.check_entity
|
||||
return [entity_id for entity_id in entity_ids if check_entity(entity_id, key)]
|
||||
|
||||
|
||||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "denon",
|
||||
"name": "Denon",
|
||||
"integrations": ["denon", "denonavr", "heos"]
|
||||
"integrations": ["denon", "denonavr", "denon_rs232", "heos"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "honeywell",
|
||||
"name": "Honeywell",
|
||||
"integrations": ["lyric", "evohome", "honeywell"]
|
||||
"integrations": ["lyric", "evohome", "honeywell", "honeywell_string_lights"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"unifi",
|
||||
"unifi_access",
|
||||
"unifi_direct",
|
||||
"unifi_discovery",
|
||||
"unifiled",
|
||||
"unifiprotect"
|
||||
]
|
||||
|
||||
@@ -30,7 +30,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_POLLING, DOMAIN, DOMAIN_DATA, LOGGER
|
||||
from .const import CONF_POLLING, DOMAIN, LOGGER
|
||||
from .services import async_setup_services
|
||||
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
@@ -67,13 +67,16 @@ class AbodeSystem:
|
||||
logout_listener: CALLBACK_TYPE | None = None
|
||||
|
||||
|
||||
type AbodeConfigEntry = ConfigEntry[AbodeSystem]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Abode component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
|
||||
"""Set up Abode integration from a config entry."""
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
@@ -99,50 +102,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex
|
||||
|
||||
hass.data[DOMAIN_DATA] = AbodeSystem(abode, polling)
|
||||
entry.runtime_data = AbodeSystem(abode, polling)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
await setup_hass_events(hass)
|
||||
await hass.async_add_executor_job(setup_abode_events, hass)
|
||||
await setup_hass_events(hass, entry)
|
||||
await hass.async_add_executor_job(setup_abode_events, hass, entry)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
def _shutdown_client(abode: Abode) -> None:
|
||||
"""Shutdown client."""
|
||||
abode.events.stop()
|
||||
abode.logout()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.events.stop)
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.logout)
|
||||
await hass.async_add_executor_job(_shutdown_client, entry.runtime_data.abode)
|
||||
|
||||
if logout_listener := hass.data[DOMAIN_DATA].logout_listener:
|
||||
if logout_listener := entry.runtime_data.logout_listener:
|
||||
logout_listener()
|
||||
hass.data.pop(DOMAIN_DATA)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def setup_hass_events(hass: HomeAssistant) -> None:
|
||||
async def setup_hass_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
|
||||
"""Home Assistant start and stop callbacks."""
|
||||
|
||||
def logout(event: Event) -> None:
|
||||
"""Logout of Abode."""
|
||||
if not hass.data[DOMAIN_DATA].polling:
|
||||
hass.data[DOMAIN_DATA].abode.events.stop()
|
||||
if not entry.runtime_data.polling:
|
||||
entry.runtime_data.abode.events.stop()
|
||||
|
||||
hass.data[DOMAIN_DATA].abode.logout()
|
||||
entry.runtime_data.abode.logout()
|
||||
LOGGER.info("Logged out of Abode")
|
||||
|
||||
if not hass.data[DOMAIN_DATA].polling:
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.events.start)
|
||||
if not entry.runtime_data.polling:
|
||||
await hass.async_add_executor_job(entry.runtime_data.abode.events.start)
|
||||
|
||||
hass.data[DOMAIN_DATA].logout_listener = hass.bus.async_listen_once(
|
||||
entry.runtime_data.logout_listener = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, logout
|
||||
)
|
||||
|
||||
|
||||
def setup_abode_events(hass: HomeAssistant) -> None:
|
||||
def setup_abode_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
|
||||
"""Event callbacks."""
|
||||
|
||||
def event_callback(event: str, event_json: dict[str, str]) -> None:
|
||||
@@ -179,6 +186,6 @@ def setup_abode_events(hass: HomeAssistant) -> None:
|
||||
]
|
||||
|
||||
for event in events:
|
||||
hass.data[DOMAIN_DATA].abode.events.add_event_callback(
|
||||
entry.runtime_data.abode.events.add_event_callback(
|
||||
event, partial(event_callback, event)
|
||||
)
|
||||
|
||||
@@ -9,21 +9,20 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN_DATA
|
||||
from . import AbodeConfigEntry
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AbodeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode alarm control panel device."""
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
data = entry.runtime_data
|
||||
async_add_entities(
|
||||
[AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
|
||||
)
|
||||
|
||||
@@ -10,22 +10,21 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from .const import DOMAIN_DATA
|
||||
from . import AbodeConfigEntry
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AbodeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode binary sensor devices."""
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
data = entry.runtime_data
|
||||
|
||||
device_types = [
|
||||
"connectivity",
|
||||
|
||||
@@ -12,14 +12,13 @@ import requests
|
||||
from requests.models import Response
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN_DATA, LOGGER
|
||||
from . import AbodeConfigEntry, AbodeSystem
|
||||
from .const import LOGGER
|
||||
from .entity import AbodeDevice
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
@@ -27,11 +26,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AbodeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode camera devices."""
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AbodeCamera(data, device, timeline.CAPTURE_IMAGE)
|
||||
|
||||
@@ -3,17 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AbodeSystem
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "abode"
|
||||
DOMAIN_DATA: HassKey[AbodeSystem] = HassKey(DOMAIN)
|
||||
ATTRIBUTION = "Data provided by goabode.com"
|
||||
|
||||
CONF_POLLING = "polling"
|
||||
|
||||
@@ -5,21 +5,20 @@ from typing import Any
|
||||
from jaraco.abode.devices.cover import Cover
|
||||
|
||||
from homeassistant.components.cover import CoverEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN_DATA
|
||||
from . import AbodeConfigEntry
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AbodeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode cover devices."""
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AbodeCover(data, device)
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import ATTRIBUTION, DOMAIN, DOMAIN_DATA
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
|
||||
|
||||
class AbodeEntity(Entity):
|
||||
@@ -29,7 +29,7 @@ class AbodeEntity(Entity):
|
||||
self._update_connection_status,
|
||||
)
|
||||
|
||||
self.hass.data[DOMAIN_DATA].entity_ids.add(self.entity_id)
|
||||
self._data.entity_ids.add(self.entity_id)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe from Abode connection status updates."""
|
||||
|
||||
@@ -16,21 +16,20 @@ from homeassistant.components.light import (
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN_DATA
|
||||
from . import AbodeConfigEntry
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AbodeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode light devices."""
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AbodeLight(data, device)
|
||||
|
||||
@@ -5,21 +5,20 @@ from typing import Any
|
||||
from jaraco.abode.devices.lock import Lock
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN_DATA
|
||||
from . import AbodeConfigEntry
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AbodeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode lock devices."""
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AbodeLock(data, device)
|
||||
|
||||
@@ -14,13 +14,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN_DATA
|
||||
from . import AbodeConfigEntry, AbodeSystem
|
||||
from .entity import AbodeDevice
|
||||
|
||||
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
||||
@@ -66,11 +64,11 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AbodeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode sensor devices."""
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AbodeSensor(data, device, description)
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from jaraco.abode.exceptions import Exception as AbodeException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import DOMAIN, DOMAIN_DATA, LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AbodeConfigEntry, AbodeSystem
|
||||
|
||||
ATTR_SETTING = "setting"
|
||||
ATTR_VALUE = "value"
|
||||
@@ -25,13 +31,21 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
|
||||
def _get_abode_system(hass: HomeAssistant) -> AbodeSystem:
|
||||
"""Return the Abode system for the loaded config entry."""
|
||||
entries: list[AbodeConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if not entries:
|
||||
raise ServiceValidationError("Abode integration is not loaded")
|
||||
return entries[0].runtime_data
|
||||
|
||||
|
||||
def _change_setting(call: ServiceCall) -> None:
|
||||
"""Change an Abode system setting."""
|
||||
setting = call.data[ATTR_SETTING]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
try:
|
||||
call.hass.data[DOMAIN_DATA].abode.set_setting(setting, value)
|
||||
_get_abode_system(call.hass).abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
@@ -42,7 +56,7 @@ def _capture_image(call: ServiceCall) -> None:
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN_DATA].entity_ids
|
||||
for entity_id in _get_abode_system(call.hass).entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
@@ -57,7 +71,7 @@ def _trigger_automation(call: ServiceCall) -> None:
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN_DATA].entity_ids
|
||||
for entity_id in _get_abode_system(call.hass).entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
|
||||
@@ -7,12 +7,11 @@ from typing import Any, cast
|
||||
from jaraco.abode.devices.switch import Switch
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN_DATA
|
||||
from . import AbodeConfigEntry
|
||||
from .entity import AbodeAutomation, AbodeDevice
|
||||
|
||||
DEVICE_TYPES = ["switch", "valve"]
|
||||
@@ -20,11 +19,11 @@ DEVICE_TYPES = ["switch", "valve"]
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AbodeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode switch devices."""
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
data = entry.runtime_data
|
||||
|
||||
entities: list[SwitchEntity] = [
|
||||
AbodeSwitch(data, device)
|
||||
|
||||
@@ -143,4 +143,4 @@ class AcaiaRestoreSensor(AcaiaEntity, RestoreSensor):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available or self._restored_data is not None
|
||||
return super().available or self.native_value is not None
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp import ClientError
|
||||
@@ -12,7 +12,7 @@ from aiohttp.client_exceptions import ClientConnectorError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
@@ -55,8 +55,11 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert accuweather.location_name is not None
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
title=accuweather.location_name, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -70,9 +73,6 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
vol.Optional(
|
||||
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||
): cv.longitude,
|
||||
vol.Optional(
|
||||
CONF_NAME, default=self.hass.config.location_name
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
|
||||
@@ -64,7 +64,7 @@ class AccuWeatherObservationDataUpdateCoordinator(
|
||||
"""Initialize."""
|
||||
self.accuweather = accuweather
|
||||
self.location_key = accuweather.location_key
|
||||
name = config_entry.data[CONF_NAME]
|
||||
name = config_entry.data.get(CONF_NAME) or config_entry.title
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.location_key is not None
|
||||
@@ -122,7 +122,7 @@ class AccuWeatherForecastDataUpdateCoordinator(
|
||||
self.accuweather = accuweather
|
||||
self.location_key = accuweather.location_key
|
||||
self._fetch_method = fetch_method
|
||||
name = config_entry.data[CONF_NAME]
|
||||
name = config_entry.data.get(CONF_NAME) or config_entry.title
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.location_key is not None
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "API key generated in the AccuWeather APIs portal."
|
||||
|
||||
@@ -6,10 +6,11 @@ from typing import Final
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
|
||||
CONF_READ_TIMEOUT: Final = "timeout"
|
||||
CONF_WRITE_TIMEOUT: Final = "write_timeout"
|
||||
|
||||
DEFAULT_NAME: Final = "Acer Projector"
|
||||
DEFAULT_TIMEOUT: Final = 1
|
||||
DEFAULT_READ_TIMEOUT: Final = 1
|
||||
DEFAULT_WRITE_TIMEOUT: Final = 1
|
||||
|
||||
ECO_MODE: Final = "ECO Mode"
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyserial==3.5"]
|
||||
"requirements": ["serialx==1.4.1"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import serial
|
||||
from serialx import Serial, SerialException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
@@ -16,21 +16,22 @@ from homeassistant.components.switch import (
|
||||
from homeassistant.const import (
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
CONF_TIMEOUT,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CMD_DICT,
|
||||
CONF_READ_TIMEOUT,
|
||||
CONF_WRITE_TIMEOUT,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_TIMEOUT,
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
DEFAULT_WRITE_TIMEOUT,
|
||||
ECO_MODE,
|
||||
ICON,
|
||||
@@ -45,7 +46,7 @@ PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_FILENAME): cv.isdevice,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_READ_TIMEOUT, default=DEFAULT_READ_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT
|
||||
): cv.positive_int,
|
||||
@@ -62,10 +63,10 @@ def setup_platform(
|
||||
"""Connect with serial port and return Acer Projector."""
|
||||
serial_port = config[CONF_FILENAME]
|
||||
name = config[CONF_NAME]
|
||||
timeout = config[CONF_TIMEOUT]
|
||||
read_timeout = config[CONF_READ_TIMEOUT]
|
||||
write_timeout = config[CONF_WRITE_TIMEOUT]
|
||||
|
||||
add_entities([AcerSwitch(serial_port, name, timeout, write_timeout)], True)
|
||||
add_entities([AcerSwitch(serial_port, name, read_timeout, write_timeout)], True)
|
||||
|
||||
|
||||
class AcerSwitch(SwitchEntity):
|
||||
@@ -77,14 +78,14 @@ class AcerSwitch(SwitchEntity):
|
||||
self,
|
||||
serial_port: str,
|
||||
name: str,
|
||||
timeout: int,
|
||||
read_timeout: int,
|
||||
write_timeout: int,
|
||||
) -> None:
|
||||
"""Init of the Acer projector."""
|
||||
self.serial = serial.Serial(
|
||||
port=serial_port, timeout=timeout, write_timeout=write_timeout
|
||||
)
|
||||
self._serial_port = serial_port
|
||||
self._read_timeout = read_timeout
|
||||
self._write_timeout = write_timeout
|
||||
|
||||
self._attr_name = name
|
||||
self._attributes = {
|
||||
LAMP_HOURS: STATE_UNKNOWN,
|
||||
@@ -94,22 +95,26 @@ class AcerSwitch(SwitchEntity):
|
||||
|
||||
def _write_read(self, msg: str) -> str:
|
||||
"""Write to the projector and read the return."""
|
||||
ret = ""
|
||||
|
||||
# Sometimes the projector won't answer for no reason or the projector
|
||||
# was disconnected during runtime.
|
||||
# This way the projector can be reconnected and will still work
|
||||
try:
|
||||
if not self.serial.is_open:
|
||||
self.serial.open()
|
||||
self.serial.write(msg.encode("utf-8"))
|
||||
# Size is an experience value there is no real limit.
|
||||
# AFAIK there is no limit and no end character so we will usually
|
||||
# need to wait for timeout
|
||||
ret = self.serial.read_until(size=20).decode("utf-8")
|
||||
except serial.SerialException:
|
||||
_LOGGER.error("Problem communicating with %s", self._serial_port)
|
||||
self.serial.close()
|
||||
return ret
|
||||
with Serial.from_url(
|
||||
self._serial_port,
|
||||
read_timeout=self._read_timeout,
|
||||
write_timeout=self._write_timeout,
|
||||
) as serial:
|
||||
serial.write(msg.encode("utf-8"))
|
||||
|
||||
# Size is an experience value there is no real limit.
|
||||
# AFAIK there is no limit and no end character so we will usually
|
||||
# need to wait for timeout
|
||||
return serial.read_until(size=20).decode("utf-8")
|
||||
except (OSError, SerialException, TimeoutError) as exc:
|
||||
raise HomeAssistantError(
|
||||
f"Problem communicating with {self._serial_port}"
|
||||
) from exc
|
||||
|
||||
def _write_read_format(self, msg: str) -> str:
|
||||
"""Write msg, obtain answer and format output."""
|
||||
|
||||
@@ -15,8 +15,10 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
|
||||
from .entity import ActronAirAcEntity, ActronAirZoneEntity, actron_air_command
|
||||
|
||||
@@ -36,6 +38,7 @@ HVAC_MODE_MAPPING_ACTRONAIR_TO_HA = {
|
||||
"HEAT": HVACMode.HEAT,
|
||||
"FAN": HVACMode.FAN_ONLY,
|
||||
"AUTO": HVACMode.AUTO,
|
||||
"DRY": HVACMode.DRY,
|
||||
"OFF": HVACMode.OFF,
|
||||
}
|
||||
HVAC_MODE_MAPPING_HA_TO_ACTRONAIR = {
|
||||
@@ -77,7 +80,6 @@ class ActronAirClimateEntity(ClimateEntity):
|
||||
)
|
||||
_attr_name = None
|
||||
_attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values())
|
||||
_attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values())
|
||||
|
||||
|
||||
class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
|
||||
@@ -91,6 +93,17 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = self._serial_number
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Return the list of supported HVAC modes."""
|
||||
modes = [
|
||||
HVAC_MODE_MAPPING_ACTRONAIR_TO_HA[mode]
|
||||
for mode in self._status.user_aircon_settings.supported_modes
|
||||
if mode in HVAC_MODE_MAPPING_ACTRONAIR_TO_HA
|
||||
]
|
||||
modes.append(HVACMode.OFF)
|
||||
return modes
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum temperature that can be set."""
|
||||
@@ -139,20 +152,24 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
|
||||
@actron_air_command
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set a new fan mode."""
|
||||
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode)
|
||||
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR[fan_mode]
|
||||
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
|
||||
|
||||
@actron_air_command
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set the HVAC mode."""
|
||||
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode)
|
||||
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR[hvac_mode]
|
||||
await self._status.ac_system.set_system_mode(ac_mode)
|
||||
|
||||
@actron_air_command
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the temperature."""
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
await self._status.user_aircon_settings.set_temperature(temperature=temp)
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="temperature_missing",
|
||||
)
|
||||
await self._status.user_aircon_settings.set_temperature(temperature=temperature)
|
||||
|
||||
|
||||
class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
|
||||
@@ -173,6 +190,18 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
|
||||
super().__init__(coordinator, zone)
|
||||
self._attr_unique_id: str = self._zone_identifier
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Return the list of supported HVAC modes."""
|
||||
status = self.coordinator.data
|
||||
modes = [
|
||||
HVAC_MODE_MAPPING_ACTRONAIR_TO_HA[mode]
|
||||
for mode in status.user_aircon_settings.supported_modes
|
||||
if mode in HVAC_MODE_MAPPING_ACTRONAIR_TO_HA
|
||||
]
|
||||
modes.append(HVACMode.OFF)
|
||||
return modes
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum temperature that can be set."""
|
||||
@@ -221,4 +250,9 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
|
||||
@actron_air_command
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the temperature."""
|
||||
await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE))
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="temperature_missing",
|
||||
)
|
||||
await self._zone.set_temperature(temperature=temperature)
|
||||
|
||||
@@ -23,7 +23,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._user_code: str = ""
|
||||
self._verification_uri: str = ""
|
||||
self._expires_minutes: str = "30"
|
||||
self.login_task: asyncio.Task | None = None
|
||||
self.login_task: asyncio.Task[None] | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -94,7 +94,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.error("Error getting user info: %s", err)
|
||||
return self.async_abort(reason="oauth2_error")
|
||||
|
||||
unique_id = str(user_data["id"])
|
||||
unique_id = user_data.sub
|
||||
await self.async_set_unique_id(unique_id)
|
||||
|
||||
# Check if this is a reauth flow
|
||||
@@ -107,7 +107,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=user_data["email"],
|
||||
title=user_data.email,
|
||||
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
||||
)
|
||||
|
||||
|
||||
@@ -78,7 +78,14 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirStatus]):
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||
status = self.api.state_manager.get_status(self.serial_number)
|
||||
if status is None:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": "Status not available"},
|
||||
)
|
||||
self.status = status
|
||||
self.last_seen = dt_util.utcnow()
|
||||
return self.status
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ def actron_air_command[_EntityT: ActronAirEntity, **_P](
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
async def wrapper(self: _EntityT, /, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
"""Wrap API calls with exception handling."""
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["actron-neo-api==0.5.0"]
|
||||
"requirements": ["actron-neo-api==0.5.6"]
|
||||
}
|
||||
|
||||
@@ -54,18 +54,12 @@ rules:
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: todo
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: This integration does not use entity categories.
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: This integration does not use entity device classes.
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: Not required for this integration at this stage.
|
||||
entity-translations: todo
|
||||
entity-category: done
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
@@ -75,4 +69,4 @@ rules:
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
strict-typing: done
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
"setup_connection_error": {
|
||||
"message": "Failed to connect to the Actron Air API"
|
||||
},
|
||||
"temperature_missing": {
|
||||
"message": "Provide a temperature value when adjusting the climate entity."
|
||||
},
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from the Actron Air API: {error}"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
hass.data[DATA_MEDIA_SOURCE] = source = local_source.LocalSource(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"AI Generated Images",
|
||||
"AI generated images",
|
||||
{IMAGE_DIR: str(media_dir)},
|
||||
f"/{DOMAIN}",
|
||||
)
|
||||
|
||||
@@ -36,7 +36,9 @@ def _make_detected_condition(
|
||||
) -> type[Condition]:
|
||||
"""Create a detected condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
|
||||
STATE_ON,
|
||||
support_duration=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +47,9 @@ def _make_cleared_condition(
|
||||
) -> type[Condition]:
|
||||
"""Create a cleared condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
|
||||
STATE_OFF,
|
||||
support_duration=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,11 +4,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
|
||||
# --- Unit lists for multi-unit pollutants ---
|
||||
|
||||
@@ -249,6 +246,11 @@
|
||||
.condition_binary_common: &condition_binary_common
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_gas_detected:
|
||||
<<: *condition_binary_common
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least",
|
||||
"trigger_threshold_name": "Threshold type"
|
||||
},
|
||||
"conditions": {
|
||||
@@ -23,6 +25,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide cleared"
|
||||
@@ -32,6 +37,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide detected"
|
||||
@@ -53,6 +61,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas cleared"
|
||||
@@ -62,6 +73,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas detected"
|
||||
@@ -167,6 +181,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke cleared"
|
||||
@@ -176,6 +193,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke detected"
|
||||
@@ -217,21 +237,6 @@
|
||||
"name": "Volatile organic compounds value"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Air Quality",
|
||||
"triggers": {
|
||||
"co2_changed": {
|
||||
@@ -249,6 +254,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -269,6 +277,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide cleared"
|
||||
@@ -279,6 +290,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -290,6 +304,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide detected"
|
||||
@@ -299,6 +316,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas cleared"
|
||||
@@ -308,6 +328,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas detected"
|
||||
@@ -327,6 +350,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -348,6 +374,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -369,6 +398,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -390,6 +422,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -411,6 +446,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -432,6 +470,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -453,6 +494,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -474,6 +518,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -485,6 +532,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke cleared"
|
||||
@@ -494,6 +544,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke detected"
|
||||
@@ -513,6 +566,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -534,6 +590,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -555,6 +614,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for: &trigger_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
# --- Unit lists for multi-unit pollutants ---
|
||||
|
||||
@@ -163,6 +164,7 @@
|
||||
# Binary sensor detected/cleared trigger fields
|
||||
.trigger_binary_fields: &trigger_binary_fields
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
|
||||
# --- Binary sensor targets ---
|
||||
|
||||
@@ -294,6 +296,7 @@ co_crossed_threshold:
|
||||
target: *target_co_sensor
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -320,6 +323,7 @@ co2_crossed_threshold:
|
||||
target: *target_co2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -344,6 +348,7 @@ pm1_crossed_threshold:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -368,6 +373,7 @@ pm25_crossed_threshold:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -392,6 +398,7 @@ pm4_crossed_threshold:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -416,6 +423,7 @@ pm10_crossed_threshold:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -442,6 +450,7 @@ ozone_crossed_threshold:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -470,6 +479,7 @@ voc_crossed_threshold:
|
||||
target: *target_voc
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -498,6 +508,7 @@ voc_ratio_crossed_threshold:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -526,6 +537,7 @@ no_crossed_threshold:
|
||||
target: *target_no
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -554,6 +566,7 @@ no2_crossed_threshold:
|
||||
target: *target_no2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -580,6 +593,7 @@ n2o_crossed_threshold:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
@@ -606,6 +620,7 @@ so2_crossed_threshold:
|
||||
target: *target_so2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
|
||||
@@ -12,11 +12,11 @@ from airly.exceptions import AirlyError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS
|
||||
from .const import CONF_USE_NEAREST, DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS
|
||||
|
||||
DESCRIPTION_PLACEHOLDERS = {
|
||||
"developer_registration_url": "https://developer.airly.eu/register",
|
||||
@@ -45,16 +45,16 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
location_point_valid = await check_location(
|
||||
websession,
|
||||
user_input["api_key"],
|
||||
user_input["latitude"],
|
||||
user_input["longitude"],
|
||||
user_input[CONF_API_KEY],
|
||||
user_input[CONF_LATITUDE],
|
||||
user_input[CONF_LONGITUDE],
|
||||
)
|
||||
if not location_point_valid:
|
||||
location_nearest_valid = await check_location(
|
||||
websession,
|
||||
user_input["api_key"],
|
||||
user_input["latitude"],
|
||||
user_input["longitude"],
|
||||
user_input[CONF_API_KEY],
|
||||
user_input[CONF_LATITUDE],
|
||||
user_input[CONF_LONGITUDE],
|
||||
use_nearest=True,
|
||||
)
|
||||
except AirlyError as err:
|
||||
@@ -68,7 +68,7 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="wrong_location")
|
||||
use_nearest = True
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME],
|
||||
title=DEFAULT_NAME,
|
||||
data={**user_input, CONF_USE_NEAREST: use_nearest},
|
||||
)
|
||||
|
||||
@@ -83,9 +83,6 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
vol.Optional(
|
||||
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||
): cv.longitude,
|
||||
vol.Optional(
|
||||
CONF_NAME, default=self.hass.config.location_name
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
|
||||
@@ -37,3 +37,5 @@ MAX_UPDATE_INTERVAL: Final = 90
|
||||
MIN_UPDATE_INTERVAL: Final = 5
|
||||
NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet."
|
||||
URL = "https://airly.org/map/#{latitude},{longitude}"
|
||||
|
||||
DEFAULT_NAME: Final = "Airly"
|
||||
|
||||
@@ -127,7 +127,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_CO,
|
||||
translation_key="co",
|
||||
device_class=SensorDeviceClass.CO,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
@@ -178,7 +178,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Airly sensor entities based on a config entry."""
|
||||
name = entry.data[CONF_NAME]
|
||||
name = entry.data.get(CONF_NAME) or entry.title
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]"
|
||||
},
|
||||
"description": "To generate API key go to {developer_registration_url}"
|
||||
}
|
||||
@@ -24,9 +23,6 @@
|
||||
"sensor": {
|
||||
"caqi": {
|
||||
"name": "Common air quality index"
|
||||
},
|
||||
"co": {
|
||||
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -36,6 +36,8 @@ class AirTouch5ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors = {"base": "cannot_connect"}
|
||||
else:
|
||||
# Uses the host/IP value from CONF_HOST as unique ID, which is no longer allowed
|
||||
# pylint: disable-next=hass-unique-id-ip-based
|
||||
await self.async_set_unique_id(user_input[CONF_HOST])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
|
||||
@@ -4,6 +4,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR,
|
||||
Condition,
|
||||
EntityStateConditionBase,
|
||||
make_entity_state_condition,
|
||||
@@ -25,6 +26,7 @@ class EntityStateRequiredFeaturesCondition(EntityStateConditionBase):
|
||||
"""State condition."""
|
||||
|
||||
_required_features: int
|
||||
_schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR
|
||||
|
||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain with the required features."""
|
||||
@@ -82,9 +84,11 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||
),
|
||||
"is_disarmed": make_entity_state_condition(DOMAIN, AlarmControlPanelState.DISARMED),
|
||||
"is_disarmed": make_entity_state_condition(
|
||||
DOMAIN, AlarmControlPanelState.DISARMED, support_duration=True
|
||||
),
|
||||
"is_triggered": make_entity_state_condition(
|
||||
DOMAIN, AlarmControlPanelState.TRIGGERED
|
||||
DOMAIN, AlarmControlPanelState.TRIGGERED, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
target: &condition_common_target
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
fields: &condition_common_fields
|
||||
behavior:
|
||||
behavior: &condition_common_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
|
||||
.condition_common_for: &condition_common_for
|
||||
target: *condition_common_target
|
||||
fields: &condition_common_for_fields
|
||||
behavior: *condition_common_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_armed: *condition_common
|
||||
|
||||
is_armed_away:
|
||||
fields: *condition_common_fields
|
||||
fields: *condition_common_for_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
@@ -24,7 +31,7 @@ is_armed_away:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
|
||||
is_armed_home:
|
||||
fields: *condition_common_fields
|
||||
fields: *condition_common_for_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
@@ -32,7 +39,7 @@ is_armed_home:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
|
||||
|
||||
is_armed_night:
|
||||
fields: *condition_common_fields
|
||||
fields: *condition_common_for_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
@@ -40,13 +47,13 @@ is_armed_night:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
|
||||
is_armed_vacation:
|
||||
fields: *condition_common_fields
|
||||
fields: *condition_common_for_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
supported_features:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
|
||||
|
||||
is_disarmed: *condition_common
|
||||
is_disarmed: *condition_common_for
|
||||
|
||||
is_triggered: *condition_common
|
||||
is_triggered: *condition_common_for
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
"conditions": {
|
||||
"is_armed": {
|
||||
@@ -18,6 +20,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed away"
|
||||
@@ -27,6 +32,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed home"
|
||||
@@ -36,6 +44,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed night"
|
||||
@@ -45,6 +56,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed vacation"
|
||||
@@ -54,6 +68,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is disarmed"
|
||||
@@ -63,6 +80,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is triggered"
|
||||
@@ -140,21 +160,6 @@
|
||||
"message": "Arming requires a code but none was given for {entity_id}."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"alarm_arm_away": {
|
||||
"description": "Arms an alarm in the away mode.",
|
||||
@@ -234,6 +239,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm armed"
|
||||
@@ -243,6 +251,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm armed away"
|
||||
@@ -252,6 +263,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm armed home"
|
||||
@@ -261,6 +275,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm armed night"
|
||||
@@ -270,6 +287,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm armed vacation"
|
||||
@@ -279,6 +299,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm disarmed"
|
||||
@@ -288,6 +311,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm triggered"
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
armed: *trigger_common
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from .services import async_setup_services
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.NOTIFY,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Support for buttons."""
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .entity import AmazonServiceEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up button entities for Alexa Devices."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
known_routines: set[str] = set()
|
||||
|
||||
def _check_routines() -> None:
|
||||
current_routines = set(coordinator.api.routines)
|
||||
new_routines = current_routines - known_routines
|
||||
if new_routines:
|
||||
known_routines.update(new_routines)
|
||||
async_add_entities(
|
||||
AmazonRoutineButton(coordinator, routine) for routine in new_routines
|
||||
)
|
||||
|
||||
_check_routines()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_routines))
|
||||
|
||||
|
||||
class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
|
||||
"""Button entity for Alexa routine."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: AmazonDevicesCoordinator, routine: str) -> None:
|
||||
"""Initialize the routine button entity."""
|
||||
self._coordinator = coordinator
|
||||
self._routine = routine
|
||||
super().__init__(
|
||||
coordinator,
|
||||
EntityDescription(key=slugify(routine), name=routine),
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press action."""
|
||||
await self._coordinator.api.call_routine(self._routine)
|
||||
@@ -12,12 +12,13 @@ from aioamazondevices.structures import AmazonDevice
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
@@ -54,7 +55,23 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
)
|
||||
self.previous_devices: set[str] = set()
|
||||
device_registry = dr.async_get(hass)
|
||||
self.previous_devices: set[str] = {
|
||||
identifier
|
||||
for device in device_registry.devices.get_devices_for_config_entry_id(
|
||||
entry.entry_id
|
||||
)
|
||||
if device.entry_type != dr.DeviceEntryType.SERVICE
|
||||
for identifier_domain, identifier in device.identifiers
|
||||
if identifier_domain == DOMAIN
|
||||
}
|
||||
self.previous_routines: set[str] = {
|
||||
routine.unique_id
|
||||
for routine in er.async_entries_for_config_entry(
|
||||
er.async_get(hass), entry.entry_id
|
||||
)
|
||||
if routine.domain == Platform.BUTTON
|
||||
}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||
"""Update device data."""
|
||||
@@ -83,8 +100,13 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
current_devices = set(data.keys())
|
||||
if stale_devices := self.previous_devices - current_devices:
|
||||
await self._async_remove_device_stale(stale_devices)
|
||||
|
||||
self.previous_devices = current_devices
|
||||
|
||||
current_routines = {slugify(routine) for routine in self.api.routines}
|
||||
if stale_routines := self.previous_routines - current_routines:
|
||||
await self._async_remove_routine_stale(stale_routines)
|
||||
self.previous_routines = current_routines
|
||||
|
||||
return data
|
||||
|
||||
async def _async_remove_device_stale(
|
||||
@@ -107,3 +129,23 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
device_id=device.id,
|
||||
remove_config_entry_id=self.config_entry.entry_id,
|
||||
)
|
||||
|
||||
async def _async_remove_routine_stale(
|
||||
self,
|
||||
stale_routines: set[str],
|
||||
) -> None:
|
||||
"""Remove stale routine."""
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
for routine in stale_routines:
|
||||
_LOGGER.debug(
|
||||
"Detected change in routines: routine %s removed",
|
||||
routine,
|
||||
)
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON,
|
||||
DOMAIN,
|
||||
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
|
||||
)
|
||||
if entity_id:
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AmazonDevicesCoordinator
|
||||
@@ -50,3 +51,32 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
and self._serial_num in self.coordinator.data
|
||||
and self.device.online
|
||||
)
|
||||
|
||||
|
||||
class AmazonServiceEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Defines Alexa Devices entity for service device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the service entity."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, service_device_id(coordinator))},
|
||||
manufacturer="Amazon",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{slugify(coordinator.config_entry.unique_id)}-{description.key}"
|
||||
)
|
||||
|
||||
|
||||
def service_device_id(coordinator: AmazonDevicesCoordinator) -> str:
|
||||
"""Return service device id."""
|
||||
return slugify(f"{coordinator.config_entry.unique_id}_service_device")
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.3.2"]
|
||||
"requirements": ["aioamazondevices==13.4.3"]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from .binary_sensor import BINARY_SENSOR_KEYS, BINARY_SENSORS, check_binary_sensors
|
||||
from .camera import STREAM_SOURCE_LIST
|
||||
from .const import (
|
||||
CAMERAS,
|
||||
COMM_RETRIES,
|
||||
COMM_TIMEOUT,
|
||||
DATA_AMCREST,
|
||||
@@ -359,7 +358,7 @@ def _start_event_monitor(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
hass.data.setdefault(DATA_AMCREST, {DEVICES: {}, CAMERAS: []})
|
||||
hass.data.setdefault(DATA_AMCREST, {DEVICES: {}})
|
||||
|
||||
for device in config[DOMAIN]:
|
||||
name: str = device[CONF_NAME]
|
||||
|
||||
@@ -12,13 +12,11 @@ import aiohttp
|
||||
from aiohttp import web
|
||||
from amcrest import AmcrestError
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream,
|
||||
async_aiohttp_proxy_web,
|
||||
@@ -29,11 +27,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
ATTR_COLOR_BW,
|
||||
CAMERA_WEB_SESSION_TIMEOUT,
|
||||
CAMERAS,
|
||||
CBW,
|
||||
COMM_TIMEOUT,
|
||||
DATA_AMCREST,
|
||||
DEVICES,
|
||||
MOV,
|
||||
RESOLUTION_TO_STREAM,
|
||||
SERVICE_UPDATE,
|
||||
SNAPSHOT_TIMEOUT,
|
||||
@@ -49,65 +49,11 @@ SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
STREAM_SOURCE_LIST = ["snapshot", "mjpeg", "rtsp"]
|
||||
|
||||
_ATTR_PTZ_TT = "travel_time"
|
||||
_ATTR_PTZ_MOV = "movement"
|
||||
_MOV = [
|
||||
"zoom_out",
|
||||
"zoom_in",
|
||||
"right",
|
||||
"left",
|
||||
"up",
|
||||
"down",
|
||||
"right_down",
|
||||
"right_up",
|
||||
"left_down",
|
||||
"left_up",
|
||||
]
|
||||
_ZOOM_ACTIONS = ["ZoomWide", "ZoomTele"]
|
||||
_MOVE_1_ACTIONS = ["Right", "Left", "Up", "Down"]
|
||||
_MOVE_2_ACTIONS = ["RightDown", "RightUp", "LeftDown", "LeftUp"]
|
||||
_ACTION = _ZOOM_ACTIONS + _MOVE_1_ACTIONS + _MOVE_2_ACTIONS
|
||||
|
||||
_DEFAULT_TT = 0.2
|
||||
|
||||
_ATTR_PRESET = "preset"
|
||||
_ATTR_COLOR_BW = "color_bw"
|
||||
|
||||
_CBW_COLOR = "color"
|
||||
_CBW_AUTO = "auto"
|
||||
_CBW_BW = "bw"
|
||||
_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
|
||||
|
||||
_SRV_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids})
|
||||
_SRV_GOTO_SCHEMA = _SRV_SCHEMA.extend(
|
||||
{vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))}
|
||||
)
|
||||
_SRV_CBW_SCHEMA = _SRV_SCHEMA.extend({vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)})
|
||||
_SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV),
|
||||
vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float,
|
||||
}
|
||||
)
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
"enable_recording": (_SRV_SCHEMA, "async_enable_recording", ()),
|
||||
"disable_recording": (_SRV_SCHEMA, "async_disable_recording", ()),
|
||||
"enable_audio": (_SRV_SCHEMA, "async_enable_audio", ()),
|
||||
"disable_audio": (_SRV_SCHEMA, "async_disable_audio", ()),
|
||||
"enable_motion_recording": (_SRV_SCHEMA, "async_enable_motion_recording", ()),
|
||||
"disable_motion_recording": (_SRV_SCHEMA, "async_disable_motion_recording", ()),
|
||||
"goto_preset": (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)),
|
||||
"set_color_bw": (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)),
|
||||
"start_tour": (_SRV_SCHEMA, "async_start_tour", ()),
|
||||
"stop_tour": (_SRV_SCHEMA, "async_stop_tour", ()),
|
||||
"ptz_control": (
|
||||
_SRV_PTZ_SCHEMA,
|
||||
"async_ptz_control",
|
||||
(_ATTR_PTZ_MOV, _ATTR_PTZ_TT),
|
||||
),
|
||||
}
|
||||
|
||||
_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
|
||||
|
||||
|
||||
@@ -275,7 +221,7 @@ class AmcrestCam(Camera):
|
||||
self._motion_recording_enabled
|
||||
)
|
||||
if self._color_bw is not None:
|
||||
attr[_ATTR_COLOR_BW] = self._color_bw
|
||||
attr[ATTR_COLOR_BW] = self._color_bw
|
||||
return attr
|
||||
|
||||
@property
|
||||
@@ -322,15 +268,7 @@ class AmcrestCam(Camera):
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to signals and add camera to list."""
|
||||
self._unsub_dispatcher.extend(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
service_signal(service, self.entity_id),
|
||||
getattr(self, callback_name),
|
||||
)
|
||||
for service, (_, callback_name, _) in CAMERA_SERVICES.items()
|
||||
)
|
||||
"""Subscribe to signals."""
|
||||
self._unsub_dispatcher.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
@@ -338,11 +276,9 @@ class AmcrestCam(Camera):
|
||||
self.async_on_demand_update,
|
||||
)
|
||||
)
|
||||
self.hass.data[DATA_AMCREST][CAMERAS].append(self.entity_id)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove camera from list and disconnect from signals."""
|
||||
self.hass.data[DATA_AMCREST][CAMERAS].remove(self.entity_id)
|
||||
"""Disconnect from signals."""
|
||||
for unsub_dispatcher in self._unsub_dispatcher:
|
||||
unsub_dispatcher()
|
||||
|
||||
@@ -456,7 +392,7 @@ class AmcrestCam(Camera):
|
||||
|
||||
async def async_ptz_control(self, movement: str, travel_time: float) -> None:
|
||||
"""Move or zoom camera in specified direction."""
|
||||
code = _ACTION[_MOV.index(movement)]
|
||||
code = _ACTION[MOV.index(movement)]
|
||||
|
||||
kwargs = {"code": code, "arg1": 0, "arg2": 0, "arg3": 0}
|
||||
if code in _MOVE_1_ACTIONS:
|
||||
@@ -613,10 +549,10 @@ class AmcrestCam(Camera):
|
||||
)
|
||||
|
||||
async def _async_get_color_mode(self) -> str:
|
||||
return _CBW[await self._api.async_day_night_color]
|
||||
return CBW[await self._api.async_day_night_color]
|
||||
|
||||
async def _async_set_color_mode(self, cbw: str) -> None:
|
||||
await self._api.async_set_day_night_color(_CBW.index(cbw), channel=0)
|
||||
await self._api.async_set_day_night_color(CBW.index(cbw), channel=0)
|
||||
|
||||
async def _async_set_color_bw(self, cbw: str) -> None:
|
||||
"""Set camera color mode."""
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
DOMAIN = "amcrest"
|
||||
DATA_AMCREST = DOMAIN
|
||||
CAMERAS = "cameras"
|
||||
DEVICES = "devices"
|
||||
|
||||
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
|
||||
@@ -17,3 +16,18 @@ SERVICE_UPDATE = "update"
|
||||
|
||||
RESOLUTION_LIST = {"high": 0, "low": 1}
|
||||
RESOLUTION_TO_STREAM = {0: "Main", 1: "Extra"}
|
||||
|
||||
ATTR_COLOR_BW = "color_bw"
|
||||
CBW = ["color", "auto", "bw"]
|
||||
MOV = [
|
||||
"zoom_out",
|
||||
"zoom_in",
|
||||
"right",
|
||||
"left",
|
||||
"up",
|
||||
"down",
|
||||
"right_down",
|
||||
"right_up",
|
||||
"left_down",
|
||||
"left_up",
|
||||
]
|
||||
|
||||
@@ -1,62 +1,67 @@
|
||||
"""Support for Amcrest IP cameras."""
|
||||
"""Services for Amcrest IP cameras."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
import voluptuous as vol
|
||||
|
||||
from .camera import CAMERA_SERVICES
|
||||
from .const import CAMERAS, DATA_AMCREST, DOMAIN
|
||||
from .helpers import service_signal
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import ATTR_COLOR_BW, CBW, DOMAIN, MOV
|
||||
|
||||
_ATTR_PRESET = "preset"
|
||||
_ATTR_PTZ_MOV = "movement"
|
||||
_ATTR_PTZ_TT = "travel_time"
|
||||
_DEFAULT_TT = 0.2
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the Amcrest IP Camera services."""
|
||||
for service_name, func in (
|
||||
("enable_recording", "async_enable_recording"),
|
||||
("disable_recording", "async_disable_recording"),
|
||||
("enable_audio", "async_enable_audio"),
|
||||
("disable_audio", "async_disable_audio"),
|
||||
("enable_motion_recording", "async_enable_motion_recording"),
|
||||
("disable_motion_recording", "async_disable_motion_recording"),
|
||||
("start_tour", "async_start_tour"),
|
||||
("stop_tour", "async_stop_tour"),
|
||||
):
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
service_name,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func=func,
|
||||
)
|
||||
|
||||
def have_permission(user: User | None, entity_id: str) -> bool:
|
||||
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call: ServiceCall) -> list[str]:
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
||||
return []
|
||||
|
||||
call_ids = await async_extract_entity_ids(call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call: ServiceCall) -> None:
|
||||
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"goto_preset",
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))},
|
||||
func="async_goto_preset",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"set_color_bw",
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={vol.Required(ATTR_COLOR_BW): vol.In(CBW)},
|
||||
func="async_set_color_bw",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"ptz_control",
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={
|
||||
vol.Required(_ATTR_PTZ_MOV): vol.In(MOV),
|
||||
vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float,
|
||||
},
|
||||
func="async_ptz_control",
|
||||
)
|
||||
|
||||
@@ -76,6 +76,7 @@ from .const import (
|
||||
ATTR_HEALTHY,
|
||||
ATTR_INTEGRATION_COUNT,
|
||||
ATTR_INTEGRATIONS,
|
||||
ATTR_ISSUE_TRACKER,
|
||||
ATTR_OPERATING_SYSTEM,
|
||||
ATTR_PROTECTED,
|
||||
ATTR_RECORDER,
|
||||
@@ -414,6 +415,7 @@ class Analytics:
|
||||
custom_integrations.append(
|
||||
{
|
||||
ATTR_DOMAIN: integration.domain,
|
||||
ATTR_ISSUE_TRACKER: integration.issue_tracker,
|
||||
ATTR_VERSION: integration.version,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ ATTR_HEALTHY = "healthy"
|
||||
ATTR_INSTALLATION_TYPE = "installation_type"
|
||||
ATTR_INTEGRATION_COUNT = "integration_count"
|
||||
ATTR_INTEGRATIONS = "integrations"
|
||||
ATTR_ISSUE_TRACKER = "issue_tracker"
|
||||
ATTR_ONBOARDED = "onboarded"
|
||||
ATTR_OPERATING_SYSTEM = "operating_system"
|
||||
ATTR_PREFERENCES = "preferences"
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"tracked_apps": "Apps",
|
||||
"tracked_custom_integrations": "Community integrations",
|
||||
"tracked_custom_integrations": "Custom integrations",
|
||||
"tracked_integrations": "Integrations"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_apps": "Select the apps you want to track",
|
||||
"tracked_custom_integrations": "Select the community integrations you want to track",
|
||||
"tracked_custom_integrations": "Select the custom integrations you want to track",
|
||||
"tracked_integrations": "Select the integrations you want to track"
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"unit_of_measurement": "[%key:component::analytics_insights::entity::sensor::apps::unit_of_measurement%]"
|
||||
},
|
||||
"custom_integrations": {
|
||||
"name": "{custom_integration_domain} (community)",
|
||||
"name": "{custom_integration_domain} (custom)",
|
||||
"unit_of_measurement": "[%key:component::analytics_insights::entity::sensor::apps::unit_of_measurement%]"
|
||||
},
|
||||
"total_active_installations": {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from anthropic.resources.messages.messages import DEPRECATED_MODELS
|
||||
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -13,13 +15,7 @@ from homeassistant.helpers import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DEPRECATED_MODELS,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .const import CONF_CHAT_MODEL, DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
|
||||
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
|
||||
|
||||
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
|
||||
@@ -37,15 +33,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
||||
coordinator = AnthropicCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
LOGGER.debug("Available models: %s", coordinator.data)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
for subentry in entry.subentries.values():
|
||||
if (model := subentry.data.get(CONF_CHAT_MODEL)) and model.startswith(
|
||||
tuple(DEPRECATED_MODELS)
|
||||
):
|
||||
if (model := subentry.data.get(CONF_CHAT_MODEL)) and model in DEPRECATED_MODELS:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
@@ -235,6 +230,19 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 3:
|
||||
# Remove Temperature parameter
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
|
||||
for subentry in entry.subentries.values():
|
||||
data = subentry.data.copy()
|
||||
if CONF_TEMPERATURE not in data:
|
||||
continue
|
||||
data.pop(CONF_TEMPERATURE, None)
|
||||
hass.config_entries.async_update_subentry(entry, subentry, data=data)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=4)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import llm
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.selector import (
|
||||
NumberSelector,
|
||||
@@ -42,14 +43,12 @@ from homeassistant.helpers.selector import (
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import (
|
||||
CODE_EXECUTION_UNSUPPORTED_MODELS,
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_CODE_EXECUTION,
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_PROMPT,
|
||||
CONF_PROMPT_CACHING,
|
||||
CONF_RECOMMENDED,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_THINKING_BUDGET,
|
||||
CONF_THINKING_EFFORT,
|
||||
CONF_TOOL_SEARCH,
|
||||
@@ -64,10 +63,8 @@ from .const import (
|
||||
DEFAULT_AI_TASK_NAME,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DOMAIN,
|
||||
NON_ADAPTIVE_THINKING_MODELS,
|
||||
NON_THINKING_MODELS,
|
||||
MIN_THINKING_BUDGET,
|
||||
TOOL_SEARCH_UNSUPPORTED_MODELS,
|
||||
WEB_SEARCH_UNSUPPORTED_MODELS,
|
||||
PromptCaching,
|
||||
)
|
||||
from .coordinator import model_alias
|
||||
@@ -105,27 +102,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
await client.models.list(timeout=10.0)
|
||||
|
||||
|
||||
async def get_model_list(client: anthropic.AsyncAnthropic) -> list[SelectOptionDict]:
|
||||
"""Get list of available models."""
|
||||
try:
|
||||
models = (await client.models.list()).data
|
||||
except anthropic.AnthropicError:
|
||||
models = []
|
||||
_LOGGER.debug("Available models: %s", models)
|
||||
return [
|
||||
SelectOptionDict(
|
||||
label=model_info.display_name,
|
||||
value=model_alias(model_info.id),
|
||||
)
|
||||
for model_info in models
|
||||
]
|
||||
|
||||
|
||||
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Anthropic."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
MINOR_VERSION = 4
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -217,6 +198,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Flow for managing conversation subentries."""
|
||||
|
||||
options: dict[str, Any]
|
||||
model_info: anthropic.types.ModelInfo
|
||||
|
||||
@property
|
||||
def _is_new(self) -> bool:
|
||||
@@ -330,24 +312,15 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Manage advanced options."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
step_schema: VolDictType = {
|
||||
vol.Optional(
|
||||
CONF_CHAT_MODEL,
|
||||
default=DEFAULT[CONF_CHAT_MODEL],
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=await self._get_model_list(), custom_value=True
|
||||
)
|
||||
SelectSelectorConfig(options=self._get_model_list(), custom_value=True)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=DEFAULT[CONF_MAX_TOKENS],
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=DEFAULT[CONF_TEMPERATURE],
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
||||
vol.Optional(
|
||||
CONF_PROMPT_CACHING,
|
||||
default=DEFAULT[CONF_PROMPT_CACHING],
|
||||
@@ -363,6 +336,25 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
if user_input is not None:
|
||||
self.options.update(user_input)
|
||||
|
||||
coordinator = self._get_entry().runtime_data
|
||||
self.model_info, status = coordinator.get_model_info(
|
||||
self.options[CONF_CHAT_MODEL]
|
||||
)
|
||||
if not status:
|
||||
# Couldn't find the model in the cached list, try to fetch it directly
|
||||
client = coordinator.client
|
||||
try:
|
||||
self.model_info = await client.models.retrieve(
|
||||
self.options[CONF_CHAT_MODEL], timeout=10.0
|
||||
)
|
||||
except anthropic.NotFoundError:
|
||||
errors[CONF_CHAT_MODEL] = "model_not_found"
|
||||
except anthropic.AnthropicError as err:
|
||||
errors[CONF_CHAT_MODEL] = "api_error"
|
||||
description_placeholders["message"] = (
|
||||
err.message if isinstance(err, anthropic.APIError) else str(err)
|
||||
)
|
||||
|
||||
if not errors:
|
||||
return await self.async_step_model()
|
||||
|
||||
@@ -372,6 +364,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
vol.Schema(step_schema), self.options
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_model(
|
||||
@@ -380,30 +373,59 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Manage model-specific options."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
step_schema: VolDictType = {}
|
||||
step_schema: VolDictType = {
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=DEFAULT[CONF_MAX_TOKENS],
|
||||
): vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(min=0, max=self.model_info.max_tokens)
|
||||
),
|
||||
vol.Coerce(int),
|
||||
)
|
||||
if self.model_info.max_tokens
|
||||
else cv.positive_int,
|
||||
}
|
||||
|
||||
model = self.options[CONF_CHAT_MODEL]
|
||||
|
||||
if not model.startswith(tuple(NON_THINKING_MODELS)) and model.startswith(
|
||||
tuple(NON_ADAPTIVE_THINKING_MODELS)
|
||||
if (
|
||||
self.model_info.capabilities
|
||||
and self.model_info.capabilities.thinking.supported
|
||||
and not self.model_info.capabilities.thinking.types.adaptive.supported
|
||||
):
|
||||
step_schema[
|
||||
vol.Optional(
|
||||
CONF_THINKING_BUDGET, default=DEFAULT[CONF_THINKING_BUDGET]
|
||||
)
|
||||
] = vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=0,
|
||||
max=self.options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
|
||||
)
|
||||
),
|
||||
vol.Coerce(int),
|
||||
] = (
|
||||
vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(min=0, max=self.model_info.max_tokens)
|
||||
),
|
||||
vol.Coerce(int),
|
||||
)
|
||||
if self.model_info.max_tokens
|
||||
else cv.positive_int
|
||||
)
|
||||
else:
|
||||
self.options.pop(CONF_THINKING_BUDGET, None)
|
||||
|
||||
if not model.startswith(tuple(NON_ADAPTIVE_THINKING_MODELS)):
|
||||
if (
|
||||
self.model_info.capabilities
|
||||
and (effort_capability := self.model_info.capabilities.effort).supported
|
||||
):
|
||||
effort_options: list[str] = []
|
||||
if self.model_info.capabilities.thinking.types.adaptive.supported:
|
||||
effort_options.append("none")
|
||||
if effort_capability.low.supported:
|
||||
effort_options.append("low")
|
||||
if effort_capability.medium.supported:
|
||||
effort_options.append("medium")
|
||||
if effort_capability.high.supported:
|
||||
effort_options.append("high")
|
||||
if effort_capability.xhigh and effort_capability.xhigh.supported:
|
||||
effort_options.append("xhigh")
|
||||
if effort_capability.max.supported:
|
||||
effort_options.append("max")
|
||||
step_schema[
|
||||
vol.Optional(
|
||||
CONF_THINKING_EFFORT,
|
||||
@@ -411,7 +433,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
)
|
||||
] = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=["none", "low", "medium", "high", "max"],
|
||||
options=effort_options,
|
||||
translation_key=CONF_THINKING_EFFORT,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
@@ -419,43 +441,34 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
else:
|
||||
self.options.pop(CONF_THINKING_EFFORT, None)
|
||||
|
||||
if not model.startswith(tuple(CODE_EXECUTION_UNSUPPORTED_MODELS)):
|
||||
step_schema[
|
||||
step_schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_CODE_EXECUTION,
|
||||
default=DEFAULT[CONF_CODE_EXECUTION],
|
||||
)
|
||||
] = bool
|
||||
else:
|
||||
self.options.pop(CONF_CODE_EXECUTION, None)
|
||||
|
||||
if not model.startswith(tuple(WEB_SEARCH_UNSUPPORTED_MODELS)):
|
||||
step_schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH,
|
||||
default=DEFAULT[CONF_WEB_SEARCH],
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_MAX_USES,
|
||||
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.options.pop(CONF_WEB_SEARCH, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_MAX_USES, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_USER_LOCATION, None)
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH,
|
||||
default=DEFAULT[CONF_WEB_SEARCH],
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_MAX_USES,
|
||||
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
self.options.pop(CONF_WEB_SEARCH_CITY, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_REGION, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_COUNTRY, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_TIMEZONE, None)
|
||||
|
||||
model = self.options[CONF_CHAT_MODEL]
|
||||
|
||||
if not model.startswith(tuple(TOOL_SEARCH_UNSUPPORTED_MODELS)):
|
||||
step_schema[
|
||||
vol.Optional(
|
||||
@@ -467,9 +480,19 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
self.options.pop(CONF_TOOL_SEARCH, None)
|
||||
|
||||
if not step_schema:
|
||||
user_input = {}
|
||||
# Currently our schema is always present, but if one day it becomes empty,
|
||||
# then the below line is needed to skip this step
|
||||
user_input = {} # pragma: no cover
|
||||
|
||||
if user_input is not None:
|
||||
if (
|
||||
CONF_THINKING_BUDGET in user_input
|
||||
and user_input[CONF_THINKING_BUDGET] >= MIN_THINKING_BUDGET
|
||||
and user_input[CONF_THINKING_BUDGET]
|
||||
>= user_input.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS])
|
||||
):
|
||||
errors[CONF_THINKING_BUDGET] = "thinking_budget_too_large"
|
||||
|
||||
if user_input.get(CONF_WEB_SEARCH, DEFAULT[CONF_WEB_SEARCH]) and not errors:
|
||||
if user_input.get(
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
@@ -501,13 +524,16 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
async def _get_model_list(self) -> list[SelectOptionDict]:
|
||||
def _get_model_list(self) -> list[SelectOptionDict]:
|
||||
"""Get list of available models."""
|
||||
client = anthropic.AsyncAnthropic(
|
||||
api_key=self._get_entry().data[CONF_API_KEY],
|
||||
http_client=get_async_client(self.hass),
|
||||
)
|
||||
return await get_model_list(client)
|
||||
coordinator = self._get_entry().runtime_data
|
||||
return [
|
||||
SelectOptionDict(
|
||||
label=model_info.display_name,
|
||||
value=model_alias(model_info.id),
|
||||
)
|
||||
for model_info in coordinator.data or []
|
||||
]
|
||||
|
||||
async def _get_location_data(self) -> dict[str, str]:
|
||||
"""Get approximate location data of the user."""
|
||||
|
||||
@@ -15,7 +15,6 @@ CONF_CHAT_MODEL = "chat_model"
|
||||
CONF_CODE_EXECUTION = "code_execution"
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
CONF_PROMPT_CACHING = "prompt_caching"
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
CONF_THINKING_BUDGET = "thinking_budget"
|
||||
CONF_THINKING_EFFORT = "thinking_effort"
|
||||
CONF_TOOL_SEARCH = "tool_search"
|
||||
@@ -43,7 +42,6 @@ DEFAULT = {
|
||||
CONF_CODE_EXECUTION: False,
|
||||
CONF_MAX_TOKENS: 3000,
|
||||
CONF_PROMPT_CACHING: PromptCaching.PROMPT.value,
|
||||
CONF_TEMPERATURE: 1.0,
|
||||
CONF_THINKING_BUDGET: MIN_THINKING_BUDGET,
|
||||
CONF_THINKING_EFFORT: "low",
|
||||
CONF_TOOL_SEARCH: False,
|
||||
@@ -52,54 +50,6 @@ DEFAULT = {
|
||||
CONF_WEB_SEARCH_MAX_USES: 5,
|
||||
}
|
||||
|
||||
NON_THINKING_MODELS = [
|
||||
"claude-3-haiku",
|
||||
]
|
||||
|
||||
NON_ADAPTIVE_THINKING_MODELS = [
|
||||
"claude-opus-4-5",
|
||||
"claude-sonnet-4-5",
|
||||
"claude-haiku-4-5",
|
||||
"claude-opus-4-1",
|
||||
"claude-opus-4-0",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-sonnet-4-0",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-3-haiku",
|
||||
]
|
||||
|
||||
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS = [
|
||||
"claude-opus-4-1",
|
||||
"claude-opus-4-0",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-sonnet-4-0",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-3-haiku",
|
||||
]
|
||||
|
||||
WEB_SEARCH_UNSUPPORTED_MODELS = [
|
||||
"claude-3-haiku",
|
||||
]
|
||||
|
||||
CODE_EXECUTION_UNSUPPORTED_MODELS = [
|
||||
"claude-3-haiku",
|
||||
]
|
||||
|
||||
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS = [
|
||||
"claude-haiku-4-5",
|
||||
"claude-opus-4-1",
|
||||
"claude-opus-4-0",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-sonnet-4-0",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-3-haiku",
|
||||
]
|
||||
|
||||
TOOL_SEARCH_UNSUPPORTED_MODELS = [
|
||||
"claude-3",
|
||||
"claude-haiku",
|
||||
]
|
||||
|
||||
DEPRECATED_MODELS = [
|
||||
"claude-3",
|
||||
]
|
||||
|
||||
@@ -28,9 +28,7 @@ _model_short_form = re.compile(r"[^\d]-\d$")
|
||||
@callback
|
||||
def model_alias(model_id: str) -> str:
|
||||
"""Resolve alias from versioned model name."""
|
||||
if model_id == "claude-3-haiku-20240307" or model_id.endswith("-preview"):
|
||||
return model_id
|
||||
if model_id[-2:-1] != "-":
|
||||
if model_id[-2:-1] != "-" and not model_id.endswith("-preview"):
|
||||
model_id = model_id[:-9]
|
||||
if _model_short_form.search(model_id):
|
||||
return model_id + "-0"
|
||||
@@ -95,21 +93,21 @@ class AnthropicCoordinator(DataUpdateCoordinator[list[anthropic.types.ModelInfo]
|
||||
self._schedule_refresh()
|
||||
|
||||
@callback
|
||||
def get_model_info(self, model_id: str) -> anthropic.types.ModelInfo:
|
||||
def get_model_info(self, model_id: str) -> tuple[anthropic.types.ModelInfo, bool]:
|
||||
"""Get model info for a given model ID."""
|
||||
# First try: exact name match
|
||||
for model in self.data or []:
|
||||
if model.id == model_id:
|
||||
return model
|
||||
return model, True
|
||||
# Second try: match by alias
|
||||
alias = model_alias(model_id)
|
||||
for model in self.data or []:
|
||||
if model_alias(model.id) == alias:
|
||||
return model
|
||||
return model, True
|
||||
# Model not found, return safe defaults
|
||||
return anthropic.types.ModelInfo(
|
||||
type="model",
|
||||
id=model_id,
|
||||
created_at=datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC),
|
||||
display_name=model_id,
|
||||
)
|
||||
display_name=alias,
|
||||
), False
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Base entity for Anthropic."""
|
||||
|
||||
import base64
|
||||
from collections.abc import AsyncGenerator, Callable, Iterable
|
||||
from collections import deque
|
||||
from collections.abc import AsyncIterator, Callable, Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
import json
|
||||
@@ -20,17 +21,22 @@ from anthropic.types import (
|
||||
CitationWebSearchResultLocationParam,
|
||||
CodeExecutionTool20250825Param,
|
||||
CodeExecutionToolResultBlock,
|
||||
CodeExecutionToolResultBlockContent,
|
||||
CodeExecutionToolResultBlockParamContentParam,
|
||||
Container,
|
||||
ContentBlock,
|
||||
ContentBlockParam,
|
||||
DocumentBlockParam,
|
||||
ImageBlockParam,
|
||||
InputJSONDelta,
|
||||
JSONOutputFormatParam,
|
||||
Message,
|
||||
MessageDeltaUsage,
|
||||
MessageParam,
|
||||
MessageStreamEvent,
|
||||
ModelInfo,
|
||||
OutputConfigParam,
|
||||
RawContentBlockDelta,
|
||||
RawContentBlockDeltaEvent,
|
||||
RawContentBlockStartEvent,
|
||||
RawContentBlockStopEvent,
|
||||
@@ -67,18 +73,30 @@ from anthropic.types import (
|
||||
WebSearchTool20250305Param,
|
||||
WebSearchTool20260209Param,
|
||||
WebSearchToolResultBlock,
|
||||
WebSearchToolResultBlockContent,
|
||||
WebSearchToolResultBlockParamContentParam,
|
||||
)
|
||||
from anthropic.types.bash_code_execution_tool_result_block import (
|
||||
Content as BashCodeExecutionToolResultBlockContent,
|
||||
)
|
||||
from anthropic.types.bash_code_execution_tool_result_block_param import (
|
||||
Content as BashCodeExecutionToolResultBlockParamContentParam,
|
||||
)
|
||||
from anthropic.types.message_create_params import MessageCreateParamsStreaming
|
||||
from anthropic.types.raw_message_delta_event import Delta
|
||||
from anthropic.types.text_editor_code_execution_tool_result_block import (
|
||||
Content as TextEditorCodeExecutionToolResultBlockContent,
|
||||
)
|
||||
from anthropic.types.text_editor_code_execution_tool_result_block_param import (
|
||||
Content as TextEditorCodeExecutionToolResultBlockParamContentParam,
|
||||
)
|
||||
from anthropic.types.tool_search_tool_result_block import (
|
||||
Content as ToolSearchToolResultBlockContent,
|
||||
)
|
||||
from anthropic.types.tool_search_tool_result_block_param import (
|
||||
Content as ToolSearchToolResultBlockParamContentParam,
|
||||
)
|
||||
from anthropic.types.tool_use_block import Caller
|
||||
import voluptuous as vol
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
@@ -90,14 +108,13 @@ from homeassistant.helpers import device_registry as dr, llm
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
from homeassistant.util.json import JsonArrayType, JsonObjectType
|
||||
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_CODE_EXECUTION,
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_PROMPT_CACHING,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_THINKING_BUDGET,
|
||||
CONF_THINKING_EFFORT,
|
||||
CONF_TOOL_SEARCH,
|
||||
@@ -112,10 +129,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
MIN_THINKING_BUDGET,
|
||||
NON_ADAPTIVE_THINKING_MODELS,
|
||||
NON_THINKING_MODELS,
|
||||
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS,
|
||||
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS,
|
||||
PromptCaching,
|
||||
)
|
||||
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
|
||||
@@ -128,10 +141,14 @@ def _format_tool(
|
||||
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
|
||||
) -> ToolParam:
|
||||
"""Format tool specification."""
|
||||
unsupported_keys = {"oneOf", "anyOf", "allOf"}
|
||||
schema = convert(tool.parameters, custom_serializer=custom_serializer)
|
||||
schema = {k: v for k, v in schema.items() if k not in unsupported_keys}
|
||||
|
||||
return ToolParam(
|
||||
name=tool.name,
|
||||
description=tool.description or "",
|
||||
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
|
||||
input_schema=schema,
|
||||
)
|
||||
|
||||
|
||||
@@ -445,13 +462,7 @@ def _convert_content( # noqa: C901
|
||||
return messages, container_id
|
||||
|
||||
|
||||
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
|
||||
chat_log: conversation.ChatLog,
|
||||
stream: AsyncStream[MessageStreamEvent],
|
||||
output_tool: str | None = None,
|
||||
) -> AsyncGenerator[
|
||||
conversation.AssistantContentDeltaDict | conversation.ToolResultContentDeltaDict
|
||||
]:
|
||||
class AnthropicDeltaStream:
|
||||
"""Transform the response stream into HA format.
|
||||
|
||||
A typical stream of responses might look something like the following:
|
||||
@@ -481,201 +492,376 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
||||
|
||||
Each message could contain multiple blocks of the same type.
|
||||
"""
|
||||
if stream is None or not hasattr(stream, "__aiter__"):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="unexpected_stream_object"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chat_log: conversation.ChatLog,
|
||||
stream: AsyncStream[MessageStreamEvent],
|
||||
output_tool: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the delta stream."""
|
||||
self._chat_log: conversation.ChatLog = chat_log
|
||||
self._stream: AsyncStream[MessageStreamEvent] = stream
|
||||
self._output_tool: str | None = output_tool
|
||||
|
||||
self._buffer: deque[
|
||||
conversation.AssistantContentDeltaDict
|
||||
| conversation.ToolResultContentDeltaDict
|
||||
] = deque()
|
||||
self._stream_iterator: AsyncIterator[MessageStreamEvent] | None = None
|
||||
|
||||
self._current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = (
|
||||
None
|
||||
)
|
||||
self._current_tool_args: str = ""
|
||||
self._content_details = ContentDetails()
|
||||
self._content_details.add_citation_detail()
|
||||
self._input_usage: Usage | None = None
|
||||
self._first_block: bool = True
|
||||
|
||||
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
|
||||
current_tool_args: str
|
||||
content_details = ContentDetails()
|
||||
content_details.add_citation_detail()
|
||||
input_usage: Usage | None = None
|
||||
first_block: bool = True
|
||||
def __aiter__(
|
||||
self,
|
||||
) -> AsyncIterator[
|
||||
conversation.AssistantContentDeltaDict | conversation.ToolResultContentDeltaDict
|
||||
]:
|
||||
"""Initialize the stream and return the async iterator."""
|
||||
if self._stream is None or not hasattr(self._stream, "__aiter__"):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="unexpected_stream_object"
|
||||
)
|
||||
if self._stream_iterator is None:
|
||||
self._stream_iterator = self._stream.__aiter__()
|
||||
return self
|
||||
|
||||
async for response in stream:
|
||||
LOGGER.debug("Received response: %s", response)
|
||||
async def __anext__(
|
||||
self,
|
||||
) -> (
|
||||
conversation.AssistantContentDeltaDict | conversation.ToolResultContentDeltaDict
|
||||
):
|
||||
"""Get the next item from the stream."""
|
||||
while True:
|
||||
if self._buffer:
|
||||
return self._buffer.popleft()
|
||||
|
||||
if isinstance(response, RawMessageStartEvent):
|
||||
input_usage = response.message.usage
|
||||
first_block = True
|
||||
elif isinstance(response, RawContentBlockStartEvent):
|
||||
if isinstance(response.content_block, ToolUseBlock):
|
||||
current_tool_block = ToolUseBlockParam(
|
||||
type="tool_use",
|
||||
id=response.content_block.id,
|
||||
name=response.content_block.name,
|
||||
input=response.content_block.input or {},
|
||||
)
|
||||
current_tool_args = ""
|
||||
if response.content_block.name == output_tool:
|
||||
if first_block or content_details.has_content():
|
||||
if content_details:
|
||||
content_details.delete_empty()
|
||||
yield {"native": content_details}
|
||||
content_details = ContentDetails()
|
||||
content_details.add_citation_detail()
|
||||
yield {"role": "assistant"}
|
||||
first_block = False
|
||||
elif isinstance(response.content_block, TextBlock):
|
||||
if ( # Do not start a new assistant content just for citations, concatenate consecutive blocks with citations instead.
|
||||
first_block
|
||||
or (
|
||||
not content_details.has_citations()
|
||||
and response.content_block.citations is None
|
||||
and content_details.has_content()
|
||||
)
|
||||
):
|
||||
if content_details:
|
||||
content_details.delete_empty()
|
||||
yield {"native": content_details}
|
||||
content_details = ContentDetails()
|
||||
yield {"role": "assistant"}
|
||||
first_block = False
|
||||
content_details.add_citation_detail()
|
||||
if response.content_block.text:
|
||||
content_details.citation_details[-1].length += len(
|
||||
response.content_block.text
|
||||
)
|
||||
yield {"content": response.content_block.text}
|
||||
elif isinstance(response.content_block, ThinkingBlock):
|
||||
if first_block or content_details.thinking_signature:
|
||||
if content_details:
|
||||
content_details.delete_empty()
|
||||
yield {"native": content_details}
|
||||
content_details = ContentDetails()
|
||||
content_details.add_citation_detail()
|
||||
yield {"role": "assistant"}
|
||||
first_block = False
|
||||
elif isinstance(response.content_block, RedactedThinkingBlock):
|
||||
LOGGER.debug(
|
||||
"Some of Claude’s internal reasoning has been automatically "
|
||||
"encrypted for safety reasons. This doesn’t affect the quality of "
|
||||
"responses"
|
||||
)
|
||||
if first_block or content_details.redacted_thinking:
|
||||
if content_details:
|
||||
content_details.delete_empty()
|
||||
yield {"native": content_details}
|
||||
content_details = ContentDetails()
|
||||
content_details.add_citation_detail()
|
||||
yield {"role": "assistant"}
|
||||
first_block = False
|
||||
content_details.redacted_thinking = response.content_block.data
|
||||
elif isinstance(response.content_block, ServerToolUseBlock):
|
||||
current_tool_block = ServerToolUseBlockParam(
|
||||
type="server_tool_use",
|
||||
id=response.content_block.id,
|
||||
name=response.content_block.name,
|
||||
input=response.content_block.input or {},
|
||||
)
|
||||
current_tool_args = ""
|
||||
elif isinstance(
|
||||
response.content_block,
|
||||
(
|
||||
WebSearchToolResultBlock,
|
||||
CodeExecutionToolResultBlock,
|
||||
BashCodeExecutionToolResultBlock,
|
||||
TextEditorCodeExecutionToolResultBlock,
|
||||
ToolSearchToolResultBlock,
|
||||
),
|
||||
):
|
||||
if content_details:
|
||||
content_details.delete_empty()
|
||||
yield {"native": content_details}
|
||||
content_details = ContentDetails()
|
||||
content_details.add_citation_detail()
|
||||
yield {
|
||||
"role": "tool_result",
|
||||
"tool_call_id": response.content_block.tool_use_id,
|
||||
"tool_name": response.content_block.type.removesuffix(
|
||||
"_tool_result"
|
||||
),
|
||||
"tool_result": {
|
||||
"content": cast(
|
||||
JsonObjectType, response.content_block.to_dict()["content"]
|
||||
)
|
||||
}
|
||||
if isinstance(response.content_block.content, list)
|
||||
else cast(JsonObjectType, response.content_block.content.to_dict()),
|
||||
response = await self._stream_iterator.__anext__() # type: ignore[union-attr]
|
||||
|
||||
LOGGER.debug("Received response: %s", response)
|
||||
self.on_message_stream_event(response)
|
||||
|
||||
def on_message_stream_event(self, event: MessageStreamEvent) -> None:
|
||||
"""Handle MessageStreamEvent."""
|
||||
if isinstance(event, RawMessageStartEvent):
|
||||
self.on_message_start_event(event.message)
|
||||
return
|
||||
if isinstance(event, RawContentBlockStartEvent):
|
||||
self.on_content_block_start_event(event.content_block, event.index)
|
||||
return
|
||||
if isinstance(event, RawContentBlockDeltaEvent):
|
||||
self.on_content_block_delta_event(event.delta)
|
||||
return
|
||||
if isinstance(event, RawContentBlockStopEvent):
|
||||
self.on_content_block_stop_event(event.index)
|
||||
return
|
||||
if isinstance(event, RawMessageDeltaEvent):
|
||||
self.on_message_delta_event(event.delta, event.usage)
|
||||
return
|
||||
if isinstance(event, RawMessageStopEvent):
|
||||
self.on_message_stop_event()
|
||||
return
|
||||
LOGGER.debug("Unhandled event type: %s", event.type) # type: ignore[unreachable] # pragma: no cover - All types are handled but we want to verify that
|
||||
|
||||
def on_message_start_event(self, message: Message) -> None:
|
||||
"""Handle RawMessageStartEvent."""
|
||||
self._input_usage = message.usage
|
||||
self._first_block = True
|
||||
|
||||
def on_content_block_start_event(
|
||||
self, content_block: ContentBlock, index: int
|
||||
) -> None:
|
||||
"""Handle RawContentBlockStartEvent."""
|
||||
if isinstance(content_block, ToolUseBlock):
|
||||
self.on_tool_use_block(
|
||||
content_block.id,
|
||||
content_block.input,
|
||||
content_block.name,
|
||||
content_block.caller,
|
||||
)
|
||||
return
|
||||
if isinstance(content_block, TextBlock):
|
||||
self.on_text_block(content_block.text, content_block.citations)
|
||||
return
|
||||
if isinstance(content_block, ThinkingBlock):
|
||||
self.on_thinking_block(content_block.thinking, content_block.signature)
|
||||
return
|
||||
if isinstance(content_block, RedactedThinkingBlock):
|
||||
self.on_redacted_thinking_block(content_block.data)
|
||||
return
|
||||
if isinstance(content_block, ServerToolUseBlock):
|
||||
self.on_server_tool_use_block(
|
||||
content_block.id,
|
||||
content_block.name,
|
||||
content_block.input,
|
||||
content_block.caller,
|
||||
)
|
||||
return
|
||||
if isinstance(
|
||||
content_block,
|
||||
(
|
||||
WebSearchToolResultBlock,
|
||||
CodeExecutionToolResultBlock,
|
||||
BashCodeExecutionToolResultBlock,
|
||||
TextEditorCodeExecutionToolResultBlock,
|
||||
ToolSearchToolResultBlock,
|
||||
),
|
||||
):
|
||||
self.on_server_tool_result_block(
|
||||
content_block.tool_use_id,
|
||||
content_block.type,
|
||||
content_block.content,
|
||||
content_block.caller if hasattr(content_block, "caller") else None,
|
||||
)
|
||||
return
|
||||
LOGGER.debug("Unhandled content block type: %s", content_block.type)
|
||||
|
||||
def on_tool_use_block(
|
||||
self, id: str, input: dict[str, Any], name: str, caller: Caller | None
|
||||
) -> None:
|
||||
"""Handle ToolUseBlock."""
|
||||
self._current_tool_block = ToolUseBlockParam(
|
||||
type="tool_use",
|
||||
id=id,
|
||||
name=name,
|
||||
input=input,
|
||||
)
|
||||
self._current_tool_args = ""
|
||||
if name == self._output_tool:
|
||||
if self._first_block or self._content_details.has_content():
|
||||
if self._content_details:
|
||||
self._content_details.delete_empty()
|
||||
self._buffer.append({"native": self._content_details})
|
||||
self._content_details = ContentDetails()
|
||||
self._content_details.add_citation_detail()
|
||||
self._buffer.append({"role": "assistant"})
|
||||
self._first_block = False
|
||||
|
||||
def on_text_block(self, text: str, citations: list[TextCitation] | None) -> None:
|
||||
"""Handle TextBlock."""
|
||||
if ( # Do not start a new assistant content just for citations, concatenate consecutive blocks with citations instead.
|
||||
self._first_block
|
||||
or (
|
||||
not self._content_details.has_citations()
|
||||
and citations is None
|
||||
and self._content_details.has_content()
|
||||
)
|
||||
):
|
||||
if self._content_details:
|
||||
self._content_details.delete_empty()
|
||||
self._buffer.append({"native": self._content_details})
|
||||
self._content_details = ContentDetails()
|
||||
self._buffer.append({"role": "assistant"})
|
||||
self._first_block = False
|
||||
self._content_details.add_citation_detail()
|
||||
if text:
|
||||
self._content_details.citation_details[-1].length += len(text)
|
||||
self._buffer.append({"content": text})
|
||||
|
||||
def on_thinking_block(self, thinking: str, signature: str) -> None:
|
||||
"""Handle ThinkingBlock."""
|
||||
if self._first_block or self._content_details.thinking_signature:
|
||||
if self._content_details:
|
||||
self._content_details.delete_empty()
|
||||
self._buffer.append({"native": self._content_details})
|
||||
self._content_details = ContentDetails()
|
||||
self._content_details.add_citation_detail()
|
||||
self._buffer.append({"role": "assistant"})
|
||||
self._first_block = False
|
||||
|
||||
def on_redacted_thinking_block(self, data: str) -> None:
|
||||
"""Handle RedactedThinkingBlock."""
|
||||
LOGGER.debug(
|
||||
"Some of Claude’s internal reasoning has been automatically "
|
||||
"encrypted for safety reasons. This doesn’t affect the quality of "
|
||||
"responses"
|
||||
)
|
||||
if self._first_block or self._content_details.redacted_thinking:
|
||||
if self._content_details:
|
||||
self._content_details.delete_empty()
|
||||
self._buffer.append({"native": self._content_details})
|
||||
self._content_details = ContentDetails()
|
||||
self._content_details.add_citation_detail()
|
||||
self._buffer.append({"role": "assistant"})
|
||||
self._first_block = False
|
||||
self._content_details.redacted_thinking = data
|
||||
|
||||
def on_server_tool_use_block(
|
||||
self,
|
||||
id: str,
|
||||
name: Literal[
|
||||
"web_search",
|
||||
"web_fetch",
|
||||
"code_execution",
|
||||
"bash_code_execution",
|
||||
"text_editor_code_execution",
|
||||
"tool_search_tool_regex",
|
||||
"tool_search_tool_bm25",
|
||||
],
|
||||
input: dict[str, Any],
|
||||
caller: Caller | None,
|
||||
) -> None:
|
||||
"""Handle ServerToolUseBlock."""
|
||||
self._current_tool_block = ServerToolUseBlockParam(
|
||||
type="server_tool_use",
|
||||
id=id,
|
||||
name=name,
|
||||
input=input,
|
||||
)
|
||||
self._current_tool_args = ""
|
||||
|
||||
def on_server_tool_result_block(
|
||||
self,
|
||||
tool_use_id: str,
|
||||
tool_name: Literal[
|
||||
"web_search_tool_result",
|
||||
"code_execution_tool_result",
|
||||
"bash_code_execution_tool_result",
|
||||
"text_editor_code_execution_tool_result",
|
||||
"tool_search_tool_result",
|
||||
],
|
||||
content: WebSearchToolResultBlockContent
|
||||
| CodeExecutionToolResultBlockContent
|
||||
| BashCodeExecutionToolResultBlockContent
|
||||
| TextEditorCodeExecutionToolResultBlockContent
|
||||
| ToolSearchToolResultBlockContent,
|
||||
caller: Caller | None,
|
||||
) -> None:
|
||||
"""Handle various server tool result blocks."""
|
||||
if self._content_details:
|
||||
self._content_details.delete_empty()
|
||||
self._buffer.append({"native": self._content_details})
|
||||
self._content_details = ContentDetails()
|
||||
self._content_details.add_citation_detail()
|
||||
self._buffer.append(
|
||||
{
|
||||
"role": "tool_result",
|
||||
"tool_call_id": tool_use_id,
|
||||
"tool_name": tool_name.removesuffix("_tool_result"),
|
||||
"tool_result": {
|
||||
"content": cast(JsonArrayType, [x.to_dict() for x in content])
|
||||
}
|
||||
first_block = True
|
||||
elif isinstance(response, RawContentBlockDeltaEvent):
|
||||
if isinstance(response.delta, InputJSONDelta):
|
||||
if (
|
||||
current_tool_block is not None
|
||||
and current_tool_block["name"] == output_tool
|
||||
):
|
||||
content_details.citation_details[-1].length += len(
|
||||
response.delta.partial_json
|
||||
)
|
||||
yield {"content": response.delta.partial_json}
|
||||
else:
|
||||
current_tool_args += response.delta.partial_json
|
||||
elif isinstance(response.delta, TextDelta):
|
||||
if response.delta.text:
|
||||
content_details.citation_details[-1].length += len(
|
||||
response.delta.text
|
||||
)
|
||||
yield {"content": response.delta.text}
|
||||
elif isinstance(response.delta, ThinkingDelta):
|
||||
if response.delta.thinking:
|
||||
yield {"thinking_content": response.delta.thinking}
|
||||
elif isinstance(response.delta, SignatureDelta):
|
||||
content_details.thinking_signature = response.delta.signature
|
||||
elif isinstance(response.delta, CitationsDelta):
|
||||
content_details.add_citation(response.delta.citation)
|
||||
elif isinstance(response, RawContentBlockStopEvent):
|
||||
if current_tool_block is not None:
|
||||
if current_tool_block["name"] == output_tool:
|
||||
current_tool_block = None
|
||||
continue
|
||||
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
||||
current_tool_block["input"] |= tool_args
|
||||
yield {
|
||||
if isinstance(content, list)
|
||||
else cast(JsonObjectType, content.to_dict()),
|
||||
}
|
||||
)
|
||||
self._first_block = True
|
||||
|
||||
def on_content_block_delta_event(self, delta: RawContentBlockDelta) -> None:
|
||||
"""Handle RawContentBlockDeltaEvent."""
|
||||
if isinstance(delta, InputJSONDelta):
|
||||
self.on_input_json_delta(delta.partial_json)
|
||||
return
|
||||
if isinstance(delta, TextDelta):
|
||||
self.on_text_delta(delta.text)
|
||||
return
|
||||
if isinstance(delta, ThinkingDelta):
|
||||
self.on_thinking_delta(delta.thinking)
|
||||
return
|
||||
if isinstance(delta, SignatureDelta):
|
||||
self.on_signature_delta(delta.signature)
|
||||
return
|
||||
if isinstance(delta, CitationsDelta):
|
||||
self.on_citations_delta(delta.citation)
|
||||
return
|
||||
LOGGER.debug("Unhandled content delta type: %s", delta.type) # type: ignore[unreachable] # pragma: no cover - All types are handled but we want to verify that
|
||||
|
||||
def on_input_json_delta(self, partial_json: str) -> None:
|
||||
"""Handle InputJSONDelta."""
|
||||
if (
|
||||
self._current_tool_block is not None
|
||||
and self._current_tool_block["name"] == self._output_tool
|
||||
):
|
||||
self._content_details.citation_details[-1].length += len(partial_json)
|
||||
self._buffer.append({"content": partial_json})
|
||||
else:
|
||||
self._current_tool_args += partial_json
|
||||
|
||||
def on_text_delta(self, text: str) -> None:
|
||||
"""Handle TextDelta."""
|
||||
if text:
|
||||
self._content_details.citation_details[-1].length += len(text)
|
||||
self._buffer.append({"content": text})
|
||||
|
||||
def on_thinking_delta(self, thinking: str) -> None:
|
||||
"""Handle ThinkingDelta."""
|
||||
if thinking:
|
||||
self._buffer.append({"thinking_content": thinking})
|
||||
|
||||
def on_signature_delta(self, signature: str) -> None:
|
||||
"""Handle SignatureDelta."""
|
||||
self._content_details.thinking_signature = signature
|
||||
|
||||
def on_citations_delta(self, citation: TextCitation) -> None:
|
||||
"""Handle CitationsDelta."""
|
||||
self._content_details.add_citation(citation)
|
||||
|
||||
def on_content_block_stop_event(self, index: int) -> None:
|
||||
"""Handle RawContentBlockStopEvent."""
|
||||
if self._current_tool_block is not None:
|
||||
if self._current_tool_block["name"] == self._output_tool:
|
||||
self._current_tool_block = None
|
||||
return
|
||||
tool_args = (
|
||||
json.loads(self._current_tool_args) if self._current_tool_args else {}
|
||||
)
|
||||
self._current_tool_block["input"] |= tool_args
|
||||
self._buffer.append(
|
||||
{
|
||||
"tool_calls": [
|
||||
llm.ToolInput(
|
||||
id=current_tool_block["id"],
|
||||
tool_name=current_tool_block["name"],
|
||||
tool_args=current_tool_block["input"],
|
||||
external=current_tool_block["type"] == "server_tool_use",
|
||||
id=self._current_tool_block["id"],
|
||||
tool_name=self._current_tool_block["name"],
|
||||
tool_args=self._current_tool_block["input"],
|
||||
external=self._current_tool_block["type"]
|
||||
== "server_tool_use",
|
||||
)
|
||||
]
|
||||
}
|
||||
current_tool_block = None
|
||||
elif isinstance(response, RawMessageDeltaEvent):
|
||||
if (usage := response.usage) is not None:
|
||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||
content_details.container = response.delta.container
|
||||
if response.delta.stop_reason == "refusal":
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_refusal"
|
||||
)
|
||||
elif isinstance(response, RawMessageStopEvent):
|
||||
if content_details:
|
||||
content_details.delete_empty()
|
||||
yield {"native": content_details}
|
||||
content_details = ContentDetails()
|
||||
content_details.add_citation_detail()
|
||||
)
|
||||
self._current_tool_block = None
|
||||
|
||||
def on_message_delta_event(self, delta: Delta, usage: MessageDeltaUsage) -> None:
|
||||
"""Handle RawMessageDeltaEvent."""
|
||||
self._chat_log.async_trace(self._create_token_stats(self._input_usage, usage))
|
||||
self._content_details.container = delta.container
|
||||
if delta.stop_reason == "refusal":
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_refusal"
|
||||
)
|
||||
|
||||
def _create_token_stats(
|
||||
input_usage: Usage | None, response_usage: MessageDeltaUsage
|
||||
) -> dict[str, Any]:
|
||||
"""Create token stats for conversation agent tracing."""
|
||||
input_tokens = 0
|
||||
cached_input_tokens = 0
|
||||
if input_usage:
|
||||
input_tokens = input_usage.input_tokens
|
||||
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
|
||||
output_tokens = response_usage.output_tokens
|
||||
return {
|
||||
"stats": {
|
||||
"input_tokens": input_tokens,
|
||||
"cached_input_tokens": cached_input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
def on_message_stop_event(self) -> None:
|
||||
"""Handle RawMessageStopEvent."""
|
||||
if self._content_details:
|
||||
self._content_details.delete_empty()
|
||||
self._buffer.append({"native": self._content_details})
|
||||
self._content_details = ContentDetails()
|
||||
self._content_details.add_citation_detail()
|
||||
|
||||
def _create_token_stats(
|
||||
self, input_usage: Usage | None, response_usage: MessageDeltaUsage
|
||||
) -> dict[str, Any]:
|
||||
"""Create token stats for conversation agent tracing."""
|
||||
input_tokens = 0
|
||||
cached_input_tokens = 0
|
||||
if input_usage:
|
||||
input_tokens = input_usage.input_tokens
|
||||
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
|
||||
output_tokens = response_usage.output_tokens
|
||||
return {
|
||||
"stats": {
|
||||
"input_tokens": input_tokens,
|
||||
"cached_input_tokens": cached_input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
@@ -690,7 +876,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
self.entry = entry
|
||||
self.subentry = subentry
|
||||
coordinator = entry.runtime_data
|
||||
self.model_info = coordinator.get_model_info(
|
||||
self.model_info, _ = coordinator.get_model_info(
|
||||
subentry.data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
|
||||
)
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
@@ -703,15 +889,14 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
async def _async_handle_chat_log( # noqa: C901
|
||||
async def _get_model_args( # noqa: C901
|
||||
self,
|
||||
chat_log: conversation.ChatLog,
|
||||
structure_name: str | None = None,
|
||||
structure: vol.Schema | None = None,
|
||||
max_iterations: int = MAX_TOOL_ITERATIONS,
|
||||
) -> None:
|
||||
"""Generate an answer for the chat log."""
|
||||
options = self.subentry.data
|
||||
) -> tuple[MessageCreateParamsStreaming, str | None]:
|
||||
"""Get the model arguments."""
|
||||
options: dict[str, Any] = DEFAULT | self.subentry.data
|
||||
|
||||
preloaded_tools = [
|
||||
"HassTurnOn",
|
||||
@@ -729,21 +914,18 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
|
||||
messages, container_id = _convert_content(chat_log.content[1:])
|
||||
|
||||
model = options.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
|
||||
model = options[CONF_CHAT_MODEL]
|
||||
|
||||
model_args = MessageCreateParamsStreaming(
|
||||
model=model,
|
||||
messages=messages,
|
||||
max_tokens=options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
|
||||
max_tokens=options[CONF_MAX_TOKENS],
|
||||
system=system.content,
|
||||
stream=True,
|
||||
container=container_id,
|
||||
)
|
||||
|
||||
if (
|
||||
options.get(CONF_PROMPT_CACHING, DEFAULT[CONF_PROMPT_CACHING])
|
||||
== PromptCaching.PROMPT
|
||||
):
|
||||
if options[CONF_PROMPT_CACHING] == PromptCaching.PROMPT:
|
||||
model_args["system"] = [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -751,39 +933,40 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
]
|
||||
elif (
|
||||
options.get(CONF_PROMPT_CACHING, DEFAULT[CONF_PROMPT_CACHING])
|
||||
== PromptCaching.AUTOMATIC
|
||||
):
|
||||
elif options[CONF_PROMPT_CACHING] == PromptCaching.AUTOMATIC:
|
||||
model_args["cache_control"] = {"type": "ephemeral"}
|
||||
|
||||
if not model.startswith(tuple(NON_ADAPTIVE_THINKING_MODELS)):
|
||||
thinking_effort = options.get(
|
||||
CONF_THINKING_EFFORT, DEFAULT[CONF_THINKING_EFFORT]
|
||||
)
|
||||
if (
|
||||
self.model_info.capabilities
|
||||
and self.model_info.capabilities.thinking.types.adaptive.supported
|
||||
):
|
||||
thinking_effort = options[CONF_THINKING_EFFORT]
|
||||
if thinking_effort != "none":
|
||||
model_args["thinking"] = ThinkingConfigAdaptiveParam(type="adaptive")
|
||||
model_args["thinking"] = ThinkingConfigAdaptiveParam(
|
||||
type="adaptive", display="summarized"
|
||||
)
|
||||
model_args["output_config"] = OutputConfigParam(effort=thinking_effort)
|
||||
else:
|
||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||
model_args["temperature"] = options.get(
|
||||
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
|
||||
)
|
||||
else:
|
||||
thinking_budget = options.get(
|
||||
CONF_THINKING_BUDGET, DEFAULT[CONF_THINKING_BUDGET]
|
||||
)
|
||||
thinking_budget = options[CONF_THINKING_BUDGET]
|
||||
if (
|
||||
not model.startswith(tuple(NON_THINKING_MODELS))
|
||||
self.model_info.capabilities
|
||||
and self.model_info.capabilities.thinking.types.enabled.supported
|
||||
and thinking_budget >= MIN_THINKING_BUDGET
|
||||
):
|
||||
model_args["thinking"] = ThinkingConfigEnabledParam(
|
||||
type="enabled", budget_tokens=thinking_budget
|
||||
type="enabled", display="summarized", budget_tokens=thinking_budget
|
||||
)
|
||||
else:
|
||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||
model_args["temperature"] = options.get(
|
||||
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
|
||||
|
||||
if (
|
||||
self.model_info.capabilities
|
||||
and self.model_info.capabilities.effort.supported
|
||||
):
|
||||
model_args["output_config"] = OutputConfigParam(
|
||||
effort=options[CONF_THINKING_EFFORT]
|
||||
)
|
||||
|
||||
tools: list[ToolUnionParam] = []
|
||||
@@ -793,11 +976,13 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
for tool in chat_log.llm_api.tools
|
||||
]
|
||||
|
||||
if options.get(CONF_CODE_EXECUTION):
|
||||
if options[CONF_CODE_EXECUTION]:
|
||||
# The `web_search_20260209` tool automatically enables `code_execution_20260120` tool
|
||||
if model.startswith(
|
||||
tuple(PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS)
|
||||
) or not options.get(CONF_WEB_SEARCH):
|
||||
if (
|
||||
not self.model_info.capabilities
|
||||
or not self.model_info.capabilities.code_execution.supported
|
||||
or not options[CONF_WEB_SEARCH]
|
||||
):
|
||||
tools.append(
|
||||
CodeExecutionTool20250825Param(
|
||||
name="code_execution",
|
||||
@@ -805,24 +990,26 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
),
|
||||
)
|
||||
|
||||
if options.get(CONF_WEB_SEARCH):
|
||||
if model.startswith(
|
||||
tuple(PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS)
|
||||
) or not options.get(CONF_CODE_EXECUTION):
|
||||
if options[CONF_WEB_SEARCH]:
|
||||
if (
|
||||
not self.model_info.capabilities
|
||||
or not self.model_info.capabilities.code_execution.supported
|
||||
or not options[CONF_CODE_EXECUTION]
|
||||
):
|
||||
web_search: WebSearchTool20250305Param | WebSearchTool20260209Param = (
|
||||
WebSearchTool20250305Param(
|
||||
name="web_search",
|
||||
type="web_search_20250305",
|
||||
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
|
||||
max_uses=options[CONF_WEB_SEARCH_MAX_USES],
|
||||
)
|
||||
)
|
||||
else:
|
||||
web_search = WebSearchTool20260209Param(
|
||||
name="web_search",
|
||||
type="web_search_20260209",
|
||||
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
|
||||
max_uses=options[CONF_WEB_SEARCH_MAX_USES],
|
||||
)
|
||||
if options.get(CONF_WEB_SEARCH_USER_LOCATION):
|
||||
if options[CONF_WEB_SEARCH_USER_LOCATION]:
|
||||
web_search["user_location"] = {
|
||||
"type": "approximate",
|
||||
"city": options.get(CONF_WEB_SEARCH_CITY, ""),
|
||||
@@ -846,12 +1033,17 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
]
|
||||
last_message["content"].extend( # type: ignore[union-attr]
|
||||
await async_prepare_files_for_prompt(
|
||||
self.hass, [(a.path, a.mime_type) for a in last_content.attachments]
|
||||
self.hass,
|
||||
self.model_info,
|
||||
[(a.path, a.mime_type) for a in last_content.attachments],
|
||||
)
|
||||
)
|
||||
|
||||
if structure and structure_name:
|
||||
if not model.startswith(tuple(UNSUPPORTED_STRUCTURED_OUTPUT_MODELS)):
|
||||
if (
|
||||
self.model_info.capabilities
|
||||
and self.model_info.capabilities.structured_outputs.supported
|
||||
):
|
||||
# Native structured output for those models who support it.
|
||||
structure_name = None
|
||||
model_args.setdefault("output_config", OutputConfigParam())[
|
||||
@@ -918,10 +1110,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
preloaded_tools.append(structure_name)
|
||||
|
||||
if tools:
|
||||
if (
|
||||
options.get(CONF_TOOL_SEARCH, DEFAULT[CONF_TOOL_SEARCH])
|
||||
and len(tools) > len(preloaded_tools) + 1
|
||||
):
|
||||
if options[CONF_TOOL_SEARCH] and len(tools) > len(preloaded_tools) + 1:
|
||||
for tool in tools:
|
||||
if not tool["name"].endswith(tuple(preloaded_tools)):
|
||||
tool["defer_loading"] = True
|
||||
@@ -934,6 +1123,19 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
|
||||
model_args["tools"] = tools
|
||||
|
||||
return model_args, structure_name
|
||||
|
||||
async def _async_handle_chat_log(
|
||||
self,
|
||||
chat_log: conversation.ChatLog,
|
||||
structure_name: str | None = None,
|
||||
structure: vol.Schema | None = None,
|
||||
max_iterations: int = MAX_TOOL_ITERATIONS,
|
||||
) -> None:
|
||||
"""Generate an answer for the chat log."""
|
||||
model_args, structure_name = await self._get_model_args(
|
||||
chat_log, structure_name, structure
|
||||
)
|
||||
coordinator = self.entry.runtime_data
|
||||
client = coordinator.client
|
||||
|
||||
@@ -947,7 +1149,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
self.entity_id,
|
||||
_transform_stream(
|
||||
AnthropicDeltaStream(
|
||||
chat_log,
|
||||
stream,
|
||||
output_tool=structure_name or None,
|
||||
@@ -955,7 +1157,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
)
|
||||
]
|
||||
)
|
||||
messages.extend(new_messages)
|
||||
cast(list[MessageParam], model_args["messages"]).extend(new_messages)
|
||||
except anthropic.AuthenticationError as err:
|
||||
# Trigger coordinator to confirm the auth failure and trigger the reauth flow.
|
||||
await coordinator.async_request_refresh()
|
||||
@@ -992,7 +1194,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
|
||||
|
||||
async def async_prepare_files_for_prompt(
|
||||
hass: HomeAssistant, files: list[tuple[Path, str | None]]
|
||||
hass: HomeAssistant, model_info: ModelInfo, files: list[tuple[Path, str | None]]
|
||||
) -> Iterable[ImageBlockParam | DocumentBlockParam]:
|
||||
"""Append files to a prompt.
|
||||
|
||||
@@ -1013,13 +1215,26 @@ async def async_prepare_files_for_prompt(
|
||||
if mime_type is None:
|
||||
mime_type = guess_file_type(file_path)[0]
|
||||
|
||||
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
|
||||
if (
|
||||
not mime_type
|
||||
or not mime_type.startswith(("image/", "application/pdf"))
|
||||
or not model_info.capabilities
|
||||
or (
|
||||
mime_type.startswith("image/")
|
||||
and not model_info.capabilities.image_input.supported
|
||||
)
|
||||
or (
|
||||
mime_type.startswith("application/pdf")
|
||||
and not model_info.capabilities.pdf_input.supported
|
||||
)
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="wrong_file_type",
|
||||
translation_placeholders={
|
||||
"file_path": file_path.as_posix(),
|
||||
"mime_type": mime_type or "unknown",
|
||||
"model": model_info.display_name,
|
||||
},
|
||||
)
|
||||
if mime_type == "image/jpg":
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["anthropic==0.83.0"]
|
||||
"requirements": ["anthropic==0.96.0"]
|
||||
}
|
||||
|
||||
@@ -81,7 +81,10 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
No entities disabled by default.
|
||||
entity-translations: todo
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities explicitly set `_attr_name` to `None`, so entity name translations are not used.
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
|
||||
@@ -5,6 +5,8 @@ from __future__ import annotations
|
||||
from collections.abc import Iterator
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import anthropic
|
||||
from anthropic.resources.messages.messages import DEPRECATED_MODELS
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
@@ -18,8 +20,8 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorConfig,
|
||||
)
|
||||
|
||||
from .config_flow import get_model_list
|
||||
from .const import CONF_CHAT_MODEL, DEPRECATED_MODELS, DOMAIN
|
||||
from .const import CONF_CHAT_MODEL, DOMAIN
|
||||
from .coordinator import model_alias
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AnthropicConfigEntry
|
||||
@@ -61,8 +63,8 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
client = entry.runtime_data.client
|
||||
model_list = [
|
||||
model_option
|
||||
for model_option in await get_model_list(client)
|
||||
if not model_option["value"].startswith(tuple(DEPRECATED_MODELS))
|
||||
for model_option in await self.get_model_list(client)
|
||||
if model_option["value"] not in DEPRECATED_MODELS
|
||||
]
|
||||
self._model_list_cache[entry.entry_id] = model_list
|
||||
|
||||
@@ -104,9 +106,26 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
"model": model,
|
||||
"subentry_name": subentry.title,
|
||||
"subentry_type": self._format_subentry_type(subentry.subentry_type),
|
||||
"retirement_date": DEPRECATED_MODELS[model],
|
||||
},
|
||||
)
|
||||
|
||||
async def get_model_list(
|
||||
self, client: anthropic.AsyncAnthropic
|
||||
) -> list[SelectOptionDict]:
|
||||
"""Get list of available models."""
|
||||
try:
|
||||
models = (await client.models.list(timeout=10.0)).data
|
||||
except anthropic.AnthropicError:
|
||||
models = []
|
||||
return [
|
||||
SelectOptionDict(
|
||||
label=model_info.display_name,
|
||||
value=model_alias(model_info.id),
|
||||
)
|
||||
for model_info in models
|
||||
]
|
||||
|
||||
def _iter_deprecated_subentries(self) -> Iterator[tuple[str, str]]:
|
||||
"""Yield entry/subentry pairs that use deprecated models."""
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
@@ -114,7 +133,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
continue
|
||||
for subentry in entry.subentries.values():
|
||||
model = subentry.data.get(CONF_CHAT_MODEL)
|
||||
if model and model.startswith(tuple(DEPRECATED_MODELS)):
|
||||
if model and model in DEPRECATED_MODELS:
|
||||
yield entry.entry_id, subentry.subentry_id
|
||||
|
||||
async def _async_next_target(
|
||||
@@ -141,7 +160,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
continue
|
||||
|
||||
model = subentry.data.get(CONF_CHAT_MODEL)
|
||||
if not model or not model.startswith(tuple(DEPRECATED_MODELS)):
|
||||
if not model or model not in DEPRECATED_MODELS:
|
||||
continue
|
||||
|
||||
self._current_entry_id = entry_id
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"entry_type": "AI task",
|
||||
"error": {
|
||||
"api_error": "[%key:component::anthropic::config_subentries::conversation::error::api_error%]",
|
||||
"model_not_found": "[%key:component::anthropic::config_subentries::conversation::error::model_not_found%]",
|
||||
"thinking_budget_too_large": "[%key:component::anthropic::config_subentries::conversation::error::thinking_budget_too_large%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"reconfigure": "Reconfigure AI task",
|
||||
"user": "Add AI task"
|
||||
@@ -46,13 +51,11 @@
|
||||
"advanced": {
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::max_tokens%]",
|
||||
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::prompt_caching%]",
|
||||
"temperature": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::temperature%]"
|
||||
},
|
||||
"data_description": {
|
||||
"chat_model": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::chat_model%]",
|
||||
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::max_tokens%]",
|
||||
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::prompt_caching%]",
|
||||
"temperature": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::temperature%]"
|
||||
},
|
||||
@@ -72,6 +75,7 @@
|
||||
"model": {
|
||||
"data": {
|
||||
"code_execution": "[%key:component::anthropic::config_subentries::conversation::step::model::data::code_execution%]",
|
||||
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::model::data::max_tokens%]",
|
||||
"thinking_budget": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_budget%]",
|
||||
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_effort%]",
|
||||
"tool_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data::tool_search%]",
|
||||
@@ -81,6 +85,7 @@
|
||||
},
|
||||
"data_description": {
|
||||
"code_execution": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::code_execution%]",
|
||||
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::max_tokens%]",
|
||||
"thinking_budget": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_budget%]",
|
||||
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_effort%]",
|
||||
"tool_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::tool_search%]",
|
||||
@@ -98,6 +103,11 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"entry_type": "Conversation agent",
|
||||
"error": {
|
||||
"api_error": "Unable to get model info: {message}",
|
||||
"model_not_found": "Model not found",
|
||||
"thinking_budget_too_large": "Thinking budget must be less than the Maximum tokens."
|
||||
},
|
||||
"initiate_flow": {
|
||||
"reconfigure": "Reconfigure conversation agent",
|
||||
"user": "Add conversation agent"
|
||||
@@ -106,13 +116,11 @@
|
||||
"advanced": {
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"prompt_caching": "Caching strategy",
|
||||
"temperature": "Temperature"
|
||||
},
|
||||
"data_description": {
|
||||
"chat_model": "The model to serve the responses.",
|
||||
"max_tokens": "Limit the number of response tokens.",
|
||||
"prompt_caching": "Optimize your API cost and response times based on your usage.",
|
||||
"temperature": "Control the randomness of the response, trading off between creativity and coherence."
|
||||
},
|
||||
@@ -136,6 +144,7 @@
|
||||
"model": {
|
||||
"data": {
|
||||
"code_execution": "Code execution",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"thinking_budget": "Thinking budget",
|
||||
"thinking_effort": "Thinking effort",
|
||||
"tool_search": "Enable tool search tool",
|
||||
@@ -145,6 +154,7 @@
|
||||
},
|
||||
"data_description": {
|
||||
"code_execution": "Allow the model to execute code in a secure sandbox environment, enabling it to analyze data and perform complex calculations.",
|
||||
"max_tokens": "Limit the number of response tokens.",
|
||||
"thinking_budget": "The number of tokens the model can use to think about the response out of the total maximum number of tokens. Set to 1024 or greater to enable extended thinking.",
|
||||
"thinking_effort": "Control how many tokens Claude uses when responding, trading off between response thoroughness and token efficiency",
|
||||
"tool_search": "Enable dynamic tool discovery instead of preloading all tools into the context",
|
||||
@@ -195,7 +205,7 @@
|
||||
"message": "`{file_path}` does not exist."
|
||||
},
|
||||
"wrong_file_type": {
|
||||
"message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF."
|
||||
"message": "The {model} model does not support {mime_type} file types (for `{file_path}`)."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
@@ -209,7 +219,7 @@
|
||||
"data_description": {
|
||||
"chat_model": "Select the new model to use."
|
||||
},
|
||||
"description": "You are updating {subentry_name} ({subentry_type}) in {entry_name}. The current model {model} is deprecated. Select a supported model to continue.",
|
||||
"description": "You are updating {subentry_name} ({subentry_type}) in {entry_name}. The current model {model} is deprecated and will reach end-of-life on {retirement_date}. Select a supported model to continue.",
|
||||
"title": "Update model"
|
||||
}
|
||||
}
|
||||
@@ -231,7 +241,8 @@
|
||||
"low": "[%key:common::state::low%]",
|
||||
"max": "Max",
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"none": "None"
|
||||
"none": "None",
|
||||
"xhigh": "X-High"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from . import SIGNAL_CONNECTED, AppleTvConfigEntry
|
||||
from .entity import AppleTVEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -155,7 +155,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.data[DATA_COMPONENT] = storage_collection
|
||||
|
||||
collection.DictStorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
CREATE_FIELDS,
|
||||
UPDATE_FIELDS,
|
||||
admin_only=True,
|
||||
).async_setup(hass)
|
||||
|
||||
websocket_api.async_register_command(hass, handle_integration_list)
|
||||
@@ -341,6 +346,7 @@ async def handle_integration_list(
|
||||
vol.Required("config_entry_id"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def handle_config_entry(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
|
||||
@@ -28,7 +28,7 @@ class AquacellEntity(CoordinatorEntity[AquacellCoordinator]):
|
||||
self._attr_unique_id = f"{softener_key}-{entity_key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=self.softener.name,
|
||||
hw_version=self.softener.fwVersion,
|
||||
hw_version=self.softener.diagnostics.fw_version,
|
||||
identifiers={(DOMAIN, str(softener_key))},
|
||||
manufacturer=self.softener.brand,
|
||||
model=self.softener.ssn,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaquacell"],
|
||||
"requirements": ["aioaquacell==0.2.0"]
|
||||
"requirements": ["aioaquacell==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -38,39 +38,39 @@ SENSORS: tuple[SoftenerSensorEntityDescription, ...] = (
|
||||
translation_key="salt_left_side_percentage",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda softener: softener.salt.leftPercent,
|
||||
value_fn=lambda softener: softener.salt.left_percent,
|
||||
),
|
||||
SoftenerSensorEntityDescription(
|
||||
key="salt_right_side_percentage",
|
||||
translation_key="salt_right_side_percentage",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda softener: softener.salt.rightPercent,
|
||||
value_fn=lambda softener: softener.salt.right_percent,
|
||||
),
|
||||
SoftenerSensorEntityDescription(
|
||||
key="salt_left_side_time_remaining",
|
||||
translation_key="salt_left_side_time_remaining",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
value_fn=lambda softener: softener.salt.leftDays,
|
||||
value_fn=lambda softener: softener.salt.left_days,
|
||||
),
|
||||
SoftenerSensorEntityDescription(
|
||||
key="salt_right_side_time_remaining",
|
||||
translation_key="salt_right_side_time_remaining",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
value_fn=lambda softener: softener.salt.rightDays,
|
||||
value_fn=lambda softener: softener.salt.right_days,
|
||||
),
|
||||
SoftenerSensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda softener: softener.battery,
|
||||
value_fn=lambda softener: softener.diagnostics.battery,
|
||||
),
|
||||
SoftenerSensorEntityDescription(
|
||||
key="wi_fi_strength",
|
||||
translation_key="wi_fi_strength",
|
||||
value_fn=lambda softener: softener.wifiLevel,
|
||||
value_fn=lambda softener: softener.diagnostics.wifi_level,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"high",
|
||||
@@ -82,7 +82,7 @@ SENSORS: tuple[SoftenerSensorEntityDescription, ...] = (
|
||||
key="last_update",
|
||||
translation_key="last_update",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda softener: softener.lastUpdate,
|
||||
value_fn=lambda softener: softener.diagnostics.last_update,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from arcam.fmj import IncomingVideoAspectRatio, IncomingVideoColorspace
|
||||
from arcam.fmj import IncomingVideoAspectRatio, IncomingVideoColorspace, IntOrTypeEnum
|
||||
from arcam.fmj.state import IncomingAudioConfig, IncomingAudioFormat, State
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -21,6 +22,25 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .coordinator import ArcamFmjConfigEntry
|
||||
from .entity import ArcamFmjEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _enum_options(value: type[IntOrTypeEnum]) -> list[str]:
|
||||
return [
|
||||
member.name.lower() for member in value if not member.name.startswith("CODE_")
|
||||
]
|
||||
|
||||
|
||||
def _enum_value(value: IntOrTypeEnum | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if value.name.startswith("CODE_"):
|
||||
_LOGGER.debug("Undefined enum value %s ignored", value)
|
||||
return None
|
||||
|
||||
return value.name.lower()
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ArcamFmjSensorEntityDescription(SensorEntityDescription):
|
||||
@@ -75,9 +95,9 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
|
||||
translation_key="incoming_video_aspect_ratio",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[member.name.lower() for member in IncomingVideoAspectRatio],
|
||||
options=_enum_options(IncomingVideoAspectRatio),
|
||||
value_fn=lambda state: (
|
||||
vp.aspect_ratio.name.lower()
|
||||
_enum_value(vp.aspect_ratio)
|
||||
if (vp := state.get_incoming_video_parameters()) is not None
|
||||
else None
|
||||
),
|
||||
@@ -87,11 +107,10 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
|
||||
translation_key="incoming_video_colorspace",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[member.name.lower() for member in IncomingVideoColorspace],
|
||||
options=_enum_options(IncomingVideoColorspace),
|
||||
value_fn=lambda state: (
|
||||
vp.colorspace.name.lower()
|
||||
_enum_value(vp.colorspace)
|
||||
if (vp := state.get_incoming_video_parameters()) is not None
|
||||
and vp.colorspace is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
@@ -100,24 +119,16 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
|
||||
translation_key="incoming_audio_format",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[member.name.lower() for member in IncomingAudioFormat],
|
||||
value_fn=lambda state: (
|
||||
result.name.lower()
|
||||
if (result := state.get_incoming_audio_format()[0]) is not None
|
||||
else None
|
||||
),
|
||||
options=_enum_options(IncomingAudioFormat),
|
||||
value_fn=lambda state: _enum_value(state.get_incoming_audio_format()[0]),
|
||||
),
|
||||
ArcamFmjSensorEntityDescription(
|
||||
key="incoming_audio_config",
|
||||
translation_key="incoming_audio_config",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[member.name.lower() for member in IncomingAudioConfig],
|
||||
value_fn=lambda state: (
|
||||
result.name.lower()
|
||||
if (result := state.get_incoming_audio_format()[1]) is not None
|
||||
else None
|
||||
),
|
||||
options=_enum_options(IncomingAudioConfig),
|
||||
value_fn=lambda state: _enum_value(state.get_incoming_audio_format()[1]),
|
||||
),
|
||||
ArcamFmjSensorEntityDescription(
|
||||
key="incoming_audio_sample_rate",
|
||||
|
||||
@@ -945,7 +945,10 @@ class PipelineRun:
|
||||
try:
|
||||
# Transcribe audio stream
|
||||
stt_vad: VoiceCommandSegmenter | None = None
|
||||
if self.audio_settings.is_vad_enabled:
|
||||
if (
|
||||
self.audio_settings.is_vad_enabled
|
||||
and self.stt_provider.audio_processing.requires_external_vad
|
||||
):
|
||||
stt_vad = VoiceCommandSegmenter(
|
||||
silence_seconds=self.audio_settings.silence_seconds
|
||||
)
|
||||
|
||||
@@ -13,11 +13,12 @@ from hassil.util import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -103,6 +104,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def handle_ask_question(call: ServiceCall) -> dict[str, Any]:
|
||||
"""Handle a Show View service call."""
|
||||
satellite_entity_id: str = call.data[ATTR_ENTITY_ID]
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(
|
||||
context=call.context,
|
||||
permission=POLICY_CONTROL,
|
||||
user_id=call.context.user_id,
|
||||
)
|
||||
if not user.permissions.check_entity(satellite_entity_id, POLICY_CONTROL):
|
||||
raise Unauthorized(
|
||||
context=call.context,
|
||||
permission=POLICY_CONTROL,
|
||||
user_id=call.context.user_id,
|
||||
perm_category=CAT_ENTITIES,
|
||||
)
|
||||
|
||||
satellite_entity: AssistSatelliteEntity | None = component.get_entity(
|
||||
satellite_entity_id
|
||||
)
|
||||
|
||||
@@ -7,13 +7,17 @@ from .const import DOMAIN
|
||||
from .entity import AssistSatelliteState
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_idle": make_entity_state_condition(DOMAIN, AssistSatelliteState.IDLE),
|
||||
"is_listening": make_entity_state_condition(DOMAIN, AssistSatelliteState.LISTENING),
|
||||
"is_idle": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.IDLE, support_duration=True
|
||||
),
|
||||
"is_listening": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.LISTENING, support_duration=True
|
||||
),
|
||||
"is_processing": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.PROCESSING
|
||||
DOMAIN, AssistSatelliteState.PROCESSING, support_duration=True
|
||||
),
|
||||
"is_responding": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.RESPONDING
|
||||
DOMAIN, AssistSatelliteState.RESPONDING, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_idle: *condition_common
|
||||
is_listening: *condition_common
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
"conditions": {
|
||||
"is_idle": {
|
||||
@@ -9,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is idle"
|
||||
@@ -18,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is listening"
|
||||
@@ -27,6 +35,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is processing"
|
||||
@@ -36,6 +47,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is responding"
|
||||
@@ -58,19 +72,6 @@
|
||||
"id": "Answer ID",
|
||||
"sentences": "Sentences"
|
||||
}
|
||||
},
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
@@ -160,6 +161,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite became idle"
|
||||
@@ -169,6 +173,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite started listening"
|
||||
@@ -178,6 +185,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite started processing"
|
||||
@@ -187,6 +197,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite started responding"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user