diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 00000000..fbff14b8 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,1587 @@ +{ + "projectName": "adaptive-lighting", + "projectOwner": "basnijholt", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "angular", + "contributors": [ + { + "login": "basnijholt", + "name": "Bas Nijholt", + "avatar_url": "https://avatars.githubusercontent.com/u/6897215?v=4", + "profile": "http://www.nijho.lt/", + "contributions": [ + "code", + "maintenance", + "bug" + ] + }, + { + "login": "wrt54g", + "name": "Sven Serlier", + "avatar_url": "https://avatars.githubusercontent.com/u/85389871?v=4", + "profile": "https://github.com/wrt54g", + "contributions": [ + "doc" + ] + }, + { + "login": "willpuckett", + "name": "Will Puckett", + "avatar_url": "https://avatars.githubusercontent.com/u/12959477?v=4", + "profile": "https://github.com/willpuckett", + "contributions": [ + "doc" + ] + }, + { + "login": "vapescherov", + "name": "vapescherov", + "avatar_url": "https://avatars.githubusercontent.com/u/9620482?v=4", + "profile": "https://github.com/vapescherov", + "contributions": [ + "code" + ] + }, + { + "login": "travisp", + "name": "Travis Pew", + "avatar_url": "https://avatars.githubusercontent.com/u/165698?v=4", + "profile": "https://github.com/travisp", + "contributions": [ + "doc" + ] + }, + { + "login": "sindrebroch", + "name": "Sindre Broch", + "avatar_url": "https://avatars.githubusercontent.com/u/10772085?v=4", + "profile": "https://github.com/sindrebroch", + "contributions": [ + "doc" + ] + }, + { + "login": "Shulyaka", + "name": "Denis Shulyaka", + "avatar_url": "https://avatars.githubusercontent.com/u/2741408?v=4", + "profile": "https://github.com/Shulyaka", + "contributions": [ + "code" + ] + }, + { + "login": "RubenKelevra", + "name": "@RubenKelevra", + "avatar_url": "https://avatars.githubusercontent.com/u/614929?v=4", + "profile": "https://github.com/RubenKelevra", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "Repsionu", + "name": "Jüri Rebane", + "avatar_url": "https://avatars.githubusercontent.com/u/46962963?v=4", + "profile": "https://github.com/Repsionu", + "contributions": [ + "translation" + ] + }, + { + "login": "quantumlemur", + "name": "quantumlemur", + "avatar_url": "https://avatars.githubusercontent.com/u/229782?v=4", + "profile": "https://github.com/quantumlemur", + "contributions": [ + "code" + ] + }, + { + "login": "Oekn5w", + "name": "Michael Kirsch", + "avatar_url": "https://avatars.githubusercontent.com/u/38046255?v=4", + "profile": "https://github.com/Oekn5w", + "contributions": [ + "code" + ] + }, + { + "login": "Nicholaiii", + "name": "Nicholai Nissen", + "avatar_url": "https://avatars.githubusercontent.com/u/7280931?v=4", + "profile": "https://nicholai.dev/", + "contributions": [ + "translation" + ] + }, + { + "login": "myhrmans", + "name": "Martin Myhrman", + "avatar_url": "https://avatars.githubusercontent.com/u/14261388?v=4", + "profile": "https://github.com/myhrmans", + "contributions": [ + "translation" + ] + }, + { + "login": "mpeterson", + "name": "Michel Peterson", + "avatar_url": "https://avatars.githubusercontent.com/u/11870?v=4", + "profile": "https://github.com/mpeterson", + "contributions": [ + "code" + ] + }, + { + "login": "MangoScango", + "name": "MangoScango", + "avatar_url": "https://avatars.githubusercontent.com/u/7623678?v=4", + "profile": "https://github.com/MangoScango", + "contributions": [ + "code" + ] + }, + { + "login": "Lynilia", + "name": "Lynilia", + "avatar_url": "https://avatars.githubusercontent.com/u/89228568?v=4", + "profile": "https://github.com/Lynilia", + "contributions": [ + "translation" + ] + }, + { + "login": "LukaszP2", + "name": "LukaszP2", + "avatar_url": "https://avatars.githubusercontent.com/u/44735995?v=4", + "profile": "https://github.com/LukaszP2", + "contributions": [ + "translation" + ] + }, + { + "login": "jowgn", + "name": "Joscha Wagner", + "avatar_url": "https://avatars.githubusercontent.com/u/24966042?v=4", + "profile": "https://github.com/jowgn", + "contributions": [ + "translation" + ] + }, + { + "login": "josecarlosfernandez", + "name": "skdzzz", + "avatar_url": "https://avatars.githubusercontent.com/u/624242?v=4", + "profile": "https://github.com/josecarlosfernandez", + "contributions": [ + "translation" + ] + }, + { + "login": "itssimon", + "name": "Simon Gurcke", + "avatar_url": "https://avatars.githubusercontent.com/u/1176585?v=4", + "profile": "https://github.com/itssimon", + "contributions": [ + "code" + ] + }, + { + "login": "Hypfer", + "name": "Sören Beye", + "avatar_url": "https://avatars.githubusercontent.com/u/974410?v=4", + "profile": "http://hypfer.de/", + "contributions": [ + "code" + ] + }, + { + "login": "hudsonbrendon", + "name": "Hudson Brendon", + "avatar_url": "https://avatars.githubusercontent.com/u/5201888?v=4", + "profile": "http://medium.com/@hudsonbrendon", + "contributions": [ + "translation" + ] + }, + { + "login": "gvssr", + "name": "Gabriel Visser", + "avatar_url": "https://avatars.githubusercontent.com/u/61377476?v=4", + "profile": "https://github.com/gvssr", + "contributions": [ + "doc" + ] + }, + { + "login": "glebsterx", + "name": "Gleb", + "avatar_url": "https://avatars.githubusercontent.com/u/8779304?v=4", + "profile": "https://github.com/glebsterx", + "contributions": [ + "translation" + ] + }, + { + "login": "ghost", + "name": "Deleted user", + "avatar_url": "https://avatars.githubusercontent.com/u/10137?v=4", + "profile": "https://github.com/ghost", + "contributions": [ + "translation" + ] + }, + { + "login": "Djelibeybi", + "name": "Avi Miller", + "avatar_url": "https://avatars.githubusercontent.com/u/103232?v=4", + "profile": "https://omg.dje.li/", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "denysdovhan", + "name": "Denys Dovhan", + "avatar_url": "https://avatars.githubusercontent.com/u/3459374?v=4", + "profile": "https://github.com/denysdovhan", + "contributions": [ + "translation" + ] + }, + { + "login": "Davst", + "name": "David Stenbeck", + "avatar_url": "https://avatars.githubusercontent.com/u/3330933?v=4", + "profile": "http://davidstenbeck.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "danaues", + "name": "Kevin Addeman", + "avatar_url": "https://avatars.githubusercontent.com/u/24459240?v=4", + "profile": "https://github.com/danaues", + "contributions": [ + "code" + ] + }, + { + "login": "covid10", + "name": "covid10", + "avatar_url": "https://avatars.githubusercontent.com/u/71146231?v=4", + "profile": "https://github.com/covid10", + "contributions": [ + "translation", + "code" + ] + }, + { + "login": "chishm", + "name": "Michael Chisholm", + "avatar_url": "https://avatars.githubusercontent.com/u/18148723?v=4", + "profile": "https://github.com/chishm", + "contributions": [ + "code" + ] + }, + { + "login": "blueshiftlabs", + "name": "Justin Paupore", + "avatar_url": "https://avatars.githubusercontent.com/u/1445520?v=4", + "profile": "https://github.com/blueshiftlabs", + "contributions": [ + "code" + ] + }, + { + "login": "bedaes", + "name": "bedaes", + "avatar_url": "https://avatars.githubusercontent.com/u/8410205?v=4", + "profile": "https://github.com/bedaes", + "contributions": [ + "code" + ] + }, + { + "login": "awashingmachine", + "name": "awashingmachine", + "avatar_url": "https://avatars.githubusercontent.com/u/79043726?v=4", + "profile": "https://github.com/awashingmachine", + "contributions": [ + "translation" + ] + }, + { + "login": "claytonjn", + "name": "Clayton Nummer", + "avatar_url": "https://avatars.githubusercontent.com/u/3850252?v=4", + "profile": "https://github.com/claytonjn", + "contributions": [ + "code" + ] + }, + { + "login": "robert-crandall", + "name": "Robert Crandall", + "avatar_url": "https://avatars.githubusercontent.com/u/86014438?v=4", + "profile": "https://github.com/robert-crandall", + "contributions": [ + "code" + ] + }, + { + "login": "matt-forster", + "name": "Matt Forster", + "avatar_url": "https://avatars.githubusercontent.com/u/3375444?v=4", + "profile": "https://mattforster.ca/", + "contributions": [ + "code" + ] + }, + { + "login": "Mark-Niemeyer", + "name": "Mark Niemeyer", + "avatar_url": "https://avatars.githubusercontent.com/u/64665067?v=4", + "profile": "https://www.dfki.de/en/web/about-us/employee/person/maho10", + "contributions": [ + "translation", + "code" + ] + }, + { + "login": "talllguy", + "name": "Elliott Plack", + "avatar_url": "https://avatars.githubusercontent.com/u/1827881?v=4", + "profile": "https://www.linkedin.com/in/elliottplack/", + "contributions": [ + "doc" + ] + }, + { + "login": "ngommers", + "name": "ngommers", + "avatar_url": "https://avatars.githubusercontent.com/u/82467671?v=4", + "profile": "https://github.com/ngommers", + "contributions": [ + "translation" + ] + }, + { + "login": "deviantintegral", + "name": "Andrew Berry", + "avatar_url": "https://avatars.githubusercontent.com/u/255023?v=4", + "profile": "https://github.com/deviantintegral", + "contributions": [ + "doc" + ] + }, + { + "login": "brebtatv", + "name": "Tomáš Valigura", + "avatar_url": "https://avatars.githubusercontent.com/u/10747062?v=4", + "profile": "https://github.com/brebtatv", + "contributions": [ + "translation" + ] + }, + { + "login": "th3w1zard1", + "name": "Benjamin Auquite", + "avatar_url": "https://avatars.githubusercontent.com/u/2219836?v=4", + "profile": "https://github.com/th3w1zard1", + "contributions": [ + "code", + "bug", + "maintenance" + ] + }, + { + "login": "skycarl", + "name": "Skyler Carlson", + "avatar_url": "https://avatars.githubusercontent.com/u/43375685?v=4", + "profile": "https://github.com/skycarl", + "contributions": [ + "doc" + ] + }, + { + "login": "firstof9", + "name": "Chris", + "avatar_url": "https://avatars.githubusercontent.com/u/1105672?v=4", + "profile": "https://github.com/firstof9", + "contributions": [ + "code" + ] + }, + { + "login": "raman325", + "name": "Raman Gupta", + "avatar_url": "https://avatars.githubusercontent.com/u/7243222?v=4", + "profile": "https://github.com/raman325", + "contributions": [ + "code" + ] + }, + { + "login": "igiannakas", + "name": "igiannakas", + "avatar_url": "https://avatars.githubusercontent.com/u/59056762?v=4", + "profile": "https://github.com/igiannakas", + "contributions": [ + "code" + ] + }, + { + "login": "protyposis", + "name": "Mario Guggenberger", + "avatar_url": "https://avatars.githubusercontent.com/u/189372?v=4", + "profile": "http://protyposis.net", + "contributions": [ + "code", + "ideas" + ] + }, + { + "login": "KTibow", + "name": "Kendell R", + "avatar_url": "https://avatars.githubusercontent.com/u/10727862?v=4", + "profile": "https://ktibow.github.io/", + "contributions": [ + "design" + ] + }, + { + "login": "lukerix", + "name": "lukerix", + "avatar_url": "https://avatars.githubusercontent.com/u/93864731?v=4", + "profile": "https://github.com/lukerix", + "contributions": [ + "translation" + ] + }, + { + "login": "Mexx62", + "name": "Maxime Bailleul", + "avatar_url": "https://avatars.githubusercontent.com/u/8066485?v=4", + "profile": "https://github.com/Mexx62", + "contributions": [ + "translation" + ] + }, + { + "login": "michelbalzer", + "name": "Michel Balzer", + "avatar_url": "https://avatars.githubusercontent.com/u/1337412?v=4", + "profile": "https://michelbalzer.de", + "contributions": [ + "translation" + ] + }, + { + "login": "enrico1036", + "name": "Enrico Gambini", + "avatar_url": "https://avatars.githubusercontent.com/u/9280405?v=4", + "profile": "https://github.com/enrico1036", + "contributions": [ + "translation" + ] + }, + { + "login": "MirCore", + "name": "MirCore", + "avatar_url": "https://avatars.githubusercontent.com/u/9919366?v=4", + "profile": "https://github.com/MirCore", + "contributions": [ + "translation" + ] + }, + { + "login": "KilFer", + "name": "Fernando Belaza", + "avatar_url": "https://avatars.githubusercontent.com/u/290854?v=4", + "profile": "http://kilfer.es/", + "contributions": [ + "translation" + ] + }, + { + "login": "wilcomir", + "name": "Vladimir Cravero", + "avatar_url": "https://avatars.githubusercontent.com/u/795981?v=4", + "profile": "https://github.com/wilcomir", + "contributions": [ + "translation" + ] + }, + { + "login": "letroll", + "name": "Julien Quiévreux", + "avatar_url": "https://avatars.githubusercontent.com/u/255774?v=4", + "profile": "http://www.latavernedutroll.fr", + "contributions": [ + "translation" + ] + }, + { + "login": "lightrabbit", + "name": "lightrabbit", + "avatar_url": "https://avatars.githubusercontent.com/u/1521765?v=4", + "profile": "https://github.com/lightrabbit", + "contributions": [ + "translation" + ] + }, + { + "login": "Arie6414", + "name": "Arie6414", + "avatar_url": "https://avatars.githubusercontent.com/u/129661911?v=4", + "profile": "https://github.com/Arie6414", + "contributions": [ + "translation" + ] + }, + { + "login": "luixcaetano", + "name": "luixcaetano", + "avatar_url": "https://avatars.githubusercontent.com/u/4554163?v=4", + "profile": "https://github.com/luixcaetano", + "contributions": [ + "translation" + ] + }, + { + "login": "fmarcu", + "name": "fmarcu", + "avatar_url": "https://avatars.githubusercontent.com/u/81946691?v=4", + "profile": "https://github.com/fmarcu", + "contributions": [ + "translation" + ] + }, + { + "login": "michaelkmoch", + "name": "michaelkmoch", + "avatar_url": "https://avatars.githubusercontent.com/u/107689026?v=4", + "profile": "https://github.com/michaelkmoch", + "contributions": [ + "translation" + ] + }, + { + "login": "fbloemhof", + "name": "Fred", + "avatar_url": "https://avatars.githubusercontent.com/u/8753211?v=4", + "profile": "https://github.com/fbloemhof", + "contributions": [ + "translation" + ] + }, + { + "login": "Z-weapon", + "name": "Z-weapon", + "avatar_url": "https://avatars.githubusercontent.com/u/13939632?v=4", + "profile": "https://github.com/Z-weapon", + "contributions": [ + "translation" + ] + }, + { + "login": "kylebjordahl", + "name": "Kyle Bjordahl", + "avatar_url": "https://avatars.githubusercontent.com/u/3489222?v=4", + "profile": "https://github.com/kylebjordahl", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "olekbruks", + "name": "Olek Bruks", + "avatar_url": "https://avatars.githubusercontent.com/u/8738016?v=4", + "profile": "https://github.com/olekbruks", + "contributions": [ + "translation" + ] + }, + { + "login": "theclue", + "name": "Gabriele Baldassarre", + "avatar_url": "https://avatars.githubusercontent.com/u/1724406?v=4", + "profile": "https://gabrielebaldassarre.com", + "contributions": [ + "translation" + ] + }, + { + "login": "pbaart", + "name": "Pepijn Baart", + "avatar_url": "https://avatars.githubusercontent.com/u/2856849?v=4", + "profile": "https://github.com/pbaart", + "contributions": [ + "translation" + ] + }, + { + "login": "pastukhov", + "name": "Artem Pastukhov", + "avatar_url": "https://avatars.githubusercontent.com/u/3490616?v=4", + "profile": "http://ovoi.io", + "contributions": [ + "translation" + ] + }, + { + "login": "mstefany", + "name": "Martin Štefany", + "avatar_url": "https://avatars.githubusercontent.com/u/57348587?v=4", + "profile": "https://stefany.eu", + "contributions": [ + "translation" + ] + }, + { + "login": "quenthal", + "name": "quenthal", + "avatar_url": "https://avatars.githubusercontent.com/u/17827203?v=4", + "profile": "https://github.com/quenthal", + "contributions": [ + "translation" + ] + }, + { + "login": "Luki72", + "name": "Luki72", + "avatar_url": "https://avatars.githubusercontent.com/u/22493116?v=4", + "profile": "https://github.com/Luki72", + "contributions": [ + "translation" + ] + }, + { + "login": "pantan-cymk", + "name": "pantan-cymk", + "avatar_url": "https://avatars.githubusercontent.com/u/87476229?v=4", + "profile": "https://github.com/pantan-cymk", + "contributions": [ + "translation" + ] + }, + { + "login": "yousaf465", + "name": "yousaf465", + "avatar_url": "https://avatars.githubusercontent.com/u/83491212?v=4", + "profile": "https://github.com/yousaf465", + "contributions": [ + "translation" + ] + }, + { + "login": "baylanger", + "name": "Pierre Belanger", + "avatar_url": "https://avatars.githubusercontent.com/u/5240348?v=4", + "profile": "https://github.com/baylanger", + "contributions": [ + "doc" + ] + }, + { + "login": "jansigu", + "name": "Jan-Sigurd Sørensen", + "avatar_url": "https://avatars.githubusercontent.com/u/8410766?v=4", + "profile": "http://www.jan-sigurd.com", + "contributions": [ + "translation" + ] + }, + { + "login": "EF01", + "name": "EF01", + "avatar_url": "https://avatars.githubusercontent.com/u/20759250?v=4", + "profile": "https://github.com/EF01", + "contributions": [ + "translation" + ] + }, + { + "login": "MrSnakeSPb", + "name": "Mr Snake", + "avatar_url": "https://avatars.githubusercontent.com/u/68160409?v=4", + "profile": "https://github.com/MrSnakeSPb", + "contributions": [ + "translation" + ] + }, + { + "login": "hungrymachine1", + "name": "hungrymachine1", + "avatar_url": "https://avatars.githubusercontent.com/u/73683742?v=4", + "profile": "https://github.com/hungrymachine1", + "contributions": [ + "translation" + ] + }, + { + "login": "4D4M-Github", + "name": "4D4M-Github", + "avatar_url": "https://avatars.githubusercontent.com/u/123521171?v=4", + "profile": "https://github.com/4D4M-Github", + "contributions": [ + "translation" + ] + }, + { + "login": "sayaivan", + "name": "Ivan", + "avatar_url": "https://avatars.githubusercontent.com/u/49090860?v=4", + "profile": "https://github.com/sayaivan", + "contributions": [ + "translation" + ] + }, + { + "login": "Fllorent0D", + "name": "Florent Cardoen", + "avatar_url": "https://avatars.githubusercontent.com/u/13313104?v=4", + "profile": "https://www.floca.be", + "contributions": [ + "translation" + ] + }, + { + "login": "moemeli", + "name": "moemeli", + "avatar_url": "https://avatars.githubusercontent.com/u/73445184?v=4", + "profile": "https://github.com/moemeli", + "contributions": [ + "translation" + ] + }, + { + "login": "saya6k", + "name": "saya6k", + "avatar_url": "https://avatars.githubusercontent.com/u/63517312?v=4", + "profile": "https://github.com/saya6k", + "contributions": [ + "translation" + ] + }, + { + "login": "droans", + "name": "droans", + "avatar_url": "https://avatars.githubusercontent.com/u/49721649?v=4", + "profile": "https://github.com/droans", + "contributions": [ + "code" + ] + }, + { + "login": "JonathanKang", + "name": "Jonathan Kang", + "avatar_url": "https://avatars.githubusercontent.com/u/5607743?v=4", + "profile": "http://blogs.gnome.org/jonathankang/", + "contributions": [ + "code" + ] + }, + { + "login": "scuricvladimir", + "name": "scuricvladimir", + "avatar_url": "https://avatars.githubusercontent.com/u/46634162?v=4", + "profile": "https://github.com/scuricvladimir", + "contributions": [ + "translation" + ] + }, + { + "login": "Welsyntoffie", + "name": "Pieter", + "avatar_url": "https://avatars.githubusercontent.com/u/47089904?v=4", + "profile": "https://github.com/Welsyntoffie", + "contributions": [ + "translation" + ] + }, + { + "login": "san80068259", + "name": "san80068259", + "avatar_url": "https://avatars.githubusercontent.com/u/68324107?v=4", + "profile": "https://github.com/san80068259", + "contributions": [ + "translation" + ] + }, + { + "login": "erdnaxela02", + "name": "Frosh", + "avatar_url": "https://avatars.githubusercontent.com/u/21007415?v=4", + "profile": "https://github.com/erdnaxela02", + "contributions": [ + "code" + ] + }, + { + "login": "rafaeltmiranda", + "name": "Rafael Miranda", + "avatar_url": "https://avatars.githubusercontent.com/u/47206949?v=4", + "profile": "https://www.linkedin.com/in/rafaeltmiranda/", + "contributions": [ + "translation" + ] + }, + { + "login": "rVlad93", + "name": "rVlad93", + "avatar_url": "https://avatars.githubusercontent.com/u/60452666?v=4", + "profile": "https://github.com/rVlad93", + "contributions": [ + "translation" + ] + }, + { + "login": "MrEbbinghaus", + "name": "Björn Ebbinghaus", + "avatar_url": "https://avatars.githubusercontent.com/u/2965273?v=4", + "profile": "https://blog.ebbinghaus.me/", + "contributions": [ + "code" + ] + }, + { + "login": "Marck", + "name": "Marck", + "avatar_url": "https://avatars.githubusercontent.com/u/18088281?v=4", + "profile": "https://github.com/Marck", + "contributions": [ + "code" + ] + }, + { + "login": "lachezar-gizdov", + "name": "Lucho Gizdov", + "avatar_url": "https://avatars.githubusercontent.com/u/11273726?v=4", + "profile": "https://carmodsheaven.com", + "contributions": [ + "translation" + ] + }, + { + "login": "MizterB", + "name": "MizterB", + "avatar_url": "https://avatars.githubusercontent.com/u/5458030?v=4", + "profile": "https://github.com/MizterB", + "contributions": [ + "code" + ] + }, + { + "login": "brietman", + "name": "brietman", + "avatar_url": "https://avatars.githubusercontent.com/u/17436537?v=4", + "profile": "https://github.com/brietman", + "contributions": [ + "translation" + ] + }, + { + "login": "TamilNeram", + "name": "தமிழ் நேரம்", + "avatar_url": "https://avatars.githubusercontent.com/u/67970539?v=4", + "profile": "https://github.com/TamilNeram", + "contributions": [ + "translation" + ] + }, + { + "login": "Thunderstrike116", + "name": "Thunderstrike116", + "avatar_url": "https://avatars.githubusercontent.com/u/23220766?v=4", + "profile": "https://github.com/Thunderstrike116", + "contributions": [ + "translation" + ] + }, + { + "login": "immeteor2", + "name": "immeteor2", + "avatar_url": "https://avatars.githubusercontent.com/u/125735487?v=4", + "profile": "https://github.com/immeteor2", + "contributions": [ + "translation" + ] + }, + { + "login": "pbassut", + "name": "Patrick Bassut", + "avatar_url": "https://avatars.githubusercontent.com/u/1500037?v=4", + "profile": "https://github.com/pbassut", + "contributions": [ + "translation" + ] + }, + { + "login": "Ricky-Tigg", + "name": "Ricky Tigg", + "avatar_url": "https://avatars.githubusercontent.com/u/26058215?v=4", + "profile": "https://github.com/Ricky-Tigg", + "contributions": [ + "translation" + ] + }, + { + "login": "marazmarci", + "name": "Márton Maráz", + "avatar_url": "https://avatars.githubusercontent.com/u/1349654?v=4", + "profile": "https://github.com/marazmarci", + "contributions": [ + "code" + ] + }, + { + "login": "Sara492", + "name": "Sara492", + "avatar_url": "https://avatars.githubusercontent.com/u/63058202?v=4", + "profile": "https://github.com/Sara492", + "contributions": [ + "translation" + ] + }, + { + "login": "enpaga", + "name": "enpaga", + "avatar_url": "https://avatars.githubusercontent.com/u/180730931?v=4", + "profile": "https://github.com/enpaga", + "contributions": [ + "translation" + ] + }, + { + "login": "xuars", + "name": "xuars", + "avatar_url": "https://avatars.githubusercontent.com/u/197080354?v=4", + "profile": "https://github.com/xuars", + "contributions": [ + "translation" + ] + }, + { + "login": "tinutac", + "name": "tinutac", + "avatar_url": "https://avatars.githubusercontent.com/u/2151553?v=4", + "profile": "https://github.com/tinutac", + "contributions": [ + "translation" + ] + }, + { + "login": "defaultpage", + "name": "Default User", + "avatar_url": "https://avatars.githubusercontent.com/u/22825202?v=4", + "profile": "https://github.com/defaultpage", + "contributions": [ + "translation" + ] + }, + { + "login": "amelenty", + "name": "amelenty", + "avatar_url": "https://avatars.githubusercontent.com/u/29466876?v=4", + "profile": "https://github.com/amelenty", + "contributions": [ + "translation" + ] + }, + { + "login": "yeaxi", + "name": "Rostyslav Dudka", + "avatar_url": "https://avatars.githubusercontent.com/u/15959384?v=4", + "profile": "https://ua.linkedin.com/in/rostyslav-dudka", + "contributions": [ + "translation" + ] + }, + { + "login": "helderfmf", + "name": "Helder Ferreira", + "avatar_url": "https://avatars.githubusercontent.com/u/5622687?v=4", + "profile": "https://github.com/helderfmf", + "contributions": [ + "translation" + ] + }, + { + "login": "mrpiotr-dev", + "name": "Piotr Laszczkowski", + "avatar_url": "https://avatars.githubusercontent.com/u/11849621?v=4", + "profile": "http://mrpiotr.dev", + "contributions": [ + "translation" + ] + }, + { + "login": "rezaalmanda", + "name": "Reza", + "avatar_url": "https://avatars.githubusercontent.com/u/22217419?v=4", + "profile": "http://rezaalmanda.github.io", + "contributions": [ + "translation" + ] + }, + { + "login": "bittin", + "name": "Luna Jernberg", + "avatar_url": "https://avatars.githubusercontent.com/u/43197?v=4", + "profile": "https://github.com/bittin", + "contributions": [ + "translation" + ] + }, + { + "login": "jawilson", + "name": "Jeff Wilson", + "avatar_url": "https://avatars.githubusercontent.com/u/1368827?v=4", + "profile": "http://jeffalwilson.com", + "contributions": [ + "code" + ] + }, + { + "login": "TermeHansen", + "name": "Rasmus Lundsgaard", + "avatar_url": "https://avatars.githubusercontent.com/u/6922018?v=4", + "profile": "https://github.com/TermeHansen", + "contributions": [ + "code" + ] + }, + { + "login": "Tommatheussen", + "name": "Tom Matheussen", + "avatar_url": "https://avatars.githubusercontent.com/u/13683094?v=4", + "profile": "https://github.com/Tommatheussen", + "contributions": [ + "code" + ] + }, + { + "login": "ams2990", + "name": "ams2990", + "avatar_url": "https://avatars.githubusercontent.com/u/488907?v=4", + "profile": "https://github.com/ams2990", + "contributions": [ + "code" + ] + }, + { + "login": "DataGhost", + "name": "DataGhost", + "avatar_url": "https://avatars.githubusercontent.com/u/3911340?v=4", + "profile": "https://github.com/DataGhost", + "contributions": [ + "code" + ] + }, + { + "login": "Wijt", + "name": "Furkan Kaya", + "avatar_url": "https://avatars.githubusercontent.com/u/23127261?v=4", + "profile": "https://iamfurkan.com", + "contributions": [ + "translation" + ] + }, + { + "login": "Rafael4A", + "name": "Rafael do Amaral Porciuncula", + "avatar_url": "https://avatars.githubusercontent.com/u/32150173?v=4", + "profile": "https://github.com/Rafael4A", + "contributions": [ + "translation" + ] + }, + { + "login": "hhjuhl", + "name": "hhjuhl", + "avatar_url": "https://avatars.githubusercontent.com/u/84127693?v=4", + "profile": "https://github.com/hhjuhl", + "contributions": [ + "translation" + ] + }, + { + "login": "Athishbalu", + "name": "B.Athish", + "avatar_url": "https://avatars.githubusercontent.com/u/177029556?v=4", + "profile": "https://github.com/Athishbalu", + "contributions": [ + "translation" + ] + }, + { + "login": "maksim2005UKR", + "name": "Горпиніч Максим Олександрович", + "avatar_url": "https://avatars.githubusercontent.com/u/233082001?v=4", + "profile": "https://github.com/maksim2005UKR", + "contributions": [ + "translation" + ] + }, + { + "login": "plageoj", + "name": "Masayuki Sugahara", + "avatar_url": "https://avatars.githubusercontent.com/u/10688301?v=4", + "profile": "https://plageoj.me", + "contributions": [ + "translation" + ] + }, + { + "login": "therealmate", + "name": "therealmate", + "avatar_url": "https://avatars.githubusercontent.com/u/61843503?v=4", + "profile": "https://github.com/therealmate", + "contributions": [ + "translation" + ] + }, + { + "login": "dobby5", + "name": "Dobby", + "avatar_url": "https://avatars.githubusercontent.com/u/1346316?v=4", + "profile": "https://github.com/dobby5", + "contributions": [ + "code" + ] + }, + { + "login": "lenucksi", + "name": "lenucksi", + "avatar_url": "https://avatars.githubusercontent.com/u/2451899?v=4", + "profile": "https://github.com/lenucksi", + "contributions": [ + "code" + ] + }, + { + "login": "edgimar", + "name": "edgimar", + "avatar_url": "https://avatars.githubusercontent.com/u/393850?v=4", + "profile": "https://gitlab.com/edgimar", + "contributions": [ + "code" + ] + }, + { + "login": "andrei-lazarov", + "name": "Andrei LAZAROV", + "avatar_url": "https://avatars.githubusercontent.com/u/51081857?v=4", + "profile": "https://github.com/andrei-lazarov", + "contributions": [ + "doc" + ] + }, + { + "login": "ademuri", + "name": "Adam DeMuri", + "avatar_url": "https://avatars.githubusercontent.com/u/3051618?v=4", + "profile": "https://github.com/ademuri", + "contributions": [ + "code" + ] + }, + { + "login": "NatanDosAnjos", + "name": "Natanael", + "avatar_url": "https://avatars.githubusercontent.com/u/45629905?v=4", + "profile": "https://github.com/NatanDosAnjos", + "contributions": [ + "translation" + ] + }, + { + "login": "Yllelder", + "name": "Yllelder Bamir", + "avatar_url": "https://avatars.githubusercontent.com/u/6941502?v=4", + "profile": "https://github.com/Yllelder", + "contributions": [ + "translation" + ] + }, + { + "login": "Esspel", + "name": "Esspel", + "avatar_url": "https://avatars.githubusercontent.com/u/47383506?v=4", + "profile": "https://github.com/Esspel", + "contributions": [ + "translation" + ] + }, + { + "login": "cperuffo3", + "name": "Corey Peruffo", + "avatar_url": "https://avatars.githubusercontent.com/u/87686305?v=4", + "profile": "https://github.com/cperuffo3", + "contributions": [ + "code" + ] + }, + { + "login": "imwithsam", + "name": "Samson Brock", + "avatar_url": "https://avatars.githubusercontent.com/u/1934074?v=4", + "profile": "http://badmotivator.io/", + "contributions": [ + "code" + ] + }, + { + "login": "Dennis-Dekker", + "name": "Dennis Dekker", + "avatar_url": "https://avatars.githubusercontent.com/u/48018095?v=4", + "profile": "https://github.com/Dennis-Dekker", + "contributions": [ + "code" + ] + }, + { + "login": "proscar87", + "name": "proscar87", + "avatar_url": "https://avatars.githubusercontent.com/u/68169114?v=4", + "profile": "https://github.com/proscar87", + "contributions": [ + "code" + ] + }, + { + "login": "jaredjxyz", + "name": "Jared Jensen", + "avatar_url": "https://avatars.githubusercontent.com/u/10385335?v=4", + "profile": "http://jaredj.xyz/", + "contributions": [ + "code" + ] + }, + { + "login": "mueslo", + "name": "mueslo", + "avatar_url": "https://avatars.githubusercontent.com/u/847751?v=4", + "profile": "https://github.com/mueslo", + "contributions": [ + "code" + ] + }, + { + "login": "marijneken", + "name": "Marijn Eken", + "avatar_url": "https://avatars.githubusercontent.com/u/928998?v=4", + "profile": "https://github.com/marijneken", + "contributions": [ + "doc" + ] + }, + { + "login": "kasiom", + "name": "Milan K.", + "avatar_url": "https://avatars.githubusercontent.com/u/2422245?v=4", + "profile": "https://github.com/kasiom", + "contributions": [ + "translation" + ] + }, + { + "login": "callistoprime", + "name": "Callisto", + "avatar_url": "https://avatars.githubusercontent.com/u/178052328?v=4", + "profile": "https://github.com/callistoprime", + "contributions": [ + "bug" + ] + }, + { + "login": "davidgeiger", + "name": "David Geiger", + "avatar_url": "https://avatars.githubusercontent.com/u/5699049?v=4", + "profile": "https://github.com/davidgeiger", + "contributions": [ + "bug" + ] + }, + { + "login": "MSL-DA", + "name": "Jan", + "avatar_url": "https://avatars.githubusercontent.com/u/134940586?v=4", + "profile": "https://github.com/MSL-DA", + "contributions": [ + "translation" + ] + }, + { + "login": "frankysan", + "name": "frankysan", + "avatar_url": "https://avatars.githubusercontent.com/u/6353605?v=4", + "profile": "https://github.com/frankysan", + "contributions": [ + "translation" + ] + }, + { + "login": "belkin", + "name": "Belkin", + "avatar_url": "https://avatars.githubusercontent.com/u/3419659?v=4", + "profile": "http://belkinfahri.com", + "contributions": [ + "translation" + ] + }, + { + "login": "LukTyn", + "name": "LukTyn", + "avatar_url": "https://avatars.githubusercontent.com/u/1812796?v=4", + "profile": "https://github.com/LukTyn", + "contributions": [ + "translation" + ] + }, + { + "login": "rutgerkra", + "name": "rutgerkra", + "avatar_url": "https://avatars.githubusercontent.com/u/7963187?v=4", + "profile": "https://github.com/rutgerkra", + "contributions": [ + "translation" + ] + }, + { + "login": "sergeybelozorov", + "name": "sergeybelozorov", + "avatar_url": "https://avatars.githubusercontent.com/u/94930734?v=4", + "profile": "https://github.com/sergeybelozorov", + "contributions": [ + "translation" + ] + }, + { + "login": "weblate-Lamster", + "name": "Allan Himidi-Rattenborg", + "avatar_url": "https://hosted.weblate.org/avatar/128/Lamster.png", + "profile": "https://hosted.weblate.org/user/Lamster/", + "contributions": [ + "translation" + ] + }, + { + "login": "weblate-posemartonis", + "name": "Pose marto", + "avatar_url": "https://hosted.weblate.org/avatar/128/posemartonis.png", + "profile": "https://hosted.weblate.org/user/posemartonis/", + "contributions": [ + "translation" + ] + }, + { + "login": "weblate-jf.cosse", + "name": "Jean-Francois Cosse", + "avatar_url": "https://hosted.weblate.org/avatar/128/jf.cosse.png", + "profile": "https://hosted.weblate.org/user/jf.cosse/", + "contributions": [ + "translation" + ] + }, + { + "login": "bisquit2003", + "name": "bisquit2003", + "avatar_url": "https://avatars.githubusercontent.com/u/98059406?v=4", + "profile": "https://github.com/bisquit2003", + "contributions": [ + "bug" + ] + }, + { + "login": "chewth91", + "name": "chewth91", + "avatar_url": "https://avatars.githubusercontent.com/u/29686018?v=4", + "profile": "https://github.com/chewth91", + "contributions": [ + "bug" + ] + }, + { + "login": "rhtenhove", + "name": "rhtenhove", + "avatar_url": "https://avatars.githubusercontent.com/u/10206967?v=4", + "profile": "https://github.com/rhtenhove", + "contributions": [ + "code" + ] + }, + { + "login": "mrbillpapas", + "name": "Bill Papas", + "avatar_url": "https://avatars.githubusercontent.com/u/45721000?v=4", + "profile": "https://github.com/mrbillpapas", + "contributions": [ + "ideas" + ] + }, + { + "login": "haasn", + "name": "Niklas Haas", + "avatar_url": "https://avatars.githubusercontent.com/u/1149047?v=4", + "profile": "https://niklashaas.de", + "contributions": [ + "ideas" + ] + }, + { + "login": "djurny", + "name": "Tom Urlings", + "avatar_url": "https://avatars.githubusercontent.com/u/950171?v=4", + "profile": "https://github.com/djurny", + "contributions": [ + "ideas" + ] + }, + { + "login": "BenoitAnastay", + "name": "Benoit Anastay", + "avatar_url": "https://avatars.githubusercontent.com/u/45088785?v=4", + "profile": "http://anastay.dev", + "contributions": [ + "ideas" + ] + }, + { + "login": "GollyJer", + "name": "Jeremy Gollehon", + "avatar_url": "https://avatars.githubusercontent.com/u/689204?v=4", + "profile": "https://isjustawesome.com", + "contributions": [ + "ideas", + "bug" + ] + }, + { + "login": "jrbergen", + "name": "jrbergen", + "avatar_url": "https://avatars.githubusercontent.com/u/6237646?v=4", + "profile": "https://github.com/jrbergen", + "contributions": [ + "ideas" + ] + }, + { + "login": "b-rad15", + "name": "Bradley O'Connell", + "avatar_url": "https://avatars.githubusercontent.com/u/25830163?v=4", + "profile": "https://github.com/b-rad15", + "contributions": [ + "ideas" + ] + }, + { + "login": "00schteven", + "name": "00schteven", + "avatar_url": "https://avatars.githubusercontent.com/u/76514745?v=4", + "profile": "https://github.com/00schteven", + "contributions": [ + "ideas" + ] + }, + { + "login": "Mariuss811", + "name": "Wosten", + "avatar_url": "https://avatars.githubusercontent.com/u/54115696?v=4", + "profile": "https://github.com/Mariuss811", + "contributions": [ + "bug" + ] + }, + { + "login": "zpriddy", + "name": "Zachary Priddy", + "avatar_url": "https://avatars.githubusercontent.com/u/1858679?v=4", + "profile": "http://zpriddy.com", + "contributions": [ + "ideas" + ] + }, + { + "login": "abkslm", + "name": "Andrew Blakeslee Moore", + "avatar_url": "https://avatars.githubusercontent.com/u/60765958?v=4", + "profile": "http://blakeslee.me", + "contributions": [ + "bug" + ] + }, + { + "login": "jaynis", + "name": "jaynis", + "avatar_url": "https://avatars.githubusercontent.com/u/1553675?v=4", + "profile": "https://github.com/jaynis", + "contributions": [ + "code" + ] + }, + { + "login": "alistairg", + "name": "Alistair Galbraith", + "avatar_url": "https://avatars.githubusercontent.com/u/272786?v=4", + "profile": "https://github.com/alistairg", + "contributions": [ + "code" + ] + }, + { + "login": "hesseleo", + "name": "Leonhard Hesse", + "avatar_url": "https://avatars.githubusercontent.com/u/44778508?v=4", + "profile": "https://github.com/hesseleo", + "contributions": [ + "code" + ] + }, + { + "login": "timstallmann", + "name": "Tim Stallmann", + "avatar_url": "https://avatars.githubusercontent.com/u/6741938?v=4", + "profile": "http://www.tim-maps.com", + "contributions": [ + "code" + ] + }, + { + "login": "lehneres", + "name": "lehneres", + "avatar_url": "https://avatars.githubusercontent.com/u/7437288?v=4", + "profile": "https://github.com/lehneres", + "contributions": [ + "ideas" + ] + }, + { + "login": "ahmadtawakol", + "name": "Ahmad Tawakol", + "avatar_url": "https://avatars.githubusercontent.com/u/2355493?v=4", + "profile": "https://github.com/ahmadtawakol", + "contributions": [ + "code", + "bug", + "maintenance" + ] + } + ], + "contributorsPerLine": 7, + "linkToUsage": true, + "commitType": "docs" +} diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 00000000..8d5e2bd9 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,40 @@ +{ + "name": "basnijholt/adaptive_lighting", + "image": "mcr.microsoft.com/devcontainers/python:3-3.13", + "postCreateCommand": "./scripts/setup-devcontainer && . .venv/bin/activate", + "forwardPorts": [ + 8123 + ], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "github.vscode-pull-request-github", + "ryanluker.vscode-coverage-gutters", + "ms-python.vscode-pylance" + ], + "settings": { + "files.eol": "\n", + "editor.tabSize": 4, + "python.pythonPath": "/usr/bin/python3", + "python.analysis.autoSearchPaths": false, + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } + } + }, + "remoteUser": "vscode", + "features": {} +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..767b6084 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# The Home Assistant core checkout. tests/README.md has you clone it to ./core, +# but the Dockerfile clones its own copy to /core and links /app/core to it. +# Without this entry `COPY . /app/` ships ~300MB into every build and leaves +# /app/core as a real directory, so `ln -s /core /app/core` links *inside* it +# rather than creating the intended symlink. +core/ + +# Local virtualenvs +.venv/ +venv/ +env/ +ENV/ + +# Not used by the build +.git/ +.vscode/ +.idea/ + +# Caches and test output +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +htmlcov/ +.coverage +.coverage.* +coverage.xml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..847961cb --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @basnijholt diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..8bb28be4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [basnijholt] diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000..548276d0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,63 @@ +--- +name: Bug Report +about: Report a bug in adaptive-lighting. +title: '' +labels: kind/bug, kind/feature, need/triage +assignees: '' + +--- + +# Home Assistant Adaptive Lighting Issue Template + +## Bug Reports + +If you need help with using or configuring Adaptive Lighting, please [open a Q&A discussion thread here](https://github.com/basnijholt/adaptive-lighting/discussions/new?category=q-a) instead. + +### Before submitting a bug report, please follow these troubleshooting steps: + +Please confirm that you have completed the following steps: + +- [ ] I have updated to the [latest Adaptive Lighting version](https://github.com/basnijholt/adaptive-lighting/releases) available in [HACS](https://hacs.xyz/). +- [ ] I have reviewed the [Troubleshooting Section](https://github.com/basnijholt/adaptive-lighting#sos-troubleshooting) in the [README](https://github.com/basnijholt/adaptive-lighting#readme). +- [ ] (If using Zigbee2MQTT) I have read the [Zigbee2MQTT troubleshooting guide](https://github.com/basnijholt/adaptive-lighting#zigbee2mqtt) in the [README](https://github.com/basnijholt/adaptive-lighting#readme). +- [ ] I have checked the [V2 Roadmap](https://github.com/basnijholt/adaptive-lighting/discussions/291) and [open issues](https://github.com/basnijholt/adaptive-lighting/issues) to ensure my issue isn't a duplicate. + + +### Required information for bug reports: + +Please include the following information in your issue. + +*Issues missing this information may not be addressed.* + +1. **Debug logs** captured while the issue occurred. [See here for instructions on enabling debug logging](https://github.com/basnijholt/adaptive-lighting#troubleshooting): + +``` + +``` + +2. [Your Adaptive Lighting configuration](https://github.com/basnijholt/adaptive-lighting#gear-configuration): + +``` + +``` + +3. (If using Zigbee2MQTT), provide your configuration files (**remove all personal information before posting**): + - `devices.yaml` + - `groups.yaml` + - `configuration.yaml` ⚠️; **Warning** _**REMOVE ALL of the PERSONAL INFORMATION BELOW before posting**_ ⚠️; + - mqtt: `server`: + - mqtt: `user`: + - mqtt: `password`: + - advanced: `pan_id`: + - advanced: `network_key`: + - anything in `log_syslog` if you use this + - Brand and model number of problematic light(s) +``` + +``` + +4. Describe the bug and how to reproduce it: + + + +5. Steps to reproduce the behavior: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..c4eeda14 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Getting Help on adaptive-lighting + url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/q-a + about: Q&A section of the discussion tab + - name: Share your idea + url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/ideas + about: And discuss it with the community + - name: General discussions about this component + url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/general + about: General discussions about this component + - name: Share your setup with adaptive-lighting + url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/show-and-tell + about: Or see what other people do with this component diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md new file mode 100644 index 00000000..f6458d77 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -0,0 +1,16 @@ +--- +name: Documentation Issue +about: Report missing, erroneous docs, broken links or propose new docs +title: '' +labels: kind/docs_issue, need/triage +assignees: '' + +--- + +#### Location + + + +#### Description + + diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 00000000..d25a9689 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,8 @@ +--- +name: Enhancement +about: Suggest an improvement to an existing feature. +title: '' +labels: kind/enhancement, need/triage +assignees: '' + +--- diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..088b35e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,8 @@ +--- +name: Feature +about: Suggest a new feature +title: '' +labels: kind/feature, need/triage +assignees: '' + +--- diff --git a/.github/auto-comment.yml b/.github/auto-comment.yml new file mode 100644 index 00000000..c1272240 --- /dev/null +++ b/.github/auto-comment.yml @@ -0,0 +1,6 @@ +# Comment to a new issue. +# Disabled +# issueOpened: "" + +# Disabled +# pullRequestOpened: "" diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..915efced --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,64 @@ +# Configuration for welcome - https://github.com/behaviorbot/welcome + +# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome +# Comment to be posted to on first time issues +newIssueWelcomeComment: > + Thank you for submitting your first issue to this repository! A maintainer + will be here shortly to triage and review. + + In the meantime, please double-check that you have provided all the + necessary information to make this process easy! Any information that can + help save additional round trips is useful! We currently aim to give + initial feedback within **two business days**. If this does not happen, feel + free to leave a comment. + + Please keep an eye on how this issue will be labeled, as labels give an + overview of priorities, assignments and additional actions requested by the + maintainers: + + - "Priority" labels will show how urgent this is for the team. + - "Status" labels will show if this is ready to be worked on, blocked, or in progress. + - "Need" labels will indicate if additional input or analysis is required. + + Finally, remember to use [the discussion tab](https://github.com/basnijholt/adaptive-lighting/discussions) if you just need general + support. + +# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome +# Comment to be posted to on PRs from first time contributors in your repository +newPRWelcomeComment: > + Thank you for submitting this PR! + + A maintainer will be here shortly to review it. + + We are super grateful! Help us by making sure that: + + * The context for this PR is clear, with relevant discussion, decisions + and stakeholders linked/mentioned. + + * Your contribution itself is clear (code comments, self-review for the + rest) and in its best form. + + Getting other community members to do a review would be great help too on + complex PRs. If you are unsure about something, just leave us a comment. + + Next steps: + + * A maintainer will triage and assign priority to this PR, commenting on + any missing things and potentially assigning a reviewer for high + priority items. + + * The PR gets reviews, discussed and approvals as needed. + + * The PR is merged by maintainers when it has been approved and comments addressed. + + We currently aim to provide initial feedback/triaging within **two business + days**. Please keep an eye on any labelling actions, as these will indicate + priorities and status of your contribution. + + We are very grateful for your contribution! + + +# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge +# Comment to be posted to on pull requests merged by a first time user +# Currently disabled +#firstPRMergeComment: "" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..ad38c0ea --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,6 @@ +template: | + ## What’s Changed + + $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..d27d86ea --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "rebaseWhen": "behind-base-branch", + "dependencyDashboard": true, + "labels": [ + "dependencies", + "no-stale" + ], + "commitMessagePrefix": "⬆️", + "commitMessageTopic": "{{depName}}", + "prBodyDefinitions": { + "Release": "yes" + }, + "packageRules": [ + { + "matchManagers": [ + "github-actions" + ], + "addLabels": [ + "github_actions" + ], + "rangeStrategy": "pin" + }, + { + "matchManagers": [ + "github-actions" + ], + "matchUpdateTypes": [ + "minor", + "patch" + ], + "automerge": true + } + ], + "extends": [ + "config:recommended" + ] +} diff --git a/.github/update-services.py b/.github/update-services.py new file mode 100644 index 00000000..9ea1860f --- /dev/null +++ b/.github/update-services.py @@ -0,0 +1,27 @@ +"""Creates a services.yaml file with the latest docs.""" + +import sys +from pathlib import Path + +import yaml + +sys.path.append(str(Path(__file__).parent.parent)) + +from custom_components.adaptive_lighting import const + +services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml" +with open(services_filename) as f: # noqa: PTH123 + services = yaml.safe_load(f) + +for service_name, dct in services.items(): + _docs = {"set_manual_control": const.DOCS_MANUAL_CONTROL, "apply": const.DOCS_APPLY} + alternative_docs = _docs.get(service_name, const.DOCS) + for field_name, field in dct["fields"].items(): + description = alternative_docs.get(field_name, const.DOCS[field_name]) + field["description"] = description + +comment = "# This file is auto-generated by .github/update-services.py." + +with services_filename.open("w") as f: + f.write(comment + "\n") + yaml.dump(services, f, sort_keys=False, width=1000, allow_unicode=True) diff --git a/.github/update-strings.py b/.github/update-strings.py new file mode 100644 index 00000000..0e6a4500 --- /dev/null +++ b/.github/update-strings.py @@ -0,0 +1,117 @@ +"""Update strings.json and en.json from const.py.""" + +import json +import sys +from copy import deepcopy +from pathlib import Path + +import homeassistant.helpers.config_validation as cv +import yaml + +sys.path.append(str(Path(__file__).parent.parent)) + +from custom_components.adaptive_lighting import const + +folder = Path("custom_components") / "adaptive_lighting" +strings_fname = folder / "strings.json" +en_fname = folder / "translations" / "en.json" +translation_fnames = (folder / "translations").glob("*.json") +with strings_fname.open() as f: + strings = json.load(f) + + +def _partition_options(values): + """Partition option translations into basic and advanced dictionaries.""" + basic = { + key: values[key] + for key, _, _ in const.VALIDATION_TUPLES + if key in const.BASIC_OPTIONS and key in values + } + advanced = { + key: values[key] + for key, _, _ in const.VALIDATION_TUPLES + if key not in const.BASIC_OPTIONS and key in values + } + return basic, advanced + + +def _migrate_translation_options(step): + """Move translated advanced options under the advanced section.""" + sections = step.setdefault("sections", {}) + advanced = sections.setdefault("advanced", {}) + for key in ("data", "data_description"): + values = {**advanced.get(key, {}), **step.get(key, {})} + step[key], advanced[key] = _partition_options(values) + + +# Set "options" +data = {} +data_description = {} +for k, _, typ in const.VALIDATION_TUPLES: + desc = const.DOCS[k] + if len(desc) > 40 and typ not in (bool, cv.entity_ids): + data[k] = k + data_description[k] = desc + else: + data[k] = f"{k}: {desc}" +basic_data, advanced_data = _partition_options(data) +basic_descriptions, advanced_descriptions = _partition_options(data_description) +options_step = strings["options"]["step"]["init"] +options_step["data"] = basic_data +options_step["data_description"] = basic_descriptions +options_step["sections"] = { + "advanced": { + "name": "Advanced settings", + "description": "Additional settings for fine-tuning Adaptive Lighting.", + "data": advanced_data, + "data_description": advanced_descriptions, + }, +} + +# Set "services" +services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml" +with open(services_filename) as f: # noqa: PTH123 + services = yaml.safe_load(f) +services_json = {} +for service_name, dct in services.items(): + services_json[service_name] = { + "name": service_name, + "description": dct["description"], + "fields": {}, + } + for field_name, field in dct["fields"].items(): + services_json[service_name]["fields"][field_name] = { + "description": field["description"], + "name": field_name, + } +strings["services"] = services_json + +# Write changes to strings.json +with strings_fname.open("w") as f: + json.dump(strings, f, indent=2, ensure_ascii=False) + f.write("\n") + +# Sync changes from strings.json to en.json +with en_fname.open() as f: + en = json.load(f) + +en["config"]["step"]["user"] = strings["config"]["step"]["user"] +en["options"]["step"]["init"] = deepcopy(options_step) +en["services"] = services_json + +with en_fname.open("w") as f: + json.dump(en, f, indent=2, ensure_ascii=False) + f.write("\n") + +# Keep translated labels and descriptions when moving advanced options into a section. +for translation_fname in translation_fnames: + if translation_fname == en_fname: + continue + with translation_fname.open() as f: + translation = json.load(f) + if "options" not in translation: + continue + _migrate_translation_options(translation["options"]["step"]["init"]) + with translation_fname.open("w") as f: + json.dump(translation, f, indent=2, ensure_ascii=False) + f.write("\n") diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..8444ac4e --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,50 @@ +name: Docker + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + matrix: + platform: [linux/amd64, linux/arm64] + steps: + - uses: actions/checkout@v7.0.1 + - uses: docker/setup-qemu-action@v4.3.0 + - uses: docker/setup-buildx-action@v4.3.0 + - uses: docker/login-action@v4.6.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + uses: docker/metadata-action@v6.2.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + - uses: docker/build-push-action@v7.3.0 + with: + context: . + platforms: ${{ matrix.platform }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..8eae2558 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,64 @@ +name: Documentation + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.1 + + - name: Set up Python + uses: actions/setup-python@v7.0.0 + with: + python-version: '3.14.7' + + - name: Install uv + uses: astral-sh/setup-uv@v10.0.1 + + - name: Install dependencies + run: uv sync --group docs + + - name: Build documentation + run: uv run zensical build + + - name: Build webapp (simulator) + run: uv run shinylive export webapp webapp-site + + - name: Integrate webapp into docs + run: | + # Copy webapp into docs site at /simulator/ + mkdir -p site/simulator + cp -r webapp-site/* site/simulator/ + echo "Webapp integrated at site/simulator/" + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5.0.0 + with: + path: ./site + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5.0.1 diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml new file mode 100644 index 00000000..7313c4cc --- /dev/null +++ b/.github/workflows/hassfest.yaml @@ -0,0 +1,15 @@ +name: Validate with hassfest + +on: + push: + branches: [master] + pull_request: + schedule: + - cron: "0 0 * * *" + +jobs: + validate_hassfest: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v7.0.1" + - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml new file mode 100644 index 00000000..0afbe9ba --- /dev/null +++ b/.github/workflows/install_dependencies/action.yml @@ -0,0 +1,41 @@ +name: 'Install Dependencies' +description: 'Install Home Assistant and test dependencies' +inputs: + python-version: + description: 'Python version' + required: true + default: '3.10' + core-version: + description: 'Home Assistant core version' + required: false + default: 'dev' + +runs: + using: "composite" + steps: + - name: Check out code from GitHub + uses: actions/checkout@v7.0.1 + with: + repository: ${{ github.repository }} + ref: ${{ github.sha }} + persist-credentials: false + fetch-depth: 0 + - name: Check out code from GitHub + uses: actions/checkout@v7.0.1 + with: + repository: home-assistant/core + path: core + ref: ${{ inputs.core-version }} + - name: Set up Python ${{ inputs.python-version }} + id: python + uses: actions/setup-python@v7.0.0 + with: + python-version: ${{ inputs.python-version }} + - name: Set up UV + uses: astral-sh/setup-uv@v10.0.1 + - name: Install dependencies + shell: bash + run: | + uv venv --python ${{ inputs.python-version }} + ./scripts/setup-dependencies + ./scripts/setup-symlinks diff --git a/.github/workflows/main-to-master-sync.yml b/.github/workflows/main-to-master-sync.yml new file mode 100644 index 00000000..6b16b8e5 --- /dev/null +++ b/.github/workflows/main-to-master-sync.yml @@ -0,0 +1,22 @@ +name: Sync Main to Master + +on: + push: + branches: + - main + +jobs: + sync: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.1 + with: + ref: main + fetch-depth: 0 + + - name: Push changes to master + run: | + git checkout -b master + git push origin +master diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml new file mode 100644 index 00000000..9614ee7d --- /dev/null +++ b/.github/workflows/markdown-code-runner.yml @@ -0,0 +1,48 @@ +name: markdown-code-runner + +on: + push: + branches: + - main + pull_request: + +jobs: + markdown-code-runner: + runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v7.0.1 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.head_ref || github.ref }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v7.0.0 + with: + python-version: "3.14.7" + + - name: Install uv + uses: astral-sh/setup-uv@v10.0.1 + + - name: Update generated content + run: ./scripts/update-generated-content + + - name: Check for changes + run: | + if [ -n "$(git status --porcelain)" ]; then + if [ "${{ github.event_name }}" == "pull_request" ]; then + echo "::error::Auto-generated files are not up to date. Please run './scripts/update-generated-content' locally and push the changes." + exit 1 + else + echo "Changes detected, committing and pushing..." + git add -u . + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git commit -m "Update auto-generated content" + git pull --rebase + git push + fi + else + echo "No changes detected." + fi diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 00000000..16a9cc58 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7.0.1 + - uses: actions/setup-python@v7.0.0 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml new file mode 100644 index 00000000..137cfaf9 --- /dev/null +++ b/.github/workflows/pytest.yaml @@ -0,0 +1,124 @@ +name: pytest + +on: + push: + branches: [master] + pull_request: + +jobs: + pytest: + name: Run pytest + runs-on: ubuntu-24.04 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - core-version: "2025.9.4" + python-version: "3.13" + - core-version: "2025.10.4" + python-version: "3.13" + - core-version: "2025.11.3" + python-version: "3.13" + - core-version: "2025.12.5" + python-version: "3.13" + - core-version: "2026.1.3" + python-version: "3.13" + - core-version: "2026.2.3" + python-version: "3.13" + - core-version: "2026.3.4" + python-version: "3.14.2" + - core-version: "2026.4.4" + python-version: "3.14.2" + - core-version: "2026.5.4" + python-version: "3.14.2" + - core-version: "2026.6.4" + python-version: "3.14.2" + - core-version: "2026.7.4" + python-version: "3.14.2" + - core-version: "2026.8.3" + python-version: "3.14.2" + - core-version: "2026.9.1" + python-version: "3.14.2" + - core-version: "dev" + python-version: "3.14.2" + steps: + - name: Check out code from GitHub + uses: actions/checkout@v7.0.1 + + - name: Install Home Assistant + uses: ./.github/workflows/install_dependencies + with: + python-version: ${{ matrix.python-version }} + core-version: ${{ matrix.core-version }} + + - name: Run pytest + id: pytest + timeout-minutes: 60 + run: | + export PYTHONPATH=${PYTHONPATH}:${PWD} + source .venv/bin/activate + cd core + python3 -X dev -m pytest \ + -vvv \ + -qq \ + --timeout=9 \ + --durations=10 \ + --cov=homeassistant.components.adaptive_lighting \ + --cov-branch \ + --cov-report=term-missing \ + --cov-report=xml \ + --cov-report=json \ + --cov-report=html \ + -o console_output_style=count \ + -p no:sugar \ + tests/components/adaptive_lighting + + - name: Write coverage summary + if: ${{ !cancelled() }} + env: + CORE_VERSION: ${{ matrix.core-version }} + PYTHON_VERSION: ${{ matrix.python-version }} + PYTEST_OUTCOME: ${{ steps.pytest.outcome }} + run: | + { + echo "### Coverage: Home Assistant ${CORE_VERSION}, Python ${PYTHON_VERSION}" + echo + if [[ -f core/coverage.json ]]; then + echo "| Metric | Executed | Total | Coverage |" + echo "| --- | ---: | ---: | ---: |" + jq -r ' + def percent(covered; total): + if total == 0 then 100 else (covered / total * 10000 | round) / 100 end; + .totals + | "| Lines | \(.covered_lines) | \(.num_statements) | \(percent(.covered_lines; .num_statements))% |\n" + + "| Branches | \(.covered_branches) | \(.num_branches) | \(percent(.covered_branches; .num_branches))% |" + ' core/coverage.json + else + echo "Coverage JSON was not generated. See the pytest step for details." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + if [[ ! -f core/coverage.json && "${PYTEST_OUTCOME}" == "success" ]]; then + exit 1 + fi + + if [[ "${CORE_VERSION}" != "dev" && "${PYTEST_OUTCOME}" == "success" ]]; then + echo "Required coverage: 89% lines and 80% branches." >> "${GITHUB_STEP_SUMMARY}" + if ! jq -e '.totals | .covered_lines * 100 >= .num_statements * 89 + and .covered_branches * 100 >= .num_branches * 80' core/coverage.json > /dev/null; then + echo "::error::Coverage must be at least 89% lines and 80% branches." + exit 1 + fi + fi + + - name: Upload coverage reports + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7.0.1 + with: + name: coverage-${{ matrix.core-version }}-py${{ matrix.python-version }} + path: | + core/coverage.xml + core/coverage.json + core/htmlcov/ + if-no-files-found: warn diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..97912b67 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,24 @@ +name: Release Drafter + +on: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v7.7.0 + with: + dry-run: ${{ github.event_name == 'pull_request' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml new file mode 100644 index 00000000..1556e3a0 --- /dev/null +++ b/.github/workflows/update-test-matrix.yaml @@ -0,0 +1,59 @@ +name: Update Test Matrix + +on: + schedule: + # Run weekly on Monday at 9:00 UTC + - cron: "0 9 * * 1" + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write + pull-requests: write + +jobs: + update-matrix: + name: Update HA Core versions in test matrix + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v7.0.1 + + - name: Set up Python + uses: actions/setup-python@v7.0.0 + with: + python-version: "3.14.7" + + - name: Update test matrix + run: python scripts/update-test-matrix.py + + - name: Check for changes + id: changes + run: | + if git diff --quiet .github/workflows/pytest.yaml; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + echo "Detected changes:" + git diff .github/workflows/pytest.yaml + fi + + - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v8.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "ci: update HA Core test matrix versions" + title: "ci: Update Home Assistant Core test matrix" + body: | + This PR automatically updates the pytest workflow to test against the latest Home Assistant Core versions. + + ## Changes + - Updated HA Core versions in the test matrix to include latest patch releases + + --- + 🤖 Generated automatically by the update-test-matrix workflow + branch: update-test-matrix + delete-branch: true + labels: | + automation + ci diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..885b40c2 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,19 @@ +name: Validate + +on: + push: + branches: [master] + pull_request: + schedule: + - cron: "0 0 * * *" + +jobs: + validate_hacs: + runs-on: "ubuntu-latest" + if: github.repository == 'basnijholt/adaptive-lighting' # Don't run on forked repos + steps: + - uses: "actions/checkout@v7.0.1" + - name: HACS validation + uses: "hacs/action@main" + with: + category: "integration" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f40a1605 --- /dev/null +++ b/.gitignore @@ -0,0 +1,140 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDEs +.vscode +.idea + +# Home Assistant configuration +config/* +!config/configuration.yaml + +# Home Assistant core +core/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..80893d1c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/thlorenz/doctoc + rev: v2.5.0 + hooks: + - id: doctoc + files: ^README[^/]*\.md$ + args: ["--notitle"] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.6 + hooks: + - id: ruff + args: ["--fix"] + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 26.5.1 + hooks: + - id: black diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..6a2187b0 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,62 @@ +# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml + +target-version = "py312" +[lint] +select = ["ALL"] + +# All the ones without a comment were the ones that are currently violated +# by the codebase. The plan is to fix them all (when sensible) and then enable them. +ignore = [ + "ANN", + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} + "CPY001", # Missing copyright notice at top of file + "D401", # First line of docstring should be in imperative mood + "E501", # line too long + "FBT001", # Boolean positional arg in function definition + "FBT002", # Boolean default value in function definition + "FIX004", # Line contains HACK, consider resolving the issue + "PERF203", # `try`-`except` within a loop incurs performance overhead + "PLC0415", # `import` should be at the top-level of a file + "PLR0913", # Too many arguments to function call (N > 5) + "PLR0917", # Too many positional arguments + "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable + "RUF059", # Unpacked variable is never used + "S101", # Use of assert detected + "SLF001", # Private member accessed + "UP017", # Use datetime.UTC alias + "UP042", # Replace str, Enum inheritance with StrEnum +] + +[lint.per-file-ignores] +"tests/*.py" = [ + "ARG001", # Unused function argument: `call` + "D100", # Missing docstring in public module + "D103", # Missing docstring in public function + "D205", # 1 blank line required between summary line and description + "D400", # First line should end with a period + "D415", # First line should end with a period, question mark, or + "DTZ001", # The use of `datetime.datetime()` without `tzinfo` + "ERA001", # Found commented-out code + "FBT003", # Boolean positional value in function call + "FIX002", # Line contains TODO, consider resolving the issue + "G004", # Logging statement uses f-string + "PLR0915", # Too many statements (94 > 50) + "PT004", # Fixture `cleanup` does not return anything, add leading underscore + "PT007", # Wrong values type in `@pytest.mark.parametrize` expected `list` of + "S311", # Standard pseudo-random generators are not suitable for cryptographic + "TD002", # Missing author in TODO; try: `# TODO(): ...` or `# TODO + "TD003", # Missing issue link on the line following this TODO +] +".github/*py" = ["INP001"] +"webapp/homeassistant_util_color.py" = ["ALL"] +"webapp/app.py" = ["INP001", "DTZ011", "A002"] +"custom_components/adaptive_lighting/homeassistant_util_color.py" = ["ALL"] + +[lint.flake8-pytest-style] +fixture-parentheses = false + +[lint.pyupgrade] +keep-runtime-typing = true + +[lint.mccabe] +max-complexity = 25 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5cb46e1a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "files.associations": { + "*.yaml": "home-assistant" + }, + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": [ + "-vvv", + "-qq", + "--timeout=9", + "--durations=10", + "--cov=homeassistant", + "--cov-report=xml", + "-o", + "console_output_style=count", + "-p", + "no:sugar", + "core/tests/components/adaptive_lighting" + ], + "python.analysis.extraPaths": [ + "${workspaceFolder}/core" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..cd2130b2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Home Assistant on port 8123", + "type": "shell", + "command": "scripts/develop", + "problemMatcher": [] + }, + { + "label": "Lint (run pre-commit hooks)", + "type": "shell", + "command": "scripts/lint", + "problemMatcher": [] + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..db75963d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,65 @@ +# See tests/README.md for instructions on how to run the tests. + +# tl;dr: +# 1. Clone HA core into ./core: git clone --depth 1 https://github.com/home-assistant/core.git core +# 2. Setup symlinks: ./scripts/setup-symlinks +# 3. Run tests (mount entire repo, not individual dirs, or symlinks break): +# docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest + +# Optionally build the image yourself with: +# docker build -t basnijholt/adaptive-lighting:latest . + +FROM ghcr.io/astral-sh/uv:debian + +# Install build dependencies for Python extensions +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3-dev \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Clone home-assistant/core +RUN git clone --depth 1 --branch dev https://github.com/home-assistant/core.git /core + +# Copy the Adaptive Lighting repository +COPY . /app/ + +# Setup symlinks in core +RUN ln -s /core /app/core && /app/scripts/setup-symlinks + +# Install home-assistant/core dependencies +RUN mkdir -p /.venv +ENV UV_PROJECT_ENVIRONMENT=/.venv UV_PYTHON=3.14.2 PATH="/.venv/bin:$PATH" +RUN uv venv +RUN /app/scripts/setup-dependencies + +WORKDIR /app/core + +# Make 'custom_components/adaptive_lighting' imports available to tests +ENV PYTHONPATH="/app" + +ENTRYPOINT ["python3", \ + # Enable Python development mode + "-X", "dev", \ + # Run pytest + "-m", "pytest", \ + # Verbose output + "-vvv", \ + # Set a timeout of 9 seconds per test + "--timeout=9", \ + # Print the 10 slowest tests + "--durations=10", \ + # Measure code coverage for the 'homeassistant.components.adaptive_lighting' component + "--cov=homeassistant.components.adaptive_lighting", \ + # Generate an XML report of the code coverage + "--cov-report=xml", \ + # Generate an HTML report of the code coverage + "--cov-report=html", \ + # Print a summary of the code coverage in the console + "--cov-report=term", \ + # Print logs in color + "--color=yes", \ + # Print a count of test results in the console + "-o", "console_output_style=count"] + +# Run tests in the 'tests/components/adaptive_lighting' directory +CMD ["tests/components/adaptive_lighting"] diff --git a/README.md b/README.md index 97a3cd46..89685620 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,1149 @@ -# Circadian Lighting [[Home Assistant](https://www.home-assistant.io/) Component] -## Stay healthier and sleep better by syncing your lights with natural daylight to maintain your circadian rhythm! +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) +![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) + +[![All Contributors](https://img.shields.io/badge/all_contributors-173-orange.svg?style=flat-square)](#contributors-) + -![Circadian Light Rhythm|690x287](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/f/5fe7a780e9f8905fea4d1cbb66cdbe35858a6e36.jpg) +# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 -Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occuring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. +logo -In addition, Circadian Lighting can set your lights to a nice cool white at 1% in “Sleep” mode, which is far brighter than starlight but won’t reset your circadian rhythm or break down too much rhodopsin in your eyes. +[Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. +Download and install directly through [HACS (Home Assistant Community Store)](https://hacs.xyz/): -
Expand for articles explaining the benefits of maintaining a natural Circadian rhythm - -* [Circadian Rhythms - National Institute of General Medical Sciences](https://www.nigms.nih.gov/Education/Pages/Factsheet_CircadianRhythms.aspx) -* [Circadian Rhythms Linked to Aging and Well-Being | Psychology Today](https://www.psychologytoday.com/us/blog/the-athletes-way/201306/circadian-rhythms-linked-aging-and-well-being) -* [Maintaining a daily rhythm is important for mental health, study suggests - CNN](https://www.cnn.com/2018/05/15/health/circadian-rhythm-mood-disorder-study/index.html) -* [How Nobel Winning Circadian Rhythm Research Benefits Pregnancy](https://www.healthypregnancy.com/how-nobel-prize-winning-circadian-rhythms-research-benefits-a-healthy-pregnancy/) -* [Body Clock & Sleep - National Sleep Foundation](https://sleepfoundation.org/sleep-topics/sleep-drive-and-your-body-clock) -* [How our body’s circadian clocks affect our health beyond sleep](https://www.theverge.com/2018/6/12/17453398/sleep-circadian-code-satchin-panda-clock-health-science) +[![Open your Home Assistant instance and open the Adaptive Lighting integration inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=basnijholt&repository=adaptive-lighting&category=integration) + +By automatically adapting the settings of your lights throughout the day, Adaptive Lighting helps maintain your natural circadian rhythm 😴, which can lead to improved sleep, mood, and overall well-being. Experience cooler color temperatures at noon, gradually transitioning to warmer colors at sunset and sunrise. + +In addition to its regular mode, Adaptive Lighting also offers a "sleep mode" 🌜 which sets your lights to minimal brightness and a very warm color, perfect for winding down at night. + +> 🌈 Visualize Adaptive Lighting's settings with the [_🌞 Adaptive Lighting Simulator WebApp 🌛_](https://basnijholt.github.io/adaptive-lighting) + +https://github.com/basnijholt/adaptive-lighting/assets/6897215/68908f7d-fbf1-4991-98ce-3f2af6df996f + +[[ToC](#books-table-of-contents)] + + +## :bulb: Features + +When initially turning on a light that is controlled by Adaptive Lighting, the `light.turn_on` service call is intercepted, and the light's brightness and color are automatically adjusted based on the sun's position. +After that, the light's brightness and color are automatically adjusted at a regular interval. + +Adaptive Lighting provides four switches (using "living_room" as an example component name): + +- `switch.adaptive_lighting_living_room`: Turn Adaptive Lighting on or off and view current light settings through its attributes. +- `switch.adaptive_lighting_sleep_mode_living_room`: Activate "sleep mode" 😴 and set custom sleep_brightness and sleep_color_temp. +- `switch.adaptive_lighting_adapt_brightness_living_room`: Enable or disable brightness adaptation 🔆 for supported lights. +- `switch.adaptive_lighting_adapt_color_living_room`: Enable or disable color adaptation 🌈 for supported lights. + + + +### :control_knobs: Regain Manual Control + +Adaptive Lighting is designed to automatically detect when you or another source (e.g., automation) manually changes light settings 🕹️. +When this occurs, the affected light is marked as "manually controlled," and Adaptive Lighting will not make further adjustments until the light is turned off and back on or reset using the `adaptive_lighting.set_manual_control` service call. +This feature is available when `take_over_control` is enabled. + +Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. +The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. + +With `expand_light_groups: false`, manual control belongs to the group. A direct member change cannot pause adaptation for only that member; use group-level manual control or enable expansion for individual tracking. +Explicit member targets in Adaptive Lighting services stay individual targets and do not mark or command the whole group. +Changing expansion at runtime discards tracking and pending adaptation for targets no longer used by any profile. + +The Adaptive Lighting switch exposes these read-only attributes for its lights: + +- `manual_control`: lights with any attribute marked as manually controlled. +- `manual_control_brightness`: lights with brightness marked as manually controlled. +- `manual_control_color`: lights with color marked as manually controlled. + +These lists report manual-control flags. Actual adaptation also depends on `take_over_control_mode` and the brightness/color adaptation switches. For example, under the default `pause_all` mode, manually changing only brightness leaves `manual_control_color` empty while pausing both brightness and color adaptation. Under `pause_changed`, color can continue adapting. + +The attributes are absent when the Adaptive Lighting switch is off. Use a fallback when checking them in templates: + +```jinja +{{ 'light.bedroom' in (state_attr('switch.adaptive_lighting_bedroom', 'manual_control_brightness') or []) }} +``` + +> ⚠️ **_Caution: Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable `detect_non_ha_changes` if you encounter such issues._** + + +## :books: Table of Contents + + + + +- [:gear: Configuration](#gear-configuration) + - [:memo: Options](#memo-options) + - [:hammer_and_wrench: Services](#hammer_and_wrench-services) + - [`adaptive_lighting.apply`](#adaptive_lightingapply) + - [`adaptive_lighting.set_manual_control`](#adaptive_lightingset_manual_control) + - [`adaptive_lighting.change_switch_settings`](#adaptive_lightingchange_switch_settings) +- [:robot: Automation examples](#robot-automation-examples) +- [Additional Information](#additional-information) +- [:sos: Troubleshooting](#sos-troubleshooting) + - [:exclamation: Common Problems & Solutions](#exclamation-common-problems--solutions) + - [:bulb: Lights Only Adapt After Reloading](#bulb-lights-only-adapt-after-reloading) + - [:bulb: Lights Not Responding or Turning On by Themselves](#bulb-lights-not-responding-or-turning-on-by-themselves) + - [:signal_strength: WiFi Networks](#signal_strength-wifi-networks) + - [:spider_web: Zigbee, Z-Wave, and Other Mesh Networks](#spider_web-zigbee-z-wave-and-other-mesh-networks) + - [:rainbow: Light Colors Not Matching](#rainbow-light-colors-not-matching) + - [:bulb: Bulb-Specific Issues](#bulb-bulb-specific-issues) +- [:bar_chart: Graphs!](#bar_chart-graphs) + - [:sunny: Sun Position](#sunny-sun-position) + - [:thermometer: Color Temperature](#thermometer-color-temperature) + - [:high_brightness: Brightness](#high_brightness-brightness) + - [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true) + - [Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"`](#custom-brightness-ramps-using-brightness_mode-with-linear-and-tanh) +- [:eyes: See also](#eyes-see-also) +- [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors) +- [Translating Adaptive Lighting](#translating-adaptive-lighting) + + + +## :gear: Configuration + +Adaptive Lighting supports configuration through both YAML and the frontend (**Settings** -> **Devices and Services** -> **Adaptive Lighting**, **Adaptive Lighting** -> **Options**), with identical option names in both methods. + +```yaml +# Example configuration.yaml entry +adaptive_lighting: + lights: + - light.living_room_lights +``` +If you configure Adaptive Lighting through the UI, no `adaptive_lighting:` entry is needed in `configuration.yaml`. Instances configured through YAML must be edited in YAML. + +Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the benefits of intelligent, sun-synchronized lighting today! + +### :memo: Options + +All of the configuration options are listed below, along with their default values. +The YAML and frontend configuration methods support all of the options listed below. + + + + + + + + +| Variable name | Description | Default | Type | +|:--------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `manual_control_on_external_turn_on` | Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `reset_manual_control_on_sleep_mode_change` | Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴 | `True` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `expand_light_groups` | Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets. | `True` | `bool` | + + + + +Full example: + +```yaml +# Example configuration.yaml entry +adaptive_lighting: +- name: "default" + lights: [] + prefer_rgb_color: false + transition: 45 + initial_transition: 1 + interval: 90 + min_brightness: 1 + max_brightness: 100 + min_color_temp: 2000 + max_color_temp: 5500 + sleep_brightness: 1 + sleep_color_temp: 1000 + sunrise_time: "08:00:00" # override the sunrise time + sunrise_offset: + sunset_time: + sunset_offset: 1800 # in seconds or '00:30:00' + take_over_control: true + detect_non_ha_changes: false + only_once: false + +``` + + +### :hammer_and_wrench: Services + +#### `adaptive_lighting.apply` + +`adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. +Provide a switch in `entity_id`, a list of `lights`, or both. + + + + + + + + +| Service data attribute | Description | Required | Type | +|:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ❌ | list of `entity_id`s | +| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | +| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | +| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | +| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | + + +#### `adaptive_lighting.set_manual_control` + +`adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted. +Provide a switch in `entity_id`, a list of `lights`, or both. + + + + + + + + +| Service data attribute | Description | Required | Type | +|:-------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------| +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ❌ | list of `entity_id`s | +| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | +| `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` | + + + + +#### `adaptive_lighting.change_switch_settings` + +`adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. + +> [!WARNING] +> These settings will **not** be written to your config and will be reset on restart of Home Assistant! You can see the current settings in the `switch.adaptive_lighting_XXX` attributes if `include_config_in_attributes` is enabled. + +| Service data attribute | Required | Description | +| --------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `use_defaults` | ❌ | (default: `current` for current settings) Choose from `factory`, `configuration`, or `current` to reset variables not being set with this service call. `current` leaves them as they are, `configuration` resets to initial startup values, `factory` resets to default values listed in the documentation. | +| **all other keys** (except the ones in the table below ⚠️) | ❌ | See the table below for disallowed keys. | + +The following keys are disallowed: + +| **DISALLOWED** service data | Description | +| --------------------------- | ----------------------------------------------------------------------------------------------- | +| `entity_id` | You cannot change the switch's `entity_id`, as it has already been registered. | +| `lights` | You may call `adaptive_lighting.apply` with your lights or create a new config instead. | +| `name` | You can rename your switch's display name in Home Assistant's UI. | +| `interval` | The interval is used only once when the config loads. A config change and restart are required. | + + + +## :robot: Automation examples + +Replace every entity ID below with the IDs from your Home Assistant instance. Fresh Adaptive Lighting profiles use child IDs such as `switch.adaptive_lighting_living_room_sleep_mode`; profiles created before the device-based entity change may retain older IDs. + +Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`. + +Five examples also have blueprints with selectors, so you can configure them without editing YAML: + +| Blueprint | Purpose | Import | +| --- | --- | --- | +| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fsleep_mode.yaml) | +| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fturn_off_at_minimum.yaml) | +| [Pause at minimum](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) | Pause brightness through manual control, using its existing reset behavior. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fmanual_control_at_minimum.yaml) | +| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fschedule_profile.yaml) | +| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fdaylight_limit.yaml) | + +Click a blueprint's import badge, confirm the import in Home Assistant, then create an automation and select your entities. You can also copy its source link into **Settings → Automations & scenes → Blueprints → Import Blueprint**. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations. + +`change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused. + +
+Automatically reset manual control after one hour. + +Use the built-in timeout so every new manual change renews a single timer for that light: + +```yaml +adaptive_lighting: + - name: "Living Room" + lights: + - light.living_room + autoreset_control_seconds: 3600 +``` + +This is a top-level `configuration.yaml` example. The timer clears manual control and immediately readapts a light when both it and the Adaptive Lighting switch are on.
-### Visit the [Wiki](https://github.com/claytonjn/hass-circadian_lighting/wiki) for more information. -
+
+Toggle multiple Adaptive Lighting switches to "sleep mode" using an input_boolean.sleep_mode. -## Basic Installation/Configuration Instructions: +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml). Select an input boolean and the sleep-mode switches it should control. -#### Installation: -Install `custom_component` files automatically using [Custom Updater](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#automatic-installation) or [Manually](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#manual-installation). - -#### Component Configuration: ```yaml -# Example configuration.yaml entry -circadian_lighting: +- alias: "Adaptive lighting: toggle 'sleep mode'" + mode: restart + trigger: + - platform: state + entity_id: input_boolean.sleep_mode + - platform: homeassistant + event: start # apply the helper's restored state + variables: + sleep_mode: "{{ states('input_boolean.sleep_mode') }}" + conditions: + - condition: template + value_template: "{{ sleep_mode in ['on', 'off'] }}" + actions: + - action: "switch.turn_{{ sleep_mode }}" + target: + entity_id: + - switch.adaptive_lighting_living_room_sleep_mode + - switch.adaptive_lighting_bedroom_sleep_mode ``` -[_Advanced Configuration_](https://github.com/claytonjn/hass-circadian_lighting/wiki/Advanced-Configuration#component-configuration-variables) -#### Switch Configuration: +
+ +
+Turn a light off when its adaptive brightness target reaches the minimum. + +Prefer a form over editing YAML? Import the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) in Home Assistant under **Settings → Automations & scenes → Blueprints → Import Blueprint**. Select your profile, its matching adapt brightness switch, one light managed by that profile, and its minimum brightness percentage. Create one automation per light. If you change the profile's minimum later, update the automation too. The blueprint and YAML example below have the same behavior. + +The Adaptive Lighting switch already exposes its calculated `brightness_pct` target. Use its state changes to choose a power policy in an automation; no custom event is needed. This example assumes `min_brightness: 1`. Change `minimum_pct` to match your profile, and replace the switch and light entity IDs with your own. + +The comparison uses the same rounded 0–255 brightness as an adaptation command. Comparing floating-point percentages for exact equality can miss the minimum between updates. This detects the calculated target reaching its minimum command, not the bulb finishing a transition or reaching its physical dimming limit. + ```yaml -# Example configuration.yaml entry -switch: - - platform: circadian_lighting - lights_ct: - - light.desk - - light.lamp +- alias: "Adaptive lighting: turn off at minimum brightness" + mode: single + triggers: + - trigger: state + entity_id: switch.adaptive_lighting_living_room + attribute: brightness_pct + conditions: + - condition: state + entity_id: + - switch.adaptive_lighting_living_room + - switch.adaptive_lighting_living_room_adapt_brightness + state: "on" + - condition: template + value_template: >- + {% set minimum_pct = 1 %} + {% set minimum = (minimum_pct * 255 / 100) | round(0) %} + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: light.living_room + state: "on" + - condition: template + value_template: >- + {{ 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control') or []) }} + actions: + - action: light.turn_off + target: + entity_id: light.living_room ``` -Switch configuration variables: -* **name** (_Optional_): The name to use when displaying this switch. -* **lights_ct** (_Optional_): array: List of light entities which should be set in mireds. -* **lights_rgb** (_Optional_): array: List of light entities which should be set in RGB. -* **lights_xy** (_Optional_): array: List of light entities which should be set in XY. -* **lights_brightness** (_Optional_): array: List of light entities which should only have brightness adjusted. -[_Advanced Configuration_](https://github.com/claytonjn/hass-circadian_lighting/wiki/Advanced-Configuration#switch-configuration-variables) +This runs once when a valid target crosses down into the minimum range. It skips lights currently marked as manually controlled, does not repeatedly turn them off while the target remains low, and does not turn them back on later. Startup or re-enabling the profile while already at the minimum is not a new crossing. Sleep mode can also cause a crossing if its brightness is at or below the chosen minimum. Changing sleep mode clears manual control by default; set `reset_manual_control_on_sleep_mode_change: false` if you want to preserve it. For a bedtime-only policy, trigger directly on the sleep-mode switch changing to `on` instead. -
+
-### Graphs! -These graphs were generated using the values calculated by the Circadian Lighting sensor/switch(es). +
+Pause brightness at the minimum using manual control. -##### Sun Position: +Use the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) to mark an individual light's brightness as manually controlled when the calculated target reaches its minimum. The light stays on and the adaptation switches stay enabled. Set `take_over_control_mode: pause_changed` on the profile to keep adapting color; the default `pause_all` pauses both attributes. + +Select the profile, its adapt-brightness switch, and a light managed by it. Match the minimum percentage to the profile's `min_brightness`. This YAML example assumes `min_brightness: 1`; change `minimum_pct` and the entity IDs to match your setup. + +```yaml +- alias: "Adaptive lighting: pause brightness at minimum" + mode: single + variables: + minimum_pct: 1 + minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}" + triggers: + - trigger: state + entity_id: switch.adaptive_lighting_living_room + attribute: brightness_pct + conditions: + - condition: state + entity_id: + - switch.adaptive_lighting_living_room + - switch.adaptive_lighting_living_room_adapt_brightness + state: "on" + - condition: template + value_template: >- + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: light.living_room + state: "on" + - condition: template + value_template: >- + {{ 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }} + actions: + - variables: + light_session: "{{ states.light.living_room.last_changed.isoformat() }}" + profile_session: "{{ states.switch.adaptive_lighting_living_room.last_changed.isoformat() }}" + brightness_session: "{{ states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() }}" + - wait_template: >- + {% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %} + {{ not is_state('light.living_room', 'on') + or not is_state('switch.adaptive_lighting_living_room', 'on') + or not is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on') + or states.light.living_room.last_changed.isoformat() != light_session + or states.switch.adaptive_lighting_living_room.last_changed.isoformat() != profile_session + or states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() != brightness_session + or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum + or 'light.living_room' in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) + or (state_attr('light.living_room', 'brightness') | float(256)) <= minimum }} + timeout: "00:05:00" + continue_on_timeout: false + - condition: template + value_template: >- + {% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %} + {{ is_state('light.living_room', 'on') + and is_state('switch.adaptive_lighting_living_room', 'on') + and is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on') + and states.light.living_room.last_changed.isoformat() == light_session + and states.switch.adaptive_lighting_living_room.last_changed.isoformat() == profile_session + and states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() == brightness_session + and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum + and (state_attr('light.living_room', 'brightness') | float(256)) <= minimum + and 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }} + - action: adaptive_lighting.set_manual_control + data: + entity_id: switch.adaptive_lighting_living_room + lights: light.living_room + manual_control: >- + {{ true if 'light.living_room' in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_color') or []) + else 'brightness' }} +``` + +The comparison uses the rounded 0–255 target, so it does not depend on sampling an exact floating-point minimum. It waits up to five minutes for the light to report that minimum before marking manual control, so the final dimming command can complete. Reported brightness does not prove physical fade completion. If the light, profile, or adapt-brightness switch is toggled, the target rises, brightness is marked manually controlled elsewhere, or brightness never reaches the minimum, the attempt is abandoned. Lights that cannot report the configured minimum will not be paused. Existing manual color flags are preserved, and lights whose brightness is already manually controlled are left alone. Manual-control state is shared for lights managed by multiple profiles, so their existing takeover policies still apply. + +The usual resets apply: turning the light off, the configured `autoreset_control_seconds` timeout, clearing manual control through its service, and existing profile/sleep-switch reset behavior. After a reset, normal adaptation can increase brightness again. This runs once per downward crossing; resetting while the target remains at its minimum does not immediately mark the light again. Startup at the minimum is not a crossing either. + +This pauses further dimming as well as brightening. To pause brightness immediately after a brightness change made through Home Assistant, use `take_over_control_mode: pause_changed` with `take_over_control: true`; that needs no additional automation. + +
+ +
+Set sunrise and sunset from an alarm. + +Call this script from your alarm automation. It sets one Adaptive Lighting profile's sunrise to the current time and its sunset to 12 hours later on the local clock. + +```yaml +script: + set_adaptive_lighting_alarm_times: + alias: "Adaptive lighting: set times from alarm" + variables: + alarm_time: '{{ now().strftime("%H:%M:%S") }}' + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_alarm_lights + sunrise_time: "{{ alarm_time }}" + sunset_time: > + {{ (strptime(alarm_time, "%H:%M:%S") + timedelta(hours=12)) + .strftime("%H:%M:%S") }} +``` + +
+ +
+Use a Schedule helper as a step-based custom lighting profile. + +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml). Select the main profile switch and your Schedule helper. + +Create a [Schedule helper](https://www.home-assistant.io/integrations/schedule/) named `Adaptive Lighting Profile`. Add time blocks with Additional data like this: + +```yaml +brightness_pct: 20 +color_temp_kelvin: 2500 +``` + +Use different values for each block. The automation below applies the active block whenever the schedule state or its attributes change. Setting both brightness limits and both color temperature limits to the same value keeps each block at its setpoint. Outside a block, the configured Adaptive Lighting settings are restored. + +```yaml +- alias: "Adaptive lighting: apply scheduled profile" + triggers: + - trigger: state + entity_id: schedule.adaptive_lighting_profile + - trigger: homeassistant + event: start + actions: + - choose: + - conditions: + - condition: state + entity_id: schedule.adaptive_lighting_profile + state: "on" + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + min_brightness: > + {{ state_attr('schedule.adaptive_lighting_profile', 'brightness_pct') | int(1) }} + max_brightness: > + {{ state_attr('schedule.adaptive_lighting_profile', 'brightness_pct') | int(1) }} + min_color_temp: > + {{ state_attr('schedule.adaptive_lighting_profile', 'color_temp_kelvin') | int(2000) }} + max_color_temp: > + {{ state_attr('schedule.adaptive_lighting_profile', 'color_temp_kelvin') | int(2000) }} + default: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + use_defaults: configuration + mode: restart +``` + +This creates step changes at block boundaries. It does not interpolate between schedule points. Runtime settings also reset when Home Assistant restarts, so the startup trigger reapplies the active block. The default branch restores every configured setting; restore only the four fields explicitly if other automations also change runtime settings. + +
+ +
+Reduce daytime brightness when an illuminance sensor detects strong daylight. + +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml). Select the profile and sensor, then set the lux thresholds and brightness limits. The high lux threshold must exceed the low threshold; the blueprint does nothing if they are reversed or equal. + +Keep a low configured `min_brightness` for late night and let an automation lower `max_brightness` while the room has ample daylight. Use a sensor that is not significantly affected by the controlled lights to avoid a feedback loop. + +```yaml +- alias: "Adaptive lighting: limit brightness in daylight" + triggers: + - trigger: numeric_state + entity_id: sensor.living_room_illuminance + above: 300 + - trigger: numeric_state + entity_id: sensor.living_room_illuminance + below: 200 + - trigger: homeassistant + event: start + id: startup + actions: + - if: + - condition: trigger + id: startup + then: + - wait_template: > + {{ is_number(states('sensor.living_room_illuminance')) }} + timeout: "00:05:00" + continue_on_timeout: false + - choose: + - conditions: + - condition: numeric_state + entity_id: sensor.living_room_illuminance + above: 300 + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + max_brightness: 30 + - conditions: + - condition: numeric_state + entity_id: sensor.living_room_illuminance + below: 200 + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + max_brightness: 100 + mode: restart +``` + +The separate 200 and 300 lux thresholds add hysteresis. After a restart, the automation waits for a numeric sensor state before evaluating it. If the initial value is between the thresholds, Adaptive Lighting keeps its configured maximum. Replace `30` and `100` with your desired daytime limit and normal maximum. + +`min_brightness` and `max_brightness` are the solar-midnight and daytime endpoints of the brightness curve. Setting `min_brightness` higher than `max_brightness` is supported and creates an inverted curve that is brighter at night and dimmer during the day. If you only want a daytime limit, keep the reduced maximum at or above the configured minimum. + +
+ +
+Turn on Hue-controlled lights with the current Adaptive Lighting values. + +For a Hue button exposed to Home Assistant, call this script from the button automation. It turns on the listed lights directly with the current Adaptive Lighting brightness and color. + +```yaml +script: + living_room_adaptive_lighting: + alias: "Living room: adaptive lighting" + sequence: + - action: adaptive_lighting.apply + data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.living_room_ceiling + - light.living_room_table + turn_on_lights: true + transition: 0 +``` + +This requires Home Assistant to receive the button event. The one-shot `apply` call works while the main Adaptive Lighting switch is off, turns on the listed lights, and applies values even if a light is marked as manually controlled. It leaves the profile switch and manual-control state unchanged. + +Adaptive Lighting does not update scenes stored on the Hue Bridge, so scenes activated only inside Hue cannot use this script and retain Hue's operation when Home Assistant is unavailable. + +
+ +
+Use a fixed RGB stage before sleep mode. + +This script starts sleep mode with a fixed dim red color, waits 30 minutes, and then restores the configured Adaptive Lighting settings. The main profile switch and the light must already be on. + +```yaml +script: + adaptive_lighting_bedtime: + alias: "Adaptive lighting: bedtime" + mode: restart + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_bedroom + sleep_rgb_or_color_temp: rgb_color + sleep_rgb_color: [255, 56, 0] + sleep_brightness: 20 + - action: switch.turn_on + target: + entity_id: switch.adaptive_lighting_bedroom_sleep_mode + - delay: "00:30:00" + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_bedroom + use_defaults: configuration +``` + +The light must support RGB color. The first stage uses a fixed brightness rather than following the normal brightness curve. When sleep mode changes from off to on, the default `reset_manual_control_on_sleep_mode_change: true` returns manually controlled lights to Adaptive Lighting control so they receive the stage. If you disable that option, manually controlled lights remain paused. Restoring configuration defaults resets every runtime setting on this Adaptive Lighting switch, so restore only the sleep fields explicitly if other automations also change runtime settings. + +Stopping this script or reloading scripts during the delay prevents the final action, leaving the runtime overrides active. To recover, call `adaptive_lighting.change_switch_settings` for the profile with `use_defaults: configuration`. A Home Assistant restart reloads the configured settings. + +
+ +
+Run a fixed virtual day across midnight. + +Fixed virtual sunrise and sunset times can cross midnight. This configuration ramps an indoor garden from its minimum at 16:00 to its maximum at 22:00, then back to its minimum at 04:00. + +```yaml +adaptive_lighting: + - name: "Indoor Garden" + lights: + - light.indoor_garden + sunrise_time: "16:00:00" + sunset_time: "04:00:00" + min_brightness: 10 + max_brightness: 100 + brightness_mode: linear + brightness_mode_time_dark: 0 + brightness_mode_time_light: 21600 # 6 hours +``` + +Adaptive Lighting changes brightness and color while a light is on; it does not manage the light's power schedule. This separate automation turns the example light on and off: + +```yaml +- alias: "Indoor garden: power schedule" + triggers: + - trigger: time + at: "16:00:00" + id: turn_on + - trigger: time + at: "04:00:00" + id: turn_off + - trigger: homeassistant + event: start + id: startup + actions: + - choose: + - conditions: + - condition: trigger + id: turn_on + sequence: + - action: light.turn_on + target: + entity_id: light.indoor_garden + - conditions: + - condition: trigger + id: startup + - condition: time + after: "16:00:00" + before: "04:00:00" + sequence: + - action: light.turn_on + target: + entity_id: light.indoor_garden + default: + - action: light.turn_off + target: + entity_id: light.indoor_garden +``` + +Use `min_sunrise_time`, `max_sunrise_time`, `min_sunset_time`, or `max_sunset_time` instead when you want to constrain astronomical sunrise or sunset to an earliest or latest time rather than replace it. + +
+ + +## Additional Information + +For more details on adding the integration and setting options, refer to the [documentation of the PR](https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/) and [this video tutorial on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/). + +Adaptive Lighting was initially inspired by @claytonjn's [hass-circadian\_lighting](https://github.com/claytonjn/hass-circadian_lighting), but has since been entirely rewritten and expanded with new features. + +## :sos: Troubleshooting + + +Encountering issues? Enable debug logging in your `configuration.yaml`: + +```yaml +logger: + default: warning + logs: + custom_components.adaptive_lighting: debug +``` + +After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). + +For support, use Home Assistant's **Download diagnostics** action on the +Adaptive Lighting config entry. The download is an on-demand snapshot of the +profile's current switches and currently tracked light targets. It does not +refresh group membership or predict targets a disabled profile would use after +being enabled. It does not create live sensors; existing switch attributes +remain the interface for automations. +The reported last adaptation values are the shared manager's latest retained +value for each attribute. They can come from different commands and do not +represent one sent command or the current desired state. + + + +### :exclamation: Common Problems & Solutions + +#### :bulb: Lights Only Adapt After Reloading + +If lights stop adapting after you turn them on with a physical switch or a Zigbee-bound remote, check the Adaptive Lighting switch's `manual_control` attribute. With `take_over_control: true` and `detect_non_ha_changes: false`, a turn-on without a matching Home Assistant `light.turn_on` call marks the light as manually controlled. Reloading clears that state, but the next physical turn-on can trigger it again. + +To adapt these turn-ons while still detecting later manual changes, enable `detect_non_ha_changes` and leave `manual_control_on_external_turn_on` disabled. This requires the light integration to report its state reliably. If you want Adaptive Lighting to keep adapting regardless of manual changes, disable `take_over_control` along with the options that require it: `detect_non_ha_changes`, `adapt_only_on_bare_turn_on`, and `manual_control_on_external_turn_on`. + +This explains the physical-switch case in [#1056](https://github.com/basnijholt/adaptive-lighting/issues/1056), but not every report in that thread. If the light is not listed in `manual_control`, include diagnostics and debug logs from the failed turn-on when reporting it. Lights returning from `unavailable` after a power cut are a separate case from an `off` to `on` state change. + +#### :bulb: Lights Not Responding or Turning On by Themselves + +Adaptive Lighting sends more commands to lights than a typical human user would. If your light control network is unhealthy, you may experience: + +- Laggy manual commands (e.g., turning lights on or off). +- Unresponsive lights. +- Home Assistant reporting incorrect light states, causing Adaptive Lighting to inadvertently turn lights back on. + +Most issues that appear to be caused by Adaptive Lighting are actually due to unrelated problems. +Addressing these issues will significantly improve your Home Assistant experience. + +In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action. +To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`. + +To keep detecting manual changes to lights that are already on while leaving unmatched `off` to `on` state events unchanged, enable `manual_control_on_external_turn_on`. Matching uses the exact context of the most recently recorded `light.turn_on` call. Some integrations replace or omit that context, so Adaptive Lighting cannot distinguish every physical versus Home Assistant turn-on source. + +#### :signal_strength: WiFi Networks + +Ensure your light bulbs have a strong WiFi connection. If the signal strength is less than -70dBm, the connection may be weak and prone to dropping messages. + +#### :spider_web: Zigbee, Z-Wave, and Other Mesh Networks + +Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). +Most modern lights function as routers, very early models may not. +If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. +Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. +Smart plugs can be an affordable way to add more routers to your network. + +For most Zigbee networks, **using groups is essential for optimal performance**. +For example, if you want to use Adaptive Lighting in a hallway with six bulbs, adding each bulb individually to the Adaptive Lighting configuration could overwhelm the network with commands. +Instead, create a group in your Zigbee software (not a regular Home Assistant group) and add that single group to the Adaptive Lighting configuration. +This sends a single broadcast command to adjust all bulbs, improving response times and keeping the bulbs in sync. + +As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. +Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. + +> :warning: **If you control lights individually, `manual_control` cannot behave correctly! If you need to control lights individually as well, use a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/).** + +When mixing group types, avoid nesting: do not add integration-level groups (e.g., Zigbee2MQTT groups) to a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/) that is managed by Adaptive Lighting, and do not nest Home Assistant Light Groups inside each other. +Adaptive Lighting cannot expand an integration-level group into its member lights, and nested groups make it unpredictable which entity Adaptive Lighting tracks and adapts, which can prevent lights from being adapted at all (see [#1378](https://github.com/basnijholt/adaptive-lighting/issues/1378)). +Instead, add the individual light entities or a single Zigbee group directly to the Adaptive Lighting configuration. +Also note that bulbs turned on via a Zigbee group broadcast may briefly flash their last (cached) brightness and color before the adapted values arrive; this happens inside the bulbs and cannot be prevented by Home Assistant or Adaptive Lighting. + +#### :rainbow: Light Colors Not Matching + +Bulbs from different manufacturers or models may have varying color temperature specifications. For instance, if you have two Adaptive Lighting configurations—one with only Philips Hue White Ambiance bulbs and another with a mix of Philips Hue White Ambiance and Sengled bulbs—the Philips Hue bulbs may appear to have different color temperatures despite having identical settings. + +To resolve this: + +1. Include only bulbs of the same make and model in a single Adaptive Lighting configuration. +2. Rearrange bulbs so that different color temperatures are not visible simultaneously. + +#### :bulb: Bulb-Specific Issues + +These lights are known to exhibit disadvantageous behaviour due to firmware bugs, insufficient functionality, or hardware limitations: + +- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26) + - Unexpected turn-ons: If Adaptive Lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off during that time, it may turn back on after approximately 10 seconds to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs indicating the cause of the light turning on. To fix this, set a much shorter `transition` time, such as 1 second. + - Heat sensitivity: Additionally, these bulbs may perform poorly in enclosed "dome" style ceiling lights, particularly when hot. While most LEDs (even non-smart ones) state in the fine print that they do not support working in enclosed fixtures, in practice, more expensive bulbs like Philips Hue generally perform better. To resolve this issue, move the problematic bulbs to open-air fixtures. +- Ikea Tradfri bulbs/drivers (and related Ikea smart light products) + - Unsupported simultaneous transition of brightness and color: When receiving such a command, they switch the brightness instantly and only transition the color. To get smooth transitions of both brightness and color, enable `separate_turn_on_commands`. + - Unresponsiveness during color transitions: No other commands are processed during an ongoing color transition, e.g., turn-off commands are ignored and lights stay on despite being reported as off to Home Assistant. The default config with long transitions thus results in long periods of unresponsiveness. To work around this, disable transitions by setting `transition` to `0`, and increase the adaptation frequency by setting `interval` to a short time, e.g., `15` seconds, to retain the impression of smooth continuous adaptations. Keeping the `initial_transition` is recommended for a smooth fade-in (lights are usually not turned off momentarily after being turned on, in which case a short period of unresponsiveness is tolerable). +- [Lonsonho ZB-RGBCW](https://www.zigbee2mqtt.io/devices/ZB-RGBCW.html#lonsonho-zb-rgbcw) + - Some Zigbee2MQTT/eWeLight firmware combinations do not turn the bulb on when the initial `light.turn_on` call includes brightness or color, although later adjustments work. Disable `intercept` for affected bulbs. + + + +## :bar_chart: Graphs! +These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). + +### :sunny: Sun Position ![cl_percent|690x131](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/6/5/657ff98beb65a94598edeb4bdfd939095db1a22c.PNG) -##### Color Temperature: -![cl_colortemp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) +### :thermometer: Color Temperature +![cl_color_temp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) -##### Brightness: +### :high_brightness: Brightness ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) + +### While using `transition_until_sleep: true` +![image](https://user-images.githubusercontent.com/2219836/228949675-f9699624-8abc-466c-bb04-250ce0f495b8.png) + + + +### Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"` + +
+Enhance your control over brightness transitions during sunrise and sunset with brightness_mode (click here to learn more 🧠). + +With Adaptive Lighting, you can set a `brightness_mode` to specify how the brightness changes during sunrise and sunset. The `brightness_mode` can be set to `"default"` ([as illustrated in other graphs above](#high_brightness-brightness)), `"linear"`, or `"tanh"`. If you choose to deviate from the `"default"` mode, you can adjust `brightness_mode_time_dark` and `brightness_mode_time_light` to further customize the lighting transitions. + +When `brightness_mode` is set to `"linear"`: + +- During **_sunset_**, the brightness begins to gradually decrease from `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches `min_brightness` at `time=sunset_time + brightness_mode_time_dark`. +- During **_sunrise_**, the brightness begins to gradually increase from `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches `max_brightness` at `time=sunrise_time + brightness_mode_time_light`. + +When `brightness_mode` is set to `"tanh"`, it uses the smooth transition of a [hyperbolic tangent function](https://mathworld.wolfram.com/HyperbolicTangent.html): + +- During **_sunset_**, the brightness starts to decrease from 95% of `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches 5% of `min_brightness` at `time=sunset_time + brightness_mode_time_dark`. +- During **_sunrise_**, the brightness starts to increase from 5% of `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches 95% of `max_brightness` at `time=sunrise_time + brightness_mode_time_light`. +
+ +Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark` in the text box. +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/15143580-13cd-4ab2-a603-89f2b7830afd) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/f61fdac9-6d47-48c9-84ed-cbb451d5de5d) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/e5fc5d27-3c37-4e3d-93d1-6e7cf4b48e7c) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/3dcbdc42-63c4-49df-8651-d2fae53dd08d) + +> Check out the interactive webapp on https://basnijholt.github.io/adaptive-lighting/ to play with the parameters and see how the brightness changes! + + + +## :eyes: See also + +- [*Sleep better with Adaptive Lighting in Home Assistant*](https://wartner.io/sleep-better-with-adaptive-lightning-in-home-assistant/) by Florian Wartner on 2023-02-23 (blog post 📜) +- [*Automatic smart light brightness and color based on the sun*](https://www.youtube.com/watch?v=Rg3zI1Oyk3c) by Home Automation Guy on 2022-08-31 (YouTube video 📺) +- [*Adaptive Lighting Blew My Mind in Home Assistant - How to set it up*](https://www.youtube.com/watch?v=c1cnccmgl3k) by Smart Home Junkie on 2022-06-26 (YouTube video 📺) + + +## :busts_in_silhouette: Contributors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Sven Serlier
Sven Serlier

📖
Will Puckett
Will Puckett

📖
vapescherov
vapescherov

💻
Travis Pew
Travis Pew

📖
Sindre Broch
Sindre Broch

📖
Denis Shulyaka
Denis Shulyaka

💻
@RubenKelevra
@RubenKelevra

📖 💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Nicholai Nissen
Nicholai Nissen

🌍
Martin Myhrman
Martin Myhrman

🌍
Michel Peterson
Michel Peterson

💻
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
Joscha Wagner
Joscha Wagner

🌍
skdzzz
skdzzz

🌍
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Justin Paupore
Justin Paupore

💻
bedaes
bedaes

💻
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
Mark Niemeyer
Mark Niemeyer

🌍 💻
Elliott Plack
Elliott Plack

📖
ngommers
ngommers

🌍
Andrew Berry
Andrew Berry

📖
Tomáš Valigura
Tomáš Valigura

🌍
Benjamin Auquite
Benjamin Auquite

💻 🐛 🚧
Skyler Carlson
Skyler Carlson

📖
Chris
Chris

💻
Raman Gupta
Raman Gupta

💻
igiannakas
igiannakas

💻
Mario Guggenberger
Mario Guggenberger

💻 🤔
Kendell R
Kendell R

🎨
lukerix
lukerix

🌍
Maxime Bailleul
Maxime Bailleul

🌍
Michel Balzer
Michel Balzer

🌍
Enrico Gambini
Enrico Gambini

🌍
MirCore
MirCore

🌍
Fernando Belaza
Fernando Belaza

🌍
Vladimir Cravero
Vladimir Cravero

🌍
Julien Quiévreux
Julien Quiévreux

🌍
lightrabbit
lightrabbit

🌍
Arie6414
Arie6414

🌍
luixcaetano
luixcaetano

🌍
fmarcu
fmarcu

🌍
michaelkmoch
michaelkmoch

🌍
Fred
Fred

🌍
Z-weapon
Z-weapon

🌍
Kyle Bjordahl
Kyle Bjordahl

💻 🐛
Olek Bruks
Olek Bruks

🌍
Gabriele Baldassarre
Gabriele Baldassarre

🌍
Pepijn Baart
Pepijn Baart

🌍
Artem Pastukhov
Artem Pastukhov

🌍
Martin Štefany
Martin Štefany

🌍
quenthal
quenthal

🌍
Luki72
Luki72

🌍
pantan-cymk
pantan-cymk

🌍
yousaf465
yousaf465

🌍
Pierre Belanger
Pierre Belanger

📖
Jan-Sigurd Sørensen
Jan-Sigurd Sørensen

🌍
EF01
EF01

🌍
Mr Snake
Mr Snake

🌍
hungrymachine1
hungrymachine1

🌍
4D4M-Github
4D4M-Github

🌍
Ivan
Ivan

🌍
Florent Cardoen
Florent Cardoen

🌍
moemeli
moemeli

🌍
saya6k
saya6k

🌍
droans
droans

💻
Jonathan Kang
Jonathan Kang

💻
scuricvladimir
scuricvladimir

🌍
Pieter
Pieter

🌍
san80068259
san80068259

🌍
Frosh
Frosh

💻
Rafael Miranda
Rafael Miranda

🌍
rVlad93
rVlad93

🌍
Björn Ebbinghaus
Björn Ebbinghaus

💻
Marck
Marck

💻
Lucho Gizdov
Lucho Gizdov

🌍
MizterB
MizterB

💻
brietman
brietman

🌍
தமிழ் நேரம்
தமிழ் நேரம்

🌍
Thunderstrike116
Thunderstrike116

🌍
immeteor2
immeteor2

🌍
Patrick Bassut
Patrick Bassut

🌍
Ricky Tigg
Ricky Tigg

🌍
Márton Maráz
Márton Maráz

💻
Sara492
Sara492

🌍
enpaga
enpaga

🌍
xuars
xuars

🌍
tinutac
tinutac

🌍
Default User
Default User

🌍
amelenty
amelenty

🌍
Rostyslav Dudka
Rostyslav Dudka

🌍
Helder Ferreira
Helder Ferreira

🌍
Piotr Laszczkowski
Piotr Laszczkowski

🌍
Reza
Reza

🌍
Luna Jernberg
Luna Jernberg

🌍
Jeff Wilson
Jeff Wilson

💻
Rasmus Lundsgaard
Rasmus Lundsgaard

💻
Tom Matheussen
Tom Matheussen

💻
ams2990
ams2990

💻
DataGhost
DataGhost

💻
Furkan Kaya
Furkan Kaya

🌍
Rafael do Amaral Porciuncula
Rafael do Amaral Porciuncula

🌍
hhjuhl
hhjuhl

🌍
B.Athish
B.Athish

🌍
Горпиніч Максим Олександрович
Горпиніч Максим Олександрович

🌍
Masayuki Sugahara
Masayuki Sugahara

🌍
therealmate
therealmate

🌍
Dobby
Dobby

💻
lenucksi
lenucksi

💻
edgimar
edgimar

💻
Andrei LAZAROV
Andrei LAZAROV

📖
Adam DeMuri
Adam DeMuri

💻
Natanael
Natanael

🌍
Yllelder Bamir
Yllelder Bamir

🌍
Esspel
Esspel

🌍
Corey Peruffo
Corey Peruffo

💻
Samson Brock
Samson Brock

💻
Dennis Dekker
Dennis Dekker

💻
proscar87
proscar87

💻
Jared Jensen
Jared Jensen

💻
mueslo
mueslo

💻
Marijn Eken
Marijn Eken

📖
Milan K.
Milan K.

🌍
Callisto
Callisto

🐛
David Geiger
David Geiger

🐛
Jan
Jan

🌍
frankysan
frankysan

🌍
Belkin
Belkin

🌍
LukTyn
LukTyn

🌍
rutgerkra
rutgerkra

🌍
sergeybelozorov
sergeybelozorov

🌍
Allan Himidi-Rattenborg
Allan Himidi-Rattenborg

🌍
Pose marto
Pose marto

🌍
Jean-Francois Cosse
Jean-Francois Cosse

🌍
bisquit2003
bisquit2003

🐛
chewth91
chewth91

🐛
rhtenhove
rhtenhove

💻
Bill Papas
Bill Papas

🤔
Niklas Haas
Niklas Haas

🤔
Tom Urlings
Tom Urlings

🤔
Benoit Anastay
Benoit Anastay

🤔
Jeremy Gollehon
Jeremy Gollehon

🤔 🐛
jrbergen
jrbergen

🤔
Bradley O'Connell
Bradley O'Connell

🤔
00schteven
00schteven

🤔
Wosten
Wosten

🐛
Zachary Priddy
Zachary Priddy

🤔
Andrew Blakeslee Moore
Andrew Blakeslee Moore

🐛
jaynis
jaynis

💻
Alistair Galbraith
Alistair Galbraith

💻
Leonhard Hesse
Leonhard Hesse

💻
Tim Stallmann
Tim Stallmann

💻
lehneres
lehneres

🤔
Ahmad Tawakol
Ahmad Tawakol

💻 🐛 🚧
+ + Add your contributions + +
+ + + + + + +## Translating Adaptive Lighting + +Help to translate Adaptive Lighting into your language on [Hosted Weblate](https://hosted.weblate.org/engage/adaptive-lighting/)! + +Translating can be done from your webbrowser, no programming knowledge +is needed! + + +Translation status + diff --git a/blueprints/automation/daylight_limit.yaml b/blueprints/automation/daylight_limit.yaml new file mode 100644 index 00000000..bc57d434 --- /dev/null +++ b/blueprints/automation/daylight_limit.yaml @@ -0,0 +1,112 @@ +blueprint: + name: "Adaptive Lighting: limit brightness in daylight" + description: >- + Lower a profile's maximum brightness in strong daylight and restore the + chosen normal maximum when daylight falls. Separate lux thresholds prevent + repeated changes near a single threshold. Use a sensor not significantly + affected by the controlled lights. Keep the daylight maximum at or above + the profile's minimum unless you want an inverted brightness curve. + Startup waits up to five minutes for a numeric sensor reading; a reading + between the thresholds leaves the configured maximum unchanged. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + adaptive_switch: + name: Adaptive Lighting profile + description: Select the main profile switch, not a sleep or adaptation switch. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + illuminance_sensor: + name: Illuminance sensor + selector: + entity: + filter: + domain: sensor + device_class: illuminance + high_lux: + name: Strong daylight threshold + description: Must be greater than the low daylight threshold. + default: 300 + selector: + number: + min: 0 + max: 200000 + mode: box + unit_of_measurement: lx + low_lux: + name: Low daylight threshold + default: 200 + selector: + number: + min: 0 + max: 200000 + mode: box + unit_of_measurement: lx + daylight_maximum: + name: Maximum brightness in strong daylight + default: 30 + selector: + number: + min: 1 + max: 100 + mode: box + unit_of_measurement: "%" + normal_maximum: + name: Normal maximum brightness + default: 100 + selector: + number: + min: 1 + max: 100 + mode: box + unit_of_measurement: "%" + +mode: restart +variables: + illuminance_sensor: !input illuminance_sensor + high_lux: !input high_lux + low_lux: !input low_lux +triggers: + - trigger: numeric_state + entity_id: !input illuminance_sensor + above: !input high_lux + - trigger: numeric_state + entity_id: !input illuminance_sensor + below: !input low_lux + - trigger: homeassistant + event: start + id: startup +conditions: + - condition: template + value_template: "{{ high_lux > low_lux }}" +actions: + - if: + - condition: trigger + id: startup + then: + - wait_template: "{{ is_number(states(illuminance_sensor)) }}" + timeout: "00:05:00" + continue_on_timeout: false + - choose: + - conditions: + - condition: numeric_state + entity_id: !input illuminance_sensor + above: !input high_lux + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: !input adaptive_switch + max_brightness: !input daylight_maximum + - conditions: + - condition: numeric_state + entity_id: !input illuminance_sensor + below: !input low_lux + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: !input adaptive_switch + max_brightness: !input normal_maximum diff --git a/blueprints/automation/manual_control_at_minimum.yaml b/blueprints/automation/manual_control_at_minimum.yaml new file mode 100644 index 00000000..95cb5f79 --- /dev/null +++ b/blueprints/automation/manual_control_at_minimum.yaml @@ -0,0 +1,123 @@ +blueprint: + name: "Adaptive Lighting: pause brightness at minimum" + description: >- + Mark one light's brightness as manually controlled when its calculated + brightness target crosses down to the configured minimum. Set the profile's + take_over_control_mode to pause_changed to keep adapting color; pause_all + pauses both. Existing manual color control is preserved. Uses the normal + manual-control resets, including turning the light off and any configured + autoreset_control_seconds timeout. Runs once per downward crossing, so a + reset while the target remains low does not immediately pause it again. + Waits up to five minutes for reported brightness to reach the rounded + 0–255 minimum before pausing. This does not prove physical fade completion. Does not + change power or adaptation switches. Select an individual light managed by + the profile. Shared profiles retain their usual manual-control behavior. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + adaptive_switch: + name: Adaptive Lighting profile + description: Select the main Adaptive Lighting switch, not a sleep or adaptation switch. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + brightness_switch: + name: Adapt brightness switch + description: Select the adapt brightness switch belonging to the same profile. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + light_entity: + name: Light + description: Select one light managed by this profile. Create an automation for each light. + selector: + entity: + filter: + domain: light + minimum_pct: + name: Minimum brightness + description: Match the profile's min_brightness setting. Update this if that setting changes. + default: 1 + selector: + number: + min: 1 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: box + +mode: single +variables: + adaptive_switch: !input adaptive_switch + light_entity: !input light_entity + minimum_pct: !input minimum_pct + minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}" + brightness_switch: !input brightness_switch +triggers: + - trigger: state + entity_id: !input adaptive_switch + attribute: brightness_pct +conditions: + - condition: state + entity_id: !input adaptive_switch + state: "on" + - condition: state + entity_id: !input brightness_switch + state: "on" + - condition: template + value_template: >- + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: !input light_entity + state: "on" + - condition: template + value_template: >- + {{ light_entity not in (state_attr(adaptive_switch, 'manual_control_brightness') or []) }} +actions: + - variables: + light_session: "{{ states[light_entity].last_changed.isoformat() }}" + profile_session: "{{ states[adaptive_switch].last_changed.isoformat() }}" + brightness_session: "{{ states[brightness_switch].last_changed.isoformat() }}" + - wait_template: >- + {% set target = state_attr(adaptive_switch, 'brightness_pct') %} + {{ not is_state(light_entity, 'on') or not is_state(adaptive_switch, 'on') + or not is_state(brightness_switch, 'on') + or states[light_entity].last_changed.isoformat() != light_session + or states[adaptive_switch].last_changed.isoformat() != profile_session + or states[brightness_switch].last_changed.isoformat() != brightness_session + or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum + or light_entity in (state_attr(adaptive_switch, 'manual_control_brightness') or []) + or (state_attr(light_entity, 'brightness') | float(256)) <= minimum }} + timeout: "00:05:00" + continue_on_timeout: false + - condition: template + value_template: >- + {% set target = state_attr(adaptive_switch, 'brightness_pct') %} + {{ is_state(light_entity, 'on') and is_state(adaptive_switch, 'on') + and is_state(brightness_switch, 'on') + and states[light_entity].last_changed.isoformat() == light_session + and states[adaptive_switch].last_changed.isoformat() == profile_session + and states[brightness_switch].last_changed.isoformat() == brightness_session + and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum + and (state_attr(light_entity, 'brightness') | float(256)) <= minimum + and light_entity not in + (state_attr(adaptive_switch, 'manual_control_brightness') or []) }} + - action: adaptive_lighting.set_manual_control + data: + entity_id: !input adaptive_switch + lights: !input light_entity + manual_control: >- + {{ true if light_entity in + (state_attr(adaptive_switch, 'manual_control_color') or []) + else 'brightness' }} diff --git a/blueprints/automation/schedule_profile.yaml b/blueprints/automation/schedule_profile.yaml new file mode 100644 index 00000000..64ccf17a --- /dev/null +++ b/blueprints/automation/schedule_profile.yaml @@ -0,0 +1,57 @@ +blueprint: + name: "Adaptive Lighting: scheduled profile" + description: >- + Apply fixed brightness and color temperature from a Schedule helper's + brightness_pct and color_temp_kelvin attributes. Uses step changes, not + interpolation. Missing attributes fall back to 1% and 2000 K. Outside an + active block, restores ALL configured profile settings. Use this only if + other automations do not also change runtime settings on this profile. + Reapplies the active block on Home Assistant startup. Updates settings + while the profile is off without enabling it; preserves manual control. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + adaptive_switch: + name: Adaptive Lighting profile + description: Select the main profile switch, not a sleep or adaptation switch. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + schedule_entity: + name: Schedule helper + description: Add brightness_pct (1–100) and color_temp_kelvin to each block's Additional data. + selector: + entity: + filter: + domain: schedule + +mode: restart +variables: + schedule_entity: !input schedule_entity +triggers: + - trigger: state + entity_id: !input schedule_entity + - trigger: homeassistant + event: start +actions: + - choose: + - conditions: + - condition: state + entity_id: !input schedule_entity + state: "on" + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: !input adaptive_switch + min_brightness: "{{ state_attr(schedule_entity, 'brightness_pct') | int(1) }}" + max_brightness: "{{ state_attr(schedule_entity, 'brightness_pct') | int(1) }}" + min_color_temp: "{{ state_attr(schedule_entity, 'color_temp_kelvin') | int(2000) }}" + max_color_temp: "{{ state_attr(schedule_entity, 'color_temp_kelvin') | int(2000) }}" + default: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: !input adaptive_switch + use_defaults: configuration diff --git a/blueprints/automation/sleep_mode.yaml b/blueprints/automation/sleep_mode.yaml new file mode 100644 index 00000000..62225aed --- /dev/null +++ b/blueprints/automation/sleep_mode.yaml @@ -0,0 +1,42 @@ +blueprint: + name: "Adaptive Lighting: synchronize sleep mode" + description: >- + Keep the selected Adaptive Lighting sleep-mode switches in sync with an + input boolean, including its restored state at Home Assistant startup. + Unknown or unavailable helper states are ignored. Select only sleep-mode + switches, not the main profile or adaptation switches. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + sleep_helper: + name: Sleep-mode helper + selector: + entity: + filter: + domain: input_boolean + sleep_switches: + name: Adaptive Lighting sleep-mode switches + selector: + entity: + multiple: true + filter: + domain: switch + integration: adaptive_lighting + +triggers: + - trigger: state + entity_id: !input sleep_helper + - trigger: homeassistant + event: start +variables: + sleep_helper: !input sleep_helper + sleep_mode: "{{ states(sleep_helper) }}" +conditions: + - condition: template + value_template: "{{ sleep_mode in ['on', 'off'] }}" +actions: + - action: "switch.turn_{{ sleep_mode }}" + target: + entity_id: !input sleep_switches +mode: restart diff --git a/blueprints/automation/turn_off_at_minimum.yaml b/blueprints/automation/turn_off_at_minimum.yaml new file mode 100644 index 00000000..6593e398 --- /dev/null +++ b/blueprints/automation/turn_off_at_minimum.yaml @@ -0,0 +1,87 @@ +blueprint: + name: "Adaptive Lighting: turn off at minimum brightness" + description: >- + Turn one light off when its Adaptive Lighting target crosses down into the + chosen minimum brightness range. Compares rounded 0–255 commands, not the + bulb's physical dimming limit or transition completion. Skips lights currently + marked as manually controlled. Changing sleep mode clears manual control by + default; set reset_manual_control_on_sleep_mode_change to false in your + profile to preserve it. Does not turn lights back on or repeatedly turn them off + while the target stays low. Startup at the minimum does not trigger it. + Sleep mode can trigger it if its target crosses the chosen minimum. + Select a light managed by the chosen profile and its matching brightness switch. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + adaptive_switch: + name: Adaptive Lighting profile + description: Select the main Adaptive Lighting switch, not a sleep or adaptation switch. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + brightness_switch: + name: Adapt brightness switch + description: Select the adapt brightness switch belonging to the same profile. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + light_entity: + name: Light + description: Select one light managed by this profile. Create an automation for each light. + selector: + entity: + filter: + domain: light + minimum_pct: + name: Minimum brightness + description: Match the profile's min_brightness setting. Update this if that setting changes. + default: 1 + selector: + number: + min: 1 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: box + +mode: single +variables: + adaptive_switch: !input adaptive_switch + light_entity: !input light_entity + minimum_pct: !input minimum_pct +triggers: + - trigger: state + entity_id: !input adaptive_switch + attribute: brightness_pct +conditions: + - condition: state + entity_id: !input adaptive_switch + state: "on" + - condition: state + entity_id: !input brightness_switch + state: "on" + - condition: template + value_template: >- + {% set minimum = (minimum_pct * 255 / 100) | round(0) %} + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: !input light_entity + state: "on" + - condition: template + value_template: >- + {{ light_entity not in (state_attr(adaptive_switch, 'manual_control') or []) }} +actions: + - action: light.turn_off + target: + entity_id: !input light_entity diff --git a/config/configuration.yaml b/config/configuration.yaml new file mode 100644 index 00000000..b9d7d45b --- /dev/null +++ b/config/configuration.yaml @@ -0,0 +1,19 @@ +# https://www.home-assistant.io/integrations/default_config/ +default_config: + +# https://www.home-assistant.io/integrations/logger/ +logger: + default: info + logs: + custom_components.adaptive_lighting: debug + +light: + - platform: template + lights: + dummylight: + friendly_name: "Dummy Light" + turn_on: + turn_off: + set_level: + set_temperature: + supports_transition_template: "{{ true }}" diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py new file mode 100644 index 00000000..0ac9ef71 --- /dev/null +++ b/custom_components/adaptive_lighting/__init__.py @@ -0,0 +1,142 @@ +"""Adaptive Lighting integration in Home-Assistant.""" + +import logging +from functools import partial +from typing import Any + +import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_SOURCE, Platform +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import service + +from .const import ( + _DOMAIN_SCHEMA, # pyright: ignore[reportPrivateUsage] + ATTR_ADAPTIVE_LIGHTING_MANAGER, + CONF_NAME, + DOMAIN, + SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, + SERVICE_SET_MANUAL_CONTROL, + SET_MANUAL_CONTROL_SCHEMA, + UNDO_UPDATE_LISTENER, + apply_service_schema, + change_switch_settings_schema, +) +from .switch import ( + handle_apply_service, + handle_change_switch_settings, + handle_set_manual_control_service, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["switch"] + + +def _all_unique_names(value: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Validate that all entities have a unique profile name.""" + hosts = [device[CONF_NAME] for device in value] + schema = vol.Schema(vol.Unique()) + schema(hosts) + return value + + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_names)}, + extra=vol.ALLOW_EXTRA, +) + + +async def reload_configuration_yaml(event: Event) -> None: + """Reload configuration.yaml.""" + hass: HomeAssistant | None = event.data.get("hass") + if hass is not None: + await hass.services.async_call("homeassistant", "check_config", {}) + else: + _LOGGER.error("HomeAssistant instance not found in event data.") + + +async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: + """Import integration from config.""" + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_APPLY, + service_func=partial(handle_apply_service, hass), + schema=apply_service_schema(), + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_MANUAL_CONTROL, + service_func=partial(handle_set_manual_control_service, hass), + schema=SET_MANUAL_CONTROL_SCHEMA, + ) + + if register_platform_service := getattr( + service, + "async_register_platform_entity_service", + None, + ): + register_platform_service( + hass, + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + entity_domain=Platform.SWITCH, + func=handle_change_switch_settings, + schema=change_switch_settings_schema(), + ) + + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=entry, + ), + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the component.""" + data = hass.data.setdefault(DOMAIN, {}) + + # This will reload any changes the user made to any YAML configurations. + # Called during 'quick reload' or hass.reload_config_entry + hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml) + + undo_listener = config_entry.add_update_listener(async_update_options) + data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_forward_entry_unload( + config_entry, + "switch", + ) + data = hass.data[DOMAIN] + data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() + if unload_ok: + data.pop(config_entry.entry_id) + + if len(data) == 1 and ATTR_ADAPTIVE_LIGHTING_MANAGER in data: + # no more config_entries + manager = data.pop(ATTR_ADAPTIVE_LIGHTING_MANAGER) + manager.disable() + + if not data: + hass.data.pop(DOMAIN) + + return unload_ok diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py new file mode 100644 index 00000000..c90d0c87 --- /dev/null +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -0,0 +1,116 @@ +from typing import Any + +import homeassistant.helpers.config_validation as cv +import pandas as pd +import voluptuous as vol +from homeassistant.helpers import selector + +from .const import ( + DOCS, + DOCS_APPLY, + DOCS_MANUAL_CONTROL, + SET_MANUAL_CONTROL_SCHEMA, + VALIDATION_TUPLES, + apply_service_schema, +) + + +def _format_voluptuous_instance(instance: vol.All) -> str: + coerce_type = None + min_val = None + max_val = None + + for validator in instance.validators: + if isinstance(validator, vol.Coerce): + coerce_type = validator.type.__name__ + elif isinstance(validator, vol.Clamp | vol.Range): + min_val = validator.min + max_val = validator.max + + if min_val is not None and max_val is not None: + return f"`{coerce_type}` {min_val}-{max_val}" + if min_val is not None: + return f"`{coerce_type} > {min_val}`" + if max_val is not None: + return f"`{coerce_type} < {max_val}`" + return f"`{coerce_type}`" + + +def _type_to_str(type_: Any) -> str: # noqa: PLR0911 + """Convert a (voluptuous) type to a string.""" + if type_ == cv.entity_ids: + return "list of `entity_id`s" + if type_ in (bool, int, float, str): + return f"`{type_.__name__}`" + if type_ == cv.boolean: + return "bool" + if isinstance(type_, vol.All): + return _format_voluptuous_instance(type_) + if isinstance(type_, vol.Any): + return " or ".join(_type_to_str(t) for t in type_.validators) + if isinstance(type_, vol.In): + return f"one of `{type_.container}`" + if isinstance(type_, selector.SelectSelector): + return f"one of `{type_.config['options']}`" + if isinstance(type_, selector.ColorRGBSelector): + return "RGB color" + msg = f"Unknown type: {type_}" + raise ValueError(msg) + + +def generate_config_markdown_table() -> str: + rows: list[dict[str, str]] = [] + for k, default, type_ in VALIDATION_TUPLES: + description = DOCS[k] + row = { + "Variable name": f"`{k}`", + "Description": description, + "Default": f"`{default}`", + "Type": _type_to_str(type_), + } + rows.append(row) + + df = pd.DataFrame(rows) + return df.to_markdown(index=False) + + +def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[bool, Any]]: + result: dict[str, tuple[bool, Any]] = {} + for key, value in schema.schema.items(): + if isinstance(key, vol.Required | vol.Optional): + required = isinstance(key, vol.Required) and key.default == vol.UNDEFINED + result[key.schema] = (required, value) + return result + + +def _generate_service_markdown_table( + schema: vol.Schema, + alternative_docs: dict[str, str] | None = None, +) -> str: + rows: list[dict[str, str]] = [] + for k, (required, type_) in _schema_to_dict(schema).items(): + if alternative_docs is not None and k in alternative_docs: + description = alternative_docs[k] + else: + description = DOCS[k] + row = { + "Service data attribute": f"`{k}`", + "Description": description, + "Required": "✅" if required else "❌", + "Type": _type_to_str(type_), + } + rows.append(row) + + df = pd.DataFrame(rows) + return df.to_markdown(index=False) + + +def generate_apply_markdown_table() -> str: + return _generate_service_markdown_table(apply_service_schema(), DOCS_APPLY) + + +def generate_set_manual_control_markdown_table() -> str: + return _generate_service_markdown_table( + SET_MANUAL_CONTROL_SCHEMA, + DOCS_MANUAL_CONTROL, + ) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py new file mode 100644 index 00000000..91defc95 --- /dev/null +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -0,0 +1,384 @@ +"""Utility functions for adaptation commands.""" + +import logging +from collections.abc import AsyncGenerator +from dataclasses import dataclass +from enum import IntFlag, auto +from typing import Any + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP_KELVIN, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, HomeAssistant, State + +_LOGGER = logging.getLogger(__name__) + +COLOR_ATTRS = { # Should ATTR_PROFILE be in here? + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP_KELVIN, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, +} + + +BRIGHTNESS_ATTRS = { + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, +} + +# Worst-case rounding error when Home Assistant's 0-255 brightness scale +# round-trips through a device with coarser resolution (e.g., the 0-99 Z-Wave +# Multilevel Switch scale). A light cannot report back a value more precise than +# its own scale, so exact equality would never hold for such targets and +# 'skip_redundant_commands' would keep sending them forever. The tolerance sits +# far below the manual-control-detection threshold (BRIGHTNESS_CHANGE = 25), so +# it cannot mask a genuine user change. +BRIGHTNESS_TOLERANCE = 2 + +ServiceData = dict[str, Any] + + +class LightControlAttributes(IntFlag): + """Attributes of lights that the adaptation engine can control.""" + + NONE = 0 + BRIGHTNESS = auto() + COLOR = auto() + + ALL = BRIGHTNESS | COLOR + + def __str__(self) -> str: + """Return a string representation of the attributes.""" + if self == LightControlAttributes.NONE: + return "NONE" + + return "|".join( + member.name + for member in type(self) + if member is not LightControlAttributes.NONE + and member in self + and member.name is not None + ) + + def has_any(self) -> bool: + """Determine whether any attribute is selected.""" + return self != LightControlAttributes.NONE + + def has_none(self) -> bool: + """Determine whether no attribute is selected.""" + return self == LightControlAttributes.NONE + + def has_all(self) -> bool: + """Determine whether all attributes are selected.""" + return (self & LightControlAttributes.ALL) == LightControlAttributes.ALL + + +def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: + """Splits the service data by the adapted attributes. + + i.e., into separate data items for brightness and color. + """ + common_attrs = {ATTR_ENTITY_ID} + common_data = {k: service_data[k] for k in common_attrs if k in service_data} + + attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS] + service_datas: list[dict[str, Any]] = [] + + for attributes in attributes_split_sequence: + split_data = { + attribute: service_data[attribute] + for attribute in attributes + if service_data.get(attribute) + } + if split_data: + service_datas.append(common_data | split_data) + + # Distribute the transition duration across all service calls + if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None: + transition /= len(service_datas) + + for _service_data in service_datas: + _service_data[ATTR_TRANSITION] = transition + + return service_datas + + +def _is_attribute_satisfied(key: str, value: Any, attributes: dict[str, Any]) -> bool: + """Whether the light's current state already satisfies this target value.""" + if key not in attributes: + return False + current = attributes[key] + if not isinstance(current, (int, float)) or not isinstance(value, (int, float)): + return value == current + if key == ATTR_BRIGHTNESS: + return abs(value - current) <= BRIGHTNESS_TOLERANCE + if key == ATTR_COLOR_TEMP_KELVIN and value > 0 and current > 0: + # Compare in mired space: most integrations quantize color temperature + # to whole mireds, and the kelvin error of that quantization grows + # quadratically with kelvin (~21 K at 6500 K, ~50 K at 10000 K), so no + # fixed kelvin tolerance fits the whole range. The tolerance of one + # mired absorbs the difference between conversion schemes: HA core's + # helpers floor (e.g. 5500 K -> 181 mired -> 5524 K) while some + # integrations round (5500 K -> 182 mired -> 5495 K), and no exact + # equality converges for both. One mired is far below the ~5.5 mired + # just-noticeable difference for color temperature. + return abs(round(1_000_000 / value) - round(1_000_000 / current)) <= 1 + return value == current + + +def _remove_redundant_attributes( + service_data: ServiceData, + state: State, +) -> ServiceData: + """Filter service data by removing attributes already satisfied by the state. + + Removes all attributes from service call data whose values are already present + in the target entity's state. Quantized attributes (brightness, color temp) are + compared with a small tolerance: a light whose resolution is coarser than Home + Assistant's cannot report back the exact value it was given, so exact equality + would never hold and the attribute would never be filtered. + """ + attributes: dict[str, Any] = dict(state.attributes) + return { + k: v + for k, v in service_data.items() + if not _is_attribute_satisfied(k, v, attributes) + } + + +def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool: + """Determines whether the service data justifies an adaptation service call. + + A service call is not justified for data which does not contain any entries that + change relevant attributes of an adapting entity, e.g., brightness or color. + """ + common_attrs = {ATTR_ENTITY_ID, ATTR_TRANSITION} + + return any(attr not in common_attrs for attr in service_data) + + +async def _create_service_call_data_iterator( + hass: HomeAssistant, + service_datas: list[ServiceData], + filter_by_state: bool, +) -> AsyncGenerator[ServiceData]: + """Enumerates and filters a list of service datas on the fly. + + If filtering is enabled, every service data is filtered by the current state of + the related entity and only returned if it contains relevant data that justifies + a service call. + The main advantage of this generator over a list is that it applies the filter + at the time when the service data is read instead of up front. This gives greater + flexibility because entity states can change while the items are iterated. + """ + for service_data in service_datas: + if filter_by_state and (entity_id := service_data.get(ATTR_ENTITY_ID)): + current_entity_state = hass.states.get(entity_id) + + # Filter data to remove attributes that equal the current state + if current_entity_state is not None: + service_data = _remove_redundant_attributes( # noqa: PLW2901 + service_data, + state=current_entity_state, + ) + + # Emit service data if it still contains relevant attributes (else try next) + if _has_relevant_service_data_attributes(service_data): + yield service_data + else: + yield service_data + + +@dataclass +class AdaptationData: + """Holds all data required to execute an adaptation.""" + + entity_id: str + context: Context + sleep_time: float + service_call_datas: AsyncGenerator[ServiceData] + force: bool + max_length: int + attributes: LightControlAttributes + initial_sleep: bool = False + + async def next_service_call_data(self) -> ServiceData | None: + """Return data for the next service call, or none if no more data exists.""" + return await anext(self.service_call_datas, None) + + def __str__(self) -> str: + """Return a string representation of the data.""" + return ( + f"{self.__class__.__name__}(" + f"entity_id={self.entity_id}, " + f"context_id={self.context.id}, " + f"sleep_time={self.sleep_time}, " + f"force={self.force}, " + f"max_length={self.max_length}, " + f"attributes={self.attributes}, " + f"initial_sleep={self.initial_sleep}" + ")" + ) + + +class NoColorOrBrightnessInServiceDataError(Exception): + """Exception raised when no color or brightness attributes are found in service data.""" + + +def _identify_light_control_attributes( + service_data: ServiceData, +) -> LightControlAttributes: + """Extract the 'which' attribute from the service data.""" + has_brightness = ATTR_BRIGHTNESS in service_data + has_color = any(attr in service_data for attr in COLOR_ATTRS) + + parameters = LightControlAttributes.NONE + + if has_brightness: + parameters |= LightControlAttributes.BRIGHTNESS + if has_color: + parameters |= LightControlAttributes.COLOR + + if parameters == LightControlAttributes.NONE: + msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}" + raise NoColorOrBrightnessInServiceDataError(msg) + + return parameters + + +def prepare_adaptation_data( + hass: HomeAssistant, + entity_id: str, + context: Context, + transition: float | None, + split_delay: float, + service_data: ServiceData, + split: bool, + filter_by_state: bool, + force: bool, + already_applied: LightControlAttributes = LightControlAttributes.NONE, +) -> AdaptationData: + """Prepares a data object carrying all data required to execute an adaptation.""" + _LOGGER.debug( + "Preparing adaptation data for %s with service data %s", + entity_id, + service_data, + ) + service_datas = _split_service_call_data(service_data) if split else [service_data] + + service_datas_length = len(service_datas) + + if transition is not None: + transition_duration_per_data = transition / max(1, service_datas_length) + sleep_time = transition_duration_per_data + split_delay + else: + sleep_time = split_delay + + # Keep the original split timing, but omit attributes carried by an + # intercepted turn-on shared with other lights. Do this before state + # filtering: members can have different brightness/color already satisfied. + applied_attrs = ( + BRIGHTNESS_ATTRS + if LightControlAttributes.BRIGHTNESS in already_applied + else set() + ) | (COLOR_ATTRS if LightControlAttributes.COLOR in already_applied else set()) + if applied_attrs: + service_datas = [ + {key: value for key, value in data.items() if key not in applied_attrs} + for data in service_datas + ] + service_datas = [ + data + for data in service_datas + if _has_relevant_service_data_attributes(data) + ] + + service_data_iterator = _create_service_call_data_iterator( + hass, + service_datas, + filter_by_state, + ) + + attributes = _identify_light_control_attributes(service_data) + + return AdaptationData( + entity_id=entity_id, + context=context, + sleep_time=sleep_time, + service_call_datas=service_data_iterator, + force=force, + max_length=len(service_datas), + attributes=attributes & ~already_applied, + ) + + +def manual_control_event_attribute_to_flags( + manual_control_attribute: bool | str, +) -> LightControlAttributes: + """Convert manual control event data to light control attributes.""" + if isinstance(manual_control_attribute, bool) and manual_control_attribute: + return LightControlAttributes.ALL + if manual_control_attribute == "brightness": + return LightControlAttributes.BRIGHTNESS + if manual_control_attribute == "color": + return LightControlAttributes.COLOR + return LightControlAttributes.NONE + + +def has_brightness_attribute( + service_data: ServiceData, +) -> bool: + """Determine whether the service data contains brightness attributes.""" + return any(attr in BRIGHTNESS_ATTRS for attr in service_data) + + +def has_color_attribute( + service_data: ServiceData, +) -> bool: + """Determine whether the service data contains color attributes.""" + return any(attr in COLOR_ATTRS for attr in service_data) + + +def has_effect_attribute( + service_data: ServiceData, +) -> bool: + """Determine whether the service data contains effect attributes.""" + return ATTR_FLASH in service_data or ATTR_EFFECT in service_data + + +def get_light_control_attributes( + service_data: ServiceData, +) -> LightControlAttributes: + """Get the light control attributes affected by the service call data.""" + parameters = LightControlAttributes.NONE + + if has_brightness_attribute(service_data): + parameters |= LightControlAttributes.BRIGHTNESS + + if has_color_attribute(service_data): + parameters |= LightControlAttributes.COLOR + + if has_effect_attribute(service_data): + parameters |= LightControlAttributes.BRIGHTNESS + parameters |= LightControlAttributes.COLOR + + return parameters diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py new file mode 100644 index 00000000..e62beb0c --- /dev/null +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -0,0 +1,610 @@ +"""Switch for the Adaptive Lighting integration.""" + +from __future__ import annotations + +import bisect +import colorsys +import datetime +import logging +import math +from dataclasses import dataclass +from datetime import UTC, timedelta +from enum import Enum +from functools import cached_property, partial +from typing import Any, Literal, cast + +import astral.sun +from homeassistant.util.color import ( + color_RGB_to_xy, + color_temperature_to_rgb, + color_xy_to_hs, +) + + +class SunEvent(str, Enum): + """A set of sun events that happen during a day.""" + + # Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET + # We re-define them here to not depend on homeassistant in this file. + SUNRISE = "sunrise" + SUNSET = "sunset" + NOON = "solar_noon" + MIDNIGHT = "solar_midnight" + + +_ORDER = (SunEvent.SUNRISE, SunEvent.NOON, SunEvent.SUNSET, SunEvent.MIDNIGHT) +_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} + +# On polar days without a sunrise/sunset, synthetic sun events are placed this +# far from solar noon (polar night) or solar midnight (midnight sun), giving a +# 1-hour synthetic "day" or "night" so the adaptation cycle keeps working. +_POLAR_SUN_EVENT_OFFSET = timedelta(minutes=30) +_POLAR_SUN_EVENT_EPSILON = timedelta(seconds=1) + +utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) +utcnow.__doc__ = "Get now in UTC time." + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class SunEvents: + """Track the state of the sun and associated light settings.""" + + name: str + astral_observer: astral.Observer + sunrise_time: datetime.time | None + min_sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None + sunset_time: datetime.time | None + min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None + sunrise_offset: datetime.timedelta = datetime.timedelta() + sunset_offset: datetime.timedelta = datetime.timedelta() + timezone: datetime.tzinfo = UTC + + def _astral_sunrise_or_sunset( + self, + dt: datetime.date, + event: Literal[SunEvent.SUNRISE, SunEvent.SUNSET], + offset: datetime.timedelta, + ) -> datetime.datetime: + """Return the astral sunrise/sunset, with a fallback for polar regions. + + Above the polar circle the sun never crosses the horizon during polar + night and midnight sun, and `astral` raises a `ValueError` (see #1485). + On such days, synthesize a 1-hour "day" around solar noon (polar night) + or a 1-hour "night" around solar midnight (midnight sun), so the + adaptation cycle keeps working. The `(min/max)_(sunrise/sunset)_time` + options are applied on top of these synthetic times and can be used to + shape the resulting schedule. Configured offsets are limited to the + surrounding solar midnight/noon interval so they cannot invert the + required event order. + """ + astral_event = ( + astral.sun.sunrise if event == SunEvent.SUNRISE else astral.sun.sunset + ) + try: + return astral_event(self.astral_observer, dt) + offset + except ValueError: + noon = astral.sun.noon(self.astral_observer, dt) + midnight = astral.sun.midnight(self.astral_observer, dt) + next_midnight = astral.sun.midnight( + self.astral_observer, + dt + timedelta(days=1), + ) + noon_elevation = astral.sun.elevation(self.astral_observer, noon) + midnight_elevation = astral.sun.elevation(self.astral_observer, midnight) + # The sum of the sun's highest and lowest elevation of the day is + # ≈2x the solar declination, so its sign robustly distinguishes + # midnight sun from polar night, even on the boundary days where + # one elevation hovers around the horizon. + if noon_elevation + midnight_elevation > 0: + # Midnight sun: the sun stays above the horizon all day. + synthetic = ( + midnight + _POLAR_SUN_EVENT_OFFSET + if event == SunEvent.SUNRISE + else next_midnight - _POLAR_SUN_EVENT_OFFSET + ) + else: + # Polar night: the sun stays below the horizon all day. + sign = -1 if event == SunEvent.SUNRISE else 1 + synthetic = noon + sign * _POLAR_SUN_EVENT_OFFSET + + lower, upper = ( + (midnight, noon) if event == SunEvent.SUNRISE else (noon, next_midnight) + ) + return min( + max(synthetic + offset, lower + _POLAR_SUN_EVENT_EPSILON), + upper - _POLAR_SUN_EVENT_EPSILON, + ) + + def sunrise(self, dt: datetime.date) -> datetime.datetime: + """Return the (adjusted) sunrise time for the given datetime.""" + sunrise = ( + self._astral_sunrise_or_sunset( + dt, + SunEvent.SUNRISE, + self.sunrise_offset, + ) + if self.sunrise_time is None + else self._replace_time(dt, self.sunrise_time) + self.sunrise_offset + ) + if self.min_sunrise_time is not None: + min_sunrise = self._replace_time(dt, self.min_sunrise_time) + sunrise = max(min_sunrise, sunrise) + if self.max_sunrise_time is not None: + max_sunrise = self._replace_time(dt, self.max_sunrise_time) + sunrise = min(max_sunrise, sunrise) + return sunrise + + def sunset(self, dt: datetime.date) -> datetime.datetime: + """Return the (adjusted) sunset time for the given datetime.""" + sunset = ( + self._astral_sunrise_or_sunset( + dt, + SunEvent.SUNSET, + self.sunset_offset, + ) + if self.sunset_time is None + else self._replace_time(dt, self.sunset_time) + self.sunset_offset + ) + if self.min_sunset_time is not None: + min_sunset = self._replace_time(dt, self.min_sunset_time) + sunset = max(min_sunset, sunset) + if self.max_sunset_time is not None: + max_sunset = self._replace_time(dt, self.max_sunset_time) + sunset = min(max_sunset, sunset) + return sunset + + def _replace_time( + self, + dt: datetime.date, + time: datetime.time, + ) -> datetime.datetime: + date_time = datetime.datetime.combine(dt, time) + dt_with_tz = date_time.replace(tzinfo=self.timezone) + return dt_with_tz.astimezone(UTC) + + def noon_and_midnight( + self, + dt: datetime.datetime, + sunset: datetime.datetime | None = None, + sunrise: datetime.datetime | None = None, + ) -> tuple[datetime.datetime, datetime.datetime]: + """Return the (adjusted) noon and midnight times for the given datetime.""" + if ( + self.sunrise_time is None + and self.sunset_time is None + and self.min_sunrise_time is None + and self.max_sunrise_time is None + and self.min_sunset_time is None + and self.max_sunset_time is None + ): + solar_noon = astral.sun.noon(self.astral_observer, dt) + solar_midnight = astral.sun.midnight(self.astral_observer, dt) + return solar_noon, solar_midnight + + if sunset is None: + sunset = self.sunset(dt) + if sunrise is None: + sunrise = self.sunrise(dt) + + middle = abs(sunset - sunrise) / 2 + if sunset > sunrise: + noon = sunrise + middle + midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1) + else: + midnight = sunset + middle + noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1) + return noon, midnight + + def sun_events(self, dt: datetime.datetime) -> list[tuple[SunEvent, float]]: + """Get the four sun event's timestamps at 'dt'.""" + sunrise = self.sunrise(dt) + sunset = self.sunset(dt) + solar_noon, solar_midnight = self.noon_and_midnight(dt, sunset, sunrise) + events: list[tuple[SunEvent, float]] = [ + (SunEvent.SUNRISE, sunrise.timestamp()), + (SunEvent.SUNSET, sunset.timestamp()), + (SunEvent.NOON, solar_noon.timestamp()), + (SunEvent.MIDNIGHT, solar_midnight.timestamp()), + ] + self._validate_sun_event_order(events) + return events + + def _validate_sun_event_order(self, events: list[tuple[SunEvent, float]]) -> None: + """Check if the sun events are in the expected order.""" + events = sorted(events, key=lambda x: x[1]) + events_names, _ = zip(*events, strict=True) + if events_names not in _ALLOWED_ORDERS: + msg = ( + f"{self.name}: The sun events {events_names} are not in the expected" + " order. The Adaptive Lighting integration will not work!" + " This might happen if your sunrise/sunset offset is too large or" + " your manually set sunrise/sunset time is past/before noon/midnight." + ) + _LOGGER.error(msg) + raise ValueError(msg) + + def prev_and_next_events( + self, + dt: datetime.datetime, + ) -> list[tuple[SunEvent, float]]: + """Get the previous and next sun event.""" + events = [ + event + for days in [-1, 0, 1] + for event in self.sun_events(dt + timedelta(days=days)) + ] + events = sorted(events, key=lambda x: x[1]) + i_now = bisect.bisect([ts for _, ts in events], dt.timestamp()) + return events[i_now - 1 : i_now + 1] + + def sun_position(self, dt: datetime.datetime) -> float: + """Calculate the position of the sun, between [-1, 1].""" + target_ts = dt.timestamp() + (_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) + h, x = ( + (prev_ts, next_ts) + if next_event in (SunEvent.SUNSET, SunEvent.SUNRISE) + else (next_ts, prev_ts) + ) + # k = -1 between sunset and sunrise (sun below horizon) + # k = 1 between sunrise and sunset (sun above horizon) + k = 1 if next_event in (SunEvent.SUNSET, SunEvent.NOON) else -1 + return k * (1 - ((target_ts - h) / (h - x)) ** 2) + + def closest_event( + self, + dt: datetime.datetime, + ) -> tuple[Literal[SunEvent.SUNRISE, SunEvent.SUNSET], float]: + """Get the closest sunset or sunrise event.""" + (prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) + if SunEvent.SUNRISE in (prev_event, next_event): + ts_event = prev_ts if prev_event == SunEvent.SUNRISE else next_ts + return SunEvent.SUNRISE, ts_event + if SunEvent.SUNSET in (prev_event, next_event): + ts_event = prev_ts if prev_event == SunEvent.SUNSET else next_ts + return SunEvent.SUNSET, ts_event + msg = "No sunrise or sunset event found." + raise ValueError(msg) + + +@dataclass(frozen=True) +class SunLightSettings: + """Track the state of the sun and associated light settings.""" + + name: str + astral_observer: astral.Observer + adapt_until_sleep: bool + max_brightness: int + max_color_temp: int + min_brightness: int + min_color_temp: int + sleep_brightness: int + sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"] + sleep_color_temp: int + sleep_rgb_color: tuple[int, int, int] + sunrise_time: datetime.time | None + min_sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None + sunset_time: datetime.time | None + min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None + brightness_mode_time_dark: datetime.timedelta + brightness_mode_time_light: datetime.timedelta + brightness_mode: Literal["default", "linear", "tanh"] = "default" + sunrise_offset: datetime.timedelta = datetime.timedelta() + sunset_offset: datetime.timedelta = datetime.timedelta() + timezone: datetime.tzinfo = UTC + + @cached_property + def sun(self) -> SunEvents: + """Return the SunEvents object.""" + return SunEvents( + name=self.name, + astral_observer=self.astral_observer, + sunrise_time=self.sunrise_time, + sunrise_offset=self.sunrise_offset, + min_sunrise_time=self.min_sunrise_time, + max_sunrise_time=self.max_sunrise_time, + sunset_time=self.sunset_time, + sunset_offset=self.sunset_offset, + min_sunset_time=self.min_sunset_time, + max_sunset_time=self.max_sunset_time, + timezone=self.timezone, + ) + + def _brightness_pct_default(self, dt: datetime.datetime) -> float: + """Calculate the brightness percentage using the default method.""" + sun_position = self.sun.sun_position(dt) + if sun_position > 0: + return self.max_brightness + delta_brightness = self.max_brightness - self.min_brightness + return (delta_brightness * (1 + sun_position)) + self.min_brightness + + def _brightness_pct_tanh(self, dt: datetime.datetime) -> float: + event, ts_event = self.sun.closest_event(dt) + dark = self.brightness_mode_time_dark.total_seconds() + light = self.brightness_mode_time_light.total_seconds() + if event == SunEvent.SUNRISE: + brightness = scaled_tanh( + dt.timestamp() - ts_event, + x1=-dark, + x2=+light, + y1=0.05, # be at 5% of range at x1 + y2=0.95, # be at 95% of range at x2 + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + elif event == SunEvent.SUNSET: + brightness = scaled_tanh( + dt.timestamp() - ts_event, + x1=-light, # shifted timestamp for the start of sunset + x2=+dark, # shifted timestamp for the end of sunset + y1=0.95, # be at 95% of range at the start of sunset + y2=0.05, # be at 5% of range at the end of sunset + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + else: + msg = "Unsupported sun event" + raise ValueError(msg) + return clamp(brightness, self.min_brightness, self.max_brightness) + + def _brightness_pct_linear(self, dt: datetime.datetime) -> float: + event, ts_event = self.sun.closest_event(dt) + # at ts_event - dt_start, brightness == start_brightness + # at ts_event + dt_end, brightness == end_brightness + dark = self.brightness_mode_time_dark.total_seconds() + light = self.brightness_mode_time_light.total_seconds() + if event == SunEvent.SUNRISE: + brightness = lerp( + dt.timestamp() - ts_event, + x1=-dark, + x2=+light, + y1=self.min_brightness, + y2=self.max_brightness, + ) + elif event == SunEvent.SUNSET: + brightness = lerp( + dt.timestamp() - ts_event, + x1=-light, + x2=+dark, + y1=self.max_brightness, + y2=self.min_brightness, + ) + else: + msg = "Unsupported sun event" + raise ValueError(msg) + return clamp(brightness, self.min_brightness, self.max_brightness) + + def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None: + """Calculate the brightness in %.""" + if is_sleep: + return self.sleep_brightness + assert self.brightness_mode in ("default", "linear", "tanh") + if self.brightness_mode == "default": + return self._brightness_pct_default(dt) + if self.brightness_mode == "linear": + return self._brightness_pct_linear(dt) + if self.brightness_mode == "tanh": + return self._brightness_pct_tanh(dt) + return None + + def color_temp_kelvin(self, sun_position: float) -> int: + """Calculate the color temperature in Kelvin.""" + if sun_position > 0: + delta = self.max_color_temp - self.min_color_temp + ct = (delta * sun_position) + self.min_color_temp + return 5 * round(ct / 5) # round to nearest 5 + if sun_position == 0 or not self.adapt_until_sleep: + return self.min_color_temp + if self.adapt_until_sleep and sun_position < 0: + delta = abs(self.min_color_temp - self.sleep_color_temp) + ct = (delta * abs(1 + sun_position)) + self.sleep_color_temp + return 5 * round(ct / 5) # round to nearest 5 + msg = "Should not happen" + raise ValueError(msg) + + def brightness_and_color( + self, + dt: datetime.datetime, + is_sleep: bool, + ) -> dict[str, Any]: + """Calculate the brightness and color.""" + sun_position = self.sun.sun_position(dt) + rgb_color: tuple[int, int, int] + # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) + force_rgb_color = False + brightness_pct = self.brightness_pct(dt, is_sleep) + if is_sleep: + color_temp_kelvin = self.sleep_color_temp + rgb_color = self.sleep_rgb_color + elif ( + self.sleep_rgb_or_color_temp == "rgb_color" + and self.adapt_until_sleep + and sun_position < 0 + ): + # Feature requested in + # https://github.com/basnijholt/adaptive-lighting/issues/624 + # This will result in a perceptible jump in color at sunset and sunrise + # because the `color_temperature_to_rgb` function is not 100% accurate. + min_color_rgb = color_temperature_to_rgb(self.min_color_temp) + rgb_color = lerp_color_hsv( + min_color_rgb, + self.sleep_rgb_color, + sun_position, + ) + color_temp_kelvin = self.color_temp_kelvin(sun_position) + force_rgb_color = True + else: + color_temp_kelvin = self.color_temp_kelvin(sun_position) + r, g, b = color_temperature_to_rgb(color_temp_kelvin) + rgb_color = (round(r), round(g), round(b)) + # backwards compatibility for versions < 1.3.1 - see #403 + color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) + xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) + hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) + return { + "brightness_pct": brightness_pct, + "color_temp_kelvin": color_temp_kelvin, + "color_temp_mired": color_temp_mired, + "rgb_color": rgb_color, + "xy_color": xy_color, + "hs_color": hs_color, + "sun_position": sun_position, + "force_rgb_color": force_rgb_color, + } + + def get_settings( + self, + is_sleep: bool, + transition: float | None, + ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: + """Get all light settings. + + Calculating all values takes <0.5ms. + """ + dt = utcnow() + timedelta(seconds=transition or 0) + return self.brightness_and_color(dt, is_sleep) + + +def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: + """Compute the values of 'a' and 'b' for a scaled and shifted tanh function. + + Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b' + for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points. + + The derivation is as follows: + + 1. Start with the equation of the tanh function: + y = 0.5 * (tanh(a * (x - b)) + 1) + + 2. Rearrange the equation to isolate tanh: + tanh(a * (x - b)) = 2*y - 1 + + 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b': + a * (x - b) = artanh(2*y - 1) + + 4. Plug in the points (x1, y1) and (x2, y2) to get two equations. + Using these, we can solve for 'a' and 'b' as: + a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1) + b = x1 - (artanh(2*y1 - 1) / a) + + Parameters + ---------- + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). + y2 + y-coordinate of the second point (should be between 0 and 1). + + Returns + ------- + a + Coefficient 'a' for the tanh function. + b + Coefficient 'b' for the tanh function. + + Notes + ----- + The values of y1 and y2 should lie between 0 and 1, inclusive. + + """ + a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) + b = x1 - (math.atanh(2 * y1 - 1) / a) + return a, b + + +def scaled_tanh( + x: float, + x1: float, + x2: float, + y1: float = 0.05, + y2: float = 0.95, + y_min: float = 0.0, + y_max: float = 100.0, +) -> float: + """Apply a scaled and shifted tanh function to a given input. + + This function represents a transformation of the tanh function that scales and shifts + the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2' + (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min' + and 'y_max', respectively. + + The equation of the function is as follows: + y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1) + + Parameters + ---------- + x + The input to the function. + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). Defaults to 0.05. + y2 + y-coordinate of the second point (should be between 0 and 1). Defaults to 0.95. + y_min + The minimum value of the output range. Defaults to 0. + y_max + The maximum value of the output range. Defaults to 100. + + Returns + ------- + float: The output of the function, which lies in the range [y_min, y_max]. + + """ + a, b = find_a_b(x1, x2, y1, y2) + return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) + + +def lerp_color_hsv( + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], + t: float, +) -> tuple[int, int, int]: + """Linearly interpolate between two RGB colors in HSV color space.""" + t = abs(t) + assert 0 <= t <= 1 + + # Convert RGB to HSV + hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) + hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) + + # Linear interpolation in HSV space + hsv = ( + hsv1[0] + t * (hsv2[0] - hsv1[0]), + hsv1[1] + t * (hsv2[1] - hsv1[1]), + hsv1[2] + t * (hsv2[2] - hsv1[2]), + ) + + # Convert back to RGB + rgb = tuple(round(x * 255) for x in colorsys.hsv_to_rgb(*hsv)) + assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" + return cast("tuple[int, int, int]", rgb) + + +def lerp(x: float, x1: float, x2: float, y1: float, y2: float) -> float: + """Linearly interpolate between two values.""" + return y1 + (x - x1) * (y2 - y1) / (x2 - x1) + + +def clamp(value: float, minimum: float, maximum: float) -> float: + """Clamp value between minimum and maximum. + + `minimum` is not assumed to be <= `maximum`: a user may intentionally + configure `min_brightness > max_brightness` (or the equivalent for color + temperature) for an inverted timescale (#1421). Sort the bounds first so + that case clamps against the real lower/upper bound instead of + collapsing to `minimum` for every input. + """ + low, high = (minimum, maximum) if minimum <= maximum else (maximum, minimum) + return max(low, min(value, high)) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py new file mode 100644 index 00000000..3ffe5400 --- /dev/null +++ b/custom_components/adaptive_lighting/config_flow.py @@ -0,0 +1,202 @@ +"""Config flow for Adaptive Lighting integration.""" + +import logging +from typing import Any + +import voluptuous as vol +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig + +from .const import ( # pylint: disable=unused-import + BASIC_OPTIONS, + CONF_LIGHTS, + DOMAIN, + EXTRA_VALIDATION, + NONE_STR, + VALIDATION_TUPLES, +) +from .switch import validate + +_LOGGER = logging.getLogger(__name__) + +OPTIONS_FLOW_DESCRIPTION_PLACEHOLDERS = { + "webapp_url": "https://basnijholt.github.io/adaptive-lighting", + "docs_url": "https://github.com/basnijholt/adaptive-lighting#readme", +} +ADVANCED_OPTIONS_SECTION = "advanced" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Adaptive Lighting.""" + + VERSION = 1 + + source_options: dict[str, Any] | None = None + + async def async_step_user(self, user_input: dict[str, Any] | None = None): + """Handle the initial step.""" + if user_input is None and self._async_current_entries(): + return await self.async_step_menu() + return await self.async_step_wait_for_name(user_input) + + async def async_step_menu(self, user_input: dict[str, Any] | None = None): + """Handle the menu step.""" + if user_input is not None: + if user_input["action"] != "new": + entry_id = user_input["action"] + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry: + self.source_options = dict(entry.options) + return await self.async_step_wait_for_name() + + entries = self._async_current_entries() + options = {"new": "Create new instance"} + for entry in entries: + options[entry.entry_id] = f"Duplicate '{entry.title}'" + + return self.async_show_form( + step_id="menu", + data_schema=vol.Schema( + {vol.Required("action", default="new"): vol.In(options)}, + ), + ) + + async def async_step_wait_for_name(self, user_input: dict[str, Any] | None = None): + """Handle the name step.""" + errors: dict[str, str] = {} + + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_NAME]) + self._abort_if_unique_id_configured() + options = self.source_options + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + options=options, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_NAME): str}), + errors=errors, + ) + + async def async_step_import(self, user_input: dict[str, Any] | None = None): + """Handle configuration by YAML file.""" + if user_input is None: + return self.async_abort(reason="no_data") + + await self.async_set_unique_id(user_input[CONF_NAME]) + # Keep a list of switches that are configured via YAML + data = self.hass.data.setdefault(DOMAIN, {}) + data.setdefault("__yaml__", set()).add(self.unique_id) + + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry(entry, data=user_input) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, # noqa: ARG004 + ) -> "OptionsFlowHandler": + """Get the options flow for this handler.""" + return OptionsFlowHandler() + + +def validate_options(user_input: dict[str, Any], errors: dict[str, str]) -> None: + """Validate the options in the OptionsFlow. + + This is an extra validation step because the validators + in `EXTRA_VALIDATION` cannot be serialized to json. + """ + for key, (_validate, _) in EXTRA_VALIDATION.items(): + # these are unserializable validators + value = user_input.get(key) + try: + if value is not None and value != NONE_STR: + _validate(value) + except vol.Invalid: + _LOGGER.exception("Configuration option %s=%s is incorrect", key, value) + errors["base"] = "option_error" + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Adaptive Lighting.""" + + def _flatten_section_input(self, user_input: dict[str, Any]) -> dict[str, Any]: + """Flatten section input by merging nested 'advanced' dict into top level.""" + flat_input: dict[str, Any] = {} + for key, value in user_input.items(): + if key == ADVANCED_OPTIONS_SECTION and isinstance(value, dict): + flat_input.update(value) + else: + flat_input[key] = value + return flat_input + + async def async_step_init(self, user_input: dict[str, Any] | None = None): + """Handle options flow with collapsible sections.""" + conf = self.config_entry + data = validate(conf) + form_data = {**conf.data, **conf.options} + if conf.source == config_entries.SOURCE_IMPORT: + return self.async_show_form( + step_id="init", + data_schema=None, + description_placeholders=OPTIONS_FLOW_DESCRIPTION_PLACEHOLDERS, + ) + errors: dict[str, str] = {} + if user_input is not None: + flat_input = self._flatten_section_input(user_input) + validate_options(flat_input, errors) + if not errors: + return self.async_create_entry(title="", data=flat_input) + data.update(flat_input) + form_data.update(flat_input) + + # Validate that all configured lights still exist + all_lights = set(self.hass.states.async_entity_ids("light")) + for configured_light in data[CONF_LIGHTS]: + if configured_light not in all_lights: + errors[CONF_LIGHTS] = "entity_missing" + _LOGGER.error( + "%s: light entity %s is configured, but was not found", + data[CONF_NAME], + configured_light, + ) + + to_replace: dict[str, Any] = { + CONF_LIGHTS: EntitySelector( + EntitySelectorConfig( + domain="light", + multiple=True, + ), + ), + } + + basic_schema: dict[vol.Marker, Any] = {} + advanced_schema: dict[vol.Marker, Any] = {} + for name, default, validation in VALIDATION_TUPLES: + key = vol.Optional(name, default=form_data.get(name, default)) + schema = basic_schema if name in BASIC_OPTIONS else advanced_schema + schema[key] = to_replace.get(name, validation) + + full_schema = { + **basic_schema, + vol.Required(ADVANCED_OPTIONS_SECTION): data_entry_flow.section( + vol.Schema(advanced_schema), + data_entry_flow.SectionConfig(collapsed=True), + ), + } + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(full_schema), + errors=errors, + description_placeholders=OPTIONS_FLOW_DESCRIPTION_PLACEHOLDERS, + ) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py new file mode 100644 index 00000000..afead25c --- /dev/null +++ b/custom_components/adaptive_lighting/const.py @@ -0,0 +1,550 @@ +"""Constants for the Adaptive Lighting integration.""" + +from datetime import timedelta +from enum import Enum +from typing import Any + +import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from homeassistant.components.light import VALID_TRANSITION +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.helpers import selector + +ICON_MAIN = "mdi:theme-light-dark" +ICON_BRIGHTNESS = "mdi:brightness-4" +ICON_COLOR_TEMP = "mdi:sun-thermometer" +ICON_SLEEP = "mdi:sleep" + +DOMAIN = "adaptive_lighting" + + +class TakeOverControlMode(Enum): + """Modes for pausing adaptation when control of a light is taken over externally.""" + + PAUSE_ALL = "pause_all" + PAUSE_CHANGED = "pause_changed" + + +DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"} + + +CONF_NAME, DEFAULT_NAME = "name", "default" +DOCS[CONF_NAME] = "Display name for this switch. 📝" + +CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] +DOCS[CONF_LIGHTS] = "List of light entity_ids to be controlled (may be empty). 🌟" + +CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( + "detect_non_ha_changes", + False, +) +DOCS[CONF_DETECT_NON_HA_CHANGES] = ( + "Detects and halts adaptations for non-`light.turn_on` state changes. " + "Needs `take_over_control` enabled. 🕵️ " + "Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result " + "in lights turning on unexpectedly. " + "Note that this calls `homeassistant.update_entity` every `interval`! " + "Disable this feature if you encounter such issues." +) + +CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = ( + "include_config_in_attributes", + False, +) +DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = ( + "Show all options as attributes on the switch in " + "Home Assistant when set to `true`. 📝" +) + +CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +DOCS[CONF_INITIAL_TRANSITION] = ( + "Duration of the first transition when lights turn " + "from `off` to `on` in seconds. ⏲️" +) + +CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 +DOCS[CONF_SLEEP_TRANSITION] = ( + 'Duration of transition when "sleep mode" is toggled in seconds. 😴' +) + +CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 +DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄" + +CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 +DOCS[CONF_MAX_BRIGHTNESS] = "Maximum brightness percentage. 💡" + +CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 +DOCS[CONF_MAX_COLOR_TEMP] = "Coldest color temperature in Kelvin. ❄️" + +CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 +DOCS[CONF_MIN_BRIGHTNESS] = "Minimum brightness percentage. 💡" + +CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000 +DOCS[CONF_MIN_COLOR_TEMP] = "Warmest color temperature in Kelvin. 🔥" + +CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False +DOCS[CONF_ONLY_ONCE] = ( + "Adapt lights only when they are turned on (`true`) or keep adapting them " + "(`false`). 🔄" +) + +CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON = ( + "adapt_only_on_bare_turn_on", + False, +) +DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = ( + "When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is " + "invoked without specifying color or brightness. ❌🌈 " + "This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. " + "If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. " + "Needs `take_over_control` enabled. 🕵️" +) + +CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON = ( + "manual_control_on_external_turn_on", + False, +) +DOCS[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] = ( + "Treat turn-ons without a matching Home Assistant `light.turn_on` context as " + "manual control. Normal manual-control resets apply. Still allows " + "`detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️" +) + +CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False +DOCS[CONF_PREFER_RGB_COLOR] = ( + "Whether to prefer RGB color adjustment over " + "light color temperature when possible. 🌈" +) + +CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = ( + "separate_turn_on_commands", + False, +) +DOCS[CONF_SEPARATE_TURN_ON_COMMANDS] = ( + "Use separate `light.turn_on` calls for color and brightness, needed for " + "some light types. 🔀" +) + +CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 +DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness percentage of lights in sleep mode. 😴" + +CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 +DOCS[CONF_SLEEP_COLOR_TEMP] = ( + "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is " + "`color_temp`) in Kelvin. 😴" +) + +CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] +DOCS[CONF_SLEEP_RGB_COLOR] = ( + 'RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈' +) + +CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( + "sleep_rgb_or_color_temp", + "color_temp", +) +DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = ( + 'Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙' +) + +CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 +DOCS[CONF_SUNRISE_OFFSET] = ( + "Adjust sunrise time with a positive or negative offset in seconds. ⏰" +) + +CONF_SUNRISE_TIME = "sunrise_time" +DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅" + +CONF_MIN_SUNRISE_TIME = "min_sunrise_time" +DOCS[CONF_MIN_SUNRISE_TIME] = ( + "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅" +) + +CONF_MAX_SUNRISE_TIME = "max_sunrise_time" +DOCS[CONF_MAX_SUNRISE_TIME] = ( + "Set the latest virtual sunrise time (HH:MM:SS), allowing" + " for earlier sunrises. 🌅" +) + +CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 +DOCS[CONF_SUNSET_OFFSET] = ( + "Adjust sunset time with a positive or negative offset in seconds. ⏰" +) + +CONF_SUNSET_TIME = "sunset_time" +DOCS[CONF_SUNSET_TIME] = "Set a fixed time (HH:MM:SS) for sunset. 🌇" + +CONF_MIN_SUNSET_TIME = "min_sunset_time" +DOCS[CONF_MIN_SUNSET_TIME] = ( + "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇" +) + +CONF_MAX_SUNSET_TIME = "max_sunset_time" +DOCS[CONF_MAX_SUNSET_TIME] = ( + "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇" +) + +CONF_BRIGHTNESS_MODE, DEFAULT_BRIGHTNESS_MODE = "brightness_mode", "default" +DOCS[CONF_BRIGHTNESS_MODE] = ( + "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` " + "(uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈" +) +CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK = ( + "brightness_mode_time_dark", + 900, +) +DOCS[CONF_BRIGHTNESS_MODE_TIME_DARK] = ( + "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down " + "the brightness before/after sunrise/sunset. 📈📉" +) +CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT = ( + "brightness_mode_time_light", + 3600, +) +DOCS[CONF_BRIGHTNESS_MODE_TIME_LIGHT] = ( + "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down " + "the brightness after/before sunrise/sunset. 📈📉." +) + +CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True +DOCS[CONF_TAKE_OVER_CONTROL] = ( + "Pause adaptation of individual lights and hand over (manual) control to other sources that " + "issue `light.turn_on` calls for lights that are on. 🔒" +) + +CONF_TAKE_OVER_CONTROL_MODE, DEFAULT_TAKE_OVER_CONTROL_MODE = ( + "take_over_control_mode", + TakeOverControlMode.PAUSE_ALL.value, +) +DOCS[CONF_TAKE_OVER_CONTROL_MODE] = ( + "The adaptation pausing mode when other sources change brightness and/or color of lights. " + "`pause_all` always pauses both brightness and color adaptation. " + "`pause_changed` pauses the adaptation of only the changed attributes and continues adapting " + "unchanged attributes, e.g., continues color adaptation when only brightness was changed." +) + +CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 +DOCS[CONF_TRANSITION] = "Duration of transition when lights change, in seconds. 🕑" + +CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP = ( + "transition_until_sleep", + False, +) +DOCS[CONF_ADAPT_UNTIL_SLEEP] = ( + "When enabled, Adaptive Lighting will treat sleep settings as the minimum, " + "transitioning to these values after sunset. 🌙" +) + +CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 +DOCS[CONF_ADAPT_DELAY] = ( + "Wait time (seconds) between light turn on and Adaptive Lighting applying " + "changes. Might help to avoid flickering. ⏲️" +) + +CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 +DOCS[CONF_SEND_SPLIT_DELAY] = ( + "Delay (ms) between `separate_turn_on_commands` for lights that don't support " + "simultaneous brightness and color setting. ⏲️" +) + +CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL = "autoreset_control_seconds", 0 +DOCS[CONF_AUTORESET_CONTROL] = ( + "Automatically reset the manual control after a number of seconds. " + "Set to 0 to disable. ⏲️" +) + +( + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, +) = ("reset_manual_control_on_sleep_mode_change", True) +DOCS[CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE] = ( + "Reset manual control when the sleep mode switch is toggled. " + "Set to `false` to preserve manual control across sleep mode changes. 😴" +) + +CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = ( + "skip_redundant_commands", + False, +) +DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = ( + "Skip sending adaptation commands whose target state already " + "equals the light's known state. Minimizes network traffic and improves the " + "adaptation responsivity in some situations. 📉" + "Disable if physical light states get out of sync with HA's recorded state." +) + +CONF_INTERCEPT, DEFAULT_INTERCEPT = "intercept", True +DOCS[CONF_INTERCEPT] = ( + "Intercept and adapt `light.turn_on` calls to enabling instantaneous color " + "and brightness adaptation. 🏎️ Disable for lights that do not " + "support `light.turn_on` with color and brightness." +) + +CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = ( + "multi_light_intercept", + True, +) +DOCS[CONF_MULTI_LIGHT_INTERCEPT] = ( + "Intercept and adapt `light.turn_on` calls that target multiple lights. ➗" + "⚠️ This might result in splitting up a single `light.turn_on` call " + "into multiple calls, e.g., when lights are in different switches. " + "Requires `intercept` to be enabled." +) + +CONF_EXPAND_LIGHT_GROUPS, DEFAULT_EXPAND_LIGHT_GROUPS = "expand_light_groups", True +DOCS[CONF_EXPAND_LIGHT_GROUPS] = ( + "Expand light groups to their members (`true`, default). Set `false` to send " + "commands to the group and track manual control for the group. Explicit member " + "targets in services stay individual targets." +) + + +SLEEP_MODE_SWITCH = "sleep_mode_switch" +ADAPT_COLOR_SWITCH = "adapt_color_switch" +ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" +ATTR_ADAPTIVE_LIGHTING_MANAGER = "manager" +UNDO_UPDATE_LISTENER = "undo_update_listener" +NONE_STR = "None" +ATTR_ADAPT_COLOR = "adapt_color" +DOCS[ATTR_ADAPT_COLOR] = "Whether to adapt the color on supporting lights. 🌈" +ATTR_ADAPT_BRIGHTNESS = "adapt_brightness" +DOCS[ATTR_ADAPT_BRIGHTNESS] = "Whether to adapt the brightness of the light. 🌞" + +SERVICE_SET_MANUAL_CONTROL = "set_manual_control" +CONF_MANUAL_CONTROL = "manual_control" +DOCS[CONF_MANUAL_CONTROL] = "Whether to manually control the lights. 🔒" +SERVICE_APPLY = "apply" +CONF_TURN_ON_LIGHTS = "turn_on_lights" +DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off. 🔆" +SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings" +CONF_USE_DEFAULTS = "use_defaults" +DOCS[CONF_USE_DEFAULTS] = ( + "Sets the default values not specified in this service call. Options: " + '"current" (default, retains current values), "factory" (resets to ' + 'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' +) + +TURNING_OFF_DELAY = 5 + +DOCS_MANUAL_CONTROL = { + CONF_ENTITY_ID: "The `entity_id` of the switch in which to (un)mark the " + "light as being `manually controlled`. 📝", + CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the " + "switch are selected. 💡", + CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") all ' + 'adapted attributes of the light from the "manual_control" list, or the ' + "name of an attribute for selective addition. 🔒", +} + +DOCS_APPLY = { + CONF_ENTITY_ID: "The `entity_id` of the switch with the settings to apply. 📝", + CONF_LIGHTS: "A light (or list of lights) to apply the settings to. 💡", +} + +# Basic options shown at top level in options flow (not in collapsed section) +BASIC_OPTIONS: set[str] = { + CONF_LIGHTS, + CONF_MIN_BRIGHTNESS, + CONF_MAX_BRIGHTNESS, + CONF_MIN_COLOR_TEMP, + CONF_MAX_COLOR_TEMP, + CONF_SLEEP_BRIGHTNESS, + CONF_SLEEP_COLOR_TEMP, + CONF_TRANSITION, + CONF_INTERVAL, +} + + +def int_between(min_int: int, max_int: int) -> vol.All: + """Return an integer between 'min_int' and 'max_int'.""" + return vol.All(vol.Coerce(int), vol.Range(min=min_int, max=max_int)) + + +VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [ + (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), # type: ignore[arg-type] + (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), + (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), + (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), + (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), + (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), + (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), + (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), + (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), + (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), + ( + CONF_SLEEP_RGB_OR_COLOR_TEMP, + DEFAULT_SLEEP_RGB_OR_COLOR_TEMP, + selector.SelectSelector( # type: ignore[arg-type] + selector.SelectSelectorConfig( + options=["color_temp", "rgb_color"], + multiple=False, + mode=selector.SelectSelectorMode.DROPDOWN, + ), + ), + ), + (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), + ( + CONF_SLEEP_RGB_COLOR, + DEFAULT_SLEEP_RGB_COLOR, + selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), # type: ignore[arg-type] + ), + (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), + (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool), + (CONF_SUNRISE_TIME, NONE_STR, str), + (CONF_MIN_SUNRISE_TIME, NONE_STR, str), + (CONF_MAX_SUNRISE_TIME, NONE_STR, str), + (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), + (CONF_SUNSET_TIME, NONE_STR, str), + (CONF_MIN_SUNSET_TIME, NONE_STR, str), + (CONF_MAX_SUNSET_TIME, NONE_STR, str), + (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), + ( + CONF_BRIGHTNESS_MODE, + DEFAULT_BRIGHTNESS_MODE, + selector.SelectSelector( # type: ignore[arg-type] + selector.SelectSelectorConfig( + options=["default", "linear", "tanh"], + multiple=False, + mode=selector.SelectSelectorMode.DROPDOWN, + ), + ), + ), + (CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int), + (CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int), + (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), + ( + CONF_TAKE_OVER_CONTROL_MODE, + DEFAULT_TAKE_OVER_CONTROL_MODE, + selector.SelectSelector( # type: ignore[arg-type] + selector.SelectSelectorConfig( + options=[ + TakeOverControlMode.PAUSE_ALL.value, + TakeOverControlMode.PAUSE_CHANGED.value, + ], + multiple=False, + mode=selector.SelectSelectorMode.DROPDOWN, + ), + ), + ), + (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), + ( + CONF_AUTORESET_CONTROL, + DEFAULT_AUTORESET_CONTROL, + int_between(0, 365 * 24 * 60 * 60), # 1 year max + ), + (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), + (CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, bool), + ( + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + bool, + ), + ( + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + bool, + ), + (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), + (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), + (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float), + ( + CONF_SKIP_REDUNDANT_COMMANDS, + DEFAULT_SKIP_REDUNDANT_COMMANDS, + bool, + ), + (CONF_INTERCEPT, DEFAULT_INTERCEPT, bool), + (CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool), + (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), + (CONF_EXPAND_LIGHT_GROUPS, DEFAULT_EXPAND_LIGHT_GROUPS, bool), +] + + +def timedelta_as_int(value: timedelta) -> float: + """Convert a `datetime.timedelta` object to an integer. + + This integer can be serialized to json but a timedelta cannot. + """ + return value.total_seconds() + + +# conf_option: (validator, coerce) tuples +# these validators cannot be serialized but can be serialized when coerced by coerce. +EXTRA_VALIDATION: dict[str, tuple[Any, Any]] = { + CONF_INTERVAL: (cv.time_period, timedelta_as_int), + CONF_SUNRISE_OFFSET: (cv.time_period, timedelta_as_int), + CONF_SUNRISE_TIME: (cv.time, str), + CONF_MIN_SUNRISE_TIME: (cv.time, str), + CONF_MAX_SUNRISE_TIME: (cv.time, str), + CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int), + CONF_SUNSET_TIME: (cv.time, str), + CONF_MIN_SUNSET_TIME: (cv.time, str), + CONF_MAX_SUNSET_TIME: (cv.time, str), + CONF_BRIGHTNESS_MODE_TIME_LIGHT: (cv.time_period, timedelta_as_int), + CONF_BRIGHTNESS_MODE_TIME_DARK: (cv.time_period, timedelta_as_int), +} + + +def maybe_coerce(key: str, validation: Any) -> vol.All | Any: + """Coerce the validation into a json serializable type.""" + validation, coerce = EXTRA_VALIDATION.get(key, (validation, None)) + if coerce is not None: + return vol.All(validation, vol.Coerce(coerce)) + return validation + + +def replace_none_str(value: Any, replace_with: Any | None = None) -> Any: + """Replace "None" -> replace_with.""" + return value if value != NONE_STR else replace_with + + +_yaml_validation_tuples = [ + (key, default, maybe_coerce(key, validation)) + for key, default, validation in VALIDATION_TUPLES +] + [(CONF_NAME, DEFAULT_NAME, cv.string)] + +_DOMAIN_SCHEMA = vol.Schema( + { + vol.Optional(key, default=replace_none_str(default, vol.UNDEFINED)): validation + for key, default, validation in _yaml_validation_tuples + }, +) + + +def apply_service_schema() -> vol.Schema: + """Return the schema for the apply service.""" + return vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type] + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type] + vol.Optional(CONF_TRANSITION): VALID_TRANSITION, + vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, + vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, + vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, + vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, + }, + ) + + +def change_switch_settings_schema() -> dict[vol.Marker, Any]: + """Return the schema for the change_switch_settings service.""" + args: dict[vol.Marker, Any] = { + vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string, + } + # Modifying these after init isn't possible + skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS) + for k, _, valid in VALIDATION_TUPLES: + if k not in skip: + args[vol.Optional(k)] = valid + return args + + +SET_MANUAL_CONTROL_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type] + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type] + vol.Optional(CONF_MANUAL_CONTROL, default=True): vol.Any( + cv.boolean, + vol.In(["brightness", "color"]), + ), + }, +) diff --git a/custom_components/adaptive_lighting/diagnostics.py b/custom_components/adaptive_lighting/diagnostics.py new file mode 100644 index 00000000..80ae56d0 --- /dev/null +++ b/custom_components/adaptive_lighting/diagnostics.py @@ -0,0 +1,131 @@ +"""Diagnostics support for Adaptive Lighting.""" + +from typing import Any + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .adaptation_utils import LightControlAttributes +from .const import ( + ADAPT_BRIGHTNESS_SWITCH, + ADAPT_COLOR_SWITCH, + ATTR_ADAPTIVE_LIGHTING_MANAGER, + DOMAIN, + SLEEP_MODE_SWITCH, +) +from .switch import AdaptiveLightingManager, AdaptiveSwitch + +_REPORTABLE_LIGHT_STATES = { + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +} +_TARGET_ATTRIBUTES = ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, +) + + +def _last_adaptation_values( + manager: AdaptiveLightingManager, + light: str, +) -> dict[str, Any] | None: + """Return latest retained value for each allowlisted adaptation attribute. + + Values may come from different commands because the manager merges partial + service data per attribute. + """ + service_data = manager.last_service_data.get(light) + if service_data is None: + return None + target = { + attribute: ( + list(service_data[attribute]) + if attribute == ATTR_RGB_COLOR + else service_data[attribute] + ) + for attribute in _TARGET_ATTRIBUTES + if attribute in service_data + } + return target or None + + +def _autoreset_seconds( + manager: AdaptiveLightingManager, + light: str, +) -> float | None: + """Return remaining time for a running global manual-control reset.""" + timer = manager.auto_reset_manual_control_timers.get(light) + if timer is None or not timer.is_running(): + return None + remaining = timer.remaining_time() + return round(remaining, 3) if remaining > 0 else None + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, Any]: + """Return an allowlisted, on-demand snapshot for one config entry.""" + domain_data = hass.data.get(DOMAIN) + if not isinstance(domain_data, dict): + return {"loaded": False} + entry_data = domain_data.get(config_entry.entry_id) + manager = domain_data.get(ATTR_ADAPTIVE_LIGHTING_MANAGER) + if not isinstance(entry_data, dict) or not isinstance( + manager, + AdaptiveLightingManager, + ): + return {"loaded": False} + switch = entry_data.get(SWITCH_DOMAIN) + if not isinstance(switch, AdaptiveSwitch): + return {"loaded": False} + + lights: dict[str, Any] = {} + for index, light in enumerate(sorted(switch.lights), start=1): + state = hass.states.get(light) + state_value = "missing" + if state is not None: + state_value = ( + state.state + if state.state in _REPORTABLE_LIGHT_STATES + else STATE_UNKNOWN + ) + manual_control = manager.get_manual_control_attributes(light) + lights[f"light_{index}"] = { + "state": state_value, + "global_manager_manual_control": { + "brightness": bool( + manual_control & LightControlAttributes.BRIGHTNESS, + ), + "color": bool(manual_control & LightControlAttributes.COLOR), + }, + "global_manager_autoreset_seconds": _autoreset_seconds(manager, light), + "global_manager_last_adaptation_values": _last_adaptation_values( + manager, + light, + ), + } + + return { + "loaded": True, + "profile_switches": { + "profile": switch.is_on, + "adapt_brightness": entry_data[ADAPT_BRIGHTNESS_SWITCH].is_on, + "adapt_color": entry_data[ADAPT_COLOR_SWITCH].is_on, + "sleep_mode": entry_data[SLEEP_MODE_SWITCH].is_on, + }, + "manager_fact_scope": "global_shared_across_profiles", + "lights": lights, + } diff --git a/custom_components/adaptive_lighting/docs_gen.py b/custom_components/adaptive_lighting/docs_gen.py new file mode 100644 index 00000000..9a10a68b --- /dev/null +++ b/custom_components/adaptive_lighting/docs_gen.py @@ -0,0 +1,35 @@ +"""Documentation generation utilities for Adaptive Lighting. + +Provides functions to transform content for the documentation site. +Used by markdown-code-runner to generate documentation pages from README content. +""" + +from __future__ import annotations + +import re + + +def _transform_readme_links(content: str) -> str: + """Transform README internal links to docs site links.""" + # Map README anchors to doc pages + link_map = { + "#gear-configuration": "configuration.md", + "#memo-options": "configuration.md#all-options", + "#hammer_and_wrench-services": "services.md", + "#adaptive_lightingapply": "services.md#adaptive_lightingapply", + "#adaptive_lightingset_manual_control": "services.md#adaptive_lightingset_manual_control", + "#adaptive_lightingchange_switch_settings": "services.md#adaptive_lightingchange_switch_settings", + "#robot-automation-examples": "automation-examples.md", + "#sos-troubleshooting": "troubleshooting.md", + "#exclamation-common-problems--solutions": "troubleshooting.md#common-problems-solutions", + "#bar_chart-graphs": "advanced/brightness-modes.md#graphs", + "#bulb-features": "index.md#features", + "#control_knobs-regain-manual-control": "advanced/manual-control.md", + "#eyes-see-also": "see-also.md", + } + + for old_link, new_link in link_map.items(): + content = content.replace(f"]({old_link})", f"]({new_link})") + + # Remove ToC link pattern [[ToC](#...)] + return re.sub(r"\[\[ToC\]\([^)]+\)\]", "", content) diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py new file mode 100644 index 00000000..822e2e29 --- /dev/null +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -0,0 +1,107 @@ +"""Utility functions for HA core.""" + +import logging +from collections.abc import Awaitable, Callable + +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers.target import async_extract_referenced_entity_ids +from homeassistant.util.read_only_dict import ReadOnlyDict + +try: + from homeassistant.helpers.target import TargetSelection +except ImportError: # Compatibility with older Home Assistant releases + from homeassistant.helpers.target import TargetSelectorData as TargetSelection + +from .adaptation_utils import ServiceData + +_LOGGER = logging.getLogger(__name__) + + +def target_entities( + hass: HomeAssistant, + service_data: ServiceData, +) -> set[str]: + """Resolve all directly and indirectly targeted entities without groups.""" + selected = async_extract_referenced_entity_ids( + hass, + TargetSelection(service_data), + expand_group=False, + ) + return selected.referenced | selected.indirectly_referenced + + +def setup_service_call_interceptor( + hass: HomeAssistant, + domain: str, + service: str, + intercept_func: Callable[[ServiceCall, ServiceData], Awaitable[None] | None], +) -> Callable[[], None]: + """Inject a function into a registered service call to preprocess service data. + + The injected interceptor function receives the service call and a writeable data dictionary + (the data of the service call is read-only) before the service call is executed. + """ + try: + # HACK: Access protected attribute of HA service registry. + # This is necessary to replace a registered service handler with our + # proxy handler to intercept calls. + registered_services = ( + hass.services._services # pylint: disable=protected-access # type: ignore[attr-defined] + ) + except AttributeError as error: + msg = ( + "Intercept failed because registered services are no longer" + " accessible (internal API may have changed)" + ) + raise RuntimeError(msg) from error + + if domain not in registered_services or service not in registered_services[domain]: + msg = f"Intercept failed because service {domain}.{service} is not registered" + raise RuntimeError(msg) + + existing_service = registered_services[domain][service] + + async def service_func_proxy(call: ServiceCall) -> None: + try: + # Convert read-only data to writeable dictionary for modification by interceptor + data = dict(call.data) + + # Call interceptor + result = intercept_func(call, data) + if result is not None: + await result + + # Convert data back to read-only + call.data = ReadOnlyDict(data) + except Exception: + # Blindly catch all exceptions to avoid breaking light.turn_on + _LOGGER.exception( + "Error for call '%s' in service_func_proxy", + call.data, + ) + # Call original service handler with processed data + import asyncio + + target = existing_service.job.target + if asyncio.iscoroutinefunction(target): + await target(call) + else: + target(call) + + hass.services.async_register( + domain, + service, + service_func_proxy, + existing_service.schema, + ) + + def remove() -> None: + # Remove the interceptor by reinstalling the original service handler + hass.services.async_register( + domain, + service, + existing_service.job.target, + existing_service.schema, + ) + + return remove diff --git a/custom_components/adaptive_lighting/helpers.py b/custom_components/adaptive_lighting/helpers.py new file mode 100644 index 00000000..df2abb95 --- /dev/null +++ b/custom_components/adaptive_lighting/helpers.py @@ -0,0 +1,98 @@ +"""Helper functions for the Adaptive Lighting custom components.""" + +from __future__ import annotations + +import base64 +import math +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + +def clamp(value: float, minimum: float, maximum: float) -> float: + """Clamp value between minimum and maximum.""" + return max(minimum, min(value, maximum)) + + +def int_to_base36(num: int) -> str: + """Convert an integer to its base-36 representation using numbers and uppercase letters. + + Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive + alphanumeric representation. The function takes an integer `num` as input and returns + its base-36 representation as a string. + + Parameters + ---------- + num + The integer to convert to base-36. + + Returns + ------- + str + The base-36 representation of the input integer. + + Examples + -------- + >>> num = 123456 + >>> base36_num = int_to_base36(num) + >>> print(base36_num) + '2N9' + + """ + alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + if num == 0: + return alphanumeric_chars[0] + + base36_str = "" + base = len(alphanumeric_chars) + + while num: + num, remainder = divmod(num, base) + base36_str = alphanumeric_chars[remainder] + base36_str + + return base36_str + + +def short_hash(string: str, length: int = 4) -> str: + """Create a hash of 'string' with length 'length'.""" + return base64.b32encode(string.encode()).decode("utf-8").zfill(length)[:length] + + +def remove_vowels(input_str: str, length: int = 4) -> str: + """Remove vowels from a string and return a string of length 'length'.""" + vowels = "aeiouAEIOU" + output_str = "".join([char for char in input_str if char not in vowels]) + return output_str.zfill(length)[:length] + + +def color_difference_redmean( + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], +) -> float: + """Distance between colors in RGB space (redmean metric). + + The maximal distance between (255, 255, 255) and (0, 0, 0) ≈ 765. + + Sources: + - https://en.wikipedia.org/wiki/Color_difference#Euclidean + - https://www.compuphase.com/cmetric.htm + """ + r_hat = (rgb1[0] + rgb2[0]) / 2 + delta_r, delta_g, delta_b = ( + (col1 - col2) for col1, col2 in zip(rgb1, rgb2, strict=True) + ) + red_term = (2 + r_hat / 256) * delta_r**2 + green_term = 4 * delta_g**2 + blue_term = (2 + (255 - r_hat) / 256) * delta_b**2 + return math.sqrt(red_term + green_term + blue_term) + + +def get_friendly_name(hass: HomeAssistant, entity_id: str) -> str: + """Retrieve the friendly name of an entity.""" + state = hass.states.get(entity_id) + if state and hasattr(state, "attributes"): + attributes: dict[str, Any] = dict(getattr(state, "attributes", {})) + return attributes.get("friendly_name", entity_id) + return entity_id diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json new file mode 100644 index 00000000..db89265b --- /dev/null +++ b/custom_components/adaptive_lighting/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "adaptive_lighting", + "name": "Adaptive Lighting", + "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1", "@protyposis"], + "config_flow": true, + "dependencies": ["light"], + "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", + "iot_class": "calculated", + "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", + "requirements": ["ulid-transform"], + "version": "1.32.0" +} diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml new file mode 100644 index 00000000..c95e31b5 --- /dev/null +++ b/custom_components/adaptive_lighting/services.yaml @@ -0,0 +1,270 @@ +# This file is auto-generated by .github/update-services.py. +apply: + description: Applies the current Adaptive Lighting settings to lights. + fields: + entity_id: + description: The `entity_id` of the switch with the settings to apply. 📝 + selector: + entity: + integration: adaptive_lighting + domain: switch + multiple: false + lights: + description: A light (or list of lights) to apply the settings to. 💡 + selector: + entity: + domain: light + multiple: true + transition: + description: Duration of transition when lights change, in seconds. 🕑 + example: 10 + selector: + text: null + adapt_brightness: + description: Whether to adapt the brightness of the light. 🌞 + example: true + selector: + boolean: null + adapt_color: + description: Whether to adapt the color on supporting lights. 🌈 + example: true + selector: + boolean: null + prefer_rgb_color: + description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 + example: false + selector: + boolean: null + turn_on_lights: + description: Whether to turn on lights that are currently off. 🔆 + example: false + selector: + boolean: null +set_manual_control: + description: Mark whether a light is 'manually controlled'. + fields: + entity_id: + description: The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 + selector: + entity: + integration: adaptive_lighting + domain: switch + multiple: false + lights: + description: entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 + selector: + entity: + domain: light + multiple: true + manual_control: + description: Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 + example: true + default: true + selector: + boolean: null +change_switch_settings: + description: Change any settings you'd like in the switch. All options here are the same as in the config flow. + target: + entity: + integration: adaptive_lighting + domain: switch + fields: + use_defaults: + description: 'Sets the default values not specified in this service call. Options: "current" (default, retains current values), "factory" (resets to documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' + example: current + required: false + default: current + selector: + select: + options: + - current + - configuration + - factory + include_config_in_attributes: + description: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 + required: false + selector: + boolean: null + turn_on_lights: + description: Whether to turn on lights that are currently off. 🔆 + example: false + required: false + selector: + boolean: null + initial_transition: + description: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ + example: 1 + required: false + selector: + text: null + sleep_transition: + description: Duration of transition when "sleep mode" is toggled in seconds. 😴 + example: 1 + required: false + selector: + text: null + max_brightness: + description: Maximum brightness percentage. 💡 + required: false + example: 100 + selector: + text: null + max_color_temp: + description: Coldest color temperature in Kelvin. ❄️ + required: false + example: 5500 + selector: + text: null + min_brightness: + description: Minimum brightness percentage. 💡 + required: false + example: 1 + selector: + text: null + min_color_temp: + description: Warmest color temperature in Kelvin. 🔥 + required: false + example: 2000 + selector: + text: null + only_once: + description: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 + example: false + required: false + selector: + boolean: null + prefer_rgb_color: + description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 + required: false + example: false + selector: + boolean: null + expand_light_groups: + description: Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets. + required: false + example: true + selector: + boolean: null + separate_turn_on_commands: + description: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 + required: false + example: false + selector: + boolean: null + send_split_delay: + description: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ + required: false + example: 0 + selector: + boolean: null + sleep_brightness: + description: Brightness percentage of lights in sleep mode. 😴 + required: false + example: 1 + selector: + text: null + sleep_rgb_or_color_temp: + description: Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 + required: false + example: color_temp + selector: + select: + options: + - rgb_color + - color_temp + sleep_rgb_color: + description: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 + required: false + selector: + color_rgb: null + sleep_color_temp: + description: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 + required: false + example: 1000 + selector: + text: null + sunrise_offset: + description: Adjust sunrise time with a positive or negative offset in seconds. ⏰ + required: false + example: 0 + selector: + number: + min: -86400 + max: 86300 + sunrise_time: + description: Set a fixed time (HH:MM:SS) for sunrise. 🌅 + required: false + example: '' + selector: + time: null + sunset_offset: + description: Adjust sunset time with a positive or negative offset in seconds. ⏰ + required: false + example: '' + selector: + number: + min: -86400 + max: 86300 + sunset_time: + description: Set a fixed time (HH:MM:SS) for sunset. 🌇 + example: '' + required: false + selector: + time: null + max_sunrise_time: + description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 + example: '' + required: false + selector: + time: null + min_sunset_time: + description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 + example: '' + required: false + selector: + time: null + take_over_control: + description: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 + required: false + example: true + selector: + boolean: null + take_over_control_mode: + description: The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. + required: false + example: pause_changed + selector: + select: + options: + - pause_all + - pause_changed + detect_non_ha_changes: + description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.' + required: false + example: false + selector: + boolean: null + manual_control_on_external_turn_on: + description: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ + required: false + example: false + selector: + boolean: null + transition: + description: Duration of transition when lights change, in seconds. 🕑 + required: false + example: 45 + selector: + text: null + adapt_delay: + description: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ + required: false + example: 0 + selector: + text: null + autoreset_control_seconds: + description: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ + required: false + example: 0 + selector: + text: null diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json new file mode 100644 index 00000000..9e22ff57 --- /dev/null +++ b/custom_components/adaptive_lighting/strings.json @@ -0,0 +1,298 @@ +{ + "config": { + "step": { + "user": { + "title": "Choose a name for the Adaptive Lighting instance", + "description": "Every instance can contain multiple lights!", + "data": { + "name": "Name" + } + }, + "menu": { + "title": "Create or Duplicate", + "description": "Do you want to create a new instance or duplicate an existing one?", + "data": { + "action": "Action" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptive Lighting options", + "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app]({webapp_url}). For further details, see the [official documentation]({docs_url}).", + "data": { + "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", + "interval": "interval", + "transition": "transition", + "min_brightness": "min_brightness: Minimum brightness percentage. 💡", + "max_brightness": "max_brightness: Maximum brightness percentage. 💡", + "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", + "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", + "sleep_brightness": "sleep_brightness", + "sleep_color_temp": "sleep_color_temp" + }, + "data_description": { + "interval": "Frequency to adapt the lights, in seconds. 🔄", + "transition": "Duration of transition when lights change, in seconds. 🕑", + "sleep_brightness": "Brightness percentage of lights in sleep mode. 😴", + "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴" + }, + "sections": { + "advanced": { + "name": "Advanced settings", + "description": "Additional settings for fine-tuning Adaptive Lighting.", + "data": { + "initial_transition": "initial_transition", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", + "sleep_rgb_color": "sleep_rgb_color", + "sleep_transition": "sleep_transition", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", + "sunrise_time": "sunrise_time", + "min_sunrise_time": "min_sunrise_time", + "max_sunrise_time": "max_sunrise_time", + "sunrise_offset": "sunrise_offset", + "sunset_time": "sunset_time", + "min_sunset_time": "min_sunset_time", + "max_sunset_time": "max_sunset_time", + "sunset_offset": "sunset_offset", + "brightness_mode": "brightness_mode", + "brightness_mode_time_dark": "brightness_mode_time_dark", + "brightness_mode_time_light": "brightness_mode_time_light", + "take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", + "take_over_control_mode": "take_over_control_mode", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", + "autoreset_control_seconds": "autoreset_control_seconds", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "manual_control_on_external_turn_on": "manual_control_on_external_turn_on: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", + "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", + "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "send_split_delay": "send_split_delay", + "adapt_delay": "adapt_delay", + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", + "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "expand_light_groups": "expand_light_groups: Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets." + }, + "data_description": { + "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", + "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", + "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", + "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", + "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Invalid option", + "entity_missing": "One or more selected light entities are missing from Home Assistant" + } + }, + "services": { + "apply": { + "name": "apply", + "description": "Applies the current Adaptive Lighting settings to lights.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch with the settings to apply. 📝", + "name": "entity_id" + }, + "lights": { + "description": "A light (or list of lights) to apply the settings to. 💡", + "name": "lights" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "Whether to adapt the brightness of the light. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Whether to adapt the color on supporting lights. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "set_manual_control", + "description": "Mark whether a light is 'manually controlled'.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡", + "name": "lights" + }, + "manual_control": { + "description": "Whether to add (\"true\") or remove (\"false\") all adapted attributes of the light from the \"manual_control\" list, or the name of an attribute for selective addition. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "change_switch_settings", + "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.", + "fields": { + "use_defaults": { + "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Maximum brightness percentage. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "Coldest color temperature in Kelvin. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "Minimum brightness percentage. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "Warmest color temperature in Kelvin. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "expand_light_groups": { + "description": "Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets.", + "name": "expand_light_groups" + }, + "separate_turn_on_commands": { + "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "Brightness percentage of lights in sleep mode. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", + "name": "take_over_control" + }, + "take_over_control_mode": { + "description": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", + "name": "take_over_control_mode" + }, + "detect_non_ha_changes": { + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", + "name": "detect_non_ha_changes" + }, + "manual_control_on_external_turn_on": { + "description": "Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", + "name": "manual_control_on_external_turn_on" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "name": "autoreset_control_seconds" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py new file mode 100644 index 00000000..db1c2d0b --- /dev/null +++ b/custom_components/adaptive_lighting/switch.py @@ -0,0 +1,3370 @@ +"""Switch for the Adaptive Lighting integration.""" + +from __future__ import annotations + +import asyncio +import datetime +import hashlib +import logging +import zoneinfo +from copy import deepcopy +from datetime import timedelta +from typing import TYPE_CHECKING, Any + +import homeassistant.util.dt as dt_util +import ulid_transform +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + ATTR_TRANSITION, + ATTR_XY_COLOR, + VALID_TRANSITION, + ColorMode, + LightEntityFeature, + is_on, + preprocess_turn_on_alternatives, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_DEVICE_ID, + ATTR_DOMAIN, + ATTR_ENTITY_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, + ATTR_SERVICE, + ATTR_SERVICE_DATA, + ATTR_SUPPORTED_FEATURES, + CONF_NAME, + CONF_PARAMS, + EVENT_CALL_SERVICE, + EVENT_HOMEASSISTANT_STARTED, + EVENT_STATE_CHANGED, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import ( + CALLBACK_TYPE, + Context, + Event, + HomeAssistant, + ServiceCall, + State, + callback, +) +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_platform, entity_registry, service +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.helpers.event import ( + EventStateChangedData, + async_call_later, + async_track_state_change_event, + async_track_time_interval, +) +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import slugify +from homeassistant.util.color import ( + color_temperature_to_rgb, + color_xy_to_RGB, +) + +from .adaptation_utils import ( + AdaptationData, + LightControlAttributes, + ServiceData, + get_light_control_attributes, + has_effect_attribute, + manual_control_event_attribute_to_flags, + prepare_adaptation_data, +) +from .color_and_brightness import SunLightSettings +from .const import ( + ADAPT_BRIGHTNESS_SWITCH, + ADAPT_COLOR_SWITCH, + ATTR_ADAPT_BRIGHTNESS, + ATTR_ADAPT_COLOR, + ATTR_ADAPTIVE_LIGHTING_MANAGER, + CONF_ADAPT_DELAY, + CONF_ADAPT_ONLY_ON_BARE_TURN_ON, + CONF_ADAPT_UNTIL_SLEEP, + CONF_AUTORESET_CONTROL, + CONF_BRIGHTNESS_MODE, + CONF_BRIGHTNESS_MODE_TIME_DARK, + CONF_BRIGHTNESS_MODE_TIME_LIGHT, + CONF_DETECT_NON_HA_CHANGES, + CONF_EXPAND_LIGHT_GROUPS, + CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, + CONF_INITIAL_TRANSITION, + CONF_INTERCEPT, + CONF_INTERVAL, + CONF_LIGHTS, + CONF_MANUAL_CONTROL, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + CONF_MAX_BRIGHTNESS, + CONF_MAX_COLOR_TEMP, + CONF_MAX_SUNRISE_TIME, + CONF_MAX_SUNSET_TIME, + CONF_MIN_BRIGHTNESS, + CONF_MIN_COLOR_TEMP, + CONF_MIN_SUNRISE_TIME, + CONF_MIN_SUNSET_TIME, + CONF_MULTI_LIGHT_INTERCEPT, + CONF_ONLY_ONCE, + CONF_PREFER_RGB_COLOR, + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + CONF_SEND_SPLIT_DELAY, + CONF_SEPARATE_TURN_ON_COMMANDS, + CONF_SKIP_REDUNDANT_COMMANDS, + CONF_SLEEP_BRIGHTNESS, + CONF_SLEEP_COLOR_TEMP, + CONF_SLEEP_RGB_COLOR, + CONF_SLEEP_RGB_OR_COLOR_TEMP, + CONF_SLEEP_TRANSITION, + CONF_SUNRISE_OFFSET, + CONF_SUNRISE_TIME, + CONF_SUNSET_OFFSET, + CONF_SUNSET_TIME, + CONF_TAKE_OVER_CONTROL, + CONF_TAKE_OVER_CONTROL_MODE, + CONF_TRANSITION, + CONF_TURN_ON_LIGHTS, + CONF_USE_DEFAULTS, + DOMAIN, + EXTRA_VALIDATION, + ICON_BRIGHTNESS, + ICON_COLOR_TEMP, + ICON_MAIN, + ICON_SLEEP, + SERVICE_CHANGE_SWITCH_SETTINGS, + SLEEP_MODE_SWITCH, + TURNING_OFF_DELAY, + VALIDATION_TUPLES, + TakeOverControlMode, + change_switch_settings_schema, + replace_none_str, +) +from .hass_utils import setup_service_call_interceptor, target_entities +from .helpers import ( + clamp, + color_difference_redmean, + int_to_base36, + remove_vowels, + short_hash, +) + +if TYPE_CHECKING: + from collections.abc import Callable, Coroutine, Iterable + + from homeassistant.config_entries import ConfigEntry + from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.typing import NoEventData + +try: + from homeassistant.helpers.sun import get_astral_observer +except ImportError: # `get_astral_observer` was added in HA 2026.7 + from astral import Observer + + def get_astral_observer(hass: HomeAssistant) -> Observer: + """Get an astral observer for the current HA configuration.""" + return Observer( + hass.config.latitude, + hass.config.longitude, + hass.config.elevation, + ) + + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=10) + +# Consider it a significant change when attribute changes more than +BRIGHTNESS_CHANGE = 25 # ≈10% of total range +COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500) +RGB_REDMEAN_CHANGE = 80 # ≈10% of total range + + +# Keep a short domain version for the context instances (which can only be 36 chars) +_DOMAIN_SHORT = "al" + + +def create_context( + name: str, + which: str, + index: int, + parent: Context | None = None, +) -> Context: + """Create a context that can identify this integration.""" + # Use a hash for the name because otherwise the context might become + # too long (max len == 26) to fit in the database. + # Pack index with base85 to maximize the number of contexts we can create + # before we exceed the 26-character limit and are forced to wrap. + time_stamp = ulid_transform.ulid_now()[:10] # time part of a ULID + name_hash = short_hash(name) + which_short = remove_vowels(which) + context_id_start = f"{time_stamp}:{_DOMAIN_SHORT}:{name_hash}:{which_short}:" + chars_left = 26 - len(context_id_start) + index_packed = int_to_base36(index).zfill(chars_left)[-chars_left:] + context_id = context_id_start + index_packed + parent_id = parent.id if parent else None + return Context(id=context_id, parent_id=parent_id) + + +def is_our_context_id(context_id: str | None, which: str | None = None) -> bool: + """Check whether this integration created 'context_id'.""" + if context_id is None: + return False + + is_al = f":{_DOMAIN_SHORT}:" in context_id + if not is_al: + return False + if which is None: + return True + return f":{remove_vowels(which)}:" in context_id + + +def is_our_context(context: Context | None, which: str | None = None) -> bool: + """Check whether this integration created 'context'.""" + if context is None: + return False + return is_our_context_id(context.id, which) + + +def _switches_with_lights( + hass: HomeAssistant, + lights: list[str], + expand_light_groups: bool = True, +) -> AdaptiveSwitches: + """Get all switches that control at least one of the lights passed.""" + config_entries = hass.config_entries.async_entries(DOMAIN) + data = hass.data.get(DOMAIN, {}) + loaded_switches: AdaptiveSwitches = [] + for config in config_entries: + entry = data.get(config.entry_id) + if not isinstance(entry, dict) or SWITCH_DOMAIN not in entry: + continue + loaded_switches.append(entry[SWITCH_DOMAIN]) + + if not loaded_switches: + return [] + + switches: AdaptiveSwitches = [] + for switch in loaded_switches: + switch._expand_light_groups() + check_lights = switch._resolve_lights(lights) if expand_light_groups else lights + if set(switch.lights) & set(check_lights): + switches.append(switch) + return switches + + +class NoSwitchFoundError(ValueError): + """No switches found for lights.""" + + +def _switch_with_lights( + hass: HomeAssistant, + lights: list[str], + expand_light_groups: bool = True, +) -> AdaptiveSwitch: + """Find the switch that controls the lights in 'lights'.""" + switches = _switches_with_lights(hass, lights, expand_light_groups) + if len(switches) == 1: + return switches[0] + if len(switches) > 1: + on_switches = [s for s in switches if s.is_on] + if len(on_switches) == 1: + # Of the multiple switches, only one is on + return on_switches[0] + msg = ( + f"_switch_with_lights: Light(s) {lights} found in multiple switch configs" + f" ({[s.entity_id for s in switches]}). You must pass a switch under" + " 'entity_id'." + ) + raise NoSwitchFoundError(msg) + msg = ( + f"_switch_with_lights: Light(s) {lights} not found in any switch's" + " configuration. You must either include the light(s) that is/are" + " in the integration config, or pass a switch under 'entity_id'." + ) + raise NoSwitchFoundError(msg) + + +# For documentation on this function, see integration_entities() from HomeAssistant Core: +# https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109 +def _switches_from_service_call( + hass: HomeAssistant, + service_call: ServiceCall, +) -> AdaptiveSwitches: + data = service_call.data + lights = data.get(CONF_LIGHTS) + switch_entity_ids: list[str] | None = data.get("entity_id") + + if not lights and not switch_entity_ids: + msg = ( + "adaptive-lighting: Neither a switch nor a light was provided in the service call." + " If you intend to adapt all lights on all switches, please inform the" + " developers at https://github.com/basnijholt/adaptive-lighting about your" + " use case. Currently, you must pass either an adaptive-lighting switch or" + " the lights to an `adaptive_lighting` service call." + ) + raise ServiceValidationError(msg) + + domain_data = hass.data.get(DOMAIN) + if not domain_data: + msg = "adaptive-lighting: No Adaptive Lighting config entries are loaded." + raise ServiceValidationError(msg) + + if switch_entity_ids is not None: + if len(switch_entity_ids) > 1 and lights: + msg = ( + "adaptive-lighting: Cannot pass multiple switches with lights argument." + f" Invalid service data received: {service_call.data}" + ) + raise ServiceValidationError(msg) + switches: AdaptiveSwitches = [] + config_ids: set[str] = set() + ent_reg = entity_registry.async_get(hass) + for entity_id in switch_entity_ids: + ent_entry = ent_reg.async_get(entity_id) + if ent_entry is None: + msg = f"adaptive-lighting: Entity '{entity_id}' not found in registry." + raise ServiceValidationError(msg) + if ent_entry.platform != DOMAIN: + msg = ( + f"adaptive-lighting: Entity '{entity_id}' is not registered by" + " Adaptive Lighting." + ) + raise ServiceValidationError(msg) + config_id = ent_entry.config_entry_id + config_data = domain_data.get(config_id) if config_id else None + if ( + config_id is None + or not isinstance(config_data, dict) + or SWITCH_DOMAIN not in config_data + ): + msg = ( + f"adaptive-lighting: Adaptive Lighting entry for entity '{entity_id}'" + " is not loaded." + ) + raise ServiceValidationError(msg) + if config_id not in config_ids: + switches.append(config_data[SWITCH_DOMAIN]) + config_ids.add(config_id) + return switches + + if lights: + try: + switch = _switch_with_lights(hass, lights) + except NoSwitchFoundError as err: + raise ServiceValidationError(str(err)) from err + return [switch] + + msg = ( + "adaptive-lighting: Incorrect data provided in service call." + f" Entities not found in the integration. Service data: {service_call.data}" + ) + raise ServiceValidationError(msg) + + +async def handle_change_switch_settings( + switch: AdaptiveSwitch | SimpleSwitch, + service_call: ServiceCall, +) -> None: + """Allows HASS to change config values via a service call.""" + if not isinstance(switch, AdaptiveSwitch): + return + data = service_call.data + which = data.get(CONF_USE_DEFAULTS, "current") + if which == "current": # use whatever we're already using. + defaults = switch._current_settings # pylint: disable=protected-access + elif which == "factory": # use actual defaults listed in the documentation + defaults = None + elif which == "configuration": + # use whatever's in the config flow or configuration.yaml + defaults = switch._config_backup + else: + defaults = None + + # deep copy the defaults so we don't modify the original dicts + switch._set_changeable_settings(data=data, defaults=deepcopy(defaults)) + if switch.is_on: + switch._update_time_interval_listener() + + _LOGGER.debug( + "Called 'adaptive_lighting.change_switch_settings' service with '%s'", + data, + ) + + switch.manager.reset(*switch.lights, reset_manual_control=False) + if switch.is_on: + await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access + context=switch.create_context("service", parent=service_call.context), + lights=switch.lights, + transition=switch.initial_transition, + force=True, + ) + + +async def handle_apply_service(hass: HomeAssistant, service_call: ServiceCall) -> None: + """Handle the entity service apply.""" + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.apply' service with '%s'", + data, + ) + switches = _switches_from_service_call(hass, service_call) + lights = data[CONF_LIGHTS] + for switch in switches: + all_lights = switch._resolve_lights(lights or None) + switch.manager.lights.update(all_lights) + for light in all_lights: + if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): + context = switch.create_context( + "service", + parent=service_call.context, + ) + transition = data.get(CONF_TRANSITION) + if transition is None: + transition = switch.initial_transition + await switch._adapt_light( # pylint: disable=protected-access + light, + context=context, + transition=transition, + adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS], + adapt_color=data[ATTR_ADAPT_COLOR], + prefer_rgb_color=data[CONF_PREFER_RGB_COLOR], + force=True, + ) + + +async def handle_set_manual_control_service( + hass: HomeAssistant, + service_call: ServiceCall, +) -> None: + """Set or unset lights as manually controlled.""" + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.set_manual_control' service with '%s'", + data, + ) + switches = _switches_from_service_call(hass, service_call) + lights = data[CONF_LIGHTS] + for switch in switches: + all_lights = switch._resolve_lights(lights or None) + manual_attributes = manual_control_event_attribute_to_flags( + data[CONF_MANUAL_CONTROL], + ) + + if manual_attributes: + for light in all_lights: + switch.manager.set_manual_control_attributes( + light, + manual_attributes, + ) + switch.fire_manual_control_event(light, service_call.context) + else: + switch.manager.reset(*all_lights) + if switch.is_on: + context = switch.create_context( + "service", + parent=service_call.context, + ) + await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access + context=context, + lights=all_lights, + transition=switch.initial_transition, + force=True, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the AdaptiveLighting switch.""" + assert hass is not None + data = hass.data[DOMAIN] + assert config_entry.entry_id in data + _LOGGER.debug( + "Setting up AdaptiveLighting with data: %s and config_entry %s", + data, + config_entry, + ) + if ( # Skip deleted YAML config entries or first time YAML config entries + config_entry.source == SOURCE_IMPORT + and config_entry.unique_id not in data.get("__yaml__", set()) + ): + _LOGGER.warning( + "Deleting AdaptiveLighting switch '%s' because YAML" + " defined switch has been removed from YAML configuration", + config_entry.unique_id, + ) + await hass.config_entries.async_remove(config_entry.entry_id) + return + + if (manager := data.get(ATTR_ADAPTIVE_LIGHTING_MANAGER)) is None: + manager = AdaptiveLightingManager(hass) + data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = manager + + sleep_mode_switch = SimpleSwitch( + which="Sleep Mode", + initial_state=False, + hass=hass, + config_entry=config_entry, + icon=ICON_SLEEP, + ) + adapt_color_switch = SimpleSwitch( + which="Adapt Color", + initial_state=True, + hass=hass, + config_entry=config_entry, + icon=ICON_COLOR_TEMP, + ) + adapt_brightness_switch = SimpleSwitch( + which="Adapt Brightness", + initial_state=True, + hass=hass, + config_entry=config_entry, + icon=ICON_BRIGHTNESS, + ) + switch = AdaptiveSwitch( + hass, + config_entry, + manager, + sleep_mode_switch, + adapt_color_switch, + adapt_brightness_switch, + ) + + data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch + data[config_entry.entry_id][ADAPT_COLOR_SWITCH] = adapt_color_switch + data[config_entry.entry_id][ADAPT_BRIGHTNESS_SWITCH] = adapt_brightness_switch + data[config_entry.entry_id][SWITCH_DOMAIN] = switch + + async_add_entities( + [sleep_mode_switch, adapt_color_switch, adapt_brightness_switch, switch], + update_before_add=True, + ) + + if not hasattr(service, "async_register_platform_entity_service"): + platform = entity_platform.current_platform.get() + assert platform is not None + platform.async_register_entity_service( + SERVICE_CHANGE_SWITCH_SETTINGS, + change_switch_settings_schema(), + handle_change_switch_settings, + ) + + +def validate( + config_entry: ConfigEntry | None, + service_data: dict[str, Any] | None = None, + defaults: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Get the options and data from the config_entry and add defaults.""" + if defaults is None: + data = {key: default for key, default, _ in VALIDATION_TUPLES} + else: + data = deepcopy(defaults) + + if config_entry is not None: + assert service_data is None + assert defaults is None + if config_entry.source == SOURCE_IMPORT: + # YAML-configured entries: `data` is the authoritative YAML config + # and must win over any stray `options` from a prior UI setup. + data.update(config_entry.options) + data.update(config_entry.data) + else: + # UI-configured entries: settings are meant to live in `options` + # (see OptionsFlowHandler in config_flow.py). `data` here is + # either just the entry name, or - for entries created before + # data/options were split - a stale snapshot from initial setup. + # Applying it last would silently discard newer changes made + # through the options flow, so `options` must win instead. + data.update(config_entry.data) + data.update(config_entry.options) + else: + assert service_data is not None + changed_settings = { + key: value + for key, value in service_data.items() + if key not in (CONF_USE_DEFAULTS, ATTR_ENTITY_ID) + } + data.update(changed_settings) + data = {key: replace_none_str(value) for key, value in data.items()} + for key, (validate_value, _) in EXTRA_VALIDATION.items(): + value = data.get(key) + if value is not None: + data[key] = validate_value(value) # Fix the types of the inputs + return data + + +def _is_state_event( + event: Event[EventStateChangedData], + from_or_to_state: Iterable[str], +) -> bool: + """Match state event when either 'from_state' or 'to_state' matches.""" + return ( + (old_state := event.data.get("old_state")) is not None + and old_state.state in from_or_to_state + ) or ( + (new_state := event.data.get("new_state")) is not None + and new_state.state in from_or_to_state + ) + + +def _turn_off_transition(turn_off_event: Event) -> float | None: + """Normalize the raw event transition using the light service's validator. + + Service-call events retain raw data after validation, so repeat the + service's coercion and clamping before calculating transition windows. + """ + transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + if transition is None: + return None + return VALID_TRANSITION(transition) + + +def _expand_light_groups( + hass: HomeAssistant, + lights: list[str], +) -> list[str]: + """Resolve nested groups without changing another profile's tracked targets.""" + all_lights: set[str] = set() + pending = list(lights) + visited: set[str] = set() + while pending: + light = pending.pop() + if light in visited: + continue + visited.add(light) + state = hass.states.get(light) + if state is None: + _LOGGER.debug("State of %s is None", light) + all_lights.add(light) + elif _is_light_group(state): + group = state.attributes["entity_id"] + pending.extend(group) + _LOGGER.debug("Expanded %s to %s", light, group) + else: + all_lights.add(light) + return sorted(all_lights) + + +def _is_light_group(state: State) -> bool: + return "entity_id" in state.attributes and not state.attributes.get( + "is_hue_group", + False, + ) + + +def _supported_features(hass: HomeAssistant, light: str) -> set[str]: + state = hass.states.get(light) + assert state is not None + supported_features = int( + state.attributes.get(ATTR_SUPPORTED_FEATURES, 0), + ) # type: ignore[arg-type] + assert isinstance(supported_features, int) + + supported: set[str] = set() + + if supported_features & LightEntityFeature.TRANSITION: + supported.add("transition") + + supported_color_modes = state.attributes.get( + ATTR_SUPPORTED_COLOR_MODES, + set(), + ) # type: ignore[arg-type] + color_modes = { + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, + ColorMode.XY, + ColorMode.HS, + } + + # Adding brightness when color mode is supported, see + # comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011 + + for mode in color_modes: + if mode in supported_color_modes: + supported.update({"color", "brightness"}) + break + + if ColorMode.COLOR_TEMP in supported_color_modes: + supported.update({"color_temp", "brightness"}) + + if ColorMode.BRIGHTNESS in supported_color_modes: + supported.add("brightness") + + return supported + + +# All comparisons should be done with RGB since +# converting anything to color temp is inaccurate. +def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: + if ATTR_RGB_COLOR in attributes: + return attributes + + rgb = None + if (color := attributes.get(ATTR_COLOR_TEMP_KELVIN)) is not None: + rgb = color_temperature_to_rgb(color) + elif (color := attributes.get(ATTR_XY_COLOR)) is not None: + rgb = color_xy_to_RGB(*color) + + if rgb is not None: + attributes[ATTR_RGB_COLOR] = rgb + _LOGGER.debug("Converted attributes %s to rgb %s", attributes, rgb) + else: + _LOGGER.debug("No suitable color conversion found for %s", attributes) + + return attributes + + +def _add_missing_attributes( + old_attributes: dict[str, Any], + new_attributes: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any]]: + if not any( + attr in old_attributes and attr in new_attributes + for attr in [ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR] + ): + old_attributes = _convert_attributes(old_attributes) + new_attributes = _convert_attributes(new_attributes) + + return old_attributes, new_attributes + + +def _has_color_mode_changed( + light: str, + old_attributes: dict[str, Any], + new_attributes: dict[str, Any], + context: Context, +) -> bool: + """Check if the light's color mode changed (e.g., color_temp to RGB or vice versa). + + This must be called BEFORE _add_missing_attributes() to detect mode changes + using the original attributes. See issue #1275. + """ + old_has_color_temp = old_attributes.get(ATTR_COLOR_TEMP_KELVIN) is not None + old_has_rgb = old_attributes.get(ATTR_RGB_COLOR) is not None + old_has_xy = old_attributes.get(ATTR_XY_COLOR) is not None + + new_has_color_temp = new_attributes.get(ATTR_COLOR_TEMP_KELVIN) is not None + new_has_rgb = new_attributes.get(ATTR_RGB_COLOR) is not None + new_has_xy = new_attributes.get(ATTR_XY_COLOR) is not None + + # Determine old and new color modes + # Priority: color_temp > rgb > xy (matching typical light behavior) + if old_has_color_temp: + old_mode = "color_temp" + elif old_has_rgb: + old_mode = "rgb" + elif old_has_xy: + old_mode = "xy" + else: + old_mode = None + + if new_has_color_temp: + new_mode = "color_temp" + elif new_has_rgb: + new_mode = "rgb" + elif new_has_xy: + new_mode = "xy" + else: + new_mode = None + + # Check if mode changed + if old_mode is not None and new_mode is not None and old_mode != new_mode: + _LOGGER.debug( + "Light mode of %s changed from %s to %s with context.id='%s'", + light, + old_mode, + new_mode, + context.id, + ) + return True + return False + + +def _attributes_have_changed( + light: str, + old_attributes: dict[str, Any], + new_attributes: dict[str, Any], + context: Context, +) -> LightControlAttributes: + # 2023-11-19: HA core no longer removes light domain attributes when off + # so we must protect for `None` here + # see https://github.com/home-assistant/core/pull/101946 + + changed_attributes = LightControlAttributes.NONE + + # Check for color mode changes BEFORE attribute conversion + # This detects external changes like Hue scenes switching from color_temp to RGB + # See: https://github.com/basnijholt/adaptive-lighting/issues/1275 + if _has_color_mode_changed( + light, + old_attributes, + new_attributes, + context, + ): + changed_attributes |= LightControlAttributes.COLOR + + if LightControlAttributes.COLOR not in changed_attributes: + old_attributes, new_attributes = _add_missing_attributes( + old_attributes, + new_attributes, + ) + + if old_attributes.get(ATTR_BRIGHTNESS) and new_attributes.get(ATTR_BRIGHTNESS): + last_brightness = old_attributes[ATTR_BRIGHTNESS] + current_brightness = new_attributes[ATTR_BRIGHTNESS] + if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE: + _LOGGER.debug( + "Brightness of '%s' significantly changed from %s to %s with" + " context.id='%s'", + light, + last_brightness, + current_brightness, + context.id, + ) + changed_attributes |= LightControlAttributes.BRIGHTNESS + + if ( + LightControlAttributes.COLOR not in changed_attributes + and old_attributes.get(ATTR_COLOR_TEMP_KELVIN) + and new_attributes.get(ATTR_COLOR_TEMP_KELVIN) + ): + last_color_temp = old_attributes[ATTR_COLOR_TEMP_KELVIN] + current_color_temp = new_attributes[ATTR_COLOR_TEMP_KELVIN] + if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE: + _LOGGER.debug( + "Color temperature of '%s' significantly changed from %s to %s with" + " context.id='%s'", + light, + last_color_temp, + current_color_temp, + context.id, + ) + changed_attributes |= LightControlAttributes.COLOR + + if ( + LightControlAttributes.COLOR not in changed_attributes + and old_attributes.get(ATTR_RGB_COLOR) + and new_attributes.get(ATTR_RGB_COLOR) + ): + last_rgb_color = old_attributes[ATTR_RGB_COLOR] + current_rgb_color = new_attributes[ATTR_RGB_COLOR] + redmean_change = color_difference_redmean(last_rgb_color, current_rgb_color) + if redmean_change > RGB_REDMEAN_CHANGE: + _LOGGER.debug( + "color RGB of '%s' significantly changed from %s to %s with" + " context.id='%s'", + light, + last_rgb_color, + current_rgb_color, + context.id, + ) + changed_attributes |= LightControlAttributes.COLOR + + return changed_attributes + + +class AdaptiveSwitch(SwitchEntity, RestoreEntity): + """Representation of a Adaptive Lighting switch.""" + + _attr_has_entity_name = True + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + manager: AdaptiveLightingManager, + sleep_mode_switch: SimpleSwitch, + adapt_color_switch: SimpleSwitch, + adapt_brightness_switch: SimpleSwitch, + ) -> None: + """Initialize the Adaptive Lighting switch.""" + # Set attributes that can't be modified during runtime + assert hass is not None + self.hass = hass + self.manager = manager + self._removed = False + self.sleep_mode_switch = sleep_mode_switch + self.adapt_color_switch = adapt_color_switch + self.adapt_brightness_switch = adapt_brightness_switch + + data = validate(config_entry) + + self._name = data[CONF_NAME] + self._interval: timedelta = data[CONF_INTERVAL] + self._configured_lights: list[str] = list(data[CONF_LIGHTS]) + self.lights: list[str] = [] + + # backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS + self._config_backup = deepcopy(data) + self._set_changeable_settings(data=data, defaults=None) + + # Set other attributes + self._icon = ICON_MAIN + self._state: bool | None = None + + # To count the number of `Context` instances + self._context_cnt: int = 0 + + # Set in self._update_attrs_and_maybe_adapt_lights + self._settings: dict[str, Any] = {} + + # Set and unset tracker in async_turn_on and async_turn_off + self.remove_listeners: list[CALLBACK_TYPE] = [] + self.remove_interval: CALLBACK_TYPE = lambda: None + _LOGGER.debug( + "%s: Setting up with '%s'," + " config_entry.data: '%s'," + " config_entry.options: '%s', converted to '%s'.", + self._name, + self.lights, + config_entry.data, + config_entry.options, + data, + ) + + def _set_changeable_settings( + self, + data: dict[str, Any], + defaults: dict[str, Any] | None = None, + ) -> None: + # Only pass settings users can change during runtime + data = validate( + config_entry=None, + service_data=data, + defaults=defaults, + ) + + # backup data for use in change_switch_settings "current" CONF_USE_DEFAULTS + self._current_settings = data + + self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] + self._include_config_in_attributes = data[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] + self._config: dict[str, Any] = {} + if self._include_config_in_attributes: + attrdata = deepcopy(data) + for k, v in attrdata.items(): + if isinstance(v, datetime.date | datetime.datetime): + attrdata[k] = v.isoformat() + elif isinstance(v, datetime.timedelta): + attrdata[k] = v.total_seconds() + self._config.update(attrdata) + + self.initial_transition = data[CONF_INITIAL_TRANSITION] + self._sleep_transition = data[CONF_SLEEP_TRANSITION] + self._only_once = data[CONF_ONLY_ONCE] + self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] + self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] + self._transition: int = data[CONF_TRANSITION] + self._adapt_delay = data[CONF_ADAPT_DELAY] + self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] + self._take_over_control = data[CONF_TAKE_OVER_CONTROL] + if not data[CONF_TAKE_OVER_CONTROL] and ( + data[CONF_DETECT_NON_HA_CHANGES] + or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + or data[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] + ): + _LOGGER.warning( + "%s: Config mismatch: `detect_non_ha_changes`, `adapt_only_on_bare_turn_on`, " + "or `manual_control_on_external_turn_on` set to `true` requires `take_over_control` to be " + "enabled. Adjusting config and continuing setup with `take_over_control: true`.", + self._name, + ) + self._take_over_control = True + self._take_over_control_mode = TakeOverControlMode( + data[CONF_TAKE_OVER_CONTROL_MODE], + ) + self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] + self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + self._manual_control_on_external_turn_on = data[ + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON + ] + self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] + self._reset_manual_control_on_sleep_mode_change = data[ + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE + ] + self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS] + self._intercept = data[CONF_INTERCEPT] + self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT] + if not data[CONF_INTERCEPT] and data[CONF_MULTI_LIGHT_INTERCEPT]: + _LOGGER.warning( + "%s: Config mismatch: `multi_light_intercept` set to `true` requires `intercept`" + " to be enabled. Adjusting config and continuing setup with" + " `multi_light_intercept: false`.", + self._name, + ) + self._multi_light_intercept = False + self._expand_light_groups_flag = data[CONF_EXPAND_LIGHT_GROUPS] + self._expand_light_groups() # updates manual control timers + observer = get_astral_observer(self.hass) + + self._sun_light_settings = SunLightSettings( + name=self._name, + astral_observer=observer, + adapt_until_sleep=data[CONF_ADAPT_UNTIL_SLEEP], + max_brightness=data[CONF_MAX_BRIGHTNESS], + max_color_temp=data[CONF_MAX_COLOR_TEMP], + min_brightness=data[CONF_MIN_BRIGHTNESS], + min_color_temp=data[CONF_MIN_COLOR_TEMP], + sleep_brightness=data[CONF_SLEEP_BRIGHTNESS], + sleep_color_temp=data[CONF_SLEEP_COLOR_TEMP], + sleep_rgb_color=data[CONF_SLEEP_RGB_COLOR], + sleep_rgb_or_color_temp=data[CONF_SLEEP_RGB_OR_COLOR_TEMP], + sunrise_offset=data[CONF_SUNRISE_OFFSET], + sunrise_time=data[CONF_SUNRISE_TIME], + min_sunrise_time=data[CONF_MIN_SUNRISE_TIME], + max_sunrise_time=data[CONF_MAX_SUNRISE_TIME], + sunset_offset=data[CONF_SUNSET_OFFSET], + sunset_time=data[CONF_SUNSET_TIME], + min_sunset_time=data[CONF_MIN_SUNSET_TIME], + max_sunset_time=data[CONF_MAX_SUNSET_TIME], + brightness_mode=data[CONF_BRIGHTNESS_MODE], + brightness_mode_time_dark=data[CONF_BRIGHTNESS_MODE_TIME_DARK], + brightness_mode_time_light=data[CONF_BRIGHTNESS_MODE_TIME_LIGHT], + timezone=zoneinfo.ZoneInfo(self.hass.config.time_zone), + ) + _LOGGER.debug( + "%s: Set switch settings for lights '%s'. now using data: '%s'", + self._name, + self.lights, + data, + ) + + @property + def name(self) -> str | None: + """Return the name of the device if any.""" + # The main switch takes the device name "Adaptive Lighting: " + return None + + @property + def unique_id(self) -> str: + """Return the unique ID of entity.""" + return self._name + + @property + def is_on(self) -> bool | None: + """Return true if adaptive lighting is on.""" + return self._state + + @property + def device_info(self) -> DeviceInfo: + """Return the device info, used to group this and adjacent entities in the UI.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self._name), + }, + name=f"Adaptive Lighting: {self._name}", + entry_type=DeviceEntryType.SERVICE, + ) + + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + if self.hass.is_running: + await self._setup_listeners() + else: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, + self._setup_listeners, + ) + last_state: State | None = await self.async_get_last_state() + is_new_entry = last_state is None # newly added to HA + if is_new_entry or last_state.state == STATE_ON: # type: ignore[union-attr] + await self.async_turn_on(adapt_lights=not self._only_once) + else: + self._state = False + assert not self.remove_listeners + + async def async_will_remove_from_hass(self) -> None: + """Remove the listeners upon removing the component.""" + self._removed = True + self._remove_listeners() + + def _resolve_lights(self, lights: list[str] | None = None) -> list[str]: + """Apply this profile's group policy, preserving explicit member targets.""" + if lights is None: + lights = self._configured_lights + if self._expand_light_groups_flag: + return _expand_light_groups(self.hass, lights) + return sorted(set(lights)) + + def _expand_light_groups(self) -> None: + all_lights = self._resolve_lights() + removed = set(self.lights) - set(all_lights) + self.lights = all_lights + if removed: + # Other profiles may still own a retired member or group, even when off. + for entry in self.hass.data[DOMAIN].values(): + if isinstance(entry, dict) and (switch := entry.get(SWITCH_DOMAIN)): + removed.difference_update(switch._resolve_lights()) + self.manager.remove_lights(*removed) + self.manager.lights.update(all_lights) + self.manager.set_auto_reset_manual_control_times( + all_lights, + self._auto_reset_manual_control_time, + ) + + async def _setup_listeners(self, _: Event[NoEventData] | None = None) -> None: + _LOGGER.debug("%s: Called '_setup_listeners'", self._name) + if not self.is_on or not self.hass.is_running: + _LOGGER.debug("%s: Cancelled '_setup_listeners'", self._name) + return + + assert not self.remove_listeners + + self._update_time_interval_listener() + + remove_sleep = async_track_state_change_event( + self.hass, + entity_ids=self.sleep_mode_switch.entity_id, + action=self._sleep_mode_switch_state_event_action, + ) + + self.remove_listeners.append(remove_sleep) + self._expand_light_groups() + + def _stagger_offset(self, adaptation_interval: timedelta) -> timedelta: + """Return a stable relative delay to spread periodic updates. + + Hashing the switch ID gives a best-effort spread without configuration. + It does not delay the immediate turn-on adaptation or guarantee a minimum + gap between switches. + """ + digest = hashlib.sha256(self.unique_id.encode()).digest() + fraction = int.from_bytes(digest[:8], byteorder="big") / 2**64 + return adaptation_interval * fraction + + def _update_time_interval_listener(self) -> None: + """Create or recreate the adaptation interval listener. + + Recreation is necessary when the configuration has changed (e.g., `send_split_delay`). + """ + self._remove_interval_listener() + + # An adaptation takes a little longer than its nominal duration due processing overhead, + # so we factor this in to avoid overlapping adaptations. Since this is a constant value, + # it might not cover all cases, but if large enough, it covers most. + # Ideally, the interval and adaptation are a coupled process where a finished adaptation + # triggers the next, but that requires a larger architectural change. + processing_overhead_time = 0.5 + + adaptation_interval = ( + self._interval + + timedelta(milliseconds=self._send_split_delay) + + timedelta(seconds=processing_overhead_time) + ) + + @callback + def _start_periodic_listener(_now: datetime.datetime | None = None) -> None: + self.remove_interval = async_track_time_interval( + self.hass, + action=self._async_update_at_interval_action, + interval=adaptation_interval, + ) + + # Register after the offset. The first periodic tick is at offset + + # interval, then subsequent ticks keep the configured interval. + offset = self._stagger_offset(adaptation_interval) + if offset > timedelta(0): + self.remove_interval = async_call_later( + self.hass, + offset.total_seconds(), + _start_periodic_listener, + ) + else: + _start_periodic_listener() + + def _call_on_remove_callbacks(self) -> None: + """Call callbacks registered by async_on_remove.""" + # This is called when the integration is removed from HA + # and in `Entity.add_to_platform_abort`. + # For some unknown reason (to me) `async_will_remove_from_hass` + # is not called in `add_to_platform_abort`. + # See https://github.com/basnijholt/adaptive-lighting/issues/658 + self._remove_listeners() + try: + # HACK: this is a private method in `Entity` which can change + super()._call_on_remove_callbacks() + except AttributeError: + _LOGGER.exception( + "%s: Caught AttributeError in `_call_on_remove_callbacks`", + self._name, + ) + + def _remove_interval_listener(self) -> None: + self.remove_interval() + self.remove_interval = lambda: None + + def _remove_listeners(self) -> None: + self._remove_interval_listener() + + while self.remove_listeners: + remove_listener = self.remove_listeners.pop() + remove_listener() + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the attributes of the switch.""" + extra_state_attributes: dict[str, Any] = {"configuration": self._config} + if not self.is_on: + for key in self._settings: + extra_state_attributes[key] = None + return extra_state_attributes + extra_state_attributes["manual_control"] = [ + light for light in self.lights if self.manager.manual_control.get(light) + ] + extra_state_attributes["manual_control_brightness"] = [ + light + for light in self.lights + if self.manager.manual_control.get(light, LightControlAttributes.NONE) + & LightControlAttributes.BRIGHTNESS + ] + extra_state_attributes["manual_control_color"] = [ + light + for light in self.lights + if self.manager.manual_control.get(light, LightControlAttributes.NONE) + & LightControlAttributes.COLOR + ] + extra_state_attributes.update(self._settings) + timers = self.manager.auto_reset_manual_control_timers + extra_state_attributes["autoreset_time_remaining"] = { + light: time + for light in self.lights + if (timer := timers.get(light)) and (time := timer.remaining_time()) > 0 + } + return extra_state_attributes + + def create_context( + self, + which: str = "default", + parent: Context | None = None, + ) -> Context: + """Create a context that identifies this Adaptive Lighting instance.""" + context = create_context(self._name, which, self._context_cnt, parent=parent) + self._context_cnt += 1 + return context + + async def async_turn_on( # type: ignore[override] + self, + adapt_lights: bool = True, + ) -> None: + """Turn on adaptive lighting.""" + _LOGGER.debug( + "%s: Called 'async_turn_on', current state is '%s'", + self._name, + self._state, + ) + if self.is_on: + return + self._state = True + self.manager.reset(*self.lights) + await self._setup_listeners() + if adapt_lights: + await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("turn_on"), + transition=self.initial_transition, + force=True, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 + """Turn off adaptive lighting.""" + if not self.is_on: + return + self._state = False + self._remove_listeners() + self.manager.reset(*self.lights) + + async def _async_update_at_interval_action( + self, + now: Any = None, # noqa: ARG002 + ) -> None: + """Update the attributes and maybe adapt the lights.""" + await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("interval"), + transition=self._transition, + force=False, + ) + + async def prepare_adaptation_data( + self, + light: str, + transition: int | None = None, + adapt_brightness: bool | None = None, + adapt_color: bool | None = None, + prefer_rgb_color: bool | None = None, + force: bool = False, + context: Context | None = None, + already_applied: LightControlAttributes = LightControlAttributes.NONE, + ) -> AdaptationData | None: + """Prepare `AdaptationData` for adapting a light.""" + adaptation_attributes = self.manager.get_adaption_control_attributes( + self, + light, + ) + + if transition is None: + transition = self._transition + if adapt_brightness is None: + adapt_brightness = ( + LightControlAttributes.BRIGHTNESS in adaptation_attributes + ) + if adapt_color is None: + adapt_color = LightControlAttributes.COLOR in adaptation_attributes + if prefer_rgb_color is None: + prefer_rgb_color = self._prefer_rgb_color + + if not adapt_color and not adapt_brightness: + _LOGGER.debug( + "%s: Skipping adaptation of %s because both adapt_brightness and" + " adapt_color are False", + self._name, + light, + ) + return None + + # The switch might be off and not have _settings set. + self._settings = self._sun_light_settings.get_settings( + self.sleep_mode_switch.is_on, + transition, + ) + + # Build service data. + service_data: dict[str, Any] = {ATTR_ENTITY_ID: light} + features = _supported_features(self.hass, light) + + # Check transition == 0 to fix #378 + use_transition = "transition" in features and transition > 0 + if use_transition: + service_data[ATTR_TRANSITION] = transition + + if "brightness" in features and adapt_brightness: + brightness = round(255 * self._settings["brightness_pct"] / 100) + service_data[ATTR_BRIGHTNESS] = brightness + + sleep_rgb = ( + self.sleep_mode_switch.is_on + and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" + ) + if ( + "color_temp" in features + and adapt_color + and not (prefer_rgb_color and "color" in features) + and not (sleep_rgb and "color" in features) + and not (self._settings["force_rgb_color"] and "color" in features) + ): + _LOGGER.debug("%s: Setting color_temp of light %s", self._name, light) + state = self.hass.states.get(light) + assert isinstance(state, State) + attributes = state.attributes + min_kelvin = attributes["min_color_temp_kelvin"] + max_kelvin = attributes["max_color_temp_kelvin"] + color_temp_kelvin = self._settings["color_temp_kelvin"] + color_temp_kelvin = clamp(color_temp_kelvin, min_kelvin, max_kelvin) + service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin + elif "color" in features and adapt_color: + _LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light) + service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] + + required_attrs = [ATTR_RGB_COLOR, ATTR_COLOR_TEMP_KELVIN, ATTR_BRIGHTNESS] + if not any(attr in service_data for attr in required_attrs): + _LOGGER.debug( + "%s: Skipping adaptation of %s because no relevant attributes" + " are set in service_data: %s", + self._name, + light, + service_data, + ) + return None + + context = context or self.create_context("adapt_lights") + + return prepare_adaptation_data( + self.hass, + light, + context, + transition if use_transition else 0, + self._send_split_delay / 1000.0, + service_data, + split=self._separate_turn_on_commands, + filter_by_state=self._skip_redundant_commands, + force=force, + already_applied=already_applied, + ) + + async def _adapt_light( + self, + light: str, + context: Context, + transition: int | None = None, + adapt_brightness: bool | None = None, + adapt_color: bool | None = None, + prefer_rgb_color: bool | None = None, + force: bool = False, + ) -> None: + if (lock := self.manager.turn_off_locks.get(light)) and lock.locked(): + _LOGGER.debug("%s: '%s' is locked", self._name, light) + return + + data = await self.prepare_adaptation_data( + light, + transition, + adapt_brightness, + adapt_color, + prefer_rgb_color, + force, + context, + ) + if data is None: + return # nothing to adapt + + await self.execute_cancellable_adaptation_calls(data) + + async def _execute_adaptation_calls(self, data: AdaptationData) -> None: + """Executes a sequence of adaptation service calls for the given service datas.""" + for index in range(data.max_length): + is_first_call = index == 0 + + # Sleep between multiple service calls. + if not is_first_call or data.initial_sleep: + await asyncio.sleep(data.sleep_time) + + if self._removed: + return + + # Instead of directly iterating the generator in the while-loop, we get + # the next item here after the sleep to make sure it incorporates state + # changes which happened during the sleep. + service_data = await data.next_service_call_data() + + if not service_data: + # All service datas processed + break + + if ( + not data.force + and not is_on(self.hass, data.entity_id) + # if proactively adapting, we are sure that it came from a `light.turn_on` + and not self.manager.is_proactively_adapting(data.context.id) + ): + # Do a last-minute check if the entity is still on. + _LOGGER.debug( + "%s: Skipping adaptation of %s because it is now off", + self._name, + data.entity_id, + ) + return + + _LOGGER.debug( + "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" + " with context.id='%s'", + self._name, + service_data, + data.context.id, + ) + light = service_data[ATTR_ENTITY_ID] + self.manager.invalidate_manual_control_state( + light, + get_light_control_attributes(service_data), + ) + self.manager.last_service_data[light] = { + **self.manager.last_service_data.get(light, {}), + **service_data, + } + await self.hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data, + context=data.context, + ) + + async def execute_cancellable_adaptation_calls( + self, + data: AdaptationData, + ) -> None: + """Executes a cancellable sequence of adaptation service calls for the given service datas. + + Wraps the sequence of service calls in a task that can be cancelled from elsewhere, e.g., + to cancel an ongoing adaptation when a light is turned off. + """ + if self._removed: + return + + # Prevent overlap of multiple adaptation sequences + self.manager.cancel_ongoing_adaptation_calls(data.entity_id) + _LOGGER.debug( + "%s: execute_cancellable_adaptation_calls with data: %s", + self._name, + data, + ) + # Execute adaptation calls within a task + try: + task = asyncio.ensure_future(self._execute_adaptation_calls(data)) + if LightControlAttributes.BRIGHTNESS in data.attributes: + self.manager.adaptation_tasks_brightness[data.entity_id] = task + if LightControlAttributes.COLOR in data.attributes: + self.manager.adaptation_tasks_color[data.entity_id] = task + await task + except asyncio.CancelledError: + _LOGGER.debug( + "%s: Ongoing adaptation of %s cancelled, with AdaptationData: %s", + self._name, + data.entity_id, + data, + ) + + async def _update_attrs_and_maybe_adapt_lights( + self, + *, + context: Context, + lights: list[str] | None = None, + transition: int | None = None, + force: bool = False, + ) -> None: + assert context is not None + _LOGGER.debug( + "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'" + " lights: '%s', transition: '%s', force: '%s'", + self._name, + context.id, + lights, + transition, + force, + ) + assert self.is_on + self._settings.update( + self._sun_light_settings.get_settings( + self.sleep_mode_switch.is_on, + transition, + ), + ) + self.async_write_ha_state() + + if not force and self._only_once: + return + + if lights is None: + self._expand_light_groups() + lights = self.lights + + on_lights = [light for light in lights if is_on(self.hass, light)] + + if force: + filtered_lights = on_lights + else: + filtered_lights: list[str] = [] + for light in on_lights: + # Don't adapt lights that haven't finished prior transitions. + timer = self.manager.transition_timers.get(light) + if timer is not None and timer.is_running(): + _LOGGER.debug( + "%s: Light '%s' is still transitioning, context.id='%s'", + self._name, + light, + context.id, + ) + elif ( + # This is to prevent lights immediately turning on after + # being turned off in 'interval' update, see #726 + not self._detect_non_ha_changes + and is_our_context(context, "interval") + and (turn_on := self.manager.turn_on_event.get(light)) + and (turn_off := self.manager.turn_off_event.get(light)) + and turn_off.time_fired > turn_on.time_fired + ): + _LOGGER.debug( + "%s: Light '%s' was turned just turned off, context.id='%s'", + self._name, + light, + context.id, + ) + else: + filtered_lights.append(light) + + _LOGGER.debug("%s: filtered_lights: '%s'", self._name, filtered_lights) + if not filtered_lights: + return + + tasks: list[asyncio.Task[None]] = [] + for light in filtered_lights: + await self.manager.update_manually_controlled_from_untracked_change( + self, + light, + force, + context, + ) + + # Performance optimization: Skip adaptation task if all attributes are + # manually controlled and the task wouldn't actually do anything. + if self.manager.get_adaption_control_attributes(self, light).has_none(): + _LOGGER.debug( + "%s: '%s' is being manually controlled, skip adaptation, context.id=%s.", + self._name, + light, + context.id, + ) + continue + + _LOGGER.debug( + "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:" + " '%s' with transition %s and context.id=%s", + self._name, + light, + transition, + context.id, + ) + coro = self._adapt_light(light, context, transition, force=force) + task = self.hass.async_create_task( + coro, + ) + tasks.append(task) + if tasks: + await asyncio.gather(*tasks) + + async def _respond_to_off_to_on_event( + self, + entity_id: str, + event: Event[EventStateChangedData], + ) -> None: + assert not self.manager.is_proactively_adapting(event.context.id) + from_turn_on = self.manager._off_to_on_state_event_is_from_turn_on( + entity_id, + event, + ) + if ( + self._take_over_control + and ( + not self._detect_non_ha_changes + or self._manual_control_on_external_turn_on + ) + and not from_turn_on + ): + # There is an edge case where 2 switches control the same light, e.g., + # one for brightness and one for color. Now we will mark both switches + # as manually controlled, which is not 100% correct. + # + # This 'off' → 'on' event does not exactly match the most recently tracked + # `light.turn_on` context for the entity. Hand control over when either: + # - `detect_non_ha_changes` is False (we can't reliably track manual changes + # to already-on lights anyway), or + # - `manual_control_on_external_turn_on` is True (the user explicitly wants external + # turn-ons left untouched, even while `detect_non_ha_changes` is enabled). + _LOGGER.debug( + "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'" + " because it does not match a tracked 'light.turn_on' context and" + " ('detect_non_ha_changes' is False or 'manual_control_on_external_turn_on' is True)", + self._name, + entity_id, + event.context.id, + ) + self.manager.set_manual_control_attributes(entity_id) + return + + if ( + self._take_over_control + and self._adapt_only_on_bare_turn_on + and from_turn_on + # adaptive_lighting.apply can turn on light, so check this is not our context + and not is_our_context(event.context) + ): + service_data = self.manager.turn_on_event[entity_id].data[ATTR_SERVICE_DATA] + manual_attributes = get_light_control_attributes(service_data) + if self.manager._mark_manual_control_if_non_bare_turn_on( + entity_id, + service_data, + ): + new_state = event.data["new_state"] + assert new_state is not None + self.manager.update_manual_control_state( + entity_id, + new_state, + manual_attributes, + ) + _LOGGER.debug( + "Marked attributes from service_data as manually controlled for '%s' " + "with context.id='%s'. Continuing to adapt remaining attributes. " + "service_data: '%s'", + entity_id, + event.context.id, + service_data, + ) + + if self._adapt_delay > 0: + await asyncio.sleep(self._adapt_delay) + + # Runtime settings may retire this profile's target while the event waits. + if self._removed or entity_id not in self.lights: + return + + await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("light_event", parent=event.context), + lights=[entity_id], + transition=self.initial_transition, + force=True, + ) + + async def _sleep_mode_switch_state_event_action( + self, + event: Event[EventStateChangedData], + ) -> None: + new_state = event.data.get("new_state") + if new_state is None or new_state.state not in (STATE_ON, STATE_OFF): + _LOGGER.debug("%s: Ignoring sleep event %s", self._name, event) + return + _LOGGER.debug( + "%s: _sleep_mode_switch_state_event_action, event: '%s'", + self._name, + event, + ) + self.manager.reset( + *self.lights, + reset_manual_control=self._reset_manual_control_on_sleep_mode_change, + ) + await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("sleep", parent=event.context), + transition=self._sleep_transition, + force=True, + ) + + def fire_manual_control_event( + self, + light: str, + context: Context, + ) -> None: + """Fire an event that 'light' is marked as manual_control.""" + _LOGGER.debug( + "'adaptive_lighting.manual_control' event fired for %s for light %s", + self.entity_id, + light, + ) + manual_attributes = self.manager.get_manual_control_attributes(light) + self.hass.bus.async_fire( + f"{DOMAIN}.manual_control", + { + ATTR_ENTITY_ID: light, + SWITCH_DOMAIN: self.entity_id, + CONF_MANUAL_CONTROL: manual_attributes, + }, + context=context, + ) + + +class SimpleSwitch(SwitchEntity, RestoreEntity): + """Representation of a Adaptive Lighting switch.""" + + _attr_has_entity_name = True + + def __init__( + self, + which: str, + initial_state: bool, + hass: HomeAssistant, + config_entry: ConfigEntry, + icon: str, + ) -> None: + """Initialize the Adaptive Lighting switch.""" + self.hass = hass + data = validate(config_entry) + self._icon = icon + self._state: bool = initial_state + self._which = which + self._config_name = data[CONF_NAME] + self._unique_id = f"{self._config_name}_{slugify(self._which)}" + self._name = f"Adaptive Lighting {which}: {self._config_name}" + self._initial_state = initial_state + + @property + def name(self) -> str: + """Return the name of the entity within its device.""" + return self._which + + @property + def unique_id(self) -> str: + """Return the unique ID of entity.""" + return self._unique_id + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def is_on(self) -> bool | None: + """Return true if adaptive lighting is on.""" + return self._state + + @property + def device_info(self) -> DeviceInfo: + """Return the device info, used to group this and adjacent entities in the UI.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self._config_name), + }, + name=f"Adaptive Lighting: {self._config_name}", + entry_type=DeviceEntryType.SERVICE, + ) + + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + last_state = await self.async_get_last_state() + _LOGGER.debug("%s: last state is %s", self._name, last_state) + if (last_state is None and self._initial_state) or ( + last_state is not None and last_state.state == STATE_ON + ): + await self.async_turn_on() + else: + await self.async_turn_off() + + async def async_turn_on(self, **kwargs: Any) -> None: # noqa: ARG002 + """Turn on adaptive lighting sleep mode.""" + _LOGGER.debug("%s: Turning on", self._name) + self._state = True + + async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 + """Turn off adaptive lighting sleep mode.""" + _LOGGER.debug("%s: Turning off", self._name) + self._state = False + + +type AdaptiveSwitches = list[AdaptiveSwitch] +type AdaptiveSwitchMap = dict[AdaptiveSwitch, list[str]] + + +class AdaptiveLightingManager: + """Track 'light.turn_off' and 'light.turn_on' service calls.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the AdaptiveLightingManager that is shared among all switches.""" + assert hass is not None + self.hass = hass + self.lights: set[str] = set() + + # Tracks 'light.turn_off' service calls + self.turn_off_event: dict[str, Event] = {} + # Tracks 'light.turn_on' service calls + self.turn_on_event: dict[str, Event] = {} + # Tracks 'light.toggle' service calls + self.toggle_event: dict[str, Event] = {} + # Tracks 'on' → 'off' state changes + self.on_to_off_event: dict[str, Event[EventStateChangedData]] = {} + # Tracks 'off' → 'on' state changes + self.off_to_on_event: dict[str, Event[EventStateChangedData]] = {} + # Keep 'asyncio.sleep' tasks that can be cancelled by 'light.turn_on' events + self.sleep_tasks: dict[str, asyncio.Task[None]] = {} + # Locks that prevent light adjusting when waiting for a light to 'turn_off' + self.turn_off_locks: dict[str, asyncio.Lock] = {} + # Tracks which lights are manually controlled + self.manual_control: dict[str, LightControlAttributes] = {} + # Track 'state_changed' events of self.lights resulting from this integration + self.our_last_state_on_change: dict[str, list[State]] = {} + # Track last 'service_data' to 'light.turn_on' resulting from this integration + self.last_service_data: dict[str, dict[str, Any]] = {} + # Track reported states that established manual control of each axis + self.last_manual_control_state: dict[ + str, + dict[LightControlAttributes, dict[str, Any]], + ] = {} + self.pending_manual_control_state: dict[ + str, + dict[LightControlAttributes, str], + ] = {} + # Track ongoing split adaptations to be able to cancel them + self.adaptation_tasks_brightness: dict[str, asyncio.Task[None]] = {} + self.adaptation_tasks_color: dict[str, asyncio.Task[None]] = {} + + # Track auto reset of manual_control + self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {} + self.auto_reset_manual_control_times: dict[str, float] = {} + + # Track light transitions + self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} + + # Track _execute_cancellable_adaptation_calls tasks + self.adaptation_tasks: set[asyncio.Task[None]] = set() + + # Setup listeners and its callbacks to remove them later + self.listener_removers = [ + self.hass.bus.async_listen( + EVENT_CALL_SERVICE, + self.turn_on_off_event_listener, + ), + self.hass.bus.async_listen( + EVENT_STATE_CHANGED, + self.state_changed_event_listener, + ), + ] + + self._proactively_adapting_contexts: dict[str, str] = {} + self._context_cnt: int = 0 + + try: + self.listener_removers.append( + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + self._service_interceptor_turn_on_handler, + ), + ) + + self.listener_removers.append( + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TOGGLE, + self._service_interceptor_turn_on_handler, + ), + ) + except RuntimeError: + _LOGGER.warning( + "Failed to set up service call interceptors, " + "falling back to event-reactive mode", + exc_info=True, + ) + + def disable(self) -> None: + """Disable listeners and pending manual-reset and transition timers.""" + for remove in self.listener_removers: + remove() + for timer in self.auto_reset_manual_control_timers.values(): + timer.cancel() + self.auto_reset_manual_control_timers.clear() + for timer in self.transition_timers.values(): + timer.cancel() + self.transition_timers.clear() + + def set_proactively_adapting(self, context_id: str, entity_id: str) -> None: + """Declare the adaptation with context_id as proactively adapting, + and associate it to an entity_id. + """ # noqa: D205 + self._proactively_adapting_contexts[context_id] = entity_id + + def is_proactively_adapting(self, context_id: str) -> bool: + """Determine whether an adaptation with the given context_id is proactive.""" + is_proactively_adapting_context = ( + context_id in self._proactively_adapting_contexts + ) + + _LOGGER.debug( + "is_proactively_adapting_context='%s', context_id='%s'", + is_proactively_adapting_context, + context_id, + ) + + return is_proactively_adapting_context + + def clear_proactively_adapting(self, entity_id: str) -> None: + """Clear all context IDs associated with the given entity ID. + + Call this method to clear past context IDs and avoid a memory leak. + """ + # First get the keys to avoid modifying the dict while iterating it + keys = [ + k for k, v in self._proactively_adapting_contexts.items() if v == entity_id + ] + for key in keys: + self._proactively_adapting_contexts.pop(key) + + def create_context( + self, + which: str = "default", + parent: Context | None = None, + ) -> Context: + """Create a context that identifies this integration.""" + context = create_context("manager", which, self._context_cnt, parent=parent) + self._context_cnt += 1 + return context + + def _separate_entity_ids( + self, + entity_ids: list[str], + data: ServiceData, + ) -> tuple[AdaptiveSwitchMap, list[str]]: + # Create a mapping from switch to entity IDs + # AdaptiveSwitch → entity_ids mapping + switch_to_eids: AdaptiveSwitchMap = {} + skipped: list[str] = [] + for entity_id in entity_ids: + try: + switch = _switch_with_lights( + self.hass, + [entity_id], + # Do not expand light groups, because HA will make a separate light.turn_on + # call where the lights are expanded, and that call will be intercepted. + expand_light_groups=False, + ) + except NoSwitchFoundError: + # Needs to make the original call but without adaptation + skipped.append(entity_id) + _LOGGER.debug( + "No switch found for entity_id='%s', skipped='%s'", + entity_id, + skipped, + ) + else: + if ( + not switch.is_on + or not switch._intercept + # Never adapt on light groups when expanding, because HA will make a separate light.turn_on + or ( + switch._expand_light_groups_flag + and (e := self.hass.states.get(entity_id)) + and _is_light_group(e) + ) + # Prevent adaptation of TURN_ON calls when light is already on, + # and of TOGGLE calls when toggling off. + or self.hass.states.is_state(entity_id, STATE_ON) + or self.manual_control.get(entity_id, False) + or ( + switch._take_over_control + and switch._adapt_only_on_bare_turn_on + and self._mark_manual_control_if_non_bare_turn_on( + entity_id, + data[CONF_PARAMS], + ) + ) + ): + _LOGGER.debug( + "Switch is off or light is already on for entity_id='%s', skipped='%s'" + " (is_on='%s', is_state='%s', manual_control='%s', switch._intercept='%s')", + entity_id, + skipped, + switch.is_on, + self.hass.states.is_state(entity_id, STATE_ON), + self.manual_control.get(entity_id, False), + switch._intercept, + ) + skipped.append(entity_id) + else: + switch_to_eids.setdefault(switch, []).append(entity_id) + return switch_to_eids, skipped + + def _correct_for_multi_light_intercept( + self, + entity_ids: list[str], + switch_to_eids: AdaptiveSwitchMap, + skipped: list[str], + ): + # Check for `multi_light_intercept: true/false` + mli = [sw._multi_light_intercept for sw in switch_to_eids] + more_than_one_switch = len(switch_to_eids) > 1 + single_switch_with_multiple_lights = ( + len(switch_to_eids) == 1 and len(next(iter(switch_to_eids.values()))) > 1 + ) + switch_without_multi_light_intercept = not all(mli) + if more_than_one_switch and switch_without_multi_light_intercept: + _LOGGER.warning( + "Multiple switches (%s) targeted, but not all have" + " `multi_light_intercept: true`, so skipping intercept" + " for all lights.", + switch_to_eids, + ) + skipped = entity_ids + switch_to_eids = {} + elif ( + single_switch_with_multiple_lights and switch_without_multi_light_intercept + ): + _LOGGER.warning( + "Single switch with multiple lights targeted (%s), but" + " `multi_light_intercept: true` is not set, so skipping intercept" + " for all lights.", + switch_to_eids, + ) + skipped = entity_ids + switch_to_eids = {} + return switch_to_eids, skipped + + async def _service_interceptor_turn_on_handler( + self, + call: ServiceCall, + service_data: ServiceData, + ) -> None: + """Intercept `light.turn_on` and `light.toggle` service calls and adapt them. + + It is possible that the calls are made for multiple lights at once, + which in turn might be in different switches or no switches at all. + If there are lights that are not all in a single switch, we need to + make multiple calls to `light.turn_on` with the correct entity IDs. + One of these calls can be intercepted and adapted, the others need to + be adapted by calling `_adapt_light` with the correct entity IDs or + by calling `light.turn_on` directly. + + We create a mapping from switch to entity IDs and keep a list + of skipped lights which are lights in no switches or in switches that + are off or lights that are already on. + + If there is only one switch and 0 skipped lights, we just intercept the + call directly. + + If there are multiple switches and skipped lights, we can adapt the call + for one of the switches to include only the lights in that switch and + need to call `_adapt_light` for the other switches with their + entity_ids. For skipped lights, we call light.turn_on directly with the + entity_ids and original service data. + + If there are only skipped lights, we can use the intercepted call + directly. + """ + is_skipped_hash = is_our_context(call.context, "skipped") + _LOGGER.debug( + "(0) _service_interceptor_turn_on_handler: call.context.id='%s', is_skipped_hash='%s'", + call.context.id, + is_skipped_hash, + ) + if is_our_context(call.context) and not is_skipped_hash: + # Don't adapt our own service calls, but do re-adapt calls that + # were skipped by us + return + + if has_effect_attribute(service_data[CONF_PARAMS]): + return + + _LOGGER.debug( + "(1) _service_interceptor_turn_on_handler: call='%s', service_data='%s'", + call, + service_data, + ) + + # Because `_service_interceptor_turn_on_single_light_handler` modifies the + # original service data, we need to make a copy of it to use in the `skipped` call + service_data_copy = deepcopy(service_data) + + entity_ids = self._get_entity_list(service_data) + # Note: we do not expand light groups anywhere in this method, instead + # we skip them and rely on the followup call that HA will make + # with the expanded entity IDs. + + switch_to_eids, skipped = self._separate_entity_ids( + entity_ids, + service_data, + ) + + ( + switch_to_eids, + skipped, + ) = self._correct_for_multi_light_intercept( + entity_ids, + switch_to_eids, + skipped, + ) + _LOGGER.debug( + "(2) _service_interceptor_turn_on_handler: switch_to_eids='%s', skipped='%s'", + switch_to_eids, + skipped, + ) + + def modify_service_data( + service_data: ServiceData, + entity_ids: list[str], + ) -> dict[str, Any]: + """Modify the service data to contain the entity IDs.""" + for target_key in ( + ATTR_ENTITY_ID, + ATTR_AREA_ID, + ATTR_DEVICE_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, + ): + service_data.pop(target_key, None) + service_data[ATTR_ENTITY_ID] = entity_ids + return service_data + + # Intercept the call for first switch and call _adapt_light for the rest + has_intercepted = False # Can only intercept a turn_on call once + for switch, _entity_ids in switch_to_eids.items(): + transition = service_data[CONF_PARAMS].get( + ATTR_TRANSITION, + switch.initial_transition, + ) + if not has_intercepted: + _LOGGER.debug( + "(3) _service_interceptor_turn_on_handler: intercepting entity_ids='%s'", + _entity_ids, + ) + await self._service_interceptor_turn_on_single_light_handler( + entity_ids=_entity_ids, + switch=switch, + transition=transition, + call=call, + data=modify_service_data(service_data, _entity_ids), + ) + has_intercepted = True + continue + + for eid in _entity_ids: + # Must add a new context otherwise _adapt_light will bail out + context = switch.create_context("intercept", parent=call.context) + self.clear_proactively_adapting(eid) + self.set_proactively_adapting(context.id, eid) + _LOGGER.debug( + "(4) _service_interceptor_turn_on_handler: calling `_adapt_light` with eid='%s', context='%s', transition='%s'", + eid, + context, + transition, + ) + await switch._adapt_light( + light=eid, + context=context, + transition=transition, + ) + + # Call light.turn_on service for skipped entities + if skipped: + if not has_intercepted: + assert set(skipped) == set(entity_ids) + return # The call will be intercepted with the original data + # Call light turn_on service for skipped entities + context = self.create_context("skipped", parent=call.context) + _LOGGER.debug( + "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', service_data: '%s', context='%s'", + skipped, + service_data_copy, # This is the original service data + context.id, + ) + service_data = {ATTR_ENTITY_ID: skipped, **service_data_copy[CONF_PARAMS]} + await self.hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data, + blocking=True, + context=context, + ) + + async def _service_interceptor_turn_on_single_light_handler( + self, + entity_ids: list[str], + switch: AdaptiveSwitch, + transition: int, + call: ServiceCall, + data: ServiceData, + ): + _LOGGER.debug( + "Intercepted TURN_ON call with data %s (%s)", + data, + call.context.id, + ) + + # Reset because turning on the light, this also happens in + # `state_changed_event_listener`, however, this function is called + # before that one. + self.reset(*entity_ids, reset_manual_control=False) + for eid in entity_ids: + self.clear_proactively_adapting(eid) + + adaptation_data = await switch.prepare_adaptation_data( + entity_ids[0], + transition, + context=switch.create_context("adapt_lights", parent=call.context), + ) + if adaptation_data is None: + return + + # Take first adaptation item to apply it to this service call + first_service_data = await adaptation_data.next_service_call_data() + + if not first_service_data: + return + + # Update/adapt service call data + first_service_data.pop(ATTR_ENTITY_ID, None) + # This is called as a preprocessing step by the schema validation of the original + # service call and needs to be repeated here to also process the added adaptation data. + # (A more generic alternative would be re-executing the validation, but that is more + # complicated and unstable because it requires transformation of the data object back + # into its original service call structure which cannot be reliably done due to the + # lack of a bijective mapping.) + preprocess_turn_on_alternatives(self.hass, first_service_data) + data[CONF_PARAMS].update(first_service_data) + + # Schedule additional service calls for the remaining adaptation data. + # We cannot know here whether there is another call to follow (since the + # state can change until the next call), so we just schedule it and let + # it sort out by itself. + already_applied = get_light_control_attributes(first_service_data) + shared_sleep_time = adaptation_data.sleep_time + for index, entity_id in enumerate(entity_ids): + self.set_proactively_adapting(call.context.id, entity_id) + if index: + # Each member needs its own remaining commands and cancellation. + # Consuming its first iterator item could discard a color command + # when only the shared brightness command has been applied. + adaptation_data = await switch.prepare_adaptation_data( + entity_id, + transition, + context=switch.create_context("adapt_lights", parent=call.context), + already_applied=already_applied, + ) + if adaptation_data is None or not adaptation_data.max_length: + continue + self.set_proactively_adapting(adaptation_data.context.id, entity_id) + # Every follow-up waits for the shared first command, even when a + # member's capabilities give it a different number of split phases. + adaptation_data.sleep_time = shared_sleep_time + adaptation_data.initial_sleep = True + + # Don't await to avoid blocking the service call. + # Assign to a variable only to await in tests. + self.adaptation_tasks.add( + asyncio.create_task( + switch.execute_cancellable_adaptation_calls(adaptation_data), + ), + ) + # Remove tasks that are done + if done_tasks := [t for t in self.adaptation_tasks if t.done()]: + self.adaptation_tasks.difference_update(done_tasks) + + def _handle_timer( + self, + light: str, + timers_dict: dict[str, _AsyncSingleShotTimer], + delay: float | None, + reset_coroutine: Callable[[], Coroutine[Any, Any, None]], + ) -> None: + timer = timers_dict.get(light) + if timer is not None: + if delay is None: # Timer object exists, but should not anymore + timer.cancel() + timers_dict.pop(light) + else: # Timer object already exists, just update the delay and restart it + timer.delay = delay + timer.start() + elif delay is not None: # Timer object does not exist, create it + timer = _AsyncSingleShotTimer(delay, reset_coroutine) + timers_dict[light] = timer + timer.start() + + def start_transition_timer(self, light: str) -> None: + """Mark a light as manually controlled.""" + last_service_data = self.last_service_data.get(light) + if last_service_data is None: + _LOGGER.debug( + "No last service data for light %s, not starting timer.", + light, + ) + return + + last_transition = last_service_data.get(ATTR_TRANSITION) + if not last_transition: + _LOGGER.debug( + "No transition in last adapt for light %s, not starting timer.", + light, + ) + return + + _LOGGER.debug( + "Start transition timer of %s seconds for light %s", + last_transition, + light, + ) + + async def reset() -> None: + # Called when the timer expires, doesn't need to do anything + _LOGGER.debug( + "Transition finished for light %s", + light, + ) + + self._handle_timer(light, self.transition_timers, last_transition, reset) + + def set_auto_reset_manual_control_times( + self, + lights: list[str], + time: float, + ) -> None: + """Set the time after which the lights are automatically reset.""" + if time == 0: + return + for light in lights: + old_time = self.auto_reset_manual_control_times.get(light) + if (old_time is not None) and (old_time != time): + _LOGGER.info( + "Setting auto_reset_manual_control for '%s' from %s seconds to %s seconds." + " This might happen because the light is in multiple swiches" + " or because of a config change.", + light, + old_time, + time, + ) + self.auto_reset_manual_control_times[light] = time + + def get_manual_control_attributes( + self, + light: str, + ) -> LightControlAttributes: + """Get the attributes for a light that are manually controlled.""" + return self.manual_control.get(light, LightControlAttributes.NONE) + + def set_manual_control_attributes( + self, + light: str, + attributes: LightControlAttributes = LightControlAttributes.ALL, + ) -> None: + """Mark attributes of a light as manually controlled.""" + _LOGGER.debug( + "Light %s: Setting manual control attributes to %s (from %s).", + light, + attributes, + self.get_manual_control_attributes(light), + ) + self.manual_control[light] = attributes + delay = self.auto_reset_manual_control_times.get(light) + + async def reset() -> None: + _LOGGER.debug( + "Auto resetting 'manual_control' status of '%s' because" + " it was not manually controlled for %s seconds.", + light, + delay, + ) + self.reset(light) + switches = _switches_with_lights( + self.hass, + [light], + expand_light_groups=False, + ) + for switch in switches: + if not switch.is_on: + continue + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("autoreset"), + lights=[light], + transition=switch.initial_transition, + force=True, + ) + + self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) + self._schedule_manual_control_state_update(light) + + def _schedule_manual_control_state_update(self, *lights: str) -> None: + """Publish shared manual-control state on every affected switch.""" + # State publication must not expand groups or change tracked lights. + for entry in self.hass.config_entries.async_entries(DOMAIN): + entry_data = self.hass.data[DOMAIN].get(entry.entry_id) + if entry_data is None: + continue + switch = entry_data.get(SWITCH_DOMAIN) + if switch is not None and set(lights).intersection(switch.lights): + switch.async_schedule_update_ha_state() + + def add_manual_control_attributes( + self, + light: str, + attributes: LightControlAttributes, + ) -> None: + """Add attributes to the manual control status of a light.""" + current = self.get_manual_control_attributes(light) + _LOGGER.debug( + "Light %s: Adding manual control attributes %s (current: %s).", + light, + attributes, + current, + ) + new = current | attributes + self.set_manual_control_attributes(light, new) + + def invalidate_manual_control_state( + self, + light: str, + attributes: LightControlAttributes, + ) -> None: + """Stop comparing adapted attributes with an older physical state.""" + states = self.last_manual_control_state.get(light) + pending = self.pending_manual_control_state.get(light) + for attribute in LightControlAttributes: + if attribute in attributes and states is not None: + states.pop(attribute, None) + if attribute in attributes and pending is not None: + pending.pop(attribute, None) + if states == {}: + self.last_manual_control_state.pop(light) + if pending == {}: + self.pending_manual_control_state.pop(light) + + def update_manual_control_state( + self, + light: str, + state: State, + attributes: LightControlAttributes, + ) -> None: + """Record the reported state that established manual control of each axis.""" + states = self.last_manual_control_state.setdefault(light, {}) + for attribute in LightControlAttributes: + if attribute in attributes: + states[attribute] = dict(state.attributes) + + def mark_manual_control_state_pending( + self, + light: str, + attributes: LightControlAttributes, + context_id: str, + ) -> None: + """Wait for the reported state produced by a tracked service call.""" + pending = self.pending_manual_control_state.setdefault(light, {}) + for attribute in LightControlAttributes: + if attribute in attributes: + pending[attribute] = context_id + + def consume_pending_manual_control_state( + self, + light: str, + state: State, + context_id: str | None = None, + ) -> None: + """Record a tracked service's reported state once it is available.""" + pending = self.pending_manual_control_state.get(light) + if pending is None: + return + attributes = LightControlAttributes.NONE + for attribute, pending_context_id in tuple(pending.items()): + if context_id is None or context_id == pending_context_id: + attributes |= attribute + pending.pop(attribute) + if not pending: + self.pending_manual_control_state.pop(light) + if attributes: + self.update_manual_control_state(light, state, attributes) + + def get_adaption_control_attributes( + self, + switch: AdaptiveSwitch, + light: str, + ) -> LightControlAttributes: + """Get the attributes that should be adapted for a light. + + Determines the attributes that should actually be adapted from the attributes + marked as manually controlled, the state of adaptation switches, and the adaptation + configuration. + + Example 1: When no attributes are marked as manually controlled and all adaptation + switches are on, all attributes are returned. + + Example 2: When no attributes are marked as manually controlled and the brightness + adaptation switch is off, only the color attribute is returned. + + Example 3: When only brightness is marked as manually controlled, but the configuration + specifies to pause all adaptations on manual change, no attributes are returned so that + color is also not adapted. + """ + denied_adaptation_attributes = self.get_manual_control_attributes(light) + + if ( + denied_adaptation_attributes.has_any() + and switch._take_over_control_mode == TakeOverControlMode.PAUSE_ALL + ): + # Extend to pausing all only if there is at least one manually controlled attribute + denied_adaptation_attributes = LightControlAttributes.ALL + + enabled_adaptation_attributes = ( + LightControlAttributes.BRIGHTNESS + if switch.adapt_brightness_switch.is_on + else LightControlAttributes.NONE + ) | ( + LightControlAttributes.COLOR + if switch.adapt_color_switch.is_on + else LightControlAttributes.NONE + ) + + return ( + LightControlAttributes.ALL + & ~denied_adaptation_attributes + & enabled_adaptation_attributes + ) + + def cancel_ongoing_adaptation_calls( + self, + light_id: str, + ) -> None: + """Cancel ongoing adaptation service calls for a specific light entity.""" + brightness_task = self.adaptation_tasks_brightness.get(light_id) + color_task = self.adaptation_tasks_color.get(light_id) + if brightness_task is not None and not brightness_task.done(): + _LOGGER.debug( + "Cancelled ongoing brightness adaptation calls (%s) for '%s'", + brightness_task, + light_id, + ) + brightness_task.cancel() + if color_task is not None and not color_task.done(): + _LOGGER.debug( + "Cancelled ongoing color adaptation calls (%s) for '%s'", + color_task, + light_id, + ) + # color_task might be the same as brightness_task + color_task.cancel() + + def reset(self, *lights: str, reset_manual_control: bool = True) -> None: + """Reset the 'manual_control' status of the lights.""" + for light in lights: + if reset_manual_control: + _LOGGER.debug( + "Light %s: Clearing manual control attributes.", + light, + ) + self.manual_control[light] = LightControlAttributes.NONE + self.last_manual_control_state.pop(light, None) + self.pending_manual_control_state.pop(light, None) + if timer := self.auto_reset_manual_control_timers.pop(light, None): + timer.cancel() + self.our_last_state_on_change.pop(light, None) + self.last_service_data.pop(light, None) + self.cancel_ongoing_adaptation_calls(light) + if reset_manual_control: + self._schedule_manual_control_state_update(*lights) + + def remove_lights(self, *lights: str) -> None: + """Retire tracking and pending work for targets no profile owns anymore.""" + self.reset(*lights) + for light in lights: + self.lights.discard(light) + self.clear_proactively_adapting(light) + if timer := self.transition_timers.pop(light, None): + timer.cancel() + if task := self.sleep_tasks.pop(light, None): + task.cancel() + for records in ( + self.manual_control, + self.auto_reset_manual_control_times, + self.turn_on_event, + self.turn_off_event, + self.toggle_event, + self.on_to_off_event, + self.off_to_on_event, + self.turn_off_locks, + self.adaptation_tasks_brightness, + self.adaptation_tasks_color, + ): + records.pop(light, None) + + def _get_entity_list(self, service_data: ServiceData) -> list[str]: + return sorted( + entity_id + for entity_id in target_entities(self.hass, service_data) + if entity_id.startswith(f"{LIGHT_DOMAIN}.") + ) + + async def turn_on_off_event_listener(self, event: Event) -> None: + """Track 'light.turn_off' and 'light.turn_on' service calls.""" + domain = event.data.get(ATTR_DOMAIN) + if domain != LIGHT_DOMAIN: + return + + service = event.data[ATTR_SERVICE] + service_data = event.data[ATTR_SERVICE_DATA] + entity_ids = self._get_entity_list(service_data) + + if not any(eid in self.lights for eid in entity_ids): + return + + def off(eid: str, event: Event) -> None: + self.turn_off_event[eid] = event + self.reset(eid) + + async def on(eid: str, event: Event) -> None: + task = self.sleep_tasks.get(eid) + if task is not None: + task.cancel() + self.turn_on_event[eid] = event + + # Only check for manual control via this path if the light was already ON. + # Turning on from OFF is handled separately in _respond_to_off_to_on_event, + # where adapt_only_on_bare_turn_on can mark lights as manually controlled. + # Fix for https://github.com/basnijholt/adaptive-lighting/issues/1378 + state = self.hass.states.get(eid) + if state is not None and state.state == STATE_ON: + switches = _switches_with_lights( + self.hass, + [eid], + expand_light_groups=False, + ) + for switch in switches: + # Preserve tracking for a lone profile, including when off. + # Shared lights notify each enabled owner using its takeover policy. + if switch.is_on or len(switches) == 1: + await self.update_manually_controlled_from_event( + switch, + eid, + force=False, + ) + + timer = self.auto_reset_manual_control_timers.get(eid) + if ( + timer is not None + and timer.is_running() + and not is_our_context(event.context) + and not self.is_proactively_adapting(event.context.id) + and event.time_fired > timer.start_time # type: ignore[operator] + ): + # Only external turn-ons extend manual control, not our adaptations. + timer.start() + + if service == SERVICE_TURN_OFF: + transition = service_data.get(ATTR_TRANSITION) + _LOGGER.debug( + "Detected an 'light.turn_off('%s', transition=%s)' event with context.id='%s'", + entity_ids, + transition, + event.context.id, + ) + for eid in entity_ids: + off(eid, event) + + elif service == SERVICE_TURN_ON: + _LOGGER.debug( + "Detected an 'light.turn_on('%s')' event with context.id='%s'", + entity_ids, + event.context.id, + ) + for eid in entity_ids: + await on(eid, event) + + elif service == SERVICE_TOGGLE: + _LOGGER.debug( + "Detected an 'light.toggle('%s')' event with context.id='%s'", + entity_ids, + event.context.id, + ) + for eid in entity_ids: + state = self.hass.states.get(eid) + assert state + self.toggle_event[eid] = event + if state.state == STATE_ON: # is turning off + off(eid, event) + elif state.state == STATE_OFF: # is turning on + await on(eid, event) + + async def state_changed_event_listener( # noqa: PLR0912 + self, + event: Event[EventStateChangedData], + ) -> None: + """Track 'state_changed' events.""" + entity_id = event.data.get(ATTR_ENTITY_ID, "") + if entity_id not in self.lights: + return + + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + new_on = ( + new_state if new_state is not None and new_state.state == STATE_ON else None + ) + new_off = ( + new_state + if new_state is not None and new_state.state == STATE_OFF + else None + ) + old_on = ( + old_state if old_state is not None and old_state.state == STATE_ON else None + ) + old_off = ( + old_state + if old_state is not None and old_state.state == STATE_OFF + else None + ) + if new_on: + _LOGGER.debug( + "Detected a '%s' 'state_changed' event: '%s' with context.id='%s'", + entity_id, + new_on.attributes, + new_on.context.id, + ) + # It is possible to have multiple state change events with the same context. + # This can happen because a `turn_on.light(brightness_pct=100, transition=30)` + # event leads to an instant state change of + # `new_state=dict(brightness=100, ...)`. However, after polling the light + # could still only be `new_state=dict(brightness=50, ...)`. + # We save all events because the first event change might indicate at what + # settings the light will be later *or* the second event might indicate a + # final state. The latter case happens for example when a light was + # called with a color_temp outside of its range (and HA reports the + # incorrect 'min_kelvin' and 'max_kelvin', which happens e.g., for + # Philips Hue White GU10 Bluetooth lights). + last_state: list[State] | None = self.our_last_state_on_change.get( + entity_id, + ) + if is_our_context(new_on.context): + if ( + last_state is not None + and last_state[0].context.id == new_on.context.id + ): + _LOGGER.debug( + "AdaptiveLightingManager: State change event of '%s' is already" + " in 'self.our_last_state_on_change' (%s)" + " adding this state also", + entity_id, + new_on.context.id, + ) + self.our_last_state_on_change[entity_id].append(new_on) + else: + _LOGGER.debug( + "AdaptiveLightingManager: New adapt '%s' found for %s", + new_on, + entity_id, + ) + self.our_last_state_on_change[entity_id] = [new_on] + self.start_transition_timer(entity_id) + elif last_state is not None: + self.our_last_state_on_change[entity_id].append(new_on) + self.consume_pending_manual_control_state( + entity_id, + new_on, + new_on.context.id, + ) + + if old_on and not new_on: + # Availability loss invalidates pending commands, not manual state. + self.cancel_ongoing_adaptation_calls(entity_id) + + if old_on and new_off: + # Tracks 'on' → 'off' state changes + self.on_to_off_event[entity_id] = event + self.reset(entity_id) + _LOGGER.debug( + "Detected an 'on' → 'off' event for '%s' with context.id='%s'", + entity_id, + event.context.id, + ) + elif old_off and new_on: + # Tracks 'off' → 'on' state changes + self.off_to_on_event[entity_id] = event + _LOGGER.debug( + "Detected an 'off' → 'on' event for '%s' with context.id='%s'", + entity_id, + event.context.id, + ) + + if self.is_proactively_adapting(event.context.id): + _LOGGER.debug( + "Skipping responding to 'off' → 'on' event for '%s' with context.id='%s' because" + " we are already proactively adapting", + entity_id, + event.context.id, + ) + # Note: the reset below already happened in `_service_interceptor_turn_on_handler` + return + + self.reset(entity_id, reset_manual_control=False) + lock = self.turn_off_locks.setdefault(entity_id, asyncio.Lock()) + async with lock: + if await self.just_turned_off(entity_id): + # Stop if a rapid 'off' → 'on' → 'off' happens. + _LOGGER.debug( + "Cancelling adjusting lights for %s", + entity_id, + ) + return + + switches = _switches_with_lights( + self.hass, + [entity_id], + expand_light_groups=False, + ) + for switch in switches: + if switch.is_on: + await switch._respond_to_off_to_on_event( + entity_id, + event, + ) + + async def update_manually_controlled_from_event( + self, + switch: AdaptiveSwitch, + light: str, + force: bool, + ) -> None: + """Check if the light has been manually controlled by the latest turn on event.""" + if not switch._take_over_control: + return + + turn_on_event = self.turn_on_event.get(light) + + if ( + turn_on_event is None + or self.is_proactively_adapting(turn_on_event.context.id) + or is_our_context(turn_on_event.context) + or force + ): + return + + turn_on_attributes = get_light_control_attributes( + turn_on_event.data[ATTR_SERVICE_DATA], + ) + + if not turn_on_attributes: + return + + # Light was already on and 'light.turn_on' was not called by + # the adaptive_lighting integration. + self.mark_manual_control_state_pending( + light, + turn_on_attributes, + turn_on_event.context.id, + ) + self.add_manual_control_attributes(light, turn_on_attributes) + switch.fire_manual_control_event(light, turn_on_event.context) + _LOGGER.debug( + "'%s' was already on and 'light.turn_on' was not called by the" + " adaptive_lighting integration (context.id='%s'), the Adaptive" + " Lighting will stop adapting %s of the light until the switch or the" + " light turns off and then on again.", + light, + turn_on_event.context.id, + turn_on_attributes, + ) + + async def update_manually_controlled_from_untracked_change( + self, + switch: AdaptiveSwitch, + light: str, + force: bool, + context: Context, + ) -> None: + """Check if the light has been manually controlled from an untracked change. + + An untracked change is a change that has been made outsideof HA and is + therefore not visible through events. + """ + if not switch._take_over_control or not switch._detect_non_ha_changes or force: + return + + # Note: This call updates the state of the light + # so it might suddenly be off. + significantly_changed_attributes = await self.significant_change( + switch, + light, + context, + ) + + if not significantly_changed_attributes: + return + + self.add_manual_control_attributes( + light, + significantly_changed_attributes, + ) + switch.fire_manual_control_event(light, context) + + async def significant_change( + self, + switch: AdaptiveSwitch, + light: str, + context: Context, # just for logging + ) -> LightControlAttributes: + """Has the light made a significant change since last update. + + This method will detect changes that were made to the light without + calling 'light.turn_on', so outside of Home Assistant. + """ + assert switch._detect_non_ha_changes + + last_service_data = self.last_service_data.get(light) + if last_service_data is None: + return LightControlAttributes.NONE + # Update state and check for a manual change not done in HA. + # Ensure HASS is correctly updating your light's state with + # light.turn_on calls if any problems arise. This + # can happen e.g. using zigbee2mqtt with 'report: false' in device settings. + await async_update_entity(self.hass, light) + refreshed_state = self.hass.states.get(light) + assert refreshed_state is not None + self.consume_pending_manual_control_state(light, refreshed_state) + + manual_control = self.get_manual_control_attributes(light) + manual_control_states = self.last_manual_control_state.get(light, {}) + changed_attributes = LightControlAttributes.NONE + for attribute in LightControlAttributes: + old_attributes = ( + manual_control_states.get(attribute, last_service_data) + if attribute in manual_control + else last_service_data + ) + changed_attributes |= attribute & _attributes_have_changed( + old_attributes=dict(old_attributes), + new_attributes=refreshed_state.attributes, + light=light, + context=context, + ) + if changed_attributes: + self.update_manual_control_state( + light, + refreshed_state, + changed_attributes, + ) + _LOGGER.debug( + "%s: State attributes %s of '%s' changed (%s) wrt 'last_service_data' (%s) (context.id=%s)", + switch._name, + changed_attributes, + light, + refreshed_state.attributes, + last_service_data, + context.id, + ) + else: + _LOGGER.debug( + "%s: State attributes of '%s' did not change (%s) wrt 'last_service_data' (%s) (context.id=%s)", + switch._name, + light, + refreshed_state.attributes, + last_service_data, + context.id, + ) + return changed_attributes + + def _off_to_on_state_event_is_from_turn_on( + self, + entity_id: str, + off_to_on_event: Event[EventStateChangedData], + ) -> bool: + # Adaptive Lighting should never turn on lights itself + if is_our_context(off_to_on_event.context) and not is_our_context( + off_to_on_event.context, + "service", # adaptive_lighting.apply is allowed to turn on lights + ): + _LOGGER.warning( + "Detected an 'off' → 'on' event for '%s' with context.id='%s'," + " triggered by the adaptive_lighting integration itself," + " which *should* not happen. If you see this please submit an issue with" + " your full logs at https://github.com/basnijholt/adaptive-lighting", + entity_id, + off_to_on_event.context.id, + ) + _LOGGER.debug( + "Full 'off' → 'on' event for '%s': %s", + entity_id, + off_to_on_event, + ) + turn_on_event: Event | None = self.turn_on_event.get(entity_id) + id_off_to_on = off_to_on_event.context.id + return turn_on_event is not None and id_off_to_on == turn_on_event.context.id + + def _member_turn_on_explains_group_turn_on( + self, + entity_id: str, + off_event: Event, + off_to_on_event: Event[EventStateChangedData], + ) -> bool: + """Check if a light group's 'off' → 'on' is caused by a member's 'light.turn_on'. + + When a member of a light group is turned on while the group is off, the + group turns on as a side effect. Home Assistant may reuse the context of + an earlier 'light.turn_off' call for the group's state change (entities + keep their context for a few seconds), which makes the group's turn-on + look like a polling artifact of the turn-off. + See https://github.com/basnijholt/adaptive-lighting/issues/1378 + """ + state = self.hass.states.get(entity_id) + if state is None or not _is_light_group(state): + return False + members: list[str] = state.attributes[ATTR_ENTITY_ID] + for member in members: + member_turn_on = self.turn_on_event.get(member) + if ( + member_turn_on is not None + and off_event.time_fired + < member_turn_on.time_fired + <= off_to_on_event.time_fired + ): + _LOGGER.debug( + "just_turned_off: Light group '%s' turned on because its member" + " '%s' was turned on (context.id='%s'), so this is a legitimate" + " turn-on, not a polling artifact.", + entity_id, + member, + member_turn_on.context.id, + ) + return True + return False + + def _off_to_on_event_is_during_turn_off( + self, + entity_id: str, + off_to_on_event: Event[EventStateChangedData], + ) -> bool: + """Check if a reported turn-on belongs to a recent turn-off window.""" + turn_off_event = self.turn_off_event.get(entity_id) + if ( + turn_off_event is None + or off_to_on_event.context.id != turn_off_event.context.id + ): + return False + + turn_on_event = self.turn_on_event.get(entity_id) + if ( + turn_on_event is not None + and turn_off_event.time_fired + < turn_on_event.time_fired + <= off_to_on_event.time_fired + ): + return False + if self._member_turn_on_explains_group_turn_on( + entity_id, + turn_off_event, + off_to_on_event, + ): + return False + + transition = _turn_off_transition(turn_off_event) + delay = max(transition or 0, TURNING_OFF_DELAY) + elapsed = (dt_util.utcnow() - turn_off_event.time_fired).total_seconds() + if not 0 <= elapsed <= delay: + return False + + _LOGGER.debug( + "just_turned_off: Fresh 'light.turn_off' for '%s' shares the" + " 'off' → 'on' context; ignoring the state during its %s second" + " transition window.", + entity_id, + delay, + ) + return True + + async def just_turned_off( # noqa: PLR0911, PLR0912 + self, + entity_id: str, + ) -> bool: + """Cancel the adjusting of a light if it has just been turned off. + + Possibly the lights just got a 'turn_off' call, however, the light + is actually still turning off (e.g., because of a 'transition') and + HA polls the light before the light is 100% off. This might trigger + a rapid switch 'off' → 'on' → 'off'. To prevent this component + from interfering on the 'on' state, we make sure to wait at least + TURNING_OFF_DELAY (or the 'turn_off' transition time) between a + 'off' → 'on' event and then check whether the light is still 'on' or + if the brightness is still decreasing. Only if it is the case we + adjust the lights. + """ + off_to_on_event = self.off_to_on_event[entity_id] + on_to_off_event = self.on_to_off_event.get(entity_id) + if self._off_to_on_event_is_during_turn_off(entity_id, off_to_on_event): + return True + + if on_to_off_event is None: + _LOGGER.debug( + "just_turned_off: No 'on' → 'off' state change has been registered before for '%s'." + " It's possible that the light was already on when Home Assistant was turned on.", + entity_id, + ) + return False + + if off_to_on_event.context.id == on_to_off_event.context.id: + # Matching context IDs usually mean a polling artifact (HA briefly + # reports 'on' while the light is still turning off). However, the + # context is also reused when e.g. one automation turns the light + # off and later back on, or when an integration writes the state + # with the entity's cached context. Only treat the state change as + # a legitimate turn-on if a 'light.turn_on' call for this light (or + # for a member of this light group) fired between the two state + # changes. + turn_on_event = self.turn_on_event.get(entity_id) + if ( + turn_on_event is not None + and on_to_off_event.time_fired + < turn_on_event.time_fired + <= off_to_on_event.time_fired + ): + _LOGGER.debug( + "just_turned_off: 'light.turn_on' was called for '%s' between its" + " 'on' → 'off' and 'off' → 'on' state changes, so this is a" + " legitimate turn-on, not a polling artifact.", + entity_id, + ) + return False + if self._member_turn_on_explains_group_turn_on( + entity_id, + on_to_off_event, + off_to_on_event, + ): + return False + _LOGGER.debug( + "just_turned_off: 'on' → 'off' state change has the same context.id as the" + " 'off' → 'on' state change for '%s'. This is probably a false positive.", + entity_id, + ) + return True + + id_on_to_off = on_to_off_event.context.id + + turn_off_event = self.turn_off_event.get(entity_id) + if turn_off_event is not None: + transition = _turn_off_transition(turn_off_event) + else: + transition = None + + if self._off_to_on_state_event_is_from_turn_on(entity_id, off_to_on_event): + is_toggle = off_to_on_event == self.toggle_event.get(entity_id) + from_service = "light.toggle" if is_toggle else "light.turn_on" + _LOGGER.debug( + "just_turned_off: State change 'off' → 'on' triggered by '%s'", + from_service, + ) + return False + + if ( + turn_off_event is not None + and id_on_to_off == turn_off_event.context.id + and transition is not None # 'turn_off' is called with transition=... + ): + # State change 'on' → 'off' and 'light.turn_off(..., transition=...)' come + # from the same event, so wait at least the 'turn_off' transition time. + delay = max(transition, TURNING_OFF_DELAY) + else: + # State change 'off' → 'on' happened because the light state was set. + # Possibly because of polling. + delay = TURNING_OFF_DELAY + + delta_time = (dt_util.utcnow() - on_to_off_event.time_fired).total_seconds() + if delta_time > delay: + _LOGGER.debug( + "just_turned_off: delta_time='%s' > delay='%s'", + delta_time, + delay, + ) + return False + + # Here we could just `return True` but because we want to prevent any updates + # from happening to this light (through async_track_time_interval or + # sleep_state) for some time, we wait below until the light + # is 'off' or the time has passed. + + delay -= delta_time # delta_time has passed since the 'off' → 'on' event + _LOGGER.debug( + "just_turned_off: Waiting with adjusting '%s' for %s", + entity_id, + delay, + ) + total_sleep = 0 + for _ in range(3): + # It can happen that the actual transition time is longer than the + # specified time in the 'turn_off' service. + coro = asyncio.sleep(delay) + total_sleep += delay + task = self.sleep_tasks[entity_id] = asyncio.ensure_future(coro) + try: + await task + except asyncio.CancelledError: # 'light.turn_on' has been called + _LOGGER.debug( + "just_turned_off: Sleep task is cancelled due to 'light.turn_on('%s')' call", + entity_id, + ) + return False + + if not is_on(self.hass, entity_id): + _LOGGER.debug( + "just_turned_off: '%s' is off after %s seconds, cancelling adaptation", + entity_id, + total_sleep, + ) + return True + delay = TURNING_OFF_DELAY # next time only wait this long + + if transition is not None: + # Always ignore when there's a 'turn_off' transition. + # Because it seems like HA cannot detect whether a light is + # transitioning into 'off'. Maybe needs some discussion/input? + return True + + # Now we assume that the lights are still on and they were intended + # to be on. + _LOGGER.debug( + "just_turned_off: '%s' is still on after %s seconds, assuming it was intended to be on", + entity_id, + total_sleep, + ) + return False + + def _mark_manual_control_if_non_bare_turn_on( + self, + entity_id: str, + service_data: ServiceData, + ) -> bool: + """Mark light as manually controlled if turn_on call has brightness/color attributes. + + This is used by adapt_only_on_bare_turn_on to mark lights as manually controlled + when they are turned on with specific attributes (e.g., from a scene). + This ensures scenes persist and AL doesn't override them. + """ + _LOGGER.debug( + "_mark_manual_control_if_non_bare_turn_on: entity_id='%s', service_data='%s'", + entity_id, + service_data, + ) + manual_control_attributes = get_light_control_attributes(service_data) + + if manual_control_attributes: + self.set_manual_control_attributes(entity_id, manual_control_attributes) + return True + + return False + + +class _AsyncSingleShotTimer: + def __init__(self, delay: float, callback: Callable[[], Any | None]) -> None: + """Initialize the timer.""" + self.delay = delay + self.callback = callback + self.task = None + self.start_time: datetime.datetime | None = None + + async def _run(self) -> None: + """Run the timer. Don't call this directly, use start() instead.""" + await asyncio.sleep(self.delay) + if self.callback: + if asyncio.iscoroutinefunction(self.callback): + await self.callback() + else: + self.callback() + + def is_running(self) -> bool: + """Return whether the timer is running.""" + return self.task is not None and not self.task.done() + + def start(self) -> None: + """Start the timer.""" + if self.task is not None and not self.task.done(): + self.task.cancel() + # Set start_time before creating task to avoid race condition + # where is_running() returns True but start_time is still None + # See: https://github.com/basnijholt/adaptive-lighting/issues/1272 + self.start_time = dt_util.utcnow() + self.task = asyncio.create_task(self._run()) + + def cancel(self) -> None: + """Cancel the timer.""" + # Never cancel the task that is currently running our own callback, e.g. + # when the auto-reset callback calls manager.reset(), which cancels the + # timer it is running in. That used to silently cancel the rest of the + # callback (the re-adaptation), see issue #1233. + try: + current_task = asyncio.current_task() + except RuntimeError: # no running event loop + current_task = None + if self.task and self.task is not current_task: + self.task.cancel() + self.callback = None + + def remaining_time(self) -> float: + """Return the remaining time before the timer expires.""" + if self.start_time is not None: + elapsed_time = (dt_util.utcnow() - self.start_time).total_seconds() + return max(0, self.delay - elapsed_time) + return 0 diff --git a/custom_components/adaptive_lighting/translations/LICENSE.md b/custom_components/adaptive_lighting/translations/LICENSE.md new file mode 100644 index 00000000..5916442d --- /dev/null +++ b/custom_components/adaptive_lighting/translations/LICENSE.md @@ -0,0 +1,44 @@ +This license **only** applies to all files in `custom_components/adaptive_lighting/translations/` in the Adaptive Lighting repository. +For these translations we wave copyright and related rights through the CC0 1.0 Universal license. + +# Creative Commons CC0 1.0 Universal + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +### Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. **Copyright and Related Rights.** A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. + +2. **Waiver.** To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. + +3. **Public License Fallback.** Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. + +4. **Limitations and Disclaimers.** + + a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. diff --git a/custom_components/adaptive_lighting/translations/af.json b/custom_components/adaptive_lighting/translations/af.json new file mode 100644 index 00000000..84f5c9a3 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/af.json @@ -0,0 +1,46 @@ +{ + "services": { + "apply": { + "description": "Pas die huidige Adaptive Lighting-instellings op ligte toe.", + "fields": { + "lights": { + "description": "'n Lig (of lys van ligte) om die instellings op toe te pas. 💡" + } + } + }, + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Pas ligte net aan wanneer hulle aangeskakel is (`true`) of hou aan om dit aan te pas (`false`)" + }, + "sunrise_offset": { + "description": "Pas sonsopkomstyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰" + }, + "sunset_offset": { + "description": "Pas sonsondergangtyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Aanpasbare beligting opsies", + "data": {}, + "data_description": {}, + "sections": { + "advanced": { + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer ligte aanvanklik aangeskakel word. As dit op \"true\" gestel is, pas AL slegs aan as \"light.turn_on\" opgeroep word sonder om kleur of helderheid te spesifiseer. ❌🌈 Dit verhoed bv. aanpassing wanneer 'n toneel geaktiveer word. As `onwaar`, pas AL aan ongeag die teenwoordigheid van kleur of helderheid in die aanvanklike `diens_data`. Moet `oorname_beheer` geaktiveer moet word. 🕵️ " + }, + "data_description": { + "sunrise_offset": "Pas sonsopkomstyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰", + "sunset_offset": "Pas sonsondergangtyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰" + } + } + } + } + } + }, + "title": "Aanpasbare beligting" +} diff --git a/custom_components/adaptive_lighting/translations/bg.json b/custom_components/adaptive_lighting/translations/bg.json new file mode 100644 index 00000000..a4e62001 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/bg.json @@ -0,0 +1,284 @@ +{ + "title": "Адаптивно осветление", + "config": { + "step": { + "user": { + "title": "Изберете име за инстанцията на Адаптивно осветление", + "description": "Всяка инстанция може да съдържа множество лампи!", + "data": { + "name": "Име" + } + }, + "menu": { + "title": "Създай или дублирай", + "description": "Искате ли да създадете нов екземпляр или да дублирате съществуващ?", + "data": { + "action": "Действие" + } + } + }, + "abort": { + "already_configured": "Това устройство е вече конфигурирано" + } + }, + "options": { + "step": { + "init": { + "title": "Настройки на Адаптивно осветление", + "description": "Конфигурирайте компонент за Адаптивно осветление. Имената на опциите съвпадат с настройките на YAML. Ако сте дефинирали този запис в YAML, няма да се появят опции тук. За интерактивни графики, които демонстрират ефектите на параметрите, посетете [това уеб приложение]({webapp_url}). За повече подробности, вижте [официалната документация]({docs_url}).", + "data": { + "lights": "lights: Списък от entity_ids на лампи за контрол (може да е празен). 🌟", + "interval": "интервал", + "transition": "преход", + "min_brightness": "min_brightness: Минимален процент на яркост. 💡", + "max_brightness": "max_brightness: Максимален процент на яркост. 💡", + "min_color_temp": "min_color_temp: Най-топла цветова температура в Келвини. 🔥", + "max_color_temp": "max_color_temp: Най-студена цветова температура в Келвини. ❄️", + "sleep_brightness": "яркост при сън", + "sleep_color_temp": "цветова температура при сън" + }, + "data_description": { + "interval": "Честота за адаптиране на лампите, в секунди. 🔄", + "transition": "Продължителност на прехода, когато лампите се променят, в секунди. 🕑", + "sleep_brightness": "Процент на яркостта на лампите в режим на сън. 😴", + "sleep_color_temp": "Цветова температура в режим на сън (използва се, когато \"sleep_rgb_or_color_temp\" е \"color_temp\") в Келвин. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "начален преход", + "prefer_rgb_color": "prefer_rgb_color: Дали да се предпочита RGB цветова корекция пред температура на светлината, когато е възможно. 🌈", + "sleep_rgb_or_color_temp": "RGB или цветова температура при сън", + "sleep_rgb_color": "RGB цвят при сън", + "sleep_transition": "преход при сън", + "transition_until_sleep": "transition_until_sleep: Когато е активирано, Adaptive Lighting ще третира настройките за сън като минимум, преминавайки към тези стойности след залез. 🌙", + "sunrise_time": "време на изгрев", + "min_sunrise_time": "минимално време на изгрев", + "max_sunrise_time": "максимално време на изгрев", + "sunrise_offset": "отместване на изгрева", + "sunset_time": "време на залез", + "min_sunset_time": "минимално време на залез", + "max_sunset_time": "максимално време на залез", + "sunset_offset": "отместване на залеза", + "brightness_mode": "режим на яркост", + "brightness_mode_time_dark": "време на режим на яркост при тъмно", + "brightness_mode_time_light": "време на режим на яркост при светло", + "take_over_control": "take_over_control: Деактивира Adaptive Lighting, ако друг източник извика \"light.turn_on\", докато лампите са включени и се адаптират. Имайте предвид, че това извиква \"homeassistant.update_entity\" на всеки \"interval\"! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Открива и спира адаптации за промени в състоянието, които не са \"light.turn_on\". Изисква \"take_over_control\" активиран. 🕵️ Внимание: ⚠️ Някои лампиможе лъжливо да указват 'включено' състояние, което може да доведе до неочаквано включване на лампите. Деактивирайте тази функция, ако се сблъскате с такива проблеми.", + "autoreset_control_seconds": "секунди за автоматично нулиране на контрола", + "only_once": "only_once: Адаптира лампите само когато са включени (\"true\") или продължава да ги адаптира (\"false\"). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: При първоначално включване на лампите. Ако е зададено на \"true\", Адаптивно Осветление се адаптира само ако е извикано \"light.turn_on\" без указване на цвят или яркост. ❌🌈 Това например предотвратява адаптация при активиране на сцена. Ако е \"false\", Адаптивно Осветление се адаптира независимо от наличието на цвят или яркост в първоначалните \"service_data\". Изисква \"take_over_control\" активиран. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Използва отделни \"light.turn_on\" команди за цвят и яркост, необходими за някои типове светлини. 🔀", + "send_split_delay": "забавяне при изпращане на разделени", + "adapt_delay": "забавяне при адаптация", + "skip_redundant_commands": "skip_redundant_commands: Пропуска изпращането на команди за адаптация, чиято целева състояние вече е равно на известното състояние на светлината. Минимизира мрежовия трафик и подобрява отговорността на адаптацията в някои ситуации. 📉Деактивирайте, ако физическите състояния на лампите се разминават с записаното състояние на HA.", + "intercept": "intercept: Прихваща и адаптира \"light.turn_on\" повиквания, позволявайки моментална адаптация на цвета и яркостта. 🏎️ Деактивирайте за светлини, които не поддържат \"light.turn_on\" с цвят и яркост.", + "multi_light_intercept": "multi_light_intercept: Прихваща и адаптира \"light.turn_on\" повиквания, които целят множество светлини. ➗⚠️ Това може да доведе до разделяне на едно \"light.turn_on\" повикване на множество повиквания, например когато лампите са в различни превключватели. Изисква \"intercept\" да бъде активиран.", + "include_config_in_attributes": "include_config_in_attributes: Показва всички опции като атрибути на ключа в Home Assistant, когато е зададено на \"true\". 📝" + }, + "data_description": { + "initial_transition": "Продължителност на първия преход, когато лампите преминават от \"off\" на \"on\" в секунди. ⏲️", + "sleep_rgb_or_color_temp": "Използвайте или \"\"rgb_color\"\" или \"\"color_temp\"\" в режим на сън. 🌙", + "sleep_rgb_color": "RGB цвят в режим на сън (използва се, когато \"sleep_rgb_or_color_temp\" е \"rgb_color\"). 🌈", + "sleep_transition": "Продължителност на прехода, когато се превключва \"режим на сън\" в секунди. 😴", + "sunrise_time": "Задайте фиксирано време (HH:MM:SS) за изгрев. 🌅", + "min_sunrise_time": "Задайте най-ранното виртуално време за изгрев (HH:MM:SS), позволяващо по-късни изгреви. 🌅", + "max_sunrise_time": "Задайте най-късното виртуално време за изгрев (HH:MM:SS), позволяващо по-ранни изгреви. 🌅", + "sunrise_offset": "Регулирайте времето на изгрев с положителен или отрицателен отместване в секунди. ⏰", + "sunset_time": "Задайте фиксирано време (HH:MM:SS) за залез. 🌇", + "min_sunset_time": "Задайте най-ранното виртуално време за залез (HH:MM:SS), позволяващо по-късни залези. 🌇", + "max_sunset_time": "Задайте най-късното виртуално време за залез (HH:MM:SS), позволяващо по-ранни залези. 🌇", + "sunset_offset": "Регулирайте времето на залез с положителен или отрицателен отместване в секунди. ⏰", + "brightness_mode": "Режим на яркост за използване. Възможни стойности са \"default\", \"linear\" и \"tanh\" (използва \"brightness_mode_time_dark\" и \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Игнорира се, ако \"brightness_mode='default'\") Продължителност в секунди за увеличаване/намаляване на яркостта преди/след изгрев/залез. 📈📉", + "brightness_mode_time_light": "(Игнорира се, ако \"brightness_mode='default'\") Продължителност в секунди за увеличаване/намаляване на яркостта след/преди изгрев/залез. 📈📉.", + "autoreset_control_seconds": "Автоматично нулиране на ръчния контрол след определен брой секунди. Задайте на 0 за деактивиране. ⏲️", + "send_split_delay": "Забавяне (ms) между \"separate_turn_on_commands\" за светлини, които не поддържат едновременна настройка на яркост и цвят. ⏲️", + "adapt_delay": "Време за изчакване (секунди) между включване на светлината и прилагане на промени от Адаптивно Осветление. Може да помогне за избягване на трептене. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Невалидна опция", + "entity_missing": "Една или повече избрани entity-та на лампилипсват от Home Assistant" + } + }, + "services": { + "apply": { + "name": "приложи", + "description": "Прилага текущите настройки за Адаптивно осветление към лампите", + "fields": { + "entity_id": { + "description": "entity_id на ключа с настройките за прилагане. 📝", + "name": "entity_id" + }, + "lights": { + "description": "Светлина (или списък от светлини), към които да се приложат настройките. 💡", + "name": "lights" + }, + "transition": { + "description": "Продължителност на прехода, когато лампите се променят, в секунди. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "Дали да се адаптира яркостта на светлината. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Дали да се адаптира цветът на лампите, които го поддържат. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "Дали да се предпочита RGB цветова корекция пред температура на светлината, когато е възможно. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "Дали да се включат лампите, които в момента са изключени. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "set_manual_control", + "description": "Маркирай дали дадена светлина е с 'ръчно контролиранe'.", + "fields": { + "entity_id": { + "description": "entity_id на ключа, в който да се маркира или демаркира светлината като ръчно контролирана. 📝", + "name": "entity_id" + }, + "lights": { + "description": "entity_id(та) на лампите, ако не са посочени, всички лампи в ключа се избират. 💡", + "name": "lights" + }, + "manual_control": { + "description": "Дали да се добави (\"true\") или премахне (\"false\") светлината от списъка \"manual_control\". 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "change_switch_settings", + "description": "Променете всички настройки, които искате в ключа. Всички опции тук са същите като в потока на конфигурацията.", + "fields": { + "entity_id": { + "description": "Entity ID на ключа. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "Задава стойностите по подразбиране, които не са посочени в този обаждане на услугата. Опции: \"current\" (по подразбиране, запазва текущите стойности), \"factory\" (нулира до документирани стойности по подразбиране) или \"configuration\" (връща към стойностите по подразбиране на конфигурацията на превключвателя). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "Показва всички опции като атрибути на ключа в Home Assistant, когато е зададено на \"true\". 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "Дали да се включат лампите, които в момента са изключени. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Продължителност на първия преход, когато лампите преминават от 'изключено' на 'включено' в секунди. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Продължителност на прехода, когато се превключва \"режим на сън\" в секунди. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Максимален процент на яркост. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "Най-студена цветова температура в Келвини. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "Минимален процент на яркост. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "Най-топла цветова температура в Келвини. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "Адаптира лампите само когато са включени ('true') или продължава да ги адаптира ('false'). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "Дали да се предпочита RGB цветова корекция пред температура на светлината, когато е възможно. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "Използвайте отделни light.turn_on повиквания за цвят и яркост, необходими за някои типове светлини. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "Забавяне (милисекунди) между \"separate_turn_on_commands\" за лампи, които не поддържат едновременна настройка на яркост и цвят. ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "Процент на яркостта на лампите в режим на сън. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "Използвай \"rgb_color\" или \"color_temp\" в режим на сън. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "RGB цвят в режим на сън (използва се, когато sleep_rgb_or_color_temp е \"rgb_color\"). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Цветова температура в режим на сън (използва се, когато sleep_rgb_or_color_temp е \"color_temp\") в Келвини. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Коригирайте времето на изгрев с положително или отрицателно отместване в секунди. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "Задайте фиксирано време (HH:MM:SS) за изгрев. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "Коригирайте времето на залез с положително или отрицателно отместване в секунди. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "Задайте фиксирано време (HH:MM:SS) за залез. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "Задайте най-късното виртуално време за изгрев (HH:MM:SS), което позволява по-ранни изгреви. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Задайте най-ранното виртуално време за изгрев (HH:MM:SS), което позволява по-ранни залези. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "Деактивирайте адаптивното осветление, ако друг източник извика 'light.turn_on', докато лампите са включени и се адаптират. Обърнете внимание, че това извиква homeassistant.update_entity на всеки interval! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "Открива и спира адаптации за промени в състоянието, които не са 'light.turn_on'. Изисква активиран 'take_over_control'. 🕵️ Внимание: ⚠️ Някои лампи може лъжливо да указват състояние ‘включено’, което може да доведе до неочаквано включване на лампите. Деактивирайте тази функция, ако се сблъскате с такива проблеми.", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "Продължителност на прехода, когато лампите се променят, в секунди. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "Време за изчакване (в секунди) между включването на светлината и прилагането на промените от адаптивното осветление. Може да помогне за избягване на премигване. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "Автоматично нулиране на ръчното управление след определен брой секунди. Задайте на 0, за да деактивирате. ⏲️", + "name": "autoreset_control_seconds" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/bn.json b/custom_components/adaptive_lighting/translations/bn.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/bn.json @@ -0,0 +1 @@ +{} diff --git a/custom_components/adaptive_lighting/translations/ca.json b/custom_components/adaptive_lighting/translations/ca.json new file mode 100644 index 00000000..9042b60e --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ca.json @@ -0,0 +1,217 @@ +{ + "title": "Il·luminació Adaptativa", + "options": { + "step": { + "init": { + "data_description": { + "interval": "Freqüència d'adaptació de les llums, en segons. 🔄", + "transition": "Durada de la transició en canviar les llums, en segons. 🕑", + "sleep_brightness": "Percentatge de brillantor dels llums en mode nocturn. 😴", + "sleep_color_temp": "Temperatura de color en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴" + }, + "title": "Opcions Il·luminació Adaptativa", + "data": { + "lights": "lights: Llista d'entity_ids dels llums a controlar (pot estar buida). 🌟", + "min_brightness": "min_brightness: Percentatge mínim de brillantor. 💡", + "max_brightness": "max_brightness: Percentatge màxim de brillantor. 💡", + "min_color_temp": "min_color_temp: Temperatura de color més càlida en graus Kelvin. 🔥", + "max_color_temp": "max_color_temp: Temperatura de color més freda en graus Kelvin. ❄️" + }, + "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web]({webapp_url}). Per a més detalls, pots veure la [documentació oficial]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Si prefereixes l'ajustament del color RGB en lloc de la temperatura de color, quan sigui possible. 🌈", + "transition_until_sleep": "transition_until_sleep: Si s'activa, Adaptive Lighting considerarà els ajustaments del mode nocturn com a mínims, fent una transició cap aquests valors després de la posta de sol. 🌙", + "take_over_control": "take_over_control: Inhabilita Adaptive Lighting si una altra font crida `light.turn_on` quan les llums estan enceses i en procés d'adaptació. Tingues present que això cridarà `homeassistant.update_entity` cada `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita que `take_over_control` estigui habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguin inesperadament. Desactiva aquesta funció si trobes aquests problemes.", + "only_once": "only_once: Adapta els llums només quan s'encenen (`true`) o segueix adaptant-les (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quan s'encenen les llums inicialment. Si el valor és `true`, AL adapta només si s'ha cridat `light.turn_on` sense especificar color o brillantor. ❌🌈 Això impedeix l'adaptació quan s'activa una escena. Si el valor és `false`, AL adapta independentment de la presencia de color o brillantor en les dades inicials `service_data`. Necessita `take_over_control` habilitat. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Separa les crides de `light.turn_on` per a color i brillantor; necessari per alguns tipus de llums. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Evita l'enviament de d'ordres d'adaptació als objectius on el seu estat ja és el conegut del llum. Minimitza el trànsit de la xarxa i millora la resposta de l'adaptació en alguns casos. 📉 Inhabilita-ho si l'estat físic del llum queda desincronitzat amb l'estat registrat a Home Assistant.", + "intercept": "intercept: Intercepta i adapta les crides `light.turn_on` per permetre canvis instantanis de color i brillantor. 🏎️ Inhabilita-ho per a llums que no admeten `light.turn_on` amb color i brillantor.", + "multi_light_intercept": "multi_light_intercept: Intercepta i adapta les crides `light.turn_on` dirigides a múltiples llums. ➗⚠️ Pot provocar la divisió d'una crida única `light.turn_on` en múltiples crides, com ara, quan les llums són en interruptors diferents. Necessita que `intercept` estigui habilitat.", + "include_config_in_attributes": "include_config_in_attributes: Mostra totes les opcions com atributs a l'interruptor de Home Assistant quan s'estableix com a `true`. 📝" + }, + "data_description": { + "initial_transition": "Durada de la primera transició quan els llums canvien de `off` a `on` en segons. ⏲️", + "sleep_rgb_or_color_temp": "Utilitza `\"rgb_color\"` o `\"color_temp\"` durant el mode nocturn. 🌙", + "sleep_rgb_color": "Color RGB en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és \"rgb_color\"). 🌈", + "sleep_transition": "Durada de la transició en commutar el \"mode nocturn\", en segons. 🕑", + "sunrise_time": "Indica una hora fixa (HH:MM:SS) per a la sortida del sol. 🌅", + "min_sunrise_time": "Defineix la sortida de sol virtual més primerenca (HH:MM:SS), tot permetent sortides de sol posteriors. 🌅", + "max_sunrise_time": "Defineix la sortida de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌅", + "sunrise_offset": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰", + "sunset_time": "Indica una hora fixa (HH:MM:SS) per a la posta de sol. 🌇", + "min_sunset_time": "Defineix la posta de sol virtual més primerenca (HH:MM:SS), tot permetent postes de sol més tard. 🌇", + "max_sunset_time": "Defineix la sortida virtual de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌇", + "sunset_offset": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰", + "brightness_mode": "Mode de brillantor a utilitzar. Els valors possibles són `default`, \"linear\" i \"tanh\" (utilitza `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", + "brightness_mode_time_light": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", + "autoreset_control_seconds": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️", + "send_split_delay": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️", + "adapt_delay": "Temps d'espera (en segons) entre l'encesa de la llum i els canvis per part d'Adaptive Lighting. Pot ajudar a evitar els parpalleigs. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Opció invàlida", + "entity_missing": "Una o més de les entitats de llum seleccionades no es troba a Home Assistant" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Ajustar les llums només quan s'encenguin (`true`) o ajustar contínuament(`false`). 🔄" + }, + "sleep_color_temp": { + "description": "Temperatura de color en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴" + }, + "sunrise_offset": { + "description": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰" + }, + "sunset_offset": { + "description": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰" + }, + "autoreset_control_seconds": { + "description": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️" + }, + "sleep_brightness": { + "description": "Percentatge de brillantor dels llums en mode nocturn. 😴" + }, + "max_color_temp": { + "description": "Temperatura de color més freda en Kelvin. ❄️" + }, + "send_split_delay": { + "description": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️" + }, + "detect_non_ha_changes": { + "description": "Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita que `take_over_control` estigui habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguin inesperadament. Desactiva aquesta funció si trobes aquests problemes." + }, + "take_over_control": { + "description": "Desactiva Adaptive Lighting si una altra font crida `light.turn_on` mentre els llums estan encesos i adaptats. Tingues en compte que això crida `homeassistant.update_entity` cada `interval`! 🔒" + }, + "entity_id": { + "description": "ID de la entitat de l'interruptor. 📝" + }, + "turn_on_lights": { + "description": "Si s'encenen les llums que estan apagades en aquest moment. 🔆" + }, + "initial_transition": { + "description": "Durada de la primera transició quan els llums canvien `off` a `on` en segons. ⏲️" + }, + "sleep_transition": { + "description": "Durada de la transició en commutar el \"mode nocturn\", en segons. 🕑" + }, + "max_brightness": { + "description": "Percentatge màxim de brillantor. 💡" + }, + "min_brightness": { + "description": "Percentatge mínim de brillantor. 💡" + }, + "prefer_rgb_color": { + "description": "Si es prefereix ajustar el color RGB en lloc de la temperatura de color, quan sigui possible. 🌈" + }, + "min_color_temp": { + "description": "Temperatura de color més càlida, en graus Kelvin. 🔥" + }, + "separate_turn_on_commands": { + "description": "Utilitza crides independents per a `light.turn_on` per color i brillantor; necessari per alguns tipus de llums. 🔀" + }, + "sleep_rgb_or_color_temp": { + "description": "Utilitza `\"rgb_color\"` o `\"color_temp\"` durant el mode nocturn. 🌙" + }, + "sleep_rgb_color": { + "description": "Color RGB en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és \"rgb_color\"). 🌈" + }, + "sunrise_time": { + "description": "Indica una hora fixa (HH:MM:SS) per a la sortida del sol. 🌅" + }, + "sunset_time": { + "description": "Indica una hora fixa (HH:MM:SS) per a la posta de sol. 🌇" + }, + "transition": { + "description": "Durada de la transició en canviar les llums, en segons. 🕑" + }, + "adapt_delay": { + "description": "Temps d'espera (en segons) entre l'encesa de la llum i els canvis per part d'Adaptive Lighting. Pot ajudar a evitar els parpalleigs. ⏲️" + }, + "use_defaults": { + "description": "Defineix els valors per defecte que no s'especifiquin a la crida del servei. Opcions: \"current\" (per defecte, manté els valors actuals), \"factory\" (restaura els valors documentats per defecte), o \"configuration\" (retorna als valors per defecte de l'interruptor). ⚙️" + }, + "include_config_in_attributes": { + "description": "Mostra totes les opcions com a atributs de l'interruptor a Home Assistant quan es defineixi com a `true`. 📝" + }, + "max_sunrise_time": { + "description": "Defineix la sortida de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌅" + }, + "min_sunset_time": { + "description": "Defineix la posta de sol virtual més primerenca (HH:MM:SS), tot permetent postes de sol més tard. 🌇" + } + }, + "description": "Canvia les opcions de configuració que vulguis al commutador. Totes les opcions d'aquí són les mateixes que en el flux de configuració." + }, + "apply": { + "fields": { + "lights": { + "description": "Una llum (o una llista de llums) a la qual aplicar la configuració. 💡" + }, + "transition": { + "description": "Durada de la transició en canviar les llums, en segons. 🕑" + }, + "prefer_rgb_color": { + "description": "Si es prefereix ajustar el color RGB en lloc de la temperatura de color, quan sigui possible. 🌈" + }, + "turn_on_lights": { + "description": "Si s'encenen les llums que estan apagades en aquest moment. 🔆" + }, + "entity_id": { + "description": "L'`entity_id` de l'interruptor amb els paràmetres per aplicar. 📝" + }, + "adapt_brightness": { + "description": "Si cal adaptar la brillantor del llum. 🌞" + }, + "adapt_color": { + "description": "Si cal adaptar el color a les llums que ho admetin. 🌈" + } + }, + "description": "Aplica la configuració actual d'Adaptive Lighting a les llums." + }, + "set_manual_control": { + "description": "Indica quan una llum està 'controlada manualment'.", + "fields": { + "lights": { + "description": "entity_id(s) de les llums; si no s'especifica es seleccionaran totes les llums de l'interruptor. 💡" + }, + "entity_id": { + "description": "L'`entity_id` de l'interruptor al qual (des)marcar el llum com a `manually controlled`. 📝" + }, + "manual_control": { + "description": "Si cal afegir (\"true\") o treure (\"false\") el llum de la llista de \"manual_control\". 🔒" + } + } + } + }, + "config": { + "step": { + "user": { + "title": "Tria un nom per a la instància d'Adaptive Lighting", + "description": "Cada instància pot contenir múltiples llums!" + }, + "menu": { + "title": "Crear o Duplicar", + "description": "Vols crear una nova instància, o duplicar-ne una d'existent?", + "data": { + "action": "Acció" + } + } + }, + "abort": { + "already_configured": "Aquest dispositiu ja està configurat" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json new file mode 100644 index 00000000..0ac5c4a0 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -0,0 +1,233 @@ +{ + "title": "Adaptivní osvětlení", + "config": { + "step": { + "user": { + "title": "Vyberte název instance Adaptivního osvětlení", + "description": "Vyberte název pro tuto instanci. Můžete spustit několik instancí Adaptivního osvětlení, každá z nich může obsahovat více světel!", + "data": { + "name": "Název" + } + }, + "menu": { + "title": "Vytvořit nebo duplikovat", + "description": "Chcete vytvořit novou instanci nebo duplikovat stávající?" + } + }, + "abort": { + "already_configured": "Toto zařízení je již nakonfigurováno" + } + }, + "options": { + "step": { + "init": { + "title": "Nastavení Adaptivního osvětlení", + "description": "Nakonfigurujte komponentu Adaptive Lighting. Názvy voleb odpovídají nastavení YAML. Pokud je tato položka definována v YAML, žádné volby se zde nezobrazí. Interaktivní grafy znázorňující vliv parametrů najdete v [této webové aplikaci]({webapp_url}). Další podrobnosti najdete v [oficiální dokumentaci]({docs_url}).", + "data": { + "lights": "lights: Seznam světel (entity_id), které mají být ovládané (může být prázdný). 🌟", + "interval": "interval: Prodleva pro změny osvětlení (v sekundách)", + "transition": "", + "min_brightness": "min_brightness: Nejnižší jas osvětlení během cyklu. (%)", + "max_brightness": "max_brightness: Nejvyšší jas osvětlení během cyklu. (%)", + "min_color_temp": "min_color_temp, Nejteplejší odstín cyklu teploty barev. (Kelvin)", + "max_color_temp": "max_color_temp: Nejchladnější odstín cyklu teploty barev. (Kelvin)", + "sleep_brightness": "sleep_brightness, Nastavení jasu pro režim spánku. (%)", + "sleep_color_temp": "sleep_color_temp: Nastavení teploty barev pro režim spánku. (v Kelvinech)" + }, + "data_description": { + "interval": "Frekvence přizpůsobení světel v sekundách. 🔄", + "transition": "Doba trvání přechodu změny světel v sekundách. 🕑", + "sleep_brightness": "Jas světel během režimu spánku (v %). 😴", + "sleep_color_temp": "Teplota barev v režimu spánku (používá se, když `sleep_rgb_or_color_temp` je `color_temp`) v Kelvinech. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Prodlení pro změnu z 'vypnuto' do 'zapnuto' (sekundy)", + "prefer_rgb_color": "prefer_rgb_color: Upřednostněte použití 'rgb_color' před 'color_temp'.", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, použijte 'rgb_color' nebo 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, v RGB", + "sleep_transition": "sleep_transition: Prodleva pro přepnutí do „režimu spánku“ (sekundy)", + "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙", + "sunrise_time": "sunrise_time: Manuální přepsání času východu slunce, pokud je „None“, použije se skutečný čas východu slunce ve vaší lokalitě (HH:MM:SS)", + "max_sunrise_time": "max_sunrise_time: Ruční přepsání nejpozdějšího času východu slunce, pokud je „None“, použije se skutečný čas východu slunce vaší lokality (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu východu slunce (+/- v sekundách)", + "sunset_time": "sunset_time: Ruční přepsání času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Ruční přepsání nejdřívějšího času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "sunset_offset": "sunset_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu západu slunce (+/- v sekundách)", + "take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).", + "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", + "only_once": "only_once: Přizpůsobení osvětlení pouze při rozsvícení.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Jenom při prvním zapnutí světel. Je-li nastaveno na `true`, AL udělá přizpůsobení pouze tehdy, je-li vyvoláno `light.turn_on` bez zadání barvy nebo jasu. ❌🌈 Tím se zabrání přizpůsobení např. při aktivaci scény. Pokud je `false`, AL udělá přizpůsobení bez ohledu na přítomnost barvy nebo jasu `service_data` volání. Vyžaduje zapnutí `take_over_control`. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Oddělení příkazů pro každý atribut (barva, jas, atd.) v atributu 'light.turn_on' (vyžadováno pro některá světla).", + "send_split_delay": "send_split_delay: prodleva mezi příkazy (milisekundy), když je použit atribut 'separate_turn_on_commands'. Může zajistit správné zpracování obou příkazů.", + "adapt_delay": "", + "skip_redundant_commands": "skip_redundant_commands: Přeskočí odesílání adaptačních příkazů, jejichž cílový stav se již rovná známému stavu světla. Minimalizuje síťový provoz a v některých situacích zlepšuje odezvu adaptace. 📉Zakažte, pokud se fyzické stavy světel dostanou mimo synchronizaci se zaznamenaným stavem HA.", + "intercept": "intercept: Zachytit a přizpůsobit volání `light.turn_on` a umožnit tak okamžité přizpůsobení barev a jasu. 🏎️ Zakažte pro světla, která nepodporují `light.turn_on` s barvou a jasem najednou.", + "multi_light_intercept": "multi_light_intercept: Zachytí a přizpůsobí volání `light.turn_on`, která se zaměřují na více světel. ➗⚠️ To může vést k rozdělení jednoho volání `light.turn_on` na více volání, např. když jsou světla v různých vypínačích. Vyžaduje, aby bylo povoleno `intercept`.", + "include_config_in_attributes": "include_config_in_attributes: Zobrazit všechny možnosti jako atributy přepínače v Home Assistant, pokud je nastaveno na `true`. 📝" + }, + "data_description": { + "initial_transition": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️", + "sleep_rgb_or_color_temp": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙", + "sleep_rgb_color": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈", + "sleep_transition": "Doba trvání přechodu do režimu spánku v sekundách. 😴", + "sunrise_time": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅", + "min_sunrise_time": "Nastavte nejbližší virtuální čas východu slunce (HH:MM:SS), abyste mohli nastavit pozdější východ slunce. 🌅", + "max_sunrise_time": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅", + "sunrise_offset": "Upravte čas východu slunce o sekundy dopředu nebo dozadu. ⏰", + "sunset_time": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅", + "min_sunset_time": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅", + "max_sunset_time": "Nastavte nejpozdější virtuální čas západu slunce (HH:MM:SS), což umožňuje dřívější západ slunce. 🌅", + "sunset_offset": "Upravte čas západu slunce o sekundy dopředu nebo dozadu. ⏰", + "brightness_mode": "Výběr režimu jasu. Možné hodnoty jsou `default`, `linear` a `tanh` (používá `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", + "brightness_mode_time_light": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", + "autoreset_control_seconds": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️", + "send_split_delay": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️", + "adapt_delay": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Neplatná možnost", + "entity_missing": "V aplikaci Home Assistant chybí jedna nebo více vybraných entit osvětlení" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Jas světel během režimu spánku (v %). 😴" + }, + "detect_non_ha_changes": { + "description": "Zjistí a zastaví adaptace při změnách stavu, které nejsou ve stavu `light.turn_on`. Nutno mít zapnutou funkci `take_over_control`. 🕵️ Upozornění: ⚠️ Některá světla mohou falešně indikovat stav 'zapnuto', což může vést k neočekávanému zapnutí světel. Pokud se s takovými problémy setkáte, zakažte tuto funkci." + }, + "sunrise_offset": { + "description": "Upravte čas východu slunce o sekundy dopředu nebo dozadu. ⏰" + }, + "max_sunrise_time": { + "description": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅" + }, + "sleep_color_temp": { + "description": "Teplota barev v režimu spánku (používá se, když `sleep_rgb_or_color_temp` je `color_temp`) v Kelvinech. 😴" + }, + "min_brightness": { + "description": "Minimální hodnota jasu. 💡" + }, + "min_color_temp": { + "description": "Nejvyšší teplota barvy v Kelvinech. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙" + }, + "turn_on_lights": { + "description": "Zda se mají zapnout světla, která jsou aktuálně vypnutá. 🔆" + }, + "initial_transition": { + "description": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️" + }, + "entity_id": { + "description": "Entita ID přepínače. 📝" + }, + "sunrise_time": { + "description": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅" + }, + "include_config_in_attributes": { + "description": "Zobrazení všech možností jako atributů přepínače v aplikaci Home Assistant, pokud je zaškrtnuto. 📝" + }, + "max_brightness": { + "description": "Maximální hodnota jasu. 💡" + }, + "sleep_rgb_color": { + "description": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈" + }, + "take_over_control": { + "description": "Zakáže adaptivní osvětlení, pokud jiný zdroj volá `light.turn_on`, zatímco jsou světla zapnutá a přizpůsobují se. Vemte na vědomí, že `homeassistant.update_entity` volá každý `interval`! 🔒" + }, + "sleep_transition": { + "description": "Doba trvání přechodu do režimu spánku v sekundách. 😴" + }, + "autoreset_control_seconds": { + "description": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️" + }, + "adapt_delay": { + "description": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání." + }, + "only_once": { + "description": "Přizpůsobit světla pouze při zapnutí (`true`) nebo je průběžně přizpůsobovat (`false`). 🔄" + }, + "use_defaults": { + "description": "Nastaví výchozí hodnoty, které nebyly zadány v tomto volání služby. Možnosti: \"stávající\" (výchozí, zachovává aktuální hodnoty), \"výchozí\" (obnovuje do výchozích hodnot) nebo \"konfigurace\" (vrací výchozí hodnoty konfigurace přepínače). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Použití samostatných volání `light.turn_on` pro barvu a jas, které jsou potřebné pro některé typy světel. 🔀" + }, + "prefer_rgb_color": { + "description": "Zda upřednostnit nastavení barev RGB před teplotou barev světla, pokud je to možné. 🌈" + }, + "max_color_temp": { + "description": "Nejchladnější teplota barvy v Kelvinech. ❄️" + }, + "sunset_offset": { + "description": "Upravte čas západu slunce o sekundy dopředu nebo dozadu. ⏰" + }, + "send_split_delay": { + "description": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️" + }, + "sunset_time": { + "description": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅" + }, + "transition": { + "description": "Doba trvání přechodu změny světel v sekundách. 🕑" + }, + "min_sunset_time": { + "description": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅" + } + }, + "description": "V přepínači změňte libovolné nastavení. Všechny možnosti jsou zde stejné jako v konfiguračním souboru." + }, + "apply": { + "fields": { + "entity_id": { + "description": "`entity_id` přepínače s nastavením, které se má použít. 📝" + }, + "adapt_brightness": { + "description": "Přizpůsobení jasu světla. 🌞" + }, + "turn_on_lights": { + "description": "Zda se mají zapnout světla, která jsou aktuálně vypnutá. 🔆" + }, + "adapt_color": { + "description": "Zda se má přizpůsobit barva na podporovaných světlech. 🌈" + }, + "prefer_rgb_color": { + "description": "Zda upřednostnit nastavení barev RGB před teplotou barev světla, pokud je to možné. 🌈" + }, + "lights": { + "description": "Světlo (nebo seznam světel), na které se má nastavení aplikovat. 💡" + }, + "transition": { + "description": "Doba trvání přechodu změny světel v sekundách. 🕑" + } + }, + "description": "Aplikuje současné nastavení Adaptivního osvětlení na světla." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Zda přidat (\"true\") nebo odebrat (\"false\") světlo ze seznamu \"manual_control\". 🔒" + }, + "entity_id": { + "description": "`entity_id` spínače, ve kterém se světlo (ne)označí jako `ručně ovládané`. 📝" + }, + "lights": { + "description": "entity_id(s) světel, pokud není zadáno jinak, jsou vybrána všechna světla ve spínači. 💡" + } + }, + "description": "Označte, zda je světlo \"ručně ovládané\"." + } + } +} diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json new file mode 100644 index 00000000..5925dbc7 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/da.json @@ -0,0 +1,200 @@ +{ + "title": "Adaptiv Belysning", + "config": { + "step": { + "user": { + "title": "Vælg et navn for denne Adaptive Belysning", + "description": "Vælg et navn til denne konfiguration. Du kan køre flere konfigurationer af Adaptiv Belysning, og hver af dem kan indeholde flere lys!", + "data": { + "name": "Navn" + } + }, + "menu": { + "data": { + "action": "Handling" + } + } + }, + "abort": { + "already_configured": "Denne enhed er allerede konfigureret" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptiv Belysnings indstillinger", + "description": "Alle indstillinger tilhørende en Adaptiv Belysnings komponent. Indstillingernes navne svarer til YAML indstillingernes. Ingen indstillinger vises hvis du allerede har konfigureret den i YAML. For interaktive grafer, der viser parametereffekter, besøg [denne webapp]({webapp_url}). Yderligere detaljer finder du i den [officielle dokumentation]({docs_url}).", + "data": { + "lights": "lights: lyskilder", + "interval": "interval: Tid imellem opdateringer (i sekunder)", + "transition": "Overgangsperiode når en ændring i lyset udføres (i sekunder)", + "min_brightness": "min_brightness: Laveste lysstyrke i cyklussen. (%)", + "max_brightness": "max_brightness: Højeste lysstyrke i cyklussen. (%)", + "min_color_temp": "min_color_temp: Varmeste lystemperatur i cyklussen. (Kelvin)", + "max_color_temp": "max_color_temp: Koldeste lystemperatur i cyklussen. (Kelvin)", + "sleep_brightness": "sleep_brightness, Lysstyrke for Sleep Mode. (%)", + "sleep_color_temp": "sleep_color_temp: Farvetemperatur under Sleep Mode. (Kelvin)" + }, + "data_description": { + "interval": "Frekvens til at tilpasse lysene, i sekunder. 🔄", + "transition": "Varighed af overgang, når lys ændres, i sekunder. 🕑", + "sleep_brightness": "Lysstyrkeprocent af lys i søvntilstand. 😴", + "sleep_color_temp": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Hvor lang overgang når lyset går fra 'off' til 'on' eller når 'sleep_state' skiftes. (i sekunder)", + "prefer_rgb_color": "prefer_rgb_color: Brug 'rgb_color' istedet for 'color_temp' når muligt.", + "transition_until_sleep": "overgang_til_sove: Når aktiveret, vil adaptiv belysning behandle søvnindstillinger som minimum, og overgår til disse værdier efter solnedgang. 🌙", + "sunrise_time": "sunrise_time: Manuel overstyring af solopgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt din lokation. (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Hvor længe før (-) eller efter (+) at definere solopgangen i cyklussen (+/- sekunder)", + "sunset_time": "sunset_time: Manuel overstyring af solnedgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt for din lokation. (HH:MM:SS)", + "sunset_offset": "sunset_offset: Hvor længe før (-) eller efter (+) at definere solnedgangen i cyklussen (+/- sekunder)", + "take_over_control": "take_over_control: Hvis andet end Adaptiv Belysning kalder 'light.turn_on' på et lys der allerede er tændt, afbryd adaptering af lyset indtil at det tændes igen.", + "detect_non_ha_changes": "detect_non_ha_changes: Registrer alle ændringer på >10% på et lys (også udenfor HA), kræver at 'take_over_control' er slået til (kalder 'homeassistant.update_entity' hvert 'interval'!)", + "only_once": "only_once: Juster udelukkende lysene adaptivt i øjeblikket de tændes.", + "adapt_only_on_bare_turn_on": "tilpas_kun_ved_enkelt_tænd: Når du tænder lys for første gang. Hvis indstillet til 'true', tilpasser AL kun, hvis 'lys.tænd' er kaldt uden at angive farve eller lysstyrke. ❌🌈 Dette forhindrer f.eks. tilpasning, når du aktiverer en scene. Hvis indstillet til 'false' tilpasser AL sig uanset tilstanden af farve eller lysstyrke i den oprindelige 'service_data'. Har brug for at 'take_over_control' er aktiveret. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Adskil kommandoerne for hver attribut (color, brightness, etc.) ved 'light.turn_on' (nødvendigt for bestemte lys).", + "skip_redundant_commands": "skip_redundant_commands: Undlad at sende tilpasningskommando, hvis lampens kendte tilstand allerede er lig den ønskede tilstand. Mindsker mængden af netværkstrafik og forbedrer tilpasningens responsivitet i visse situationer. 📉 Slå fra, hvis lampens faktiske tilstand kommer ud af takt med den tilstand, som HA rapporterer.", + "intercept": "intercept: Indfang og tilpas »light.turn_on«-kald for at muliggøre øjeblikkelig farve- og lysstyrketilpasning. 🏎️ Slå fra for lyskilder, som ikke understøtter »light.turn_on« med farve og lysstyrke.", + "multi_light_intercept": "multi_light_intercept: Indfang og tilpas »light.turn_on«-kald til mere end en enkelt lyskilde. ➗⚠️ Dette kan bevirke at et enkelt »light.turn_on«-kald deles op i flere, f.eks. hvis lyskilderne er forbundet til forskellige kontakter. Forudsætter at »intercept« er slået til.", + "include_config_in_attributes": "include_config_in_attributes: Vis alle indstillinger som attributter for kontakten når dette er sat til »true«. 📝" + }, + "data_description": { + "initial_transition": "Den første overgangs varighed når lysene ændres fra »off« til »on« i sekunder. ⏲️", + "sleep_rgb_or_color_temp": "Brug enten `\"rgb_farve\"` eller `\"farve_temp\"` i søvntilstand. 🌙", + "sleep_rgb_color": "RGB-farve i søvntilstand (anvendes når »sleep_rgb_or_color_temp« er sat til »rgb_color«). 🌈", + "sleep_transition": "Varigheden af overgangen, når \"sovetilstand\" skiftes, i sekunder. 😴", + "sunrise_time": "Sæt en fast tid (HH:MM:SS) for solopgang. 🌅", + "min_sunrise_time": "Indstil den tidligste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for senere solopgange. 🌅", + "max_sunrise_time": "Indstil den seneste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solopgange. 🌅", + "sunrise_offset": "Juster solopgangstiden med en positiv eller negativ offset på få sekunder. ⏰", + "sunset_time": "Sæt en fast tid (HH:MM:SS) for solnedgang. 🌇", + "min_sunset_time": "Indstil den tidligste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for senere solnedgange. 🌇", + "max_sunset_time": "Indstil den seneste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solnedgange. 🌇", + "sunset_offset": "Juster solnedgang tid med et positivt eller negativt offset, i sekunder. ⏰", + "brightness_mode": "Lysstyrketilstand til brug. Mulige værdier er \"default\", \"linear\" og \"tanh\" (bruger \"brightness_mode_time_dark\" og \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Ignoreres hvis »brightness_mode='default'«) Varigheden i sekunder for tilpasningen af lysstyrken ved solopgang eller -nedgang. 📈📉", + "brightness_mode_time_light": "(Ignoreres hvis »brightness_mode='default'«) Varigheden i sekunder for tilpasningen af lysstyrken ved solopgang eller -nedgang. 📈📉", + "autoreset_control_seconds": "Nulstil automatisk den manuelle styring efter et antal sekunder. Indstil til 0 for at deaktivere. ⏲️", + "send_split_delay": "Forsinkelse (ms) mellem »separate_turn_on_commands« for lyskilder som ikke understøtter simultane styrke- og farveindstillinger. ⏲️", + "adapt_delay": "Ventetid (sekunder) mellem lyset tændes og Adaptive Lighting anvender ændringer. Kan hjælpe med at undgå flimren. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Ugyldig indstilling", + "entity_missing": "Et udvalgt lys blev ikke fundet " + } + }, + "services": { + "apply": { + "description": "Anvender de aktuelle Adaptive Lighting indstillinger på lys.", + "fields": { + "prefer_rgb_color": { + "description": "Om man vil foretrække RGB-farvejustering frem for lysfarvetemperatur, når det er muligt. 🌈" + }, + "transition": { + "description": "Varighed af overgang, når lys ændres, i sekunder. 🕑" + }, + "turn_on_lights": { + "description": "Om lys der i øjeblikket er slukket, skal tændes. 🔆" + }, + "adapt_brightness": { + "description": "Om lysstyrken skal tilpasses. 🌞" + }, + "lights": { + "description": "Et lys (eller liste over lys) som indstillingerne skal anvendes til. 💡" + }, + "adapt_color": { + "description": "Om farven på støttelys skal tilpasses. 🌈" + } + } + }, + "change_switch_settings": { + "fields": { + "entity_id": { + "description": "Entity ID af kontakten. 📝" + }, + "turn_on_lights": { + "description": "Om lys der i øjeblikket er slukket, skal tændes. 🔆" + }, + "sleep_transition": { + "description": "Varigheden af overgangen, når \"sovetilstand\" skiftes, i sekunder. 😴" + }, + "only_once": { + "description": "Tilpas kun lys, når de er tændt ('sand'), eller fortsæt med at tilpasse dem ('falsk'). 🔄" + }, + "prefer_rgb_color": { + "description": "Om man vil foretrække RGB-farvejustering frem for lysfarvetemperatur, når det er muligt. 🌈" + }, + "sleep_brightness": { + "description": "Lysstyrkeprocent af lys i søvntilstand. 😴" + }, + "sunrise_time": { + "description": "Sæt en fast tid (HH:MM:SS) til solopgang. 🌅" + }, + "sunrise_offset": { + "description": "Juster solopgangstiden med et positivt eller negativt offset, i sekunder. ⏰" + }, + "sunset_offset": { + "description": "Juster solnedgang tid med et positivt eller negativt offset, i sekunder. ⏰" + }, + "sunset_time": { + "description": "Sæt en fast tid (HH:MM:SS) for solnedgang. 🌇" + }, + "max_sunrise_time": { + "description": "Indstil den seneste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solopgange. 🌅" + }, + "min_sunset_time": { + "description": "Indstil den tidligste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for senere solnedgange. 🌇" + }, + "transition": { + "description": "Varighed af overgang, når lys ændres, i sekunder. 🕑" + }, + "autoreset_control_seconds": { + "description": "Nulstil automatisk den manuelle styring efter et antal sekunder. Indstil til 0 for at deaktivere. ⏲️" + }, + "adapt_delay": { + "description": "Ventetid (sekunder) mellem lyset tændes og Adaptive Lighting anvender ændringer. Kan hjælpe med at undgå flimren. ⏲️" + }, + "max_brightness": { + "description": "Maksimal lysstyrkeprocent. 💡" + }, + "max_color_temp": { + "description": "Koldeste farvetemperatur i Kelvin. ❄️" + }, + "min_brightness": { + "description": "Mindste lysstyrkeprocent. 💡" + }, + "min_color_temp": { + "description": "Varmste farvetemperatur i Kelvin. 🔥" + }, + "sleep_color_temp": { + "description": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴" + }, + "send_split_delay": { + "description": "Forsinkelse (ms) mellem »separate_turn_on_commands« for lyskilder som ikke understøtter simultane styrke- og farveindstillinger. ⏲️" + }, + "detect_non_ha_changes": { + "description": "Opdager og stopper tilpasningen ved tilstandsændringer, som ikke er udløst af »light.turn_on«. Indstillingen »take_over_control« skal være aktiveret. 🕵️ Advarsel: ⚠️ Nogle lyskilder kan rapportere en falsk tændt-tilstand, hvilket kan medføre af lyskilden tændes når det ikke er meningen. Slå denne funktion fra, hvis du oplever dette problem." + }, + "initial_transition": { + "description": "Den første overgangs varighed når lysene ændres fra »off« til »on« i sekunder. ⏲️" + } + }, + "description": "Skift de indstillinger du ønsker i kontakten. Alle muligheder her er de samme som i konfigurationsflowet." + }, + "set_manual_control": { + "description": "Markér om et lys er 'manuelt kontrolleret'.", + "fields": { + "lights": { + "description": "entity_id(er) af lys, hvis ikke specificeret, vil alle lys i kontakten være valgt. 💡" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json new file mode 100644 index 00000000..4885e753 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/de.json @@ -0,0 +1,238 @@ +{ + "title": "Adaptive Beleuchtung", + "config": { + "step": { + "user": { + "title": "Benenne die Adaptive Beleuchtung Instanz", + "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten", + "data": { + "name": "Name" + } + }, + "menu": { + "title": "Erstellen oder Duplizieren", + "description": "Möchtest du eine neue Instanz erstellen oder eine existierende duplizieren?", + "data": { + "action": "Aktion" + } + } + }, + "abort": { + "already_configured": "Dieses Gerät ist bereits konfiguriert." + } + }, + "options": { + "step": { + "init": { + "title": "Optionen für Adaptive Beleuchtung", + "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde. Interaktive Diagramme zur Veranschaulichung der Auswirkungen der Parameter finden Sie unter [dieser Webanwendung]({webapp_url}). Weitere Details finden Sie in der [offiziellen Dokumentation]({docs_url}).", + "data": { + "lights": "Lichter", + "interval": "interval, Zeit zwischen Updates des Switches", + "transition": "transition, Wechselzeit in Sekunden", + "min_brightness": "min_brightness: Minimale Helligkeit in Prozent. 💡", + "max_brightness": "max_brightness: Maximale Helligkeit in Prozent. 💡", + "min_color_temp": "min_color_temp: Wärmste Farbtemperatur in Kelvin. 🔥", + "max_color_temp": "max_color_temp: Kälteste Farbtemperatur in Kelvin. ❄️", + "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", + "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin" + }, + "data_description": { + "interval": "Häufigkeit der Lichtanpassung in Sekunden. 🔄", + "transition": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑", + "sleep_brightness": "Helligkeit der Lichter im Schlafmodus in Prozent. 😴", + "sleep_color_temp": "Farbtemperatur im Schlafmodus in Kelvin (wird verwendet, wenn `sleep_rgb_or_color_temp` `color_temp` ist) . 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", + "prefer_rgb_color": "prefer_rgb_color: Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", + "transition_until_sleep": "transition_until_sleep: Wenn diese Option aktiviert ist, behandelt die adaptive Beleuchtung die Schlafeinstellungen als Minimum und geht nach Sonnenuntergang zu diesen Werten über. 🌙", + "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", + "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", + "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", + "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", + "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", + "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", + "take_over_control": "take_over_control: Deaktiviere die adaptive Beleuchtung, wenn eine andere Quelle `light.turn_on` aufruft, während die Beleuchtung eingeschaltet ist und angepasst wird. Beachte, dass dies `homeassistant.update_entity` jedes `Intervall` aufruft! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Erkennt und stoppt Anpassungen für nicht-`light.turn_on`-Zustandsänderungen. Benötigt, dass `take_over_control` aktiviert ist. 🕵️ Vorsicht: ⚠️ Einige Lichter können fälschlicherweise einen 'an'-Zustand anzeigen, was dazu führen kann, dass Lichter unerwartet eingeschaltet werden. Deaktiviere diese Funktion, wenn solche Probleme auftreten.", + "only_once": "only_once: Lichter nur einmalig anpassen, wenn sie eingeschaltet werden (`true`) oder sie immer wieder anpassen (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Beim ersten Einschalten des Lichts. Wenn auf `true` gesetzt, passt AL das Licht nur an, wenn `light.turn_on` ohne eine Angabe von Farbe oder Helligkeit aufgerufen wird. ❌🌈 Dies verhindert z.B. die Anpassung durch AL beim Aktivieren einer Szene. Wenn auf \"false\" gesetzt, passt AL das licht unabhängig von der Angabe von Farbe oder Helligkeit in den ursprünglichen `service_data` an. Benötigt das `take_over_control` aktiviert ist. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Verwende getrennte `light.turn_on`-Aufrufe für Farbe und Helligkeit, erforderlich für einige Lichttypen. 🔀", + "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", + "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", + "skip_redundant_commands": "skip_redundant_commands: Überspringt das Senden von Anpassungsbefehlen, deren Zielzustand bereits mit dem bekannten Zustand der Leuchte übereinstimmt. Minimiert den Netzwerkverkehr und verbessert die Anpassungsreaktion in einigen Situationen. 📉 Deaktivieren, falls der physikalische Zustand der Lichter nicht mehr mit dem Zustand in HA übereinstimmt.", + "intercept": "intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, um eine sofortige Anpassung von Farbe und Helligkeit zu ermöglichen. 🏎️ Deaktivieren für Leuchten, die `light.turn_on` mit Farbe und Helligkeit nicht unterstützen.", + "multi_light_intercept": "multi_light_intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, die auf mehrere Lichter aufrufen. ➗⚠️ Dies kann dazu führen, dass ein einzelner `light.turn_on`-Aufruf in mehrere Aufrufe aufgeteilt wird, z.B. wenn Lichter in verschiedenen Schaltern sind. Erfordert, dass `intercept` aktiviert ist.", + "include_config_in_attributes": "include_config_in_attributes: Alle Optionen als Attribute auf dem Schalter im Home Assistant anzeigen, wenn auf `true` gesetzt. 📝", + "expand_light_groups": "expand_light_groups: Lichtgruppen auf einzelne Mitgliedsentitäten erweitern (`true`) oder die Gruppenentität direkt steuern (`false`)." + }, + "data_description": { + "initial_transition": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️", + "sleep_rgb_or_color_temp": "Verwende entweder `rgb_color` oder `color_temp` im Schlafmodus. 🌙", + "sleep_rgb_color": "RGB-Farbe im Schlafmodus (wird verwendet, wenn `sleep_rgb_or_color_temp` `rgb_color` ist). 🌈", + "sleep_transition": "Dauer des Übergangs, wenn der \"Schlafmodus\" umgeschaltet wird, in Sekunden. 😴", + "sunrise_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenaufgang ein. 🌅", + "min_sunrise_time": "Lege die früheste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen späteren Sonnenaufgang zu ermöglichen. 🌅", + "max_sunrise_time": "Lege die späteste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen früheren Sonnenaufgang zu ermöglichen. 🌅", + "sunrise_offset": "Anpassung der Sonnenaufgangszeit mit positivem oder negativem Versatz in Sekunden. ⏰", + "sunset_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenuntergang ein. 🌇", + "min_sunset_time": "Lege die früheste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um spätere Sonnenuntergänge zu ermöglichen. 🌇", + "max_sunset_time": "Lege die späteste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um frühere Sonnenuntergänge zu ermöglichen. 🌇", + "sunset_offset": "Anpassung der Sonnenuntergangszeit mit positivem oder negativem Versatz in Sekunden. ⏰", + "brightness_mode": "Helligkeitsmodus, der verwendet werden soll. Mögliche Werte sind `default`, `linear` und `tanh` (verwendet `brightness_mode_time_dark` und `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit vor/nach Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", + "brightness_mode_time_light": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit nach/vor Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", + "autoreset_control_seconds": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️", + "send_split_delay": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️", + "adapt_delay": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️", + "expand_light_groups": "Wenn auf `false` gesetzt, werden Anpassungsbefehle direkt an die Gruppenentität gesendet und nicht an die einzelnen Mitglieder." + } + } + } + } + }, + "error": { + "option_error": "Ungültige Option", + "entity_missing": "Ein oder mehrere ausgewählte Lichter fehlen in Home Assistant" + } + }, + "services": { + "apply": { + "fields": { + "lights": { + "description": "Eine Leuchte (oder eine Liste von Leuchten), auf die die Einstellungen angewendet werden sollen. 💡" + }, + "entity_id": { + "description": "Die `entity_id` des Schalters mit den zu übernehmenden Einstellungen. 📝" + }, + "prefer_rgb_color": { + "description": "Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈" + }, + "transition": { + "description": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑" + }, + "adapt_brightness": { + "description": "Ob die Helligkeit des Lichts angepasst werden soll. 🌞" + }, + "adapt_color": { + "description": "Ob die Farbtemperatur des Lichts angepasst werden soll. 🌈" + }, + "turn_on_lights": { + "description": "Ob Lichter eingeschaltet werden sollen, die derzeit ausgeschaltet sind. 🔆" + } + }, + "description": "Wendet die aktuellen Einstellungen der adaptiven Beleuchtung auf die Lichter an." + }, + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Lichter nur einmalig anpassen, wenn sie eingeschaltet werden (`true`) oder sie immer wieder anpassen (`false`). 🔄" + }, + "detect_non_ha_changes": { + "description": "Erkennt und stoppt Anpassungen für nicht-`light.turn_on`-Zustandsänderungen. Benötigt, dass `take_over_control` aktiviert ist. 🕵️ Vorsicht: ⚠️ Einige Lichter können fälschlicherweise einen 'an'-Zustand anzeigen, was dazu führen kann, dass Lichter unerwartet eingeschaltet werden. Deaktiviere diese Funktion, wenn solche Probleme auftreten." + }, + "min_brightness": { + "description": "Minimale Helligkeit in Prozent. 💡" + }, + "sunset_time": { + "description": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenuntergang ein. 🌇" + }, + "use_defaults": { + "description": "Setzt die nicht in diesem Service-Aufruf angegebenen Standardwerte. Optionen: `current` (Standard, behält die aktuellen Werte bei), `factory` (setzt auf die in der Dokumentation angegebenen Standardwerte zurück) oder `configuration` (setzt auf die Standardwerte der Switch-Konfiguration zurück). ⚙️" + }, + "max_sunrise_time": { + "description": "Lege die späteste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen früheren Sonnenaufgang zu ermöglichen. 🌅" + }, + "include_config_in_attributes": { + "description": "Zeige alle Optionen als Attribute auf dem Schalter im Home Assistant, wenn auf `true` gesetzt. 📝" + }, + "min_sunset_time": { + "description": "Lege die früheste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um spätere Sonnenuntergänge zu ermöglichen. 🌇" + }, + "sunrise_offset": { + "description": "Anpassung der Sonnenaufgangszeit mit positivem oder negativem Versatz in Sekunden. ⏰" + }, + "sunset_offset": { + "description": "Anpassung der Sonnenuntergangszeit mit positivem oder negativem Versatz in Sekunden. ⏰" + }, + "take_over_control": { + "description": "Deaktiviere die adaptive Beleuchtung, wenn eine andere Quelle `light.turn_on` aufruft, während die Beleuchtung eingeschaltet ist und angepasst wird. Beachte, dass dies `homeassistant.update_entity` jedes `Intervall` aufruft! 🔒" + }, + "max_brightness": { + "description": "Maximale Helligkeit in Prozent. 💡" + }, + "separate_turn_on_commands": { + "description": "Verwende getrennte `light.turn_on`-Aufrufe für Farbe und Helligkeit, erforderlich für einige Lichttypen. 🔀" + }, + "sleep_brightness": { + "description": "Helligkeit der Lichter im Schlafmodus in Prozent. 😴" + }, + "sleep_rgb_color": { + "description": "RGB-Farbe im Schlafmodus (wird verwendet, wenn `sleep_rgb_or_color_temp` `rgb_color` ist). 🌈" + }, + "sleep_rgb_or_color_temp": { + "description": "Verwende entweder `rgb_color` oder `color_temp` im Schlafmodus. 🌙" + }, + "sleep_color_temp": { + "description": "Farbtemperatur im Schlafmodus in Kelvin (wird verwendet, wenn `sleep_rgb_or_color_temp` `color_temp` ist) . 😴" + }, + "sunrise_time": { + "description": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenaufgang ein. 🌅" + }, + "transition": { + "description": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑" + }, + "adapt_delay": { + "description": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️" + }, + "autoreset_control_seconds": { + "description": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️" + }, + "entity_id": { + "description": "Entity ID des Schalters. 📝" + }, + "turn_on_lights": { + "description": "Ob Lichter eingeschaltet werden sollen, die derzeit ausgeschaltet sind. 🔆" + }, + "initial_transition": { + "description": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️" + }, + "sleep_transition": { + "description": "Dauer des Übergangs, wenn der \"Schlafmodus\" umgeschaltet wird, in Sekunden. 😴" + }, + "max_color_temp": { + "description": "Kälteste Farbtemperatur in Kelvin. ❄️" + }, + "min_color_temp": { + "description": "Wärmste Farbtemperatur in Kelvin. 🔥" + }, + "send_split_delay": { + "description": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️" + }, + "prefer_rgb_color": { + "description": "Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈" + } + }, + "description": "Ändern Sie alle Einstellungen, die Sie im Schalter wünschen. Alle Optionen hier sind die gleichen wie im config Flow." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Ob das Licht aus der Liste `manual_control` hinzugefügt (`true`) oder entfernt (`false`) werden soll. 🔒" + }, + "lights": { + "description": "entity_id(s) der Lichter, wenn nichts angegeben wird, werden alle Lichter des Schalters ausgewählt. 💡" + }, + "entity_id": { + "description": "Die `entity_id` des Schalters, in dem das Licht als `manuell gesteuert` (un)markiert werden soll. 📝" + } + }, + "description": "Markiere, ob ein Licht \"manuell gesteuert\" ist." + } + } +} diff --git a/custom_components/adaptive_lighting/translations/el.json b/custom_components/adaptive_lighting/translations/el.json new file mode 100644 index 00000000..e9dbbe41 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/el.json @@ -0,0 +1,27 @@ +{ + "title": "Adaptive Lighting", + "options": { + "step": { + "init": { + "title": "Επιλογές Adaptive Lighting", + "sections": { + "advanced": { + "data": {}, + "data_description": {} + } + }, + "data": {}, + "data_description": {} + } + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Προσαρμογή των φώτων μόνο όταν είναι αναμμένα (`true`) ή συνεχής προσαρμογή αυτών (`false`). 🔄" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json new file mode 100644 index 00000000..cc589156 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/en.json @@ -0,0 +1,299 @@ +{ + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Choose a name for the Adaptive Lighting instance", + "description": "Every instance can contain multiple lights!", + "data": { + "name": "Name" + } + }, + "menu": { + "title": "Create or Duplicate", + "description": "Do you want to create a new instance or duplicate an existing one?", + "data": { + "action": "Action" + } + } + }, + "abort": { + "already_configured": "This device is already configured" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptive Lighting options", + "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app]({webapp_url}). For further details, see the [official documentation]({docs_url}).", + "data": { + "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", + "interval": "interval", + "transition": "transition", + "min_brightness": "min_brightness: Minimum brightness percentage. 💡", + "max_brightness": "max_brightness: Maximum brightness percentage. 💡", + "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", + "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", + "sleep_brightness": "sleep_brightness", + "sleep_color_temp": "sleep_color_temp" + }, + "data_description": { + "interval": "Frequency to adapt the lights, in seconds. 🔄", + "transition": "Duration of transition when lights change, in seconds. 🕑", + "sleep_brightness": "Brightness percentage of lights in sleep mode. 😴", + "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴" + }, + "sections": { + "advanced": { + "name": "Advanced settings", + "description": "Additional settings for fine-tuning Adaptive Lighting.", + "data": { + "initial_transition": "initial_transition", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", + "sleep_rgb_color": "sleep_rgb_color", + "sleep_transition": "sleep_transition", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", + "sunrise_time": "sunrise_time", + "min_sunrise_time": "min_sunrise_time", + "max_sunrise_time": "max_sunrise_time", + "sunrise_offset": "sunrise_offset", + "sunset_time": "sunset_time", + "min_sunset_time": "min_sunset_time", + "max_sunset_time": "max_sunset_time", + "sunset_offset": "sunset_offset", + "brightness_mode": "brightness_mode", + "brightness_mode_time_dark": "brightness_mode_time_dark", + "brightness_mode_time_light": "brightness_mode_time_light", + "take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", + "take_over_control_mode": "take_over_control_mode", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", + "autoreset_control_seconds": "autoreset_control_seconds", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "manual_control_on_external_turn_on": "manual_control_on_external_turn_on: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", + "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", + "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "send_split_delay": "send_split_delay", + "adapt_delay": "adapt_delay", + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", + "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "expand_light_groups": "expand_light_groups: Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets." + }, + "data_description": { + "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", + "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", + "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", + "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", + "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Invalid option", + "entity_missing": "One or more selected light entities are missing from Home Assistant" + } + }, + "services": { + "apply": { + "name": "apply", + "description": "Applies the current Adaptive Lighting settings to lights.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch with the settings to apply. 📝", + "name": "entity_id" + }, + "lights": { + "description": "A light (or list of lights) to apply the settings to. 💡", + "name": "lights" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "Whether to adapt the brightness of the light. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Whether to adapt the color on supporting lights. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "set_manual_control", + "description": "Mark whether a light is 'manually controlled'.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡", + "name": "lights" + }, + "manual_control": { + "description": "Whether to add (\"true\") or remove (\"false\") all adapted attributes of the light from the \"manual_control\" list, or the name of an attribute for selective addition. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "change_switch_settings", + "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.", + "fields": { + "use_defaults": { + "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Maximum brightness percentage. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "Coldest color temperature in Kelvin. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "Minimum brightness percentage. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "Warmest color temperature in Kelvin. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "expand_light_groups": { + "description": "Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets.", + "name": "expand_light_groups" + }, + "separate_turn_on_commands": { + "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "Brightness percentage of lights in sleep mode. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", + "name": "take_over_control" + }, + "take_over_control_mode": { + "description": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", + "name": "take_over_control_mode" + }, + "detect_non_ha_changes": { + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", + "name": "detect_non_ha_changes" + }, + "manual_control_on_external_turn_on": { + "description": "Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", + "name": "manual_control_on_external_turn_on" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "name": "autoreset_control_seconds" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/es.json b/custom_components/adaptive_lighting/translations/es.json new file mode 100644 index 00000000..5944193c --- /dev/null +++ b/custom_components/adaptive_lighting/translations/es.json @@ -0,0 +1,218 @@ +{ + "title": "Iluminación Adaptativa", + "options": { + "step": { + "init": { + "title": "Configuración de la Iluminación Adaptativa", + "data_description": { + "interval": "Frecuencia de adaptación de las luces, en segundos. 🔄", + "transition": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️", + "sleep_brightness": "Porcentaje de brillo de las luces en el modo noche. 😴", + "sleep_color_temp": "Temperatura de color en modo noche (usado cuando`sleep_rgb_or_color_temp` es `color_temp`) en grados Kelvin. 😴" + }, + "data": { + "lights": "lights: Lista de entity_ids de luces a controlar (puede estar vacía). 🌟", + "min_brightness": "min_brightness: Porcentaje mínimo de brillo. 💡", + "max_brightness": "max_brightness: Porcentaje máximo de brillo. 💡", + "min_color_temp": "min_color_temp: Temperatura de color más cálida en grados Kelvin. 🔥", + "max_color_temp": "max_color_temp: Temperatura de color más fría en grados Kelvin. ❄️" + }, + "description": "Configura un componente Adaptive Lighting. Los nombres de las opciones se asemejan a las disponibles en la configuración YAML. Si has definido esta entrada en YAML, no aparecerá ninguna opción aquí. Para gráficos interactivos que demuestran los efectos de los parámetros, visita [esta web app]({webapp_url}). Para más detalles, ver la [documentación oficial]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Preferir ajustar el color RGB a la temperatura de color cuando sea posible. 🌈", + "transition_until_sleep": "transition_until_sleep: Cuando habilitado, Adaptive Lighting tratará los ajustes del modo noche como los valores mínimos, transicionando a esos valores tras la puesta del sol. 🌙", + "take_over_control": "take_over_control: Deshabilita Adaptive Lighting si otra fuente llama`light.turn_on` mientras las luces están encendidas y adaptándose. Cuidado, esto llama`homeassistant.update_entity` cada `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas.", + "only_once": "only_once: Adapta las luces sólo cuando se encienden (`true`) o mantener adaptadas (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Al encender las luces. Si el valor es `true`, AL adapta sólo si se llama `light.turn_on` sin especificar el color o brillo. ❌🌈 Esto, por ejemplo, previene la adaptación al activar una escena. Si el valor es `false`, AL adapta independientemente de la presencia de color o brillo en `service_data` inicial. Necesita `take_over_control` habilitado. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Usar llamadas independientes a `light.turn_on` para color y brillo, necesario para ciertos tipos de luces. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Evitar mandar comandos de adaptación a luces cuyo estado ya sea el esperado. Reduce tráfico en la red y mejora la respuesta de la adaptación en ciertas situaciones. 📉Deshabilitar si el estado real de las luces se desincroniza con el estado registrado en Home Assistant.", + "intercept": "intercept: Intercepta y adapta llamadas a `light.turn_on` para habilitar adaptaciones instantáneas de color y brillo. 🏎️ Deshabilitar para luces que no soporten `light.turn_on` con color y brillo.", + "multi_light_intercept": "multi_light_intercept: Intercepta y adapta llamadas a `light.turn_on` que apuntan a múltiples luces. ➗⚠️ Esto puede resultar en dividir una única llamada a `light.turn_on` en múltiples llamadas, por ejemplo, cuando las luces están vinculadas a distintos interruptores. Requiere que `intercept` esté habilitado.", + "include_config_in_attributes": "include_config_in_attributes: Muestra todas las opciones como atributes del interruptor en Home Assistant cuando sea `true`. 📝" + }, + "data_description": { + "initial_transition": "Duración de la primera transición cuando las luces pasan de `off` a `on` en segundos. ⏲️", + "sleep_rgb_or_color_temp": "Usar el modo`\"rgb_color\"` o `\"color_temp\"` en el modo noche. 🌙", + "sleep_rgb_color": "Color RGB en modo noche(usado cuando `sleep_rgb_or_color_temp` es \"rgb_color\"). 🌈", + "sleep_transition": "Duración de la transición cuando el \"modo noche\" se activa o desactiva, en segundos. 😴", + "sunrise_time": "Fijar una hora (HH:MM:SS) para el amanecer. 🌅", + "min_sunrise_time": "Define el amanecer virtual más temprano (HH:MM:SS), permitiendo amaneceres más tardíos. 🌅", + "max_sunrise_time": "Define el amanecer virtual más tardío (HH:MM:SS), permitiendo amaneceres más tempranos. 🌅", + "sunrise_offset": "Define la hora de la salida del sol con un desfase (positivo o negativo) en segundos. ⏰", + "sunset_time": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇", + "min_sunset_time": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇", + "max_sunset_time": "Define el atardecer virtual más tardío (HH:MM:SS), permitiendo atardeceres más tempranos. 🌇", + "sunset_offset": "Define la hora de la puesta del sol con un desfase (positivo o negativo) en segundos. ⏰", + "brightness_mode": "Modo de brillo a usar. Valores posibles son: `default`, `linear` y `tanh` (usa`brightness_mode_time_dark` y `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", + "brightness_mode_time_light": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", + "take_over_control_mode": "El modo de pausa de adaptación cuando otras fuentes cambian el brillo y/o el color de las luces. `pause_all` siempre pausa tanto la adaptación de brillo como la de color. `pause_changed` pausa la adaptación solo de los atributos cambiados y continúa adaptando los atributos sin cambios, por ejemplo, continúa la adaptación de color cuando solo se cambió el brillo.", + "autoreset_control_seconds": "Resetear automáticamente el control manual tras `X` segundos. Poner a 0 para deshabilitar.", + "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands` para luces que no soportan ajustes simultáneos de brillo y color. ⏲️", + "adapt_delay": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Opción no válida", + "entity_missing": "Una o más entidades de luz seleccionadas no se encuentran en Home Assistant" + } + }, + "services": { + "apply": { + "fields": { + "lights": { + "description": "Luz (o listado de luces) sobre las que aplicar la configuración. 💡" + }, + "transition": { + "description": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️" + }, + "adapt_color": { + "description": "Adaptar (o no) el color en luces que lo soporten. 🌈" + }, + "prefer_rgb_color": { + "description": "Preferir ajustes de color RGB a temperatura de color cuando sea posible. 🌈" + }, + "turn_on_lights": { + "description": "Encender (o no) luces que estén apagadas. 🔆" + }, + "entity_id": { + "description": "El `entity_id` del interruptor con los ajustes a aplicar. 📝" + }, + "adapt_brightness": { + "description": "Adaptar (o no) el brillo de la luz. 🌞" + } + }, + "description": "Aplica la configuración actual de Adaptive Lighting a las luces." + }, + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "Define la hora de la salida del sol con un desfase (positivo o negativo) en segundos. ⏰" + }, + "sunset_offset": { + "description": "Define la hora de la puesta del sol con un desfase (positivo o negativo) en segundos. ⏰" + }, + "only_once": { + "description": "Adaptar las luces solo cuando se enciendan (`true`) o hacerlo siempre (`false`). 🔄" + }, + "sleep_color_temp": { + "description": "Temperatura de color en modo noche (usado cuando`sleep_rgb_or_color_temp` es `color_temp`) en grados Kelvin. 😴" + }, + "max_color_temp": { + "description": "Temperatura de color más fría en grados Kelvin. ❄️" + }, + "send_split_delay": { + "description": "Retraso (ms) entre `separate_turn_on_commands` para luces que no soportan ajustes simultáneos de brillo y color. ⏲️" + }, + "detect_non_ha_changes": { + "description": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas." + }, + "take_over_control": { + "description": "Deshabilita Adaptive Lighting si otra fuente llama `light.turn_on` mientras las luces se estan adaptando. Cuidado porque esto llama a `homeassistant.update_entity` cada`interval`! 🔒" + }, + "initial_transition": { + "description": "Duración de la primera transición cuando las luces pasan de `off` a `on` en segundos. ⏲️" + }, + "transition": { + "description": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️" + }, + "entity_id": { + "description": "ID de la entidad del interruptor. 📝" + }, + "sleep_transition": { + "description": "Duración de la transición cuando el \"modo noche\" se activa o desactiva, en segundos. 😴" + }, + "min_brightness": { + "description": "Porcentaje mínimo de brillo. 💡" + }, + "include_config_in_attributes": { + "description": "Muestra todas las opciones como atributos del interruptor en Home Assistant cuando sea `true`. 📝" + }, + "prefer_rgb_color": { + "description": "Preferir ajustes de color RGB a temperatura de color cuando sea posible. 🌈" + }, + "turn_on_lights": { + "description": "Encender (o no) luces que estén apagadas. 🔆" + }, + "max_brightness": { + "description": "Porcentaje máximo de brillo. 💡" + }, + "use_defaults": { + "description": "Define los valores por defecto no especificados en la llamada al servicio. Opciones: \"current\" (predeterminado, mantiene los valores actuales), \"factory\" (resetea a los valores documentados predeterminados), o \"configuration\" (revierte a los valores por defecto del interruptor). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Usar llamadas independientes a`light.turn_on` para color y brillo, necesario para cierto tipo de luces. 🔀" + }, + "min_color_temp": { + "description": "Temperatura de color más cálida en grados Kelvin. 🔥" + }, + "autoreset_control_seconds": { + "description": "Resetear automáticamente el control manual tras `x` segundos. Poner a 0 para deshabilitar. ⏲️" + }, + "sleep_brightness": { + "description": "Porcentaje de brillo en el modo noche. 😴" + }, + "sleep_rgb_color": { + "description": "Color RGB en modo noche(usado cuando `sleep_rgb_or_color_temp` es \"rgb_color\"). 🌈" + }, + "sunrise_time": { + "description": "Fijar una hora (HH:MM:SS) para el amanecer. 🌅" + }, + "sunset_time": { + "description": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇" + }, + "min_sunset_time": { + "description": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇" + }, + "max_sunrise_time": { + "description": "Define el amanecer virtual más tardío (HH:MM:SS), permitiendo amaneceres más tempranos. 🌅" + }, + "sleep_rgb_or_color_temp": { + "description": "Usar el modo`\"rgb_color\"` o `\"color_temp\"` en el modo noche. 🌙" + }, + "adapt_delay": { + "description": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️" + } + }, + "description": "Modifica cualquier ajuste que quieras en el interruptor. Todas las opciones aquí presentes son idénticas a la configuración del flujo." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Añadir (\"true\") o quitar (\"false\") la luz de la lista de `manual_control`. 🔒" + }, + "lights": { + "description": "entity_id(s) de las luces, si no se especifica, se seleccionaran todas las luces vinculadas al interruptor. 💡" + }, + "entity_id": { + "description": "El `entity_id` del interruptor en el cual (des)marcar la luz como estando `manually controlled`. 📝" + } + }, + "description": "Señala si una luz está 'controlada manualmente'." + } + }, + "config": { + "step": { + "user": { + "title": "Elige un nombre para la instancia de Adaptive Lighting", + "description": "Cada instancia puede contener múltiples luces!" + }, + "menu": { + "title": "Crear o duplicar", + "description": "¿Quieres crear una nueva instancia o duplicar una existente?", + "data": { + "action": "Acción" + } + } + }, + "abort": { + "already_configured": "El dispositivo ya está configurado" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/et.json b/custom_components/adaptive_lighting/translations/et.json new file mode 100644 index 00000000..58c1c54d --- /dev/null +++ b/custom_components/adaptive_lighting/translations/et.json @@ -0,0 +1,58 @@ +{ + "title": "Kohanduv valgus", + "config": { + "step": { + "user": { + "title": "Vali kohanduva valguse üksuse nimi", + "description": "Igas üksuses võib olla mitu valgustit!", + "data": { + "name": "Nimi" + } + } + }, + "abort": { + "already_configured": "Üksus on juba seadistatud" + } + }, + "options": { + "step": { + "init": { + "title": "Kohanduva valguse suvandid", + "description": "Kohanduva valguse suvandid. Valikute nimetused ühtuvad YAML kirjes olevatega. Valikuid ei kuvata kui seadistus on tehtud YAML kirjes.", + "data": { + "lights": "valgustid", + "interval": "Intervall, aeg muutuste vahel sekundites", + "transition": "Üleminekud, sekundites", + "min_brightness": "Vähim heledus %", + "max_brightness": "Suurim heledus %", + "min_color_temp": "Vähim värvustemperatuur Kelvinites", + "max_color_temp": "Suurim värvustemperatuur Kelvinites", + "sleep_brightness": "Unerežiimi heledus %", + "sleep_color_temp": "Uneržiimi värvus Kelvinites" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "Algne üleminek kui valgustid lülituvad sisse/välja või unerežiim muutub", + "prefer_rgb_color": "Eelista RGB värve, võimalusel kasuta RGB sätteid värvustemperatuuri asemel", + "sunrise_time": "Päikesetõusu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)", + "sunrise_offset": "Nihe päikesetõusust, +/- sekundit", + "sunset_time": "Päikeseloojangu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)", + "sunset_offset": "Nihe päikeseloojangust, +/- sekundit", + "take_over_control": "Käsitsi juhtimine: kui miski peale kohanduva valguse enda lültiab valgusti sisse ja see juba põleb, katkesta kohandamine kuni järgmise välise lülitamiseni.", + "detect_non_ha_changes": "Märka väliseid lülitusi: kui mõni säte muutub üle 10% (isegi väljaspoolt HA juhituna) siis peab käsitsi juhtimine olema lubatud (kutsutakse 'homeassistant.update_entity')'interval'!)", + "only_once": "Ainult üks kord, rakendub ainult valgusti sisselülitamisel", + "separate_turn_on_commands": "Eraldi lülitused iga valiku (värvus, heledus jne.) sisselülitamiseks, mõned valgustid vajavad seda." + }, + "data_description": {} + } + }, + "data_description": {} + } + }, + "error": { + "option_error": "Vigane suvand", + "entity_missing": "Valitud valgust ei leitud" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/fi.json b/custom_components/adaptive_lighting/translations/fi.json new file mode 100644 index 00000000..b8310c7c --- /dev/null +++ b/custom_components/adaptive_lighting/translations/fi.json @@ -0,0 +1,210 @@ +{ + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Valojen kirkkausmäärä prosenteissa unitilassa (sleep mode)." + }, + "sunrise_offset": { + "description": "Muuta auringonnousun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa." + }, + "initial_transition": { + "description": "Ensimmäisen siirtymän kesto sekunneissa, kun valot kytketään 'off'-tilasta 'on'-tilaan." + }, + "autoreset_control_seconds": { + "description": "Resetoi manuaalisen ohjauksen automaattisesti määritetyn sekuntimäärän jälkeen. Aseta arvoon 0 jos et halua käyttää asetusta." + }, + "only_once": { + "description": "Adaptoi valoja vain kun ne kytketään päälle ('true') tai adaptoi niitä jatkuvasti ('false')" + }, + "max_color_temp": { + "description": "Kylmin värilämpötila Kelvin-asteikolla." + }, + "sunset_offset": { + "description": "Muuta auringonlaskun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa." + }, + "send_split_delay": { + "description": "Viive (ms) `separate_turn_on_commands` välillä valoille, jotka eivät tue yhtäaikaista kirkkauden ja värilämpötilan säätöä." + }, + "transition": { + "description": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan." + }, + "sleep_color_temp": { + "description": "Värilämpötila lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on `color_temp`) kelvineinä. 😴" + }, + "detect_non_ha_changes": { + "description": "Havaitsee ja pysäyttää mukautukset ei-\"light.turn_on\" -tilanmuutoksille. Vaatii \"take_over_control\":n käyttöönoton. 🕵️ Varoitus: ⚠️ Jotkut valot saatavat osoittaa virheellisesti 'on'-tilan, mikä voi johtaa valojen syttymiseen odottamatta. Poista tämä ominaisuus käytöstä, jos kohtaat tällaisia ongelmia." + }, + "sunrise_time": { + "description": "Aseta kiinteä aika (TT:MM:SS) auringonnousulle. 🌅" + }, + "use_defaults": { + "description": "Asettaa oletusarvot, joita ei ole määritetty tässä palvelukutsussa. Vaihtoehdot: \"nykyinen\" (oletus, säilyttää nykyiset arvot), \"tehdas\" (palauttaa dokumentoituihin oletusasetuksiin) tai \"kokoonpano\" (palaa kokoonpanon oletusasetusten vaihtamiseksi). ⚙️" + }, + "max_sunrise_time": { + "description": "Aseta viimeisin virtuaalinen auringonnousuaika (TT:MM:SS), aikaisempia auringonnousuja sallien. 🌅" + }, + "separate_turn_on_commands": { + "description": "Käytä erillisiä \"light.turn_on\" -kutsuja värin ja kirkkauden määrittämiseksi, joita tarvitaan joissakin valotyypeissä. 🔀" + }, + "entity_id": { + "description": "Kytkimen entiteettitunnus. 📝" + }, + "turn_on_lights": { + "description": "Sytytetäänkö valot, jotka ovat tällä hetkellä pois päältä. 🔆" + }, + "include_config_in_attributes": { + "description": "Näytä kaikki vaihtoehdot attribuutteina kotiavustajan kytkimellä, kun sen arvo on \"true\". 📝" + }, + "sleep_transition": { + "description": "Siirtymän kesto, kun \"lepotila\" vaihdetaan sekunneiksi. 😴" + }, + "max_brightness": { + "description": "Enimmäiskirkkausprosentti. 💡" + }, + "min_brightness": { + "description": "Vähittäiskirkkausprosentti. 💡" + }, + "sleep_rgb_color": { + "description": "RGB-väri lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on \"rgb_color\"). 🌈" + }, + "sunset_time": { + "description": "Aseta kiinteä aika (TT:MM:SS) auringonlaskulle. 🌇" + }, + "sleep_rgb_or_color_temp": { + "description": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙" + }, + "min_color_temp": { + "description": "Lämpimin värilämpötila kelvineissä. 🔥" + }, + "prefer_rgb_color": { + "description": "Halutaanko RGB-värinsäätö mieluummin valon värilämpötilan sijaan, kun mahdollista. 🌈" + }, + "take_over_control": { + "description": "Poista Adaptive Lighting käytöstä, jos toinen lähde kutsuu `light.turn_on`, kun valot ovat päällä ja niitä mukautetaan. Huomaa, että tämä kutsuu `homeassistant.update_entity` joka `interval`! 🔒" + }, + "min_sunset_time": { + "description": "Aseta aikaisin virtuaalinen auringonlaskuaika (TT:MM:SS), myöhempiä auringonlaskuja sallien. 🌇" + }, + "adapt_delay": { + "description": "Odotusaika (sekunteina) valon syttymisen ja Adaptive Lightingin muutosten käyttöönoton välillä. Saattaa auttaa välttämään välkkymistä. ⏲️" + } + }, + "description": "Muuta haluamiasi asetuksia kytkimessä. Kaikki vaihtoehdot ovat samat kuin kokoonpanon kulussa." + }, + "apply": { + "description": "Asettaa nykyiset Adaptiivisen Valaistuksen asetukset valoihin.", + "fields": { + "lights": { + "description": "Valo (tai lista valoista) joihin näitä asetuksia sovelletaan." + }, + "transition": { + "description": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan." + }, + "entity_id": { + "description": "Kytkimen `entity_id` ja käytettävät asetukset. 📝" + }, + "adapt_brightness": { + "description": "Mukautetaanko valon kirkkautta. 🌞" + }, + "adapt_color": { + "description": "Mukautetaanko tukivalojen väriä. 🌈" + }, + "prefer_rgb_color": { + "description": "Halutaanko RGB-värinsäätö mieluummin valon värilämpötilan sijaan, kun mahdollista. 🌈" + }, + "turn_on_lights": { + "description": "Sytytetäänkö valot, jotka ovat tällä hetkellä pois päältä. 🔆" + } + } + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "Valojen entity_id(s), jos sitä ei ole määritetty, kaikki kytkimen valot valitaan. 💡" + }, + "entity_id": { + "description": "Kytkimen `entity_id`, jolla valo määritetään `manuaalisesti ohjattavaksi`. 📝" + }, + "manual_control": { + "description": "Lisätäänkö (\"true\") vai poistetaanko (\"false\") valo \"manual_control\"-luettelosta. 🔒" + } + }, + "description": "Merkitse, onko valo 'manuaalisesti ohjattu'." + } + }, + "title": "Adaptiivinen valaistus", + "options": { + "step": { + "init": { + "data_description": { + "interval": "Tiheys valojen mukauttamiseen sekunneissa. 🔄", + "transition": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan.", + "sleep_brightness": "Valojen kirkkausmäärä prosenteissa unitilassa (sleep mode).", + "sleep_color_temp": "Värilämpötila lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on `color_temp`) kelvineinä. 😴" + }, + "description": "Määritä Adaptive Lighting -komponentti. Vaihtoehtojen nimet vastaavat YAML-asetuksia. Jos olet määrittänyt tämän merkinnän YAML:ssa, tässä ei näy vaihtoehtoja. Interaktiiviset kaaviot, jotka esittelevät parametrien vaikutuksia, on [tässä verkkosovelluksessa]({webapp_url}). Lisätietoja löytyy [virallisesta dokumentaatiosta]({docs_url}).", + "data": { + "lights": "lights: Luettelo ohjattavista valon entity_ids:stä (voi olla tyhjä). 🌟", + "min_brightness": "min_brightness: Vähittäiskirkkausprosentti. 💡", + "max_brightness": "max_brightness: Enimmäiskirkkausprosentti. 💡", + "min_color_temp": "min_color_temp: Lämpimin värilämpötila kelvineinä. 🔥", + "max_color_temp": "max_color_temp: Kylmin värilämpötila kelvineinä. ❄️" + }, + "title": "Adaptive Lightingin vaihtoehdot", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: valitaanko RGB-värien säätö valon värilämpötilan sijaan, kun mahdollista. 🌈", + "transition_until_sleep": "shift_until_sleep: Kun käytössä, Adaptive Lighting käsittelee lepoasetukset miniminä ja siirtyy näihin arvoihin auringonlaskun jälkeen. 🌙", + "take_over_control": "take_over_control: Poista mukautuva valaistus käytöstä, jos toinen lähde kutsuu `light.turn_on`, kun valot ovat päällä ja niitä mukautetaan. Huomaa, että tämä kutsuu `homeassistant.update_entity` joka `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Havaitsee ja pysäyttää mukautukset ei-\"light.turn_on\" -tilanmuutoksille. Vaatii \"take_over_control\" käyttöönoton. 🕵️ Varoitus: ⚠️ Jotkut valot saatavat osoittaa virheellisesti 'on'-tilan, mikä voi johtaa valojen syttymiseen odottamatta. Poista tämä ominaisuus käytöstä, jos kohtaat tällaisia ongelmia.", + "only_once": "only_once: Mukauta valot vain, kun ne ovat päällä (\"true\") tai mukauta niitä jatkuvasti (\"false\"). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kun valot sytytetään ensimmäisen kerran. Jos asetuksena on \"true\", Adaptive Lighting mukautuu vain, jos \"light.turn_on\" kutsutaan määrittelemättä väriä tai kirkkautta. ❌🌈 Tämä esimerkiksi estää mukautumisen näkymää aktivoitaessa. Jos \"false\", Adaptive Lighting mukautuu riippumatta siitä, onko alkuperäisessä \"service_data\"-arvossa väri tai kirkkaus. Vaatii \"take_over_control\":n käyttöönoton. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Käytä erillisiä `light.turn_on`-kutsuja värin ja kirkkauden määrittämiseksi, joita tarvitaan joissakin valotyypeissä. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Ohita mukautuskomentojen lähettäminen, joiden kohdetila on jo yhtä suuri kuin valon tunnettu tila. Minimoi verkkoliikenteen ja parantaa mukautumisvastetta joissain tilanteissa. 📉 Poista käytöstä, jos fyysiset valotilat eivät ole synkronoitu kotiavustajan tallennetun tilan kanssa.", + "intercept": "intercept: sieppaa ja mukauta \"light.turn_on\"-kutsut mahdollistamaan välitön värin ja kirkkauden mukauttaminen. 🏎️ Poista käytöstä valot, jotka eivät tue \"light.turn_on\" värin ja kirkkauden kanssa.", + "multi_light_intercept": "multi_light_intercept: sieppaa ja mukauta light.turn_on-kutsut, jotka kohdistuvat useisiin valoihin. ➗⚠️ Tämä saattaa johtaa yksittäisen light.turn_on-kutsun jakamiseen useiksi kutsuiksi, esimerkiksi kun valot ovat eri kytkimissä. Vaadi `intercept`:n käyttöönotto.", + "include_config_in_attributes": "include_config_in_attributes: Näytä kaikki vaihtoehdot attribuutteina Kotiavustajan kytkimessä, kun asetuksena on \"true\". 📝" + }, + "data_description": { + "initial_transition": "Ensimmäisen siirtymän kesto sekunneissa, kun valot kytketään 'off'-tilasta 'on'-tilaan.", + "sleep_rgb_or_color_temp": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙", + "sleep_rgb_color": "RGB-väri lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on \"rgb_color\"). 🌈", + "sleep_transition": "Siirtymän kesto, kun \"lepotila\" vaihdetaan sekunneiksi. 😴", + "sunrise_time": "Aseta kiinteä aika (TT:MM:SS) auringonnousulle. 🌅", + "min_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), myöhempiä auringonnousuja sallien. 🌅", + "max_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), aikaisempia auringonnousuja sallien. 🌅", + "sunrise_offset": "Muuta auringonnousun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", + "sunset_time": "Aseta kiinteä aika (TT:MM:SS) auringonlaskulle. 🌇", + "min_sunset_time": "Aseta aikaisin virtuaalinen auringonlaskuaika (TT:MM:SS), myöhempiä auringonlaskuja sallien. 🌅", + "max_sunset_time": "Aseta viimeisin virtuaalinen auringonlaskuaika (TT:MM:SS), jotta aikaisemmat auringonlaskut ovat mahdollisia. 🌇", + "sunset_offset": "Muuta auringonlaskun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", + "brightness_mode": "Kirkkaus-moodi jota käytetään. Mahdolliset arvot ovat `default`, `linear`, and `tanh` (käyttää arvoja `brightness_mode_time_dark` ja `brightness_mode_time_light`).", + "brightness_mode_time_dark": "(Ohitetaan, jos `brightness_mode='default'`) Kesto sekunteina kirkkauden lisäämiseen/vähentämiseen auringonnousun/auringonlaskun jälkeen/ennen. 📈📉.", + "brightness_mode_time_light": "(Ohitetaan, jos `brightness_mode='default'`) Kesto sekunteina kirkkauden lisäämiseen/vähentämiseen auringonnousun/auringonlaskun jälkeen/ennen. 📈📉.", + "autoreset_control_seconds": "Resetoi manuaalisen ohjauksen automaattisesti määritetyn sekuntimäärän jälkeen. Aseta arvoon 0 jos et halua käyttää asetusta.", + "send_split_delay": "Viive (ms) `separate_turn_on_commands` välillä valoille, jotka eivät tue yhtäaikaista kirkkauden ja värilämpötilan säätöä.", + "adapt_delay": "Odotusaika (sekunteina) valon syttymisen ja Adaptive Lightingin muutosten käyttöönoton välillä. Saattaa auttaa välttämään välkkymistä. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Virheellinen vaihtoehto", + "entity_missing": "Kotiavustajasta puuttuu yksi tai useampi valittu valoentiteetti" + } + }, + "config": { + "step": { + "user": { + "title": "Valitse nimi tälle Adaptiivisen Valaistuksen esiintymälle", + "description": "Jokainen esiintymä voi sisältää useita valoja!" + } + }, + "abort": { + "already_configured": "Tämä laite on jo määritetty" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json new file mode 100644 index 00000000..4feaa9e8 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -0,0 +1,227 @@ +{ + "title": "Éclairage adaptatif", + "config": { + "step": { + "user": { + "title": "Choisissez un nom pour cette instance d'éclairage adaptatif", + "description": "Chaque instance peut contenir plusieurs lumières", + "data": { + "name": "Nom" + } + }, + "menu": { + "title": "Créer ou dupliquer", + "description": "Voulez-vous créer une nouvelle instance, ou dupliquer une existante ?" + } + }, + "abort": { + "already_configured": "Cet appareil est déjà configuré" + } + }, + "options": { + "step": { + "init": { + "title": "Options d'éclairage adaptatif", + "description": "Configurer un composant d'éclairage adaptatif. Les noms correspondent aux paramètres YAML. Si vous avez défini cette entrée en YAML, aucune option n'apparaît ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visiter [cette application web]({webapp_url}). Pour plus de détail, voir la [documentation]({docs_url})", + "data": { + "lights": "lights : Liste d'\"entity_ids\" de lumières à controller (peu être vide). 🌟", + "interval": "interval : Temps (en secondes) entre deux mises à jour du commutateur.", + "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes.", + "min_brightness": "min_brightness : Luminosité minimale en pourcentage. 💡", + "max_brightness": "max_brightness : Luminosité maximum (en pourcentage). 💡", + "min_color_temp": "min_color_temp : Couleur de température la plus chaude en kelvins. 🔥", + "max_color_temp": "max_color_temp : Couleur la plus froide (en Kelvins). ❄️", + "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.", + "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit." + }, + "data_description": { + "interval": "Fréquence d'adaptation des lumières, en secondes. 🔄", + "transition": "Durée de la transition des changements lumineux, en secondes. 🕑", + "sleep_brightness": "Pourcentage de luminosité des lumières en mode nuit. 😴", + "sleep_color_temp": "Température de couleur en mode nuit en Kelvin (utilisée lorsque \"sleep_rgb_or_color_temp\" est égaler à \"color_temp\") . 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».", + "prefer_rgb_color": "prefer_rgb_color : Indique s'il est préférable d'utiliser le réglage de couleur RBG plutôt que la température de couleur lorsque cela est possible. 🌈", + "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.", + "transition_until_sleep": "transition_until_sleep : Lorsque cela est activée, l'éclairage Adaptatif considérera les paramètres du mode nuit comme le minimum, effectuant la transition vers ces valeurs après le coucher du soleil. 🌙", + "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", + "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.", + "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", + "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.", + "take_over_control": "take_over_control : Désactive l'éclairage adaptatif si une autre source appelle \"light.turn_on\" pendant que la lumière est allumée ou adoptée. Notez que cela appelle \"homeassistant.update_entity\" chaque \"interval\"! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes : Détecter et arrête les changement d'états autre que \"light.turn_on\". Nécessite que \"take_over_control\" soit activé. 🕵️ Attention : ⚠️ Certaines lumière peuvent faussement indiqué un état \"on\", ce qui pourrait occasionner des lumières s'allumant de façon inattendu. Désactivez cette fonctionnalité si vous rencontrez de tels problème.", + "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées. 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Lors de l'allumage initiale des lumières. Si le paramètre est \"vrai\", AL s'adapte uniquement si l'on invoque \"light.turn_on\" sans préciser la couleur ou la luminosité. ❌🌈 Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si \"false\", AL adapte indépendamment de la présence de couleur ou de luminosité dans le \"service_data\" initial. \"take_over_control\" doit être activé. 🕵", + "separate_turn_on_commands": "separate_turn_on_commands : Utiliser des appels \"light.turn_on\" séparés pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀", + "skip_redundant_commands": "skip_redundant_commands : Évite d'envoyer des commandes d'adaptation lorsque l'état cible est déjà égal à l'état connu de la lumière. Minimise le trafic réseau et améliore la réactivité de l'adaptation dans certaines situations. 📉 Désactivez si les états physiques des lumières ne correspondent pas à l'état enregistré de Home Assistant.", + "intercept": "intercept : Intercepter et adapter les appels à \"light.turn_on\" pour permettre une adaptation instantanée de la couleur et de la luminosité. 🏎️ Désactivez cette option pour les lumières qui ne prennent pas en charge \"light.turn_on\" avec couleur et luminosité.", + "multi_light_intercept": "multi_light_intercept : Intercepte et adapte les appels à \"light.turn_on\" qui ciblent plusieurs lumières. ➗⚠️ Cela peut entraîner la division d'un seul appel \"light.turn_on\" en plusieurs appels, par exemple, lorsque les lumières sont dans différents interrupteurs. Nécessite que \"intercept\" soit activé.", + "include_config_in_attributes": "include_config_in_attributes : Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur \"true\". 📝" + }, + "data_description": { + "initial_transition": "Durée de la première transition des lampes passent de \"off\" à \"on\" (en secondes). ⏲️", + "sleep_rgb_or_color_temp": "Utilisez soit \"rgb_color\" soit \"color_temp\" en mode nuit. 🌙", + "sleep_rgb_color": "Couleur RGB en mode nuit (utilisée lorsque \"sleep_rgb_or_color_temp\" est \"rgb_color\"). 🌈", + "sleep_transition": "Durée de la transition quand le \"mode nuit\" est déclenché. (en secondes) 😴", + "sunrise_time": "Définir une heure fixe (HH:MM:SS) pour le lever du soleil. 🌅", + "min_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus précoce (HH:MM:SS), permettant des levers de soleil tardifs. 🌅", + "max_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus tôt. 🌅", + "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰", + "sunset_time": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌇", + "min_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil tardifs. 🌇", + "max_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus tardive (HH:MM:SS), permettant des couchers de soleil plus précoces. 🌇", + "sunset_offset": "Réglez le l'heure de coucher du soleil avec un décalage positif ou négatif en quelques secondes. ⏰", + "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont \"défaut\" , \"linear\" (linéaire) et \"tanh\" (tangente) utilise \"brightness_mode_time_dark\" et \"brightness_mode_time_light\". 📈", + "brightness_mode_time_dark": "(Ignoré si \"brightness_mode='default'\") La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", + "brightness_mode_time_light": "(Ignoré si \"brightness_mode='default'\") La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", + "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲️", + "send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲️", + "adapt_delay": "Temps d'attente (en secondes) entre l'allumage de la lumière et l'application des changements par l'Éclairage Adaptatif. Peut aider à éviter les scintillements. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Option invalide", + "entity_missing": "Une ou plusieurs entités lumières sélectionnées sont manquantes de Home Assistant" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Pourcentage de luminosité des lumières en mode nuit. 😴" + }, + "only_once": { + "description": "Adapter les lumières seulement quand elles sont allumées (\"vrai\") ou quel que soit leur état (\"faux\")." + }, + "sunrise_offset": { + "description": "Ajustez l'heure de lever de soleil avec un décalage positif ou négatif en secondes. ⏰" + }, + "max_color_temp": { + "description": "Température de couleur la plus froide en Kelvin. ❄️" + }, + "send_split_delay": { + "description": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲️" + }, + "detect_non_ha_changes": { + "description": "detect_non_ha_changes : Détecter et arrête les changement d'états autre que \"light.turn_on\". Nécessite que \"take_over_control\" soit activé. 🕵️ Attention : ⚠️ Certaines lumière peuvent faussement indiqué un état \"on\", ce qui pourrait occasionner des lumières s'allumant de façon inattendu. Désactivez cette fonctionnalité si vous rencontrez de tels problème." + }, + "autoreset_control_seconds": { + "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲️" + }, + "sunset_offset": { + "description": "Ajustez l'heure de coucher de soleil avec un décalage positif ou négatif en secondes. ⏰" + }, + "sleep_color_temp": { + "description": "Température de couleur en mode nuit en Kelvin (utilisé lorsque \"sleep_rgb_or_color_temp\" est défini sur \"color_temp\") . 😴" + }, + "entity_id": { + "description": "ID de l'Entité de l'interrupteur. 📝" + }, + "initial_transition": { + "description": "Durée de la première transition des lampes passant de \"off\" à \"on\" (en secondes). ⏲️" + }, + "transition": { + "description": "Durée de la transition des changements lumineux, en secondes. 🕑" + }, + "sleep_transition": { + "description": "Durée de la transition quand le \"mode nuit\" est déclenché en secondes. 😴" + }, + "min_brightness": { + "description": "Pourcentage de luminosité minimum. 💡" + }, + "sunrise_time": { + "description": "Définir une heure fixe (HH:MM:SS) pour le lever du soleil. 🌅" + }, + "max_brightness": { + "description": "Pourcentage de luminosité maximale. 💡" + }, + "take_over_control": { + "description": "Désactiver l'Éclairage Adaptatif si une autre source appelle \"light.turn_on\" lorsque les lumières sont allumées et en cours d'adaptation. Notez que cela appelle \"homeassistant.update_entity\" à chaque \"intervalles\" ! 🔒" + }, + "use_defaults": { + "description": "Définit les valeurs par défaut non spécifiées dans cet appel de service. Options : \"current\" (par défaut, conserve les valeurs actuelles), \"factory\" (réinitialise aux valeurs par défaut documentées) ou \"configuration\" (revient aux valeurs par défaut de la configuration de l'interrupteur). ⚙️" + }, + "sunset_time": { + "description": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌇" + }, + "min_sunset_time": { + "description": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil tardifs. 🌇" + }, + "max_sunrise_time": { + "description": "Définir l'heure virtuelle du lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus tôt. 🌅" + }, + "min_color_temp": { + "description": "Température de couleur la plus chaude en Kelvin. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Utilisez soit \"rgb_color\" soit \"color_temp\" en mode nuit. 🌙" + }, + "turn_on_lights": { + "description": "Indique s'il faut allumer les lumières qui sont actuellement éteintes. 🔆" + }, + "include_config_in_attributes": { + "description": "Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur \"true\". 📝" + }, + "sleep_rgb_color": { + "description": "Couleur RGB en mode nuit (utilisée lorsque `sleep_rgb_or_color_temp` est `rgb_color`). 🌈" + }, + "adapt_delay": { + "description": "Temps d'attente (en secondes) entre l'allumage de la lumière et l'application des changements par l'Éclairage Adaptatif. Peut aider à éviter les scintillements. ⏲️" + }, + "separate_turn_on_commands": { + "description": "Utilisez des appels distincts à \"light.turn_on\" pour la couleur et la luminosité, nécessaire pour certains types de lumières. 🔀" + }, + "prefer_rgb_color": { + "description": "Indique s'il faut privilégier l'ajustement de la couleur RGB plutôt que la température de couleur de la lumière lorsque c'est possible. 🌈" + } + }, + "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flow de configuration." + }, + "apply": { + "description": "Applique les réglages d'éclairage adaptatif actuels aux lumières.", + "fields": { + "lights": { + "description": "Une lumière (ou une liste de lumières) à laquelle appliquer les réglages." + }, + "transition": { + "description": "Durée de la transition des changements lumineux, en secondes. 🕑" + }, + "entity_id": { + "description": "L'`entity_id` de l'interrupteur avec les paramètres à appliquer. 📝" + }, + "adapt_brightness": { + "description": "Indique s'il faut adapter la luminosité de la lumière. 🌞" + }, + "turn_on_lights": { + "description": "Indique s'il faut allumer les lumières qui sont actuellement éteintes. 🔆" + }, + "adapt_color": { + "description": "Indique s'il faut adapter la couleur sur les lumières compatibles. 🌈" + }, + "prefer_rgb_color": { + "description": "Indique s'il faut privilégier l'ajustement de la couleur RGB plutôt que la température de couleur de la lumière lorsque c'est possible. 🌈" + } + } + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "entity_id(s) des lumières, si non spécifié, toutes les lumières de l'interrupteur sont sélectionnées. 💡" + }, + "manual_control": { + "description": "Indique s'il faut ajouter (\"true\") ou retirer (\"false\") la lumière de la liste \"manual_control\". 🔒" + }, + "entity_id": { + "description": "L'\"entity_id\" de l'interrupteur dans lequel (dé)marquer la lumière comme étant \"manuellement contrôlée\". 📝" + } + }, + "description": "Indiquer si une lumière est \"contrôlée manuellement\"." + } + } +} diff --git a/custom_components/adaptive_lighting/translations/gl.json b/custom_components/adaptive_lighting/translations/gl.json new file mode 100644 index 00000000..17d23d0a --- /dev/null +++ b/custom_components/adaptive_lighting/translations/gl.json @@ -0,0 +1,53 @@ +{ + "options": { + "step": { + "init": { + "data_description": { + "interval": "Frecuencia para adaptar as luces, en segundos. 🔄", + "sleep_brightness": "Porcentaxe de brillo das luces en modo durmir. 😴" + }, + "title": "Configuración de Iluminación Adaptativa", + "sections": { + "advanced": { + "data": {}, + "data_description": { + "sunrise_offset": "Axusta a hora do amencer cun desfasamento positivo ou negativo en segundos. ⏰", + "sunset_offset": "Axusta a hora da posta de sol cun desfasamento positivo ou negativo en segundos. ⏰", + "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands`" + } + } + }, + "data": {} + } + } + }, + "title": "Iluminación Adaptativa", + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Porcentaxe de brillo das luces en modo durmir. 😴" + }, + "only_once": { + "description": "Adaptar luces só cando estean acesas (`true`) ou mantelas adaptándose (`false`). 🔄" + }, + "sunrise_offset": { + "description": "Axusta a hora do amencer cun desfasamento positivo ou negativo en segundos. ⏰" + }, + "sunset_offset": { + "description": "Axusta a hora da posta de sol cun desfasamento positivo ou negativo en segundos. ⏰" + }, + "transition": { + "description": "Duración da transición cando as luces cambian, en segundos. 🕑" + } + } + } + }, + "config": { + "step": { + "user": { + "title": "Escolle un nome para a instancia de Iluminación Dinámica" + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/hr.json b/custom_components/adaptive_lighting/translations/hr.json new file mode 100644 index 00000000..84301681 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/hr.json @@ -0,0 +1,38 @@ +{ + "options": { + "step": { + "init": { + "title": "Opcije prilagodljivog osvjetljenja", + "data_description": {}, + "data": {}, + "sections": { + "advanced": { + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Prilikom početnog paljenja svjetla. Ako je postavljeno na \"true\", AL se prilagođava samo ako se \"light.turn_on\" pozove bez navođenja boje ili svjetline. ❌🌈 Ovo npr. sprječava prilagodbu prilikom aktiviranja scene. Ako je \"false\", AL se prilagođava bez obzira na prisutnost boje ili svjetline u početnim \"service_data\". Potrebno je omogućiti `take_over_control`. 🕵️ " + }, + "data_description": { + "sunrise_offset": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰", + "sunset_offset": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰" + } + } + } + } + } + }, + "title": "prilagodi svjetlinu", + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Prilagodi svjetla samo kada su uključena (true) ili ih neprestano prilagođavaj (false). 🔄" + }, + "sunrise_offset": { + "description": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰" + }, + "sunset_offset": { + "description": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/hu.json b/custom_components/adaptive_lighting/translations/hu.json new file mode 100644 index 00000000..deda5379 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/hu.json @@ -0,0 +1,210 @@ +{ + "options": { + "step": { + "init": { + "data_description": { + "interval": "Gyakoriság a lights illesztéséhez, másodpercekben. 🔄", + "transition": "Az transition időtartama, amikor a lights változnak, másodpercben. 🕑", + "sleep_brightness": "Az alvó üzemmódban lévő lights fényerejének százalékos értéke. 😴", + "sleep_color_temp": "Színhőmérséklet alvó üzemmódban (amikor a `sleep_rgb_or_color_temp` értéke `color_temp`) Kelvinben megadva. 😴" + }, + "data": { + "lights": "lights: Az entity_id-k listája, amelyeket az AL vezéreljen (üresen is maradhat).🌟", + "min_brightness": "min_brightness: Minimális fényerő százalékban. 💡", + "max_brightness": "max_brightness: Maximális fényerő százalékban megadva. 💡", + "min_color_temp": "min_color_temp: A legmelegebb színhőmérséklet Kelvinben. 🔥", + "max_color_temp": "max_color_temp: A leghidegebb színhőmérséklet kelvinben. ❄️" + }, + "title": "Adaptív világítás beállításai", + "description": "Egy Adaptív világítás komponens konfigurálása. Az opciók nevei a YAML-beállításokhoz igazodnak. Ha ezt a bejegyzést YAML-ben definiálta, itt nem jelennek meg beállítások. A paraméterek hatásait bemutató interaktív grafikonokért látogasson el [erre a webes alkalmazásra]({webapp_url}). További részletekért olvasd el a [hivatalos dokumentációt]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Lehetőség szerint az RGB színbeállítás előnyben részesítése a fény színhőmérsékletével szemben. 🌈", + "transition_until_sleep": "transition_until_sleep: Ha engedélyezve van, az Adaptív világítás az alvó mód beállításokat minimálisnak tekinti, és napnyugta után ezekre az értékekre vált át. 🌙", + "take_over_control": "take_over_control: Adaptív világítás kikapcsolása, amennyiben más forrásból érkező `Világítás: Bekapcsolás` szolgáltatás hívás történik, miközben a fények be vannak kapcsolva és illesztve vannak. Vegye figyelembe, hogy ez minden `interval`-ban meghívja a `homeassistant.update_entity`-t! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: `Világítás: Bekapcsolás`- szolgáltatás meghívástól eltérő állapotváltozások esetén észleli és leállítja az illesztéseket. A `take_over_control` beállítás engedélyezése szükséges. 🕵️ Vigyázat: ⚠️ Egyes lights tévesen jelezhetik a \"bekapcsolt\" állapotot, ami váratlanul bekapcsolódó lámpákhoz vezethet. Ha ilyen problémákat tapasztal, tiltsa le ezt a funkciót.", + "only_once": "only_once: lights illesztése kizárólag, amikor azok be vannak kapcsolva (`igaz`) vagy tartsa folyamatosan illesztve őket (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kizárólag a bekapcsoláskor érvényes. A beállítást \"igaz\"-ra állítva, az AL csak akkor végzi az illesztést, amennyiben a \"Világítás: Bekapcsolás\" szolgáltatás meghívása a szín és fényerő paraméterek megadása nélkül történik.❌🌈 Ez pl. alkalmas az illesztés felfüggesztésére egy jelenet aktiválásakor. \"Hamis\" beállítás esetén az AL elvégzi a kezdeti illesztést a szín és fényerő paraméterek meghívásától függetlenül. A használatához engedélyezve kell lennie a \"take_over_control\" beállításnak. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Elkülönített `Világítás: Bekapcsolás` hívásokat használ a szín és a fényerő beállításához, ami néhány világítás típusnál szükséges. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Az olyan adaptációs parancsok küldésének kihagyása, amelyek célállapota már megegyezik a fény ismert állapotával. Minimalizálja a hálózati forgalmat, és bizonyos helyzetekben javítja az adaptációs reakciókészséget. 📉Kapcsolja ki, ha a fizikai fényállapotok nem szinkronizálódnak a HA rögzített állapotával.", + "intercept": "elfogás: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása a színek és a fényerő azonnali illesztésének lehetővé tétele érdekében. 🏎️ Letiltja az olyan lights esetében, amelyek nem támogatják a `Világítás: Bekapcsolás` szolgáltatás színnel és fényerővel történő szín- és fényerőszabályozását.", + "multi_light_intercept": "multi_light_intercept: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása, amelyek több fényt céloznak meg. ➗⚠️ Ez azt eredményezheti, hogy egyetlen `Világítás: Bekapcsolás` szolgáltatás hívás több hívásra oszlik fel, pl. ha a lights különböző kapcsolókban vannak. Az `elfogás` engedélyezése szükséges.", + "include_config_in_attributes": "include_config_in_attributes: A kapcsoló összes opciójának attribútumként való megjelenítése a Home Assistantben, ha a beállítás értéke `igaz`. 📝" + }, + "data_description": { + "initial_transition": "Az első transition időtartama, amikor a lights \"kikapcsolt\" állapotból \"bekapcsolt\" állapotba váltanak, másodpercben. ⏲️", + "sleep_rgb_or_color_temp": "Az `\"rgb_color\" vagy a `\"color_temp\" használata alvó üzemmódban. 🌙", + "sleep_rgb_color": "RGB szín alvó üzemmódban (akkor érvényes, ha a `sleep_rgb_or_color_temp` értéke \"rgb_color\"). 🌈", + "sleep_transition": "Az transition időtartama az \"alvó üzemmód\" kapcsolásakor másodpercben. 😴", + "sunrise_time": "Állítson be egy fix időpontot (HH:MM:SS) a napfelkeltéhez. 🌅", + "min_sunrise_time": "Állítsa be a legkorábbi virtuális napfelkelte időpontját (HH:MM:SS), lehetővé téve a későbbi napfelkeltét. 🌅", + "max_sunrise_time": "A legkésőbbi virtuális napfelkelte időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi napfelkeltét. 🌅", + "sunrise_offset": "A napfelkelte idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰", + "sunset_time": "Állítson be egy fix időpontot (HH:MM:SS) a naplementéhez. 🌇", + "min_sunset_time": "Állítsa be a legkorábbi virtuális naplemente időpontját (HH:MM:SS), lehetővé téve a későbbi naplementéket. 🌇", + "max_sunset_time": "A legkésőbbi virtuális napnyugta időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi naplementéket. 🌇", + "sunset_offset": "A naplemente idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰", + "brightness_mode": "Használandó fényerő üzemmód. A lehetséges értékek: `default`, `linear` és `tanh` (a `brightness_mode_time_dark` és `brightness_mode_time_light` értékeket használja). 📈", + "brightness_mode_time_dark": "(Figyelmen kívül hagyva, ha `brightness_mode='default'`) A fényerő növelésének/csökkentésének időtartama másodpercben napfelkelte/napnyugta után/előtt. 📈📉.", + "brightness_mode_time_light": "(Figyelmen kívül hagyva, ha `brightness_mode='default'`) A fényerő növelésének/csökkentésének időtartama másodpercben napfelkelte/napnyugta után/előtt. 📈📉.", + "autoreset_control_seconds": "Automatikusan visszaállítja a kézi vezérlést néhány másodperc után. A letiltáshoz állítsa 0-ra. ⏲️", + "send_split_delay": "Késleltetés (ms-ban) a `separate_turn_on_commands` (különálló_bekapcsolási_parancsok) között olyan lights esetében, amelyek nem támogatják a fényerő és a szín egyidejű beállítását. ⏲️", + "adapt_delay": "Várakozási idő (másodpercben) a világítás bekapcsolása és az Adaptív világítás alkalmazása között. Segíthet elkerülni a villódzást. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Érvénytelen beállítás", + "entity_missing": "Egy vagy több kiválasztott világítás entitás hiányzik a Home Assistantból" + } + }, + "title": "Adaptív világítás", + "services": { + "change_switch_settings": { + "fields": { + "entity_id": { + "description": "A kapcsoló entitásazonosítója. 📝" + }, + "max_brightness": { + "description": "Maximális fényerő százalékban megadva. 💡" + }, + "max_color_temp": { + "description": "Leghidegebb színhőmérséklet Kelvinben megadva. ❄️" + }, + "sleep_brightness": { + "description": "Az alvó üzemmódban lévő lights fényerejének százalékos értéke. 😴" + }, + "detect_non_ha_changes": { + "description": "`Világítás: Bekapcsolás`- szolgáltatás meghívástól eltérő állapotváltozások esetén észleli és leállítja az illesztéseket. A `take_over_control` beállítás engedélyezése szükséges. 🕵️ Vigyázat: ⚠️ Egyes lights tévesen jelezhetik a \"bekapcsolt\" állapotot, ami váratlanul bekapcsolódó lámpákhoz vezethet. Ha ilyen problémákat tapasztal, tiltsa le ezt a funkciót." + }, + "sunrise_offset": { + "description": "A napfelkelte idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰" + }, + "max_sunrise_time": { + "description": "A legkésőbbi virtuális napfelkelte időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi napfelkeltét. 🌅" + }, + "sleep_color_temp": { + "description": "Színhőmérséklet alvó üzemmódban (akkor használatos, ha a `sleep_rgb_or_color_temp` a `color_temp`-re van állítva) kelvinben. 😴" + }, + "min_brightness": { + "description": "Minimális fényerő százalékban. 💡" + }, + "min_color_temp": { + "description": "A legmelegebb színhőmérséklet Kelvinben. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Az `\"rgb_color\" vagy a `\"color_temp\" használata alvó üzemmódban. 🌙" + }, + "turn_on_lights": { + "description": "A jelenleg kikapcsolt fények bekapcsolása. 🔆" + }, + "initial_transition": { + "description": "Az első transition időtartama, amikor a lights \"kikapcsolt\" állapotból \"bekapcsolt\" állapotba váltanak, másodpercben. ⏲️" + }, + "sunrise_time": { + "description": "Állítson be egy fix időpontot (HH:MM:SS) a napfelkeltéhez. 🌅" + }, + "include_config_in_attributes": { + "description": "Az összes beállítás megjelenítése a kapcsoló attribútumaként a Home Assistantban, ha a beállítás `igaz`. 📝" + }, + "sleep_rgb_color": { + "description": "RGB szín alvó üzemmódban (akkor érvényes, ha a `sleep_rgb_or_color_temp` értéke \"rgb_color\"). 🌈" + }, + "take_over_control": { + "description": "Az Adaptív világítás letiltása, ha egy másik forrásból érkező `Világítás: Bekapcsolás` hívja, miközben a világítás be van kapcsolva és AL által illesztve van. Vegye figyelembe, hogy ez minden \"interval\"-ban meghívja a `homeassistant.update_entity`-t! 🔒" + }, + "sleep_transition": { + "description": "Az transition időtartama az \"alvó üzemmód\" kapcsolásakor másodpercben. 😴" + }, + "autoreset_control_seconds": { + "description": "Automatikusan visszaállítja a kézi vezérlést néhány másodperc után. A letiltáshoz állítsa 0-ra. ⏲️" + }, + "adapt_delay": { + "description": "Várakozási idő (másodpercben) a világítás bekapcsolása és az Adaptív világítás alkalmazása között. Segíthet elkerülni a villódzást. ⏲️" + }, + "only_once": { + "description": "A lights illesztése csak a bekapcsoláskor egy alkalommal (\"igaz\") vagy folyamatosan történjen a bekapcsolás után is (\"hamis\")." + }, + "use_defaults": { + "description": "Beállítja az ebben a szolgáltatáshívásban meg nem adott alapértelmezett értékeket. Opciók: \"(alapértelmezett, megtartja az aktuális értékeket), \"gyári\" (visszaállítja a dokumentált alapértelmezett értékeket) vagy \"konfiguráció\" (visszaállítja a kapcsoló konfigurációjának alapértelmezett értékeit). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Elkülönített `Világítás: Bekapcsolás` szolgáltatás hívások használata a szín és a fényerő számára, ami néhány világítás típusnál szükséges. 🔀" + }, + "prefer_rgb_color": { + "description": "Lehetőség szerint az RGB színbeállítás előnyben részesítése a fény színhőmérsékletével szemben. 🌈" + }, + "sunset_offset": { + "description": "A naplemente idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰" + }, + "send_split_delay": { + "description": "Késleltetés (ms-ban) a `separate_turn_on_commands` (különálló_bekapcsolási_parancsok) között olyan lights esetében, amelyek nem támogatják a fényerő és a szín egyidejű beállítását. ⏲️" + }, + "sunset_time": { + "description": "Állítson be egy fix időpontot (HH:MM:SS) a naplementéhez. 🌇" + }, + "transition": { + "description": "Az transition időtartama, amikor a lights változnak, másodpercben. 🕑" + }, + "min_sunset_time": { + "description": "Állítsa be a legkorábbi virtuális naplemente időpontját (HH:MM:SS), lehetővé téve a későbbi naplementéket. 🌇" + } + }, + "description": "Módosítsa a kapcsolóban a kívánt beállításokat. Itt minden beállítás ugyanaz, mint a konfigurációs folyamban." + }, + "set_manual_control": { + "description": "Jelölje meg, hogy egy lámpa „kézi vezérlésű”-e.", + "fields": { + "manual_control": { + "description": "A világítás hozzáadása (\"true\") vagy eltávolítása (\"false\") a \"manual_control\" listából. 🔒" + }, + "entity_id": { + "description": "A kapcsoló `entity_id`-je, amelyben a lámpát \"kézi vezérlésűnek\" kell jelölni. 📝" + }, + "lights": { + "description": "a lights entity_id-je(i), ha nincs megadva, a kapcsoló összes lights ki lesz választva. 💡" + } + } + }, + "apply": { + "fields": { + "entity_id": { + "description": "Az alkalmazandó beállításokat tartalmazó kapcsoló `entity_id`-je. 📝" + }, + "adapt_brightness": { + "description": "A világítás fényerejének beállítása. 🌞" + }, + "turn_on_lights": { + "description": "A jelenleg kikapcsolt lights bekapcsolása. 🔆" + }, + "adapt_color": { + "description": "A lights által támogatott színek beállítása. 🌈" + }, + "prefer_rgb_color": { + "description": "Lehetőség szerint az RGB színbeállítás előnyben részesítése a fény színhőmérsékletével szemben. 🌈" + }, + "lights": { + "description": "A világítás (vagy a világítások listája), amelyre a beállításokat alkalmazni kell.💡" + }, + "transition": { + "description": "Az transition időtartama, amikor a lights változnak, másodpercben. 🕑" + } + }, + "description": "Az aktuális Adaptív világítás beállításokat alkalmazza a lights-ra." + } + }, + "config": { + "step": { + "user": { + "title": "Válasszon nevet az Adaptív világítás példánynak", + "description": "Minden integrációs tétel több lights-t is tartalmazhat!" + } + }, + "abort": { + "already_configured": "Ez az eszköz már be van állítva" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/id.json b/custom_components/adaptive_lighting/translations/id.json new file mode 100644 index 00000000..6d4d8fcf --- /dev/null +++ b/custom_components/adaptive_lighting/translations/id.json @@ -0,0 +1,210 @@ +{ + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Persentase kecerahan lampu dalam mode tidur. 😴" + }, + "detect_non_ha_changes": { + "description": "Mendeteksi dan menghentikan adaptasi untuk perubahan status non-`light.turn_on`. Perlu mengaktifkan `take_over_control`. 🕵️ Perhatian: ⚠️ Beberapa lampu mungkin salah menunjukkan status 'hidup' yang dapat mengakibatkan lampu menyala secara tidak terduga. Nonaktifkan fitur ini jika Anda mengalami masalah seperti itu." + }, + "sunrise_offset": { + "description": "Sesuaikan waktu matahari terbit dengan offset positif atau negatif dalam hitungan detik. ⏰" + }, + "max_sunrise_time": { + "description": "Atur waktu matahari terbit virtual terkini (HH:MM:SS), memungkinkan matahari terbit lebih cepat. 🌅" + }, + "sleep_color_temp": { + "description": "Suhu warna dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah `color_temp`) dalam Kelvin. 😴" + }, + "min_brightness": { + "description": "Persentase kecerahan minimum. 💡" + }, + "min_color_temp": { + "description": "Suhu warna terhangat dalam Kelvin. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Gunakan `\"rgb_color\"` atau `\"color_temp\"` dalam mode tidur. 🌙" + }, + "turn_on_lights": { + "description": "Kalau ingin menyalakan lampu yang sedang mati. 🔆" + }, + "initial_transition": { + "description": "Durasi transisi pertama saat lampu berubah dari `mati` ke `hidup` dalam hitungan detik. ⏲️" + }, + "entity_id": { + "description": "ID Entitas sakelar. 📝" + }, + "sunrise_time": { + "description": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbit. 🌅" + }, + "include_config_in_attributes": { + "description": "Tampilkan semua opsi sebagai atribut pada sakelar di Home Assistant ketika diatur ke `true`. 📝" + }, + "max_brightness": { + "description": "Persentase kecerahan maksimum. 💡" + }, + "sleep_rgb_color": { + "description": "Warna RGB dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah \"rgb_color\"). 🌈" + }, + "take_over_control": { + "description": "Nonaktifkan Pencahayaan Adaptif jika sumber lain memanggil `light.turn_on` saat lampu menyala dan sedang diadaptasi. Perhatikan bahwa ini memanggil `homeassistant.update_entity` setiap `interval`! 🔒" + }, + "sleep_transition": { + "description": "Durasi transisi ketika \"mode tidur\" diubah, dalam hitungan detik. 😴" + }, + "autoreset_control_seconds": { + "description": "Secara otomatis mengatur ulang kontrol manual setelah beberapa detik. Setel ke 0 untuk menonaktifkan. ⏲️" + }, + "adapt_delay": { + "description": "Waktu tunggu (detik) antara lampu menyala dan penerapan ubahan Pencahayaan Adaptif. Mungkin membantu untuk menghindari kedipan. ⏲️" + }, + "only_once": { + "description": "Sesuaikan lampu hanya saat menyala (`true`) atau terus sesuaikan (`false`). 🔄" + }, + "use_defaults": { + "description": "Menetapkan nilai bawaan yang tidak ditentukan dalam panggilan layanan ini. Opsi: \"current\" (bawaan, mempertahankan nilai saat ini), \"factory\" (direset ke nilai bawaan yang terdokumentasi), atau \"configuration\" (kembali ke nilai bawaan konfigurasi sakelar). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Gunakan panggilan `light.turn_on` terpisah untuk warna dan kecerahan, diperlukan untuk beberapa jenis lampu. 🔀" + }, + "prefer_rgb_color": { + "description": "Kalau lebih memilih penyesuaian warna RGB dibandingkan suhu warna terang jika memungkinkan. 🌈" + }, + "max_color_temp": { + "description": "Suhu warna terdingin dalam Kelvin. ❄️" + }, + "sunset_offset": { + "description": "Sesuaikan waktu matahari terbenam dengan offset positif atau negatif dalam hitungan detik. ⏰" + }, + "send_split_delay": { + "description": "Waktu tunda (ms) antara `separate_turn_on_commands` untuk lampu yang tidak mendukung pengaturan kecerahan dan warna secara bersamaan. ⏲️" + }, + "sunset_time": { + "description": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbenam. 🌇" + }, + "transition": { + "description": "Durasi transisi saat lampu berganti, dalam hitungan detik. 🕑" + }, + "min_sunset_time": { + "description": "Tetapkan waktu matahari terbenam virtual paling awal (HH:MM:SS), memungkinkan matahari terbenam di kemudian waktu. 🌇" + } + }, + "description": "Ubah pengaturan apa pun yang Anda inginkan di sakelar. Semua opsi di sini sama seperti pada alur konfigurasi." + }, + "apply": { + "fields": { + "entity_id": { + "description": "`eEntity_id` sakelar dengan pengaturan yang akan diterapkan. 📝" + }, + "adapt_brightness": { + "description": "Kalau ingin menyesuaikan kecerahan lampu. 🌞" + }, + "turn_on_lights": { + "description": "Kalau ingin menyalakan lampu yang sedang mati. 🔆" + }, + "adapt_color": { + "description": "Kalau ingin menyesuaikan warna pada lampu pendukung. 🌈" + }, + "prefer_rgb_color": { + "description": "Kalau lebih memilih penyesuaian warna RGB dibandingkan suhu warna terang jika memungkinkan. 🌈" + }, + "lights": { + "description": "Lampu (atau daftar lampu) untuk menerapkan pengaturan. 💡" + }, + "transition": { + "description": "Durasi transisi saat lampu berganti, dalam hitungan detik. 🕑" + } + }, + "description": "Menerapkan pengaturan Pencahayaan Adaptif saat ini ke lampu." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Kalau ingin menambahkan (\"true\") atau menghapus (\"false\") lampu dari daftar \"manual_control\". 🔒" + }, + "entity_id": { + "description": "`entity_id` dari sakelar yang digunakan untuk membatalkan penandaan lampu sebagai `manually controlled`. 📝" + }, + "lights": { + "description": "entity_id(s) lampu, jika tidak ditentukan, semua lampu di sakelar dipilih. 💡" + } + }, + "description": "Tandai kalau lampu 'dikontrol secara manual'." + } + }, + "options": { + "step": { + "init": { + "data_description": { + "interval": "Frekuensi untuk menyesuaikan lampu, dalam hitungan detik. 🔄", + "transition": "Durasi transisi saat lampu berganti, dalam hitungan detik. 🕑", + "sleep_brightness": "Persentase kecerahan lampu dalam mode tidur. 😴", + "sleep_color_temp": "Suhu warna dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah `color_temp`) dalam Kelvin. 😴" + }, + "data": { + "lights": "lights: Daftar entity_ids lampu yang akan dikontrol (boleh kosong). 🌟", + "min_brightness": "min_brightness: Persentase kecerahan minimum. 💡", + "max_brightness": "max_brightness: Persentase kecerahan maksimum. 💡", + "min_color_temp": "min_color_temp: Suhu warna terhangat dalam Kelvin. 🔥", + "max_color_temp": "max_color_temp: Suhu warna terdingin dalam Kelvin. ❄️" + }, + "title": "Opsi Pencahayaan Adaptif", + "description": "Konfigurasikan komponen Pencahayaan Adaptif. Nama opsi selaras dengan pengaturan YAML. Jika Anda telah menentukan entri ini di YAML, tidak ada opsi yang akan muncul di sini. Untuk grafik interaktif yang menunjukkan efek parameter, kunjungi [aplikasi web ini]({webapp_url}). Untuk detail lebih lanjut, lihat [dokumentasi resmi]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Kalau lebih memilih penyesuaian warna RGB dibandingkan suhu warna terang jika memungkinkan. 🌈", + "transition_until_sleep": "transition_until_sleep: Jika diaktifkan, Pencahayaan Adaptif akan menganggap pengaturan tidur sebagai minimum, dan beralih ke nilai ini setelah matahari terbenam. 🌙", + "take_over_control": "take_over_control: Nonaktifkan Pencahayaan Adaptif jika sumber lain memanggil `light.turn_on` saat lampu menyala dan sedang diadaptasi. Perhatikan bahwa ini memanggil `homeassistant.update_entity` setiap `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Mendeteksi dan menghentikan adaptasi untuk perubahan status non-`light.turn_on`. Perlu mengaktifkan `take_over_control`. 🕵️ Perhatian: ⚠️ Beberapa lampu mungkin salah menunjukkan status 'hidup' yang dapat mengakibatkan lampu menyala secara tidak terduga. Nonaktifkan fitur ini jika Anda mengalami masalah seperti itu.", + "only_once": "only_once: Sesuaikan lampu hanya saat menyala (`true`) atau terus sesuaikan (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Saat menyalakan lampu pada awalnya. Jika diatur ke `true`, AL hanya beradaptasi jika `light.turn_on` dipanggil tanpa menentukan warna atau kecerahan. ❌🌈 Misalnya, mencegah adaptasi ketika mengaktifkan scene. Jika `false`, AL akan beradaptasi tanpa menghiraukan keberadaan warna atau kecerahan dalam `service_data` awal. Perlu `take_over_control` diaktifkan. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Gunakan panggilan `light.turn_on` terpisah untuk warna dan kecerahan, diperlukan untuk beberapa jenis lampu. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Lewati pengiriman perintah adaptasi yang status targetnya sudah sama dengan status cahaya yang diketahui. Meminimalkan lalu lintas jaringan dan meningkatkan respons adaptasi dalam beberapa situasi. 📉Nonaktifkan jika status cahaya fisik tidak sinkron dengan status rekaman HA.", + "intercept": "intercept: Cegat dan sesuaikan panggilan `light.turn_on` untuk mengaktifkan adaptasi warna dan kecerahan seketika. 🏎️ Nonaktifkan untuk lampu yang tidak mendukung `light.turn_on` dengan warna dan kecerahan.", + "multi_light_intercept": "multi_light_intercept: Cegat dan sesuaikan panggilan `light.turn_on` yang menargetkan banyak lampu. ➗⚠️ Hal ini dapat mengakibatkan satu panggilan `light.turn_on` terpecah menjadi beberapa panggilan, misalnya saat lampu berada di sakelar yang berbeda. Membutuhkan `intercept` untuk diaktifkan.", + "include_config_in_attributes": "include_config_in_attributes: Tampilkan semua opsi sebagai atribut pada sakelar di Home Assistant ketika diatur ke `true`. 📝" + }, + "data_description": { + "initial_transition": "Durasi transisi pertama saat lampu berubah dari `mati` ke `hidup` dalam hitungan detik. ⏲️", + "sleep_rgb_or_color_temp": "Gunakan `\"rgb_color\"` atau `\"color_temp\"` dalam mode tidur. 🌙", + "sleep_rgb_color": "Warna RGB dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah \"rgb_color\"). 🌈", + "sleep_transition": "Durasi transisi ketika \"mode tidur\" diubah, dalam hitungan detik. 😴", + "sunrise_time": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbit. 🌅", + "min_sunrise_time": "Tetapkan waktu matahari terbit virtual paling awal (HH:MM:SS), memungkinkan matahari terbit di kemudian waktu. 🌅", + "max_sunrise_time": "Atur waktu matahari terbit virtual terkini (HH:MM:SS), memungkinkan matahari terbit lebih cepat. 🌅", + "sunrise_offset": "Sesuaikan waktu matahari terbit dengan offset positif atau negatif dalam hitungan detik. ⏰", + "sunset_time": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbenam. 🌇", + "min_sunset_time": "Tetapkan waktu matahari terbenam virtual paling awal (HH:MM:SS), memungkinkan matahari terbenam di kemudian waktu. 🌇", + "max_sunset_time": "Atur waktu matahari terbenam virtual terkini (HH:MM:SS), memungkinkan matahari terbenam lebih cepat. 🌇", + "sunset_offset": "Sesuaikan waktu matahari terbenam dengan offset positif atau negatif dalam hitungan detik. ⏰", + "brightness_mode": "Mode kecerahan untuk digunakan. Nilai yang memungkinkan adalah `default`, `linear`, dan `tanh` (menggunakan `brightness_mode_time_dark` dan `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Diabaikan jika `brightness_mode='default'`) Durasi dalam hitungan detik untuk meningkatkan/menurunkan kecerahan sebelum/sesudah matahari terbit/terbenam. 📈📉", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) Durasi dalam hitungan detik untuk meningkatkan/menurunkan kecerahan setelah/sebelum matahari terbit/terbenam. 📈📉.", + "autoreset_control_seconds": "Secara otomatis mengatur ulang kontrol manual setelah beberapa detik. Setel ke 0 untuk menonaktifkan. ⏲️", + "send_split_delay": "Waktu tunda (ms) antara `separate_turn_on_commands` untuk lampu yang tidak mendukung pengaturan kecerahan dan warna secara bersamaan. ⏲️", + "adapt_delay": "Waktu tunggu (detik) antara lampu menyala dan penerapan ubahan Pencahayaan Adaptif. Mungkin membantu untuk menghindari kedipan. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Opsi tidak valid", + "entity_missing": "Satu atau lebih entitas cahaya yang dipilih hilang dari Home Assistant" + } + }, + "title": "Pencahayaan Adaptif", + "config": { + "step": { + "user": { + "description": "Setiap instance dapat berisi banyak lampu!", + "title": "Pilih nama untuk instance Pencahayaan Adaptif" + } + }, + "abort": { + "already_configured": "Perangkat ini sudah dikonfigurasi" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json new file mode 100644 index 00000000..576e7303 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/it.json @@ -0,0 +1,224 @@ +{ + "title": "Illuminazione Adattiva", + "config": { + "step": { + "user": { + "title": "Scegli un nome per l'istanza di Illuminazione Adattiva", + "description": "Scegli un nome per questa istanza. Puoi eseguire più istanze di Illuminazione adattiva, ognuna delle quali può contenere più luci!", + "data": { + "name": "Nome" + } + } + }, + "abort": { + "already_configured": "Questo dispositivo è già stato configurato" + } + }, + "options": { + "step": { + "init": { + "title": "Opzioni Illuminazione Adattiva", + "description": "Tutte le opzioni per il componente Illuminazione Adattiva. I nomi delle opzioni corrispondono con le impostazioni YAML. Non sono mostrate opzioni se hai la voce adaptive-lighting definita nella tua configurazione YAML.", + "data": { + "lights": "luci", + "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)", + "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)", + "min_brightness": "min_brightness: Luminosità minima delle luci durante un ciclo. (%)", + "max_brightness": "max_brightness: Luminosità massima delle luci durante un ciclo. (%)", + "min_color_temp": "min_color_temp: Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", + "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", + "sleep_brightness": "sleep_brightness: Impostazione della luminosità per la modalità notturna. (%)", + "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)" + }, + "data_description": { + "interval": "Frequenza di adattamento delle luci, espressa in secondi. 🔄", + "transition": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑", + "sleep_brightness": "Luminosità percentuale delle luci in modalità notturna. 😴", + "sleep_color_temp": "Temperatura colore per la modalità notturna (utilizzata quando `sleep_rgb_or_color_temp` vale `color_temp`), espressa in Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Quando le luci vengono accese (off -> on). (secondi)", + "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.", + "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)", + "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙", + "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)", + "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)", + "sunset_offset": "sunset_offset: Imposta quanto anticipare(-) o ritardare(+) il tramonto nel ciclo (+/- secondi)", + "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)", + "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)", + "only_once": "only_once: Adatta le luci solo quando vengono accese.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).", + "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.", + "skip_redundant_commands": "skip_redundant_commands: Salta l'invio di comandi di adattamento rivolti ad entità in cui lo stato desiderato è identico allo stato attuale. Minimizza il traffico sulla rete e migliora la responsività dell'adattamento in alcune situazioni. 📉 Disabilitalo se lo stato reale delle luci va fuori sincrono con quello registrato da HA.", + "intercept": "intercept: Intercetta e adatta alle chiamate a`light.turn_on` per abilitare adattamenti istantanei di colore e luminosità. 🏎️ Disabilita per quelle luci che non supportano l'impostazione di luci e colori a seguito dell'evento `light.turn_on`.", + "multi_light_intercept": "multi_light_intercept: Intercetta e adatta le chiamate a `light.turn_on` destinate a più luci. ➗⚠️ Questo potrebbe causare la divisione della singola chiamata `light.turn_on`in più chiamate, ad esempio quando le luci sono su switch diversi. Richiede che l'opzione `intercept` sia abilitata.", + "include_config_in_attributes": "include_config_in_attributes: Quando impostato come `true`, mostra tutte le opzioni come attributi dello switch in Home Assistant. 📝" + }, + "data_description": { + "initial_transition": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️", + "sleep_rgb_or_color_temp": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙", + "sleep_rgb_color": "Colore RGB in modalità notturna (usato quando `sleep_rgb_or_color_temp` è impostato su \"rgb_color\"). 🌈", + "sleep_transition": "Durata della transizione al passaggio da/verso la modalità luce notturna, espressa in secondi. 😴", + "sunrise_time": "Imposta un orario fisso (HH:MM:SS) per l'alba. 🌅", + "min_sunrise_time": "Imposta il minimo orario per l'alba (HH:MM:SS), per eventualmente ritardarla. 🌅", + "max_sunrise_time": "Imposta l'orario massimo per l'alba (HH:MM:SS), in modo da eventualmente anticiparla. 🌅", + "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰", + "sunset_time": "Imposta un orario fisso (HH:MM:SS) per il tramonto. 🌇", + "min_sunset_time": "Imposta il minimo orario per il tramonto (HH:MM:SS), per eventualmente ritardarlo. 🌅", + "max_sunset_time": "Imposta il massimo orario per il tramonto (HH:MM:SS), in modo da eventualmente anticiparlo. 🌇", + "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰", + "brightness_mode": "Modalità per la luminosità da utilizzare. I valori possibili sono `default`, `linear`, and `tanh` (usa`brightness_mode_time_dark` e `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉", + "brightness_mode_time_light": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉", + "autoreset_control_seconds": "Rimuovi automaticamente il colore impostato manualmente dopo un certo numero di secondi. Imposta 0 per disabilitare. ⏲️", + "send_split_delay": "Ritardo (ms) tra i comandi, per le luci che hanno `separate_turn_on_commands` e che non supportano l'impostazione simultanea di luminosità e colore. ⏲️", + "adapt_delay": "Tempo di attesa (in secondi) tra l'accensione della luce e i cambiamenti indotti da Illuminazione Adattativa. Può contribuire a ridurre lo sfarfallio. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Opzione non valida", + "entity_missing": "Non è stata trovata una luce selezionata" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Adatta le luci solo nel momento in cui vengono accese ('true') o continua ad adattarle ('false'). 🔄" + }, + "sunrise_offset": { + "description": "Modifica l'orario dell'alba con un offset in secondi positivo o negativo." + }, + "sleep_brightness": { + "description": "Luminosità percentuale delle luci in modalità notturna. 😴" + }, + "detect_non_ha_changes": { + "description": "Individua e arresta l'adattamento per i cambiamenti di stato diversi da `light.turn_on`. Richiede che `take_over_control` sia abilitato. 🕵️ Avvertenza: ⚠️ Alcune luci potrebbero riportare erroneamente lo stato di 'on', il che potrebbe causarne accensione inaspettata. Disabilita questa opzione se riscontri questa casistica." + }, + "max_sunrise_time": { + "description": "Imposta l'orario massimo per l'alba (HH:MM:SS), in modo da eventualmente anticiparla. 🌅" + }, + "sleep_color_temp": { + "description": "Temperatura colore per la modalità notturna (utilizzata quando `sleep_rgb_or_color_temp` vale `color_temp`), espressa in Kelvin. 😴" + }, + "min_brightness": { + "description": "Minima luminosità, in percentuale.💡" + }, + "min_color_temp": { + "description": "Temperatura colore più calda, espressa in Kelvin.🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙" + }, + "turn_on_lights": { + "description": "Seleziona per accedere le luci, qualora fossero spente. 🔆" + }, + "initial_transition": { + "description": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️" + }, + "entity_id": { + "description": "ID entità dello switch. 📝" + }, + "sunrise_time": { + "description": "Imposta un orario fisso (HH:MM:SS) per l'alba. 🌅" + }, + "include_config_in_attributes": { + "description": "Quando impostato su `true`, tutte le opzioni saranno visibili come attributi dello switch in Home Assistant. 📝" + }, + "max_brightness": { + "description": "Massima luminosità, in percentuale.💡" + }, + "sleep_rgb_color": { + "description": "Colore RGB in modalità notturna (usato quando `sleep_rgb_or_color_temp` è impostato su \"rgb_color\"). 🌈" + }, + "take_over_control": { + "description": "Disattiva Illuminazione Adattativa se un'altra sorcente chiama `light.turn_on` mentre le luci sono accese e soggette all'adattamento. Tieni conto che questo comporterà una chiamata a `homeassistant.update_entity` ad ogni `interval`! 🔒" + }, + "sleep_transition": { + "description": "Durata della transizione al passaggio da/verso la modalità luce notturna, espressa in secondi. 😴" + }, + "autoreset_control_seconds": { + "description": "Rimuovi automaticamente il colore impostato manualmente dopo un certo numero di secondi. Imposta 0 per disabilitare. ⏲️" + }, + "adapt_delay": { + "description": "Tempo di attesa (in secondi) tra l'accensione della luce e i cambiamenti indotti da Illuminazione Adattativa. Può contribuire a ridurre lo sfarfallio. ⏲️" + }, + "use_defaults": { + "description": "Imposta ai valori predefiniti non specificati nella chiamata al servizio. Opzioni possibili: \"current\" (predefinito, mantiene i valori correnti), \"factory\" (reimposta su un valore predefinito documentato) p \"configuration\" (reimposta sui valori predefiniti dello switch). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Usa chiamate distinte a `light.turn_on` per il colore e per la luminosità, necessario per alcuni tipi di luci. 🔀" + }, + "prefer_rgb_color": { + "description": "Seleziona per preferire gli aggiustamenti di colore mediante RGB piuttosto che tramite temperatura colore, dove possibile. 🌈" + }, + "max_color_temp": { + "description": "Temperatura colore più fredda, espressa in Kelvin. ❄️" + }, + "sunset_offset": { + "description": "Modifica l'orario del tramonto con un offset positivo o negativo in secondi. ⏰" + }, + "send_split_delay": { + "description": "Ritardo (ms) tra i comandi, per le luci che hanno `separate_turn_on_commands` e che non supportano l'impostazione simultanea di luminosità e colore. ⏲️" + }, + "sunset_time": { + "description": "Imposta un orario fisso (HH:MM:SS) per il tramonto. 🌇" + }, + "transition": { + "description": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑" + }, + "min_sunset_time": { + "description": "Imposta il minimo orario per il tramonto (HH:MM:SS), per eventualmente ritardarlo. 🌅" + } + }, + "description": "Cambia tutte le impostazioni che desideri nello switch. Le opzioni sono le stesse presenti nella procedura di configurazione." + }, + "apply": { + "fields": { + "entity_id": { + "description": "L'`entity_id` dello switch a cui si applicano le impostazioni.📝" + }, + "adapt_brightness": { + "description": "Seleziona per adattare la luminosità della luce. 🌞" + }, + "turn_on_lights": { + "description": "Seleziona per accedere le luci, qualora fossero spente. 🔆" + }, + "adapt_color": { + "description": "Seleziona per adattare il colore, per le luci che lo supportano. 🌈" + }, + "prefer_rgb_color": { + "description": "Seleziona per preferire gli aggiustamenti di colore mediante RGB piuttosto che tramite temperatura colore, dove possibile. 🌈" + }, + "lights": { + "description": "Una luce (o un insieme di luci) a cui applicare le impostazioni. 💡" + }, + "transition": { + "description": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑" + } + }, + "description": "Applica le impostazioni correnti di Illuminazione Adattativa alle luci." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Seleziona per aggiungere (\"true\") o rimuovere (\"false\") la luce dalla lista di quelle controllate manualmente . 🔒" + }, + "entity_id": { + "description": "L'`entity_id` dello switch che controlla se la luce deve operare in modalità manualmente controllata.📝" + }, + "lights": { + "description": "entity_id delle luci, se non specificato, tutte le luci nello switch sono selezionate. 💡" + } + }, + "description": "Evidenzia quando una luce è controllata manualmente." + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ja.json b/custom_components/adaptive_lighting/translations/ja.json new file mode 100644 index 00000000..670a127c --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ja.json @@ -0,0 +1,48 @@ +{ + "title": "明るさの自動調整", + "services": { + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "日の出時間を基準に秒単位で正値もしくは負値で調整する。⏰" + }, + "only_once": { + "description": "一度だけ明るさを自動調整するには(`true`)、常に自動調整し続ける場合は(`false`)。" + }, + "sunset_offset": { + "description": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" + }, + "entity_id": { + "description": "スイッチのエンティティID。 📝" + } + } + }, + "apply": { + "fields": { + "lights": { + "description": "適応する照明(か照明のリスト)の設定。 💡" + } + } + } + }, + "options": { + "step": { + "init": { + "data": {}, + "data_description": {}, + "title": "明るさの自動調整オプション", + "sections": { + "advanced": { + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: 最初に照明オンにするとき。`true`を設定すると、AL(適応型照明)は調色や明るさを指定せずや`light.turn_on`をしたときのみ適応します。❌🌈 例えば、適応型照明をシーンを有効にするときにしないようにする。`false`であれば、`service_data`に最初から、ALはシーンの状態に関係なく調色や明るさを適応する。`take_over_control`を有効にすることが必要。🕵️ " + }, + "data_description": { + "sunrise_offset": "日の出時間を基準に秒単位で正値もしくは負値で調整する。⏰", + "sunset_offset": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" + } + } + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ko.json b/custom_components/adaptive_lighting/translations/ko.json new file mode 100644 index 00000000..58b0424b --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ko.json @@ -0,0 +1,277 @@ +{ + "title": "적응형 조명", + "config": { + "step": { + "user": { + "title": "적응형 조명 인스턴스 이름 선택", + "description": "각 인스턴스는 여러 조명을 포함할 수 있습니다!", + "data": { + "name": "이름" + } + } + }, + "abort": { + "already_configured": "이 장치는 이미 구성되었습니다" + } + }, + "options": { + "step": { + "init": { + "title": "적응형 조명 옵션", + "description": "적응형 조명 구성요소를 구성합니다. 옵션 이름은 YAML 설정과 일치합니다. 이 항목을 YAML에서 정의한 경우 여기에 옵션이 표시되지 않습니다. 매개변수 효과를 시연하는 인터랙티브 그래프는 [이 웹 앱]({webapp_url})에서 확인할 수 있습니다. 자세한 내용은 [공식 문서]({docs_url})를 참조하세요.", + "data": { + "lights": "조명: 제어될 조명 entity_ids의 목록 (비어 있을 수 있음). 🌟", + "interval": "간격", + "transition": "전환", + "min_brightness": "최소 밝기: 밝기 최소 퍼센트. 💡", + "max_brightness": "최대 밝기: 밝기 최대 퍼센트. 💡", + "min_color_temp": "최소 색온도: 켈빈으로 표시된 가장 따뜻한 색온도. 🔥", + "max_color_temp": "최대 색온도: 켈빈으로 표시된 가장 차가운 색온도. ❄️", + "sleep_brightness": "수면 밝기", + "sleep_color_temp": "수면 색온도" + }, + "data_description": { + "interval": "조명을 조정하는 빈도, 초 단위. 🔄", + "transition": "조명이 변경될 때 전환 기간, 초 단위. 🕑", + "sleep_brightness": "수면 모드에서 조명의 밝기 퍼센트. 😴", + "sleep_color_temp": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "초기 전환", + "prefer_rgb_color": "RGB 색상 선호: 가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "sleep_rgb_or_color_temp": "수면 rgb_or_color_temp", + "sleep_rgb_color": "수면 RGB 색상", + "sleep_transition": "수면 전환", + "transition_until_sleep": "수면까지 전환: 활성화되면, 적응형 조명은 수면 설정을 최소값으로 취급하고 일몰 후 이 값으로 전환합니다. 🌙", + "sunrise_time": "일출 시간", + "min_sunrise_time": "최소 일출 시간", + "max_sunrise_time": "최대 일출 시간", + "sunrise_offset": "일출 오프셋", + "sunset_time": "일몰 시간", + "min_sunset_time": "최소 일몰 시간", + "max_sunset_time": "최대 일몰 시간", + "sunset_offset": "일몰 오프셋", + "brightness_mode": "밝기 모드", + "brightness_mode_time_dark": "어두울 때 밝기 모드 시간", + "brightness_mode_time_light": "밝을 때 밝기 모드 시간", + "take_over_control": "제어 인계: 다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", + "detect_non_ha_changes": "비HA 변경 감지: `light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", + "autoreset_control_seconds": "자동 제어 리셋 초", + "only_once": "한 번만: 조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", + "adapt_only_on_bare_turn_on": "초기 켜짐 시 조정만: 조명을 처음 켤 때. `true`로 설정하면 `light.turn_on`이 색상이나 밝기를 지정하지 않고 호출될 때만 AL이 조정합니다. ❌🌈 예를 들어, 장면을 활성화할 때 조정을 방지합니다. `false`로 설정하면, AL은 초기 `service_data`에 색상이나 밝기의 존재 여부와 관계없이 조정합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️", + "separate_turn_on_commands": "분리된 켜기 명령 사용: 일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", + "send_split_delay": "분할 전송 지연", + "adapt_delay": "조정 지연", + "skip_redundant_commands": "중복 명령 건너뛰기: 목표 상태가 이미 조명의 알려진 상태와 동일한 조정 명령을 보내지 않습니다. 네트워크 트래픽을 최소화하고 일부 상황에서 조정 반응성을 향상시킵니다. 📉 물리적 조명 상태가 HA의 기록된 상태와 동기화되지 않는 경우 비활성화하세요.", + "intercept": "가로채기: 색상과 밝기의 즉각적인 조정을 가능하게 하기 위해 `light.turn_on` 호출을 가로챕니다. 🏎️ 색상과 밝기를 지원하지 않는 조명에 대해 비활성화합니다.", + "multi_light_intercept": "다중 조명 가로채기: 여러 조명을 대상으로 하는 `light.turn_on` 호출을 가로채고 조정합니다. ➗⚠️ 이는 단일 `light.turn_on` 호출을 여러 호출로 분할할 수 있음을 의미합니다. 예를 들어, 조명이 다른 스위치에 있을 때. `intercept`가 활성화되어 있어야 합니다.", + "include_config_in_attributes": "속성에 구성 포함: `true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝" + }, + "data_description": { + "initial_transition": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", + "sleep_rgb_or_color_temp": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", + "sleep_rgb_color": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", + "sleep_transition": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", + "sunrise_time": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", + "min_sunrise_time": "가장 이른 가상 일출 시간 (HH:MM:SS)을 설정하여 더 늦은 일출을 허용. 🌅", + "max_sunrise_time": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", + "sunrise_offset": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", + "sunset_time": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", + "min_sunset_time": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", + "max_sunset_time": "가장 늦은 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 일찍 일몰을 허용. 🌇", + "sunset_offset": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", + "brightness_mode": "사용할 밝기 모드. 가능한 값은 `default`, `linear`, `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 전/후에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉", + "brightness_mode_time_light": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 후/전에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉.", + "autoreset_control_seconds": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", + "send_split_delay": "`separate_turn_on_commands`에 대한 호출 사이의 지연 시간(밀리초)으로, 밝기와 색상을 동시에 설정하지 않는 조명에 대한 지연. ⏲️", + "adapt_delay": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "잘못된 옵션", + "entity_missing": "선택한 하나 이상의 조명 엔티티가 Home Assistant에서 누락됨" + } + }, + "services": { + "apply": { + "name": "적용", + "description": "현재 적응형 조명 설정을 조명에 적용합니다.", + "fields": { + "entity_id": { + "description": "설정을 적용할 스위치의 `entity_id`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "설정을 적용할 조명(또는 조명 목록). 💡", + "name": "lights" + }, + "transition": { + "description": "조명 변경 시 전환 기간, 초 단위. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "조명의 밝기를 조정할지 여부. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "지원하는 조명의 색상을 조정할지 여부. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "현재 꺼져 있는 조명을 켤지 여부. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "수동 제어 설정", + "description": "조명이 '수동 제어됨'으로 표시되었는지 여부를 표시합니다.", + "fields": { + "entity_id": { + "description": "`수동 제어됨`으로 (표시 해제)할 스위치의 `entity_id`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "조명의 entity_id(들), 지정하지 않으면 스위치의 모든 조명이 선택됩니다. 💡", + "name": "lights" + }, + "manual_control": { + "description": "\"수동 제어\" 목록에서 조명을 추가(\"true\") 또는 제거(\"false\")할지 여부. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "스위치 설정 변경", + "description": "스위치에서 원하는 모든 설정을 변경하세요. 여기에 있는 모든 옵션은 구성 흐름에서와 같습니다.", + "fields": { + "entity_id": { + "description": "스위치의 Entity ID. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "이 서비스 호출에서 지정되지 않은 기본값을 설정합니다. 옵션: \"현재\"(기본값, 현재 값을 유지), \"공장\"(문서화된 기본값으로 재설정), 또는 \"구성\"(스위치 구성 기본값으로 되돌림). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "`true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "현재 꺼져 있는 조명을 켤지 여부. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "최대 밝기 퍼센트. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "켈빈으로 표시된 가장 차가운 색온도. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "최소 밝기 퍼센트. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "켈빈으로 표시된 가장 따뜻한 색온도. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "밝기와 색상을 동시에 설정하지 않는 조명에 대한 `separate_turn_on_commands` 호출 사이의 지연 시간(밀리초). ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "수면 모드에서 조명의 밝기 퍼센트. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "`light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "조명이 변경될 때 전환 기간, 초 단위. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", + "name": "autoreset_control_seconds" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/nb.json b/custom_components/adaptive_lighting/translations/nb.json new file mode 100644 index 00000000..50464143 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/nb.json @@ -0,0 +1,222 @@ +{ + "title": "Adaptiv Belysning", + "config": { + "step": { + "user": { + "title": "Velg et navn", + "description": "Velg et navn for denne konfigurasjonen for adaptiv belysning - hver konfigurasjon kan inneholde flere lyskilder!", + "data": { + "name": "Navn" + } + } + }, + "abort": { + "already_configured": "Denne enheten er allerede konfigurert!" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptiv Belysning Innstillinger", + "description": "Alle innstillinger for en adaptiv belysning konfigurasjon. Innstillingene er identiske med innstillingene for YAML konfigurasjon. Ingen innstillinger vises dersom du har definert adaptive_lighting i din YAML konfigurasjon.", + "data": { + "lights": "Lys / Lyskilder", + "interval": "'interval': tiden mellom oppdateringer (i sekunder)", + "transition": "'transition': varigheten (i sekunder) på overgangen når lysene oppdateres ", + "min_brightness": "'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus", + "max_brightness": "'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus", + "min_color_temp": "'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", + "max_color_temp": "'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", + "sleep_brightness": "'sleep_brightness': lysstyrken på lysene (i prosent) når 'sleep_mode' (søvnmodus) er aktiv", + "sleep_color_temp": "'sleep_color_temp': fargetemperaturen på lysene (i kelvin) når 'sleep_mode' (søvnmodus) er aktiv" + }, + "data_description": { + "interval": "Frekvens til å tilpasse lys, i sekunder.", + "transition": "Varighet på overgang når lysene endres, i sekunder.", + "sleep_brightness": "Lysstyrkeprosent på lysene i sove modus.", + "sleep_color_temp": "Fargetemperatur i sove modus (brukes når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin." + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "'initial_transition': overgangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres", + "prefer_rgb_color": "'prefer_rgb_color': benytt rgb i stedet for fargetemperatur dersom det er mulig", + "transition_until_sleep": "transition_until_sleep: Når aktivert, Adaptive lightning vil behandle sove innstillingene som minimum, bevege seg til disse verdiene etter solnedgang.", + "sunrise_time": "'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS)", + "sunrise_offset": "'sunrise_offset': hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder)", + "sunset_time": "'sunset_time': definer tidspunktet for solnedgang manuelt (i følgende format: TT:MM:SS - f. eks: '20:30:00' vil definere tidspunktet for solnegang som halv-ni på kvelden)", + "sunset_offset": "'sunset_offset': hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder)", + "take_over_control": "'take_over_control': dersom en annen tjeneste enn adaptiv belysning skrur lysene av eller på, vil automatisk adaptering av lyset stoppes inntil lyset (eller den tilhørende bryteren for adaptiv belysning) blir slått av - og på igjen", + "detect_non_ha_changes": "'detect_non_ha_changes': registrerer alle endringer i lysstyrke over 10% med opprinnelse utenfor Home Assistant - krever at 'take_over_control' er aktivert (OBS: tilkaller 'homeassistant.update_entity' ved hvert 'interval'!)", + "only_once": "'only_once': anvend innstillingene for adaptiv belysning kun når lysene skrus av eller på", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_on: Når lysene skrues på. Hvis satt til \"sann\", AL vil bare hvis \"light.turn_on\" er aktivert uten spesifisert farge og styrke. Dette f.eks. forhindrer aktivering når en scene aktiveres. Hvis \"false\", AL vil aktivere uansett om farge og stryke er satt av i opprinnelig \"service_data\". Trenger \"take_over_control\" er aktivert. ", + "separate_turn_on_commands": "'separate_turn_on_commands': separer kommandone i 'light.turn_on' for hver attributt (farge, lysstyrke, osv.). Dette kan være nødvendig for enkelte typer lys / lyskilder", + "skip_redundant_commands": "skip_redundant_commands: Dropp sending av tilpassnings kommandoer hvor målets tilstand allerede er lik den kjente tilstanden til lyset. Minimerer nettverk trafikk og forbedrer tilpasningens responsitivitet i noen situasjoner. Skru av hvis fysisk tilstand til lyset er ute av synkronisering med HA´s registrere tilstand.", + "intercept": "Bryt: Bryt og tilpass `light.turn_on` kall for å aktivere umiddelbar farge og styrke tilpassning. Deaktiver for lys som ikke støtter `light.turn_on` med farge og styrke.", + "multi_light_intercept": "multi_light_incept: Avskjære og tilpasse \"light.turn_on\" kall til flere lyskilder. Dette kan medføre oppsplitting av et enkelt \"light.turn_on\" kall til flere kall, f.eks når lys tilhører flere brytere. Dette krever at \"intercept\" er aktivert.", + "include_config_in_attributes": "include_config_in_attributes: Vis alle valg som attributes på bryteren i Home Assistant når satt til `true`." + }, + "data_description": { + "initial_transition": "Varighet på første overgang når lysene endres fra `off` til `on` i sekunder.", + "sleep_rgb_or_color_temp": "Bruk enten `\"rgb_color\"` eller `\"color_temp\"` i sove modus.", + "sleep_rgb_color": "RGB farger i sove modus (brukes når \"sleep_rgb_or_color_temp\" er \"rgb_color\")", + "sleep_transition": "Varighet på overgang når \"sleep mode\" er aktivert i sekunder.", + "sunrise_time": "Sett et fast tidspunkt (TT:MM:SS) for soloppgang.", + "min_sunrise_time": "Sett tidligste virituelle tidspunkt for soloppgang (TT:MM:SS), muliggjør for senere soloppganger", + "max_sunrise_time": "Sett det seneste virituelle tidspunktet for soloppgang (TT:MM:SS), muliggjør for tidligere soloppganger.", + "sunrise_offset": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰", + "sunset_time": "Sett et fast tidspunkt (TT:MM:SS) for solnedgang.", + "min_sunset_time": "Sett det tidligste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for senere solnedgang.", + "max_sunset_time": "Sett det seneste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for tidligere solnedgang.", + "sunset_offset": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰", + "brightness_mode": "Hvilken lysstyrke moduse skal brukes. Mulige verdier er `default`, `linear`, and `tanh` (bruker `brightness_mode_time_dark` og `brightness_mode_time_light`).", + "brightness_mode_time_dark": "(Ignorere hvis `brightness_mode='default'`) Varigheten i sekunder for å justere opp/ned lysstyrken før/etter soloppgang/solnedgang.", + "brightness_mode_time_light": "(Ignorere hvis `brightness_mode='default'`) Varigheten i sekunder for å justere opp/ned lysstyrken før/etter soloppgang/solnedgang.", + "autoreset_control_seconds": "Automatisk reset manuell kontroll etter et gitt antall sekunder. Sett til 0 for å skru av.", + "send_split_delay": "Forsinkelse (ms) mellom `separate_turn_on_commands` for lys som ikke støtter simultane styrke og farge innstillinger.", + "adapt_delay": "Ventetid (sekunder) mellom at lyset skrues på og Adaptive Lightning sender endringer. Kan hjelpe til for å unngå blinking." + } + } + } + } + }, + "error": { + "option_error": "En eller flere valgte innstillinger er ugyldige", + "entity_missing": "Et utvalgt lys ble ikke funnet" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰" + }, + "only_once": { + "description": "Tilpass lys kun når dei er skrudd på (`true`) eller fortsett å tilpasse dei (`false`)" + }, + "sunset_offset": { + "description": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰" + }, + "use_defaults": { + "description": "Sett default verdier ikke spesifisert i dette service kallet. Muligheter: \"current\" (default, fortsetter med nåværende verdier), \"factory\" (nullstiller til dokumenterte defaults), eller \"configuration\" (går tilbake til bryter defaults)." + }, + "include_config_in_attributes": { + "description": "Vis alle muligheter som valg på bryteren i Home Assistant når satt til \"true\"." + }, + "initial_transition": { + "description": "Varighet på første overgang når lysene endres fra `off` til `on` i sekunder." + }, + "entity_id": { + "description": "Bryterens Entity ID." + }, + "sleep_transition": { + "description": "Varighet på overgang når \"sleep mode\" er aktivert i sekunder." + }, + "max_brightness": { + "description": "Maksimal lysstyrke prosent." + }, + "separate_turn_on_commands": { + "description": "Bruk separat `light.turn_on` kall for farge og styrke, nødvendig for noen typer lys." + }, + "min_color_temp": { + "description": "Varmeste farge temperatur i Kelvin." + }, + "prefer_rgb_color": { + "description": "Foretrekke RGB farge inntsillinger over lysets fargetemperatur innstilling når mulig." + }, + "max_color_temp": { + "description": "Kaldeste farge temperatur i Kelvin." + }, + "min_brightness": { + "description": "Minste lysstyrke prosent." + }, + "sleep_rgb_or_color_temp": { + "description": "Bruk enten `\"rgb_color\"` eller `\"color_temp\"` i sove modus." + }, + "sleep_brightness": { + "description": "Lysstyrkeprosent på lysene i sove modus." + }, + "send_split_delay": { + "description": "Forsinkelse (ms) mellom `separate_turn_on_commands` for lys som ikke støtter simultane styrke og farge innstillinger." + }, + "sleep_rgb_color": { + "description": "RGB farger i sove modus (brukes når \"sleep_rgb_or_color_temp\" er \"rgb_color\")" + }, + "sleep_color_temp": { + "description": "Fargetemperatur i sove modus (brukes når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin." + }, + "sunrise_time": { + "description": "Sett et fast tidspunkt (TT:MM:SS) for soloppgang." + }, + "sunset_time": { + "description": "Set et fast tidspunkt (TT:MM:SS) for solnedgang" + }, + "max_sunrise_time": { + "description": "Sett det seneste virituelle tidspunktet for soloppgang (TT:MM:SS), muliggjør for tidligere soloppganger." + }, + "min_sunset_time": { + "description": "Sett det tidligeste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for senere solnedgang." + }, + "detect_non_ha_changes": { + "description": "Detekterer og stopper tilpasningen for ikke-`light.turn_on` tilstander . Trenger `take_over_control` aktivert. Advarsel: Noen lys kan gi falske signal om 'on' tilstand, som kan medføre at lysene skrur seg på av seg selv. Skru av denne funksjonen om dette inntreffer." + }, + "autoreset_control_seconds": { + "description": "Automatisk reset manuell kontroll etter et gitt antall sekunder. Sett til 0 for å skru av." + }, + "transition": { + "description": "Varighet på overgang når lysene endres, i sekunder." + }, + "adapt_delay": { + "description": "Ventetid (sekunder) mellom at lyset skrues på og Adaptive Lightning sender endringer. Kan hjelpe til for å unngå blinking." + }, + "turn_on_lights": { + "description": "Skru på lys som fortiden er skrudd av." + }, + "take_over_control": { + "description": "Skrur av Adaptive Lightning hvis en annen kilde kaller `light.turn_on` mens lysene er på og blir styrt. Merk at dette kaller `homeassistant.update_entity` hvert eneste`interval`!" + } + }, + "description": "Endre hvilken som helst innstilling i bryteren. Alle valg er det samme som i konfigurasjons prosessen." + }, + "apply": { + "fields": { + "lights": { + "description": "Et lys (eller ei liste av lys) instillingene skal påvirke. 💡" + }, + "entity_id": { + "description": "\"entity_id\" på bryteren hvor innstillingene skal legges til." + }, + "transition": { + "description": "Varighet på overgang når lysene endres, i sekunder." + }, + "adapt_brightness": { + "description": "Om å tilpasse styrken til lyset." + }, + "adapt_color": { + "description": "Om å tilpasse fargen på støttelysene." + }, + "prefer_rgb_color": { + "description": "Foretrekke RGB farge inntsillinger over lysets fargetemperatur innstilling når mulig." + }, + "turn_on_lights": { + "description": "Skru på lys som fortiden er skrudd av." + } + }, + "description": "Aktiver nåværende Adaptive Lighting innstillinger til lysene." + }, + "set_manual_control": { + "fields": { + "entity_id": { + "description": "\"entity_id\" på bryteren som skal (u)markeres med at lyset er \"manuelt kontrollert\"" + }, + "manual_control": { + "description": "Enten å legge til (\"true\") eller fjerne (\"false\") lys fra \"manual_control\" listen." + }, + "lights": { + "description": "entity_id(s) til lysene, hvis ikke spesifisert, alle lys i bryteren som er valgt." + } + }, + "description": "Marker om et lys er 'manually controlled'" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json new file mode 100644 index 00000000..cfe3e677 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -0,0 +1,240 @@ +{ + "title": "Adaptieve verlichting", + "config": { + "step": { + "user": { + "title": "Kies een naam voor de adaptieve verlichting integratie", + "description": "Elk exemplaar kan meerdere lichten bevatten!", + "data": { + "name": "Naam" + } + }, + "menu": { + "data": { + "action": "Actie" + }, + "title": "Maak of dupliceer", + "description": "Wil je een nieuwe instantie aanmaken of een bestaande dupliceren?" + } + }, + "abort": { + "already_configured": "Dit apparaat is al geconfigureerd" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptieve verlichting instellingen", + "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item `adaptive_lighting` hebt gedefinieerd in uw YAML-configuratie.\nVoor een demonstratie met interactieve grafieken, parameters en effecten, bezoek [deze web applicatie]({webapp_url}). Voor verdere details, bekijk de [officiële documentatie]({docs_url}).", + "data": { + "lights": "Lampen: lijst van `light` entiteiten om te bedienen (kan leeg zijn). 🌟", + "interval": "interval: Tijd tussen switch-updates. (seconden)", + "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", + "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", + "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", + "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (Kelvin)", + "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", + "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)", + "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)" + }, + "data_description": { + "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄", + "transition": "Duur van de overgang, in seconden, als lampen aanpassen. 🕑", + "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴", + "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan `color_temp`) in Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", + "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", + "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙", + "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)", + "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", + "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", + "take_over_control": "take_over_control: Als iets anders dan Adaptieve verlichting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", + "detect_non_ha_changes": "detect_non_ha_changes: Detecteert en stopt aanpassingen voor`light.turn_on` statuswijzigingen. Vereist dat`take_over_control` is ingeschakeld. 🕵️ Voorzichtig: ⚠️ Sommige lampen kunnen een 'aan' status vals aangeven, wat kan leiden tot onverwacht inschakelen van lampen. Schakel deze functie uit als je dergelijke problemen tegenkomt.", + "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past Av alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past Av aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", + "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.", + "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", + "skip_redundant_commands": "skip_redundant_commands: Sla het verzenden van aanpassingscommando's over waarvan de doelstatus al gelijk is aan de bekende status van de lamp. Minimaliseert netwerkverkeer en verbetert de responsiviteit van de aanpassing in sommige situaties. 📉Schakel uit als de fysieke lichtstatus niet meer synchroon loopt met de door HA geregistreerde status.", + "intercept": "intercept: Onderschep en pas `light.turn_on` oproepen aan om directe kleur- en helderheidsaanpassing mogelijk te maken. 🏎️ Schakel uit voor lampen die `light.turn_on` niet ondersteunen met kleur en helderheid.", + "multi_light_intercept": "multi_light_intercept: Onderschep en pas `light.turn_on` oproepen aan die gericht zijn op meerdere lampen. ➗⚠️ Dit kan resulteren in het opsplitsen van een enkele `light.turn_on` call in meerdere calls, bijvoorbeeld wanneer lampen zich in verschillende schakelaars bevinden. Vereist dat `intercept` is ingeschakeld.", + "include_config_in_attributes": "include_config_in_attributes: Toon alle opties als attributen op de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝" + }, + "data_description": { + "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️", + "sleep_rgb_or_color_temp": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙", + "sleep_rgb_color": "RGB kleur in slaapstand (wordt gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan \"rgb_color\"). 🌈", + "sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴", + "sunrise_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅", + "min_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsopkomst, maakt latere zonsopkomsten mogelijk. 🌅", + "max_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅", + "sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰", + "sunset_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsondergang. 🌇", + "min_sunset_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇", + "max_sunset_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsondergang, maakt eerdere zonsondergangen mogelijk. 🌇", + "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰", + "brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", + "brightness_mode_time_light": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", + "take_over_control_mode": "De adaptie pauzeermodus wanneer andere bronnen de helderheid en/of kleur van lampen veranderen. `pause_all` pauzeert altijd verandering van zowel helderheid als kleur. `pause_changed` pauzeert alleen de verandering van de extern veranderde eigenschappen en blijft onveranderde eigenschappen aanpassen, bijv. doorgaan met kleur veranderen als alleen helderheid extern is veranderd.", + "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.", + "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️", + "adapt_delay": "Wachttijd in (seconden) tussen het aanzetten van de lamp en het toepassen van Adaptieve verlichting veranderingen. Het kan helpen om knipperen tegen te gaan. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Ongeldige optie", + "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Pas lampen alleen aan wanneer ze zijn ingeschakeld (`true`) of blijf ze aanpassen (`false`). 🔄" + }, + "sunrise_offset": { + "description": "Pas de tijd van zonsopkomst aan met een positieve of negatieve verschuiving in seconden. ⏰" + }, + "sunset_offset": { + "description": "Pas de tijd van zonsondergang aan met een positieve of negatieve offset in seconden. ⏰" + }, + "sleep_transition": { + "description": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴" + }, + "entity_id": { + "description": "entiteit_id van de schakelaar. 📝" + }, + "transition": { + "description": "Duur van de overgang in seconden, als lampen aanpassen. 🕑" + }, + "autoreset_control_seconds": { + "description": "Herstel de handmatige bediening na een aantal seconden. Stel in op 0 om uit te schakelen." + }, + "sleep_brightness": { + "description": "Helderheidspercentage van lampen in slaapmodus. 😴" + }, + "sleep_color_temp": { + "description": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴" + }, + "max_color_temp": { + "description": "Koudste kleurtemperatuur in Kelvin. ❄️" + }, + "initial_transition": { + "description": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️" + }, + "take_over_control": { + "description": "Schakel Adaptieve verlichting uit als een andere bron `light.turn_on` aanroept terwijl de lampen aan zijn en worden aangepast. Let op dit roept`homeassistant.update_entity` elke `interval`aan." + }, + "detect_non_ha_changes": { + "description": "Detecteert en stopt aanpassingen voor niet-`light.turn_on` state veranderingen. `take_over_control` moet actief zijn. 🕵️ Let op:⚠️Sommige lampen kunnen incorrect een 'on' state weergeven, wat resulteert in lampen die onverwacht aan gaan. Schakel deze feature uit wanneer deze fout zich voordoet." + }, + "max_sunrise_time": { + "description": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅" + }, + "min_brightness": { + "description": "Minimale helderheid in procenten. 💡" + }, + "min_color_temp": { + "description": "Meest warme kleurtemperatuur ins Kelvin. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙" + }, + "turn_on_lights": { + "description": "Of de lampen moeten worden aangezet die momenteel uit zijn.🔆" + }, + "sunrise_time": { + "description": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅" + }, + "include_config_in_attributes": { + "description": "Toon alle opties als attributen bij de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝" + }, + "max_brightness": { + "description": "Maximale helderheid in procenten. 💡" + }, + "sleep_rgb_color": { + "description": "RGB kleur in slaapstand (wordt gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan \"rgb_color\"). 🌈" + }, + "adapt_delay": { + "description": "Wachttijd in (seconden) tussen het aanzetten van de lamp en het toepassen van Adaptieve verlichting veranderingen. Het kan helpen om knipperen tegen te gaan. ⏲️" + }, + "use_defaults": { + "description": "Stelt niet gespecificeerde waarden in voor deze service call. Opties: \"current\" (standaard, behoudt huidige waarden), \"factory\" (herstelt de gedocumenteerde standaardwaarden), of \"configuration\" (zet instellingen terug naar de standaardwaarden in de configuratie ). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Gebruik aparte `light.turn_on` calls voor kleur en helderheid, dit is nodig voor bepaalde lampen. 🔀" + }, + "prefer_rgb_color": { + "description": "Geef de voorkeur aan RGB kleuren boven de kleurtemperatuur van de lamp wanneer mogelijk. 🌈" + }, + "send_split_delay": { + "description": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️" + }, + "sunset_time": { + "description": "Stel een vaste tijd (HH:MM:SS) in voor zonsondergang. 🌇" + }, + "min_sunset_time": { + "description": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇" + }, + "take_over_control_mode": { + "description": "De adaptie pauzeermodus wanneer andere bronnen de helderheid en/of kleur van lampen veranderen. `pause_all` pauzeert altijd verandering van zowel helderheid als kleur. `pause_changed` pauzeert alleen de verandering van de extern veranderde eigenschappen en blijft onveranderde eigenschappen aanpassen, bijv. doorgaan met kleur veranderen als alleen helderheid extern is veranderd." + } + }, + "description": "Wijzig alle gewenste instellingen in de schakelaar. Alle opties hier zijn hetzelfde als in de configuratie." + }, + "apply": { + "fields": { + "lights": { + "description": "Een lamp (of een lijst van lampen) waarop de instellingen worden toegepast." + }, + "transition": { + "description": "Duur van de overgang in seconden, als lampen aanpassen. 🕑" + }, + "entity_id": { + "description": "De `entity_id` van de schakelaar met de toe te passen instellingen. 📝" + }, + "adapt_brightness": { + "description": "Of de helderheid van het licht moet worden aangepast. 🌞" + }, + "turn_on_lights": { + "description": "Of de lampen moeten worden aangezet die momenteel uit zijn.🔆" + }, + "adapt_color": { + "description": "Aanpassen aan de kleur van de omringende verlichting. 🌈" + }, + "prefer_rgb_color": { + "description": "Geef de voorkeur aan RGB kleuren boven de kleurtemperatuur van de lamp wanneer mogelijk. 🌈" + } + }, + "description": "Past de huidige Adaptieve verlichting instellingen toe op de lampen." + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "entiteit_id(s) van de lamp(en), indien niets wordt gespecificeerd, worden alle lampen in de schakelaar geselecteerd. 💡" + }, + "manual_control": { + "description": "Of de lamp moet worden toegevoegd (`\"true\"`) of verwijderd (`\"false\"`) van de `manual_control` lijst. 🔒" + }, + "entity_id": { + "description": "De `entity_id` van de schakelaar waarvan het licht moet worden (on)gemarkeerd als `manually controlled`. 📝" + } + }, + "description": "Geef aan of een lamp 'manually controlled' is." + } + } +} diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json new file mode 100644 index 00000000..8957db09 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -0,0 +1,223 @@ +{ + "title": "Adaptacyjne oświetlenie", + "config": { + "step": { + "user": { + "title": "Wybierz nazwę dla tej instancji Adaptacyjnego oświetlenia", + "description": "Każda instancja może zawierać wiele świateł.", + "data": { + "name": "Nazwa" + } + } + }, + "abort": { + "already_configured": "Już skonfigurowane!" + } + }, + "options": { + "step": { + "init": { + "title": "Opcje adaptacyjnego oświetlenia", + "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowane w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [tą aplikację webową]({webapp_url}). Aby zobaczyć więcej szczegółów odwiedź [oficjalną dokumentację]({docs_url}).", + "data": { + "lights": "lights: Lista `entity_id`, które mają być kontrolowane (może być pusta). 🌟", + "interval": "interval: Time between switch updates. (sekund)", + "transition": "Transition time when applying a change to the lights (sekund)", + "min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡", + "max_brightness": "max_brightness: Maksymalna jasność (w procentach). 💡", + "min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥", + "max_color_temp": "max_color_temp: Najzimniejsza temperatura barwowa (w Kelwinach). ❄️", + "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", + "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)" + }, + "data_description": { + "interval": "Częstotliwość adaptacji świateł w sekundach. 🔄", + "transition": "Długość przejścia do nowego stanu (w sekundach). 🕑", + "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴", + "sleep_color_temp": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)", + "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast regulacji temperatury barwowej światła. 🌈", + "sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)", + "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙", + "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", + "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", + "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia, kiedy inna usługa wywoła `light.turn_on`, gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów.", + "only_once": "only_once: Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone (`true`) to adaptacyjne oświetlenie zastosuje adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji, gdy aktywowana jest scena. Gdy wyłączone (`false`), adaptacyjne oświetlenie zastosuje adaptacje niezależnie czy `service_data` zawiera kolor lub jasność. Potrzebuje włączonej opcji `take_over_control`. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji, jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przypadkach poprawia szybkość działania. 📉 Wyłącz, jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.", + "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, aby błyskawicznie dostosować kolor i jasność. 🏎️ Wyłącz dla świateł, które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.", + "multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`.", + "include_config_in_attributes": "include_config_in_attributes: Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝" + }, + "data_description": { + "initial_transition": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️", + "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙", + "sleep_rgb_color": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈", + "sleep_transition": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴", + "sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅", + "min_sunrise_time": "Ustaw czas najwcześniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na opóźnienie wschodu słońca. 🌅", + "max_sunrise_time": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅", + "sunrise_offset": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰", + "sunset_time": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇", + "min_sunset_time": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇", + "max_sunset_time": "Ustaw czas najpóźniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na przyspieszenie zachodu słońca. 🌇", + "sunset_offset": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰", + "brightness_mode": "Tryb ustawiania jasności. Dostępne opcje to `default`, `linear` i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉", + "brightness_mode_time_light": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉", + "autoreset_control_seconds": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0, aby wyłączyć. ⏲️", + "send_split_delay": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️", + "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Błędna opcja", + "entity_missing": "Jednego lub więcej wybranych świateł nie można znaleźć w Home Assistant" + } + }, + "services": { + "apply": { + "description": "Stosuje bieżące ustawienia Adaptacyjnego oświetlenia do świateł.", + "fields": { + "entity_id": { + "description": "`entity_id` przełącznika, którego ustawienia mają być zastosowane. 📝" + }, + "lights": { + "description": "Światło (albo lista świateł), do których mają być zastosowane ustawienia. 💡" + }, + "transition": { + "description": "Długość przejścia do nowego stanu (w sekundach). 🕑" + }, + "adapt_color": { + "description": "Czy adaptować kolor światła. 🌈" + }, + "adapt_brightness": { + "description": "Czy adaptować jasność światła. 🌞" + }, + "prefer_rgb_color": { + "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła. 🌈" + }, + "turn_on_lights": { + "description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆" + } + } + }, + "set_manual_control": { + "description": "Zaznacza czy światło jest \"ręcznie sterowane\".", + "fields": { + "entity_id": { + "description": "`entity_id` encji przełącznika, w której należy zaznaczyć/odznaczyć flagę `ręczne sterowanie`. 📝" + }, + "lights": { + "description": "`entity_id` świateł, dla których należy odznaczyć flagę `ręczne sterowanie`. 💡Gdy lista będzie pusta wszystkie światła będą brane pod uwagę." + }, + "manual_control": { + "description": "Czy dodać (\"true\"), czy usunąć (\"false\") światło z listy \"ręczne sterowanie\". 🔒" + } + } + }, + "change_switch_settings": { + "description": "Zmienia dowolny parametr w przełączniku. Wszystkie opcje są takie same jak w konfiguracji.", + "fields": { + "entity_id": { + "description": "ID encji przełącznika. 📝" + }, + "use_defaults": { + "description": "Jak mają się zmienić ustawienia, które nie są wyszczególnione w tym wywołaniu. Opcje: \"current\" (domyślne, pozostawia obecne ustawienia), \"factory\" (przywraca ustawienia z dokumentacji), albo \"configuration\" (przywraca wartości z konfiguracji przełącznika). ⚙️" + }, + "include_config_in_attributes": { + "description": "Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant." + }, + "sleep_transition": { + "description": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴" + }, + "max_brightness": { + "description": "Maksymalna jasność (w procentach). 💡" + }, + "turn_on_lights": { + "description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆" + }, + "initial_transition": { + "description": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️" + }, + "min_sunset_time": { + "description": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇" + }, + "take_over_control": { + "description": "Wyłącz adaptowanie oświetlenia, kiedy inna usługa wywoła `light.turn_on`, gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co `interval`! 🔒" + }, + "transition": { + "description": "Długość przejścia do nowego stanu (w sekundach). 🕑" + }, + "autoreset_control_seconds": { + "description": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️" + }, + "adapt_delay": { + "description": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️" + }, + "max_color_temp": { + "description": "Najzimniejsza temperatura barwowa (w Kelwinach). ❄️" + }, + "min_brightness": { + "description": "Minimalna jasność (w procentach). 💡" + }, + "min_color_temp": { + "description": "Najcieplejsza temperatura barwowa (w Kelwinach). 🔥" + }, + "only_once": { + "description": "Adaptuj światło tylko podczas włączania (`true`) lub adaptuj cały czas (`false`). 🔄" + }, + "prefer_rgb_color": { + "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła. 🌈" + }, + "separate_turn_on_commands": { + "description": "Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀" + }, + "send_split_delay": { + "description": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️" + }, + "sleep_brightness": { + "description": "Jasność świateł w trybie spania (w procentach). 😴" + }, + "sleep_rgb_or_color_temp": { + "description": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙" + }, + "sleep_rgb_color": { + "description": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈" + }, + "sleep_color_temp": { + "description": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴" + }, + "sunrise_offset": { + "description": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰" + }, + "sunrise_time": { + "description": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅" + }, + "sunset_offset": { + "description": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰" + }, + "sunset_time": { + "description": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇" + }, + "max_sunrise_time": { + "description": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅" + }, + "detect_non_ha_changes": { + "description": "Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów." + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/pt-BR.json b/custom_components/adaptive_lighting/translations/pt-BR.json new file mode 100644 index 00000000..76042c8b --- /dev/null +++ b/custom_components/adaptive_lighting/translations/pt-BR.json @@ -0,0 +1,192 @@ +{ + "title": "Iluminação Adaptativa", + "config": { + "step": { + "user": { + "title": "Escolha um nome para a instância da Iluminação Adaptativa", + "description": "Escolha um nome para esta instância. Você pode executar várias instâncias de iluminação adaptativa, cada uma delas pode conter várias luzes!", + "data": { + "name": "Nome" + } + }, + "menu": { + "data": { + "action": "Ação" + }, + "description": "Você deseja criar uma nova instância ou duplicar uma já existente?", + "title": "Criar ou Duplicar" + } + }, + "abort": { + "already_configured": "Este dispositivo já está configurado" + } + }, + "options": { + "step": { + "init": { + "title": "Opções da iluminação adaptiva", + "description": "Todas as configurações de um componente de iluminação adaptativa. Os nomes das opções correspondem às configurações de YAML. Nenhuma opção será exibida se você tiver a entrada adaptive_lighting definida em sua configuração YAML.", + "data": { + "lights": "luzes", + "interval": "interval: Tempo entre as atualizações do switch. (segundos)", + "transition": "Tempo de transição ao aplicar uma mudança nas luzes (segundos)", + "min_brightness": "min_brightness: Menor brilho das luzes durante um ciclo. (%)", + "max_brightness": "max_brightness: Maior brilho das luzes durante um ciclo. (%)", + "min_color_temp": "min_color_temp, matiz mais quente do ciclo de temperatura de cor. (Kelvin)", + "max_color_temp": "max_color_temp: Matiz mais frio do ciclo de temperatura de cor. (Kelvin)", + "sleep_brightness": "sleep_brightness, configuração de brilho para o modo de suspensão. (%)", + "sleep_color_temp": "sleep_color_temp: configuração de temperatura de cor para o modo de suspensão. (Kelvin)" + }, + "data_description": { + "interval": "Frequência, em segundos, para adaptar as luzes. 🔄", + "transition": "Duração da transição, em segundos, quando as luzes mudam. 🕑", + "sleep_brightness": "Porcentagem do brilho das luzes no modo dormir. 😴", + "sleep_color_temp": "Temperatura de Cor no modo dormir (usado quando `sleep_rgb_or_color_temp` é `color_temp`) em Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Quando as luzes mudam de 'off' para 'on'. (segundos)", + "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' em vez de 'color_temp' quando possível.", + "sleep_transition": "sleep_transition: Quando 'sleep_state' muda. (segundos)", + "transition_until_sleep": "transition_until_sleep: Quando ativada, a Iluminação Adaptativa considerará as configurações de sono como o valor mínimo, transicionando para esses valores após o pôr do sol. 🌙", + "sunrise_time": "sunrise_time: substituição manual do horário do nascer do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto do nascer do sol do ciclo (+/- segundos)", + "sunset_time": "sunset_time: substituição manual do horário do pôr do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", + "sunset_offset": "Sunset_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto de pôr do sol do ciclo (+/- segundos)", + "take_over_control": "take_over_control: Se qualquer coisa, exceto Adaptive Lighting, chamar 'light.turn_on' quando uma luz já estiver acesa, pare de adaptar essa luz até que ela (ou o interruptor) desligue -> ligue.", + "detect_non_ha_changes": "detect_non_ha_changes: detecta todas as alterações > 10% feitas nas luzes (também fora do HA), requer que 'take_over_control' seja ativado (chama 'homeassistant.update_entity' a cada 'intervalo'!)", + "only_once": "only_once: Apenas adapte as luzes ao ligá-las.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ao ligar as luzes inicialmente. Se definido como `true`, a Iluminação Adaptativa se adapta somente se o comando `light.turn_on` for chamado sem especificar cor ou brilho. ❌🌈 Isso, por exemplo, impede a adaptação ao ativar uma cena. Se false, a Iluminação Adaptativa se adapta independentemente da presença de cor ou brilho nos dados iniciais do `service_data`. Precisa de `take_over_control` ativado. 🕵️", + "separate_turn_on_commands": "separar_turn_on_commands: Separe os comandos para cada atributo (cor, brilho, etc.) em 'light.turn_on' (necessário para algumas luzes).", + "skip_redundant_commands": "skip_redundant_commands: Deixar de enviar comandos de adaptação cujo estado alvo já seja igual ao estado atual da luz. Minimiza o tráfego de rede e melhora a responsividade da adaptação em algumas situações. 📉Desative se os estados físicos das luzes podem ficar diferentes do estado registrado no HA.", + "intercept": "intercept: Interceptar e adaptar os chamados `light.turn_on` para ativar a adaptação instantânea de cor e brilho. 🏎️ Desative para luzes que não suportam `light.turn_on` com cor e brilho.", + "multi_light_intercept": "multi_light_intercept: Interceptar e adaptar chamadas de 'light.turn_on' que visem múltiplas luzes. ➗⚠️ Isso pode resultar na divisão de uma única chamada de 'light.turn_on' em múltiplas chamadas, por exemplo, quando as luzes estão em interruptores diferentes. Exige que 'intercept' esteja ativado.", + "include_config_in_attributes": "include_config_in_attributes: Mostra todas as opções como atributos no interruptor do Home Assistant quando está definido para `true`. 📝" + }, + "data_description": { + "initial_transition": "Duração da primeira transição, em segundos, quando as luzes alternarem de 'desligado' para 'ligado'. ⏲️", + "sleep_rgb_or_color_temp": "Use `\"rgb_color\"` ou `\"color_temp\"` no modo dormir. 🌙", + "sleep_transition": "Duração da transição em segundos quando o modo dormir é alterado. 😴", + "sunrise_time": "Define um horário fixo (HH:MM:SS) para o nascer do sol. 🌅", + "min_sunrise_time": "Defina o horário virtual mais cedo do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais tarde. 🌅", + "max_sunrise_time": "Defina o horário virtual mais recente do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais cedo. 🌅", + "sunrise_offset": "Ajusta o horário do nascer do sol com um deslocamento positivo ou negativo em segundos. ⏰", + "sunset_time": "Definir um horário fixo (HH:MM:SS) para o pôr do sol. 🌇", + "min_sunset_time": "Defina o horário virtual mais cedo do pôr do sol (HH:MM:SS), permitindo pores do sol mais tarde. 🌇", + "max_sunset_time": "Defina o horário virtual mais recente do pôr do sol (HH:MM:SS), permitindo pores do sol mais cedo. 🌇", + "sunset_offset": "Ajusta o horário do pôr do sol com um deslocamento positivo ou negativo em segundos. ⏰", + "autoreset_control_seconds": "Redefine automaticamente o controle manual após um período de tempo definido em segundos. Defina 0 para desabilitar. ⏲️", + "adapt_delay": "Tempo de espera (segundos) entre a luz ligar e a aplicação das mudanças da iluminação adaptativa. Pode ajudar a evitar que a luz pisque. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Opção inválida", + "entity_missing": "Uma luz selecionada não foi encontrada" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sleep_transition": { + "description": "Duração da transição em segundos quando o \"modo dormir\" é alternado. 😴" + }, + "entity_id": { + "description": "ID da entidade do switch. 📝" + }, + "max_brightness": { + "description": "Porcentagem máxima do brilho. 💡" + }, + "autoreset_control_seconds": { + "description": "Redefine automaticamente o controle manual após um período de tempo definido em segundos. Defina 0 para desabilitar. ⏲️" + }, + "transition": { + "description": "Duração da transição, em segundos, quando as luzes mudam. 🕑" + }, + "sleep_brightness": { + "description": "Porcentagem do brilho das luzes no modo dormir. 😴" + }, + "turn_on_lights": { + "description": "Se deve ligar as luzes que estão atualmente desligadas. 🔆" + }, + "initial_transition": { + "description": "Duração da primeira transição, em segundos, quando as luzes alternarem de 'desligado' para 'ligado'. ⏲️" + }, + "sunset_offset": { + "description": "Ajusta o horário do pôr do sol com um deslocamento positivo ou negativo em segundos. ⏰" + }, + "sunrise_offset": { + "description": "Ajusta o horário do nascer do sol com um deslocamento positivo ou negativo em segundos. ⏰" + }, + "sunset_time": { + "description": "Definir um horário fixo (HH:MM:SS) para o pôr do sol. 🌇" + }, + "max_color_temp": { + "description": "Temperatura de cor mais fria em Kelvin. ❄️" + }, + "sleep_color_temp": { + "description": "Temperatura de Cor no modo dormir (usado quando `sleep_rgb_or_color_temp` é `color_temp`) em Kelvin. 😴" + }, + "sunrise_time": { + "description": "Define um horário fixo (HH:MM:SS) para o nascer do sol. 🌅" + }, + "include_config_in_attributes": { + "description": "Exibe todas as opções como atributos no interruptor no Home Assistant quando definido para `true`. 📝" + }, + "sleep_rgb_or_color_temp": { + "description": "Use `\"rgb_color\"` ou `\"color_temp\"` no modo dormir. 🌙" + }, + "adapt_delay": { + "description": "Tempo de espera (segundos) entre a luz ligar e a aplicação das mudanças da iluminação adaptativa. Pode ajudar a evitar que a luz pisque. ⏲️" + }, + "separate_turn_on_commands": { + "description": "Usa chamada separada de `light.turn_on` para cor e brilho, necessário para alguns tipos de luz. 🔀" + }, + "use_defaults": { + "description": "Define os valores padrão não especificados nessa chamada de serviço. Opções: \"current\" (padrão, mantém os valores atuais), \"factory\" (reinicia para os padrões documentados) ou \"configuration\" (retorna aos padrões de configuração do interruptor). ⚙️" + }, + "max_sunrise_time": { + "description": "Defina o horário virtual mais recente do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais cedo. 🌅" + }, + "min_sunset_time": { + "description": "Defina o horário virtual mais cedo do pôr do sol (HH:MM:SS), permitindo pores do sol mais tarde. 🌇" + } + }, + "description": "Altere quaisquer configurações que você quiser . Todas as opções aqui são as mesmas que no fluxo de configuração." + }, + "apply": { + "fields": { + "turn_on_lights": { + "description": "Se deve ligar as luzes que estão atualmente desligadas. 🔆" + }, + "lights": { + "description": "Uma luz (ou lista de luzes) para aplicar as configurações. 💡" + }, + "transition": { + "description": "Duração da transição, em segundos, quando as luzes mudam. 🕑" + }, + "entity_id": { + "description": "O 'entity_id' do interruptor com as configurações para aplicar. 📝" + }, + "adapt_brightness": { + "description": "Se deve adaptar o brilho da luz. 🌞" + }, + "adapt_color": { + "description": "Se deve adaptar a cor das luzes que suportam este recurso. 🌈" + } + }, + "description": "Aplica as configurações atuais de iluminação adaptativa nas luzes." + }, + "set_manual_control": { + "description": "Marque se uma luz é 'controlada manualmente'.", + "fields": { + "lights": { + "description": "entity_id(s) das luzes, se não especificadas, todas as luzes do interruptor são selecionadas. 💡" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json new file mode 100644 index 00000000..888f2e1e --- /dev/null +++ b/custom_components/adaptive_lighting/translations/pt.json @@ -0,0 +1,100 @@ +{ + "title": "Iluminação Adaptativa", + "services": { + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰" + }, + "only_once": { + "description": "Adaptar as luzes apenas quando estas estão ligadas (`true`) ou continuar a adaptá-las (`false`)." + }, + "sunset_offset": { + "description": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰" + }, + "turn_on_lights": { + "description": "Para ligar luzes que estão neste momento desligadas. 🔆" + }, + "entity_id": { + "description": "ID Entidade do interruptor. 📝" + }, + "sleep_transition": { + "description": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴" + }, + "autoreset_control_seconds": { + "description": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️" + }, + "transition": { + "description": "Duração da transição quando as luzes mudam, em segundos. 🕑" + }, + "max_color_temp": { + "description": "Cor mais fria em Kelvin. ❄️" + }, + "sleep_brightness": { + "description": "Porcentagem do brilho da lâmpadas no modo \"sleep mode\"." + } + }, + "description": "Muda alguma configuração que você quiser no interruptor. Todas as opções aqui são as mesmas que estão no processo de configuração." + }, + "apply": { + "description": "Aplica as definições atuais da Iluminação Adaptativa às luzes.", + "fields": { + "lights": { + "description": "Uma luz (ou lista de luzes) para a qual serão aplicadas as definições.💡" + }, + "transition": { + "description": "Duração da transição quando as luzes mudam, em segundos. 🕑" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Este dispositivo já está configurado" + }, + "step": { + "user": { + "description": "Cada instância pode conter múltiplas luzes!", + "title": "Escolha um nome para a instância de Iluminação Adaptativa" + } + } + }, + "options": { + "step": { + "init": { + "data_description": { + "transition": "Duração da transição quando as luzes mudam, em segundos. 🕑", + "sleep_brightness": "Porcentagem do brilho da lâmpadas no modo \"sleep mode\"." + }, + "title": "Opções da Iluminação Adaptativa", + "description": "Configure um componente da Iluminação Adaptativa. O nome das opções são as mesmas que as do YML. Se você já definiu essa configuração no YAML, nenhuma opção vai aparecer aqui. Para acessar um gráfico que demonstra o efeito dos parâmetros, acesse [esse app]({webapp_url}). Para mais detalhes, veja a [documentação oficial]({docs_url}).", + "data": { + "lights": "lights: Lista das entity_ids das luzes para serem controladas (pode ser vazia). 🌟", + "min_brightness": "min_brightness: Percentagem minima de brilho. 💡", + "max_brightness": "max_brightness: Percentagem máxima de brilho. 💡", + "min_color_temp": "min_color_temp: Cor mais quente em Kelvin. 🔥", + "max_color_temp": "max_color_temp: Cor mais fria em Kelvin. ❄️" + }, + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Quando possível escolher ajuste em RGB em vez de temperatura da cor. 🌈", + "transition_until_sleep": "transition_until_sleep: Quando ativado, Adaptive Lighting usará as definições do modo noturno como os mínimos, passando para esses valores no por do sol. 🌙", + "take_over_control": "take_over_control: Desativa Adaptive Lighting se alguma fonte chamar`light.turn_on` enquanto as luzes estiverem ligadas e a serem controladas. Tomar nota que esta opção chama o serviço `homeassistant.update_entity` a cada `interval`! 🔒" + }, + "data_description": { + "sleep_transition": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴", + "sunrise_offset": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰", + "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰", + "brightness_mode": "Brilho que irá ser usado. Possíveis valores são `default`, `linear` e `tanh`(usa `brightness_mode_time_dark` e `brightness_mode_time_light`). 📈", + "autoreset_control_seconds": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Opção inválida" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ro.json b/custom_components/adaptive_lighting/translations/ro.json new file mode 100644 index 00000000..a128e229 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ro.json @@ -0,0 +1,83 @@ +{ + "config": { + "step": { + "user": { + "description": "Fiecare instanţă poate conţine mai multe lumini!", + "title": "Alege un nume pentru instanța de Iluminare Adaptivă" + } + }, + "abort": { + "already_configured": "Acest dispozitiv este deja configurat" + } + }, + "options": { + "step": { + "init": { + "data_description": { + "interval": "Frecvenţa adaptării luminilor, în secunde.", + "sleep_brightness": "Procentul luminozităţii luminilor în modul 'somn'." + }, + "title": "Opţiuni Iluminare Adaptivă", + "data": {}, + "sections": { + "advanced": { + "data": { + "adapt_only_on_bare_turn_on": "adaptează_doar_la_comanda_de_arpindere: La aprinderea iniţială a luminilor. Dacă este activat, IA va adapta luminile doar dacă se invocă 'light.turn_on' fără a specifica culoarea sau luminozitatea. Aceasta, de exemplu, previne adaptarea atunci când se activează o scenă. Dacă este dezactivat,IA va adaptata luminile indiferent de prezența valorilor culorii sau luminozității în service_data. Necesită activarea opţiunii 'preia_controlul. " + }, + "data_description": { + "sunrise_offset": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde.⏰", + "sunset_offset": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde.", + "brightness_mode": "Mod de luminozitate de utilizat. Valorile posibile sunt: 'implicit', 'liniar' şi 'hiperbolic' ( ultilizează 'mod_luminozitate_timp_de_noapte' şi 'mod_luminozitate_timp_de_zi').", + "brightness_mode_time_light": "(Se ignoră dacă `modul_de_luminozitate='implicit'`) Durată în secunde a modificării luminozităţii în sus/jos cand poziţia sorelui este înainte sau după răsărit/apus.", + "autoreset_control_seconds": "Resetare automată al controlului manual după un număr de secunde. Setaţi la 0 pentru a dezactiva." + } + } + } + } + } + }, + "title": "Iluminare Adaptivă", + "services": { + "apply": { + "description": "Aplicaţi luminilor setările curente ale Iluminiării Adaptive luminilor.", + "fields": { + "lights": { + "description": "O lumină (sau listă de lumini) pentru care să se aplice setările." + } + } + }, + "change_switch_settings": { + "fields": { + "entity_id": { + "description": "Numele entităţii." + }, + "sleep_brightness": { + "description": "Procentul luminozităţii luminilor în modul 'somn'." + }, + "sleep_transition": { + "description": "Durata de tranziție (în secunde) atunci când modul de „somn” este activat." + }, + "autoreset_control_seconds": { + "description": "Resetare automată al controlului manual după un număr de secunde. Setaţi la 0 pentru a dezactiva." + }, + "only_once": { + "description": "Adaptează luminile doar la pornire ('activat') sau adaptează continuu ('dezactivat')." + }, + "sunrise_offset": { + "description": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde.⏰" + }, + "sunset_offset": { + "description": "Ajustați ora apusului cu un decalaj pozitiv sau negativ în secunde." + } + }, + "description": "Schimbați orice setări dorită în comutator. Toate opțiunile de aici sunt la fel ca în fluxul de configurare." + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "Numele luminii (luminilor) , dacă nu sunt specificate, sunt selectate toate luminile ce aparţin de comutator." + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ru.json b/custom_components/adaptive_lighting/translations/ru.json new file mode 100644 index 00000000..cdb4b768 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ru.json @@ -0,0 +1,233 @@ +{ + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Выберите имя для экземпляра Adaptive Lighting", + "description": "Выберите имя для этого экземпляра. Вы можете запустить несколько экземпляров Adaptive Lighting, каждый из которых может содержать несколько источников света!", + "data": { + "name": "Имя" + } + } + }, + "abort": { + "already_configured": "Это устройство уже настроено" + } + }, + "options": { + "step": { + "init": { + "title": "Настройки Adaptive Lighting", + "description": "Все настройки компонента Adaptive Lighting. Названия опций соответствуют настройкам в YAML. Параметры не отображаются, если в конфигурации YAML определена запись adaptive_lighting.", + "data": { + "lights": "Осветительные приборы: список источников света, которыми нужно управлять (может быть пустым). 🌟", + "interval": "interval: Интервал между обновлениями переключателя. (секунды)", + "transition": "Время перехода при применении изменения к источникам света. (секунды)", + "min_brightness": "min_brightness: Минимальная яркость света во время цикла. (%)", + "max_brightness": "max_brightness: Максимальная яркость света во время цикла. (%)", + "min_color_temp": "min_color_temp: Самый теплый оттенок цветовой температуры во время цикла. (Kelvin)", + "max_color_temp": "max_color_temp: Самый холодный оттенок цветовой температуры во время цикла. (Kelvin)", + "sleep_brightness": "sleep_brightness: Настройка яркости для Режима Сна (Sleep Mode). (%)", + "sleep_color_temp": "sleep_color_temp: Настройка цветовой температуры для Режима Сна (Sleep Mode). (Kelvin)" + }, + "data_description": { + "interval": "Частота адаптации освещения в секундах. 🔄", + "transition": "Продолжительность перехода при смене освещения, в секундах. 🕑", + "sleep_brightness": "Процент яркости света в спящем режиме. 😴", + "sleep_color_temp": "Цветовая температура в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение `color_temp`) в Кельвинах. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Начальный переход, когда свет переключается с 'off' на 'on'. (секунды)", + "prefer_rgb_color": "prefer_rgb_color: По возможности использовать 'rgb_color' вместо 'color_temp'.", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Использовать либо 'rgb_color', либо 'color_temp' в режиме сна. 🌙", + "sleep_rgb_color": "sleep_rgb_color: Цвет RGB в режиме сна (используется при 'sleep_rgb_or_color_temp' как 'rgb_color'). 🌈", + "sleep_transition": "sleep_transition: Когда прибор переходит в Режима Сна (Sleep Mode) и 'sleep_state' изменяется. (секунды)", + "transition_until_sleep": "transition_until_sleep: когда включено, адаптивное освещение будет рассматривать настройки сна как минимальные, переходя к этим значениям после захода солнца. 🌙", + "sunrise_time": "sunrise_time: Ручное изменение времени восхода солнца, если указано 'None', используется фактическое время восхода в Вашем местоположении. (ЧЧ:ММ:СС)", + "min_sunrise_time": "min_sunrise_time: Самое раннее время виртуального восхода (ЧЧ:ММ:СС). 🌅", + "max_sunrise_time": "max_sunrise_time: Самое позднее время виртуального восхода (ЧЧ:ММ:СС). 🌅", + "sunrise_offset": "sunrise_offset: За сколько времени до (-) или после (+) переопределить время восхода во время цикла. (+/- секунды)", + "sunset_time": "sunset_time: Ручное изменение времени заката солнца, если указано 'None', используется фактическое время заката в Вашем местоположении. (ЧЧ:ММ:СС)", + "min_sunset_time": "min_sunset_time: Самое раннее время виртуального заката (ЧЧ:ММ:СС). 🌇", + "max_sunset_time": "max_sunset_time: Самое позднее время виртуального заката (ЧЧ:ММ:СС). 🌇", + "sunset_offset": "sunset_offset: За сколько времени до (-) или после (+) переопределить время заката во время цикла. (+/- секунды)", + "brightness_mode": "brightness_mode: Режим яркости для использования (default, linear, tanh). 📈", + "take_over_control": "take_over_control: Если что-либо, кроме Adaptive Lighting, вызывает службу 'light.turn_on', когда свет уже включен, прекратить адаптацию этого осветительного прибора, пока он (или переключатель) не переключится off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes: Обнаруживает все изменения на >10% примененные к освещению (также и из-за пределов Home Assistant), требует включения 'take_over_control' (вызывает 'homeassistant.update_entity' каждый 'interval'!)", + "autoreset_control_seconds": "autoreset_control_seconds: Автосброс ручного управления через X секунд. ⏲️", + "only_once": "only_once: Адаптировать свет только при включении.", + "adapt_only_on_bare_turn_on": "Adapt_only_on_bare_turn_on: При первоначальном включении света. Если установлено значение «true», AL адаптируется только в том случае, если «light.turn_on» вызывается без указания цвета или яркости. ❌🌈 Это, например, предотвращает адаптацию при активации сцены. Если false, AL адаптируется независимо от наличия цвета или яркости в исходных service_data. Требуется включить take_over_control. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Раздельные команды для каждого атрибута (цвет, яркость и т.д.) в 'light.turn_on' (требуется для некоторых источников света).", + "send_split_delay": "send_split_delay: Задержка между отдельными командами включения для источников света. ⏲️", + "adapt_delay": "Время ожидания между включением света и применением адаптации. Может помочь избежать мерцания. (секунды)", + "skip_redundant_commands": "Skip_redundant_commands: Пропустить отправку команд адаптации, целевое состояние которых уже равно известному состоянию источника света. Минимизирует сетевой трафик и улучшает скорость адаптации в некоторых ситуациях. 📉Отключите, если физические состояния освещения не синхронизируются с записанным состоянием HA.", + "intercept": "intercept: перехватывать и адаптировать вызовы `light.turn_on` для обеспечения мгновенной адаптации цвета и яркости. 🏎️ Отключите источники света, которые не поддерживают `light.turn_on` с цветом и яркостью.", + "multi_light_intercept": "multi_light_intercept: перехватывает и адаптирует вызовы `light.turn_on`, нацеленные на несколько источников света. ➗⚠️ Это может привести к разделению одного вызова `light.turn_on` на несколько вызовов, например, когда освещение включено в разные выключатели. Требуется, чтобы `перехват` был включен.", + "include_config_in_attributes": "include_config_in_attributes: отображать все параметры в качестве атрибутов на переключателе в Home Assistant, если установлено значение `true`. 📝" + }, + "data_description": { + "initial_transition": "Продолжительность первого перехода, когда освещение переключается с `выключено` на `включено` в секундах. ⏲️", + "sleep_rgb_or_color_temp": "Используйте либо `\"rgb_color\"`, либо `\"color_temp\"` в спящем режиме. 🌙", + "sleep_rgb_color": "Цвет RGB в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение \"rgb_color\"). 🌈", + "sleep_transition": "Длительность перехода при переключении \"спящего режима\" в секундах. 😴", + "sunrise_time": "Устанавливает фиксированное время (ЧЧ:ММ:СС) восхода солнца. 🌅", + "min_sunrise_time": "Устанавливает самое раннее время виртуального восхода солнца (ЧЧ:ММ:СС), чтобы обеспечить возможность более позднего восхода солнца. 🌅", + "max_sunrise_time": "Устанавливает последнее время виртуального восхода солнца (ЧЧ:ММ:СС), что позволит восходить раньше. 🌅", + "sunrise_offset": "Регулирует время восхода солнца с положительным или отрицательным смещением в секундах. ⏰", + "sunset_time": "Устанавливает фиксированное время (ЧЧ:ММ:СС) для заката. 🌇", + "min_sunset_time": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇", + "max_sunset_time": "Устанавливает последнее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более ранние закаты. 🌇", + "sunset_offset": "Регулирует время заката с помощью положительного или отрицательного смещения в секундах. ⏰", + "brightness_mode": "Режим яркости для использования. Возможные значения: `default`, `linear` и `tanh` (используются `brightness_mode_time_dark` и `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Игнорируется, если `brightness_mode='default'`) Продолжительность в секундах увеличения/уменьшения яркости до/после восхода/заката. 📈📉", + "brightness_mode_time_light": "(Игнорируется, если `brightness_mode='default'`) Продолжительность в секундах увеличения/уменьшения яркости после/до восхода/заката. 📈📉.", + "autoreset_control_seconds": "Автоматический сброс ручного управления через несколько секунд. Установите значение 0, чтобы отключить. ⏲️", + "send_split_delay": "Задержка (миллисекунды) между отдельными командами поворота для источников света, которые не поддерживают одновременную настройку яркости и цвета. ⏲️", + "adapt_delay": "Время ожидания (в секундах) между включением света и применением изменений адаптивного освещения. Возможно поможет избежать мерцания. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Ошибка в настройках!", + "entity_missing": "Выбранный индикатор не найден" + } + }, + "services": { + "apply": { + "fields": { + "entity_id": { + "description": "`entity_id` переключателя с применяемыми настройками. 📝" + }, + "lights": { + "description": "Источник света (или список источников света), к которому нужно применить настройки. 💡" + }, + "adapt_brightness": { + "description": "Нужно ли адаптировать яркость света. 🌞" + }, + "turn_on_lights": { + "description": "Включать ли свет, который в данный момент выключен. 🔆" + }, + "adapt_color": { + "description": "Нужно ли адаптировать цвет, если поддерживается источником света. 🌈" + }, + "prefer_rgb_color": { + "description": "Предпочитать ли настройку цвета RGB цветовой температуре света, когда это возможно. 🌈" + }, + "transition": { + "description": "Длительность плавного перехода при смене освещения, в секундах. 🕑" + } + }, + "description": "Применяет текущие настройки адаптивного освещения к источникам света." + }, + "change_switch_settings": { + "fields": { + "max_sunrise_time": { + "description": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇" + }, + "min_brightness": { + "description": "Минимальный процент яркости. 💡" + }, + "sunrise_time": { + "description": "Устанавливает фиксированное время (ЧЧ:ММ:СС) восхода солнца. 🌅" + }, + "include_config_in_attributes": { + "description": "Показывать все параметры в качестве атрибутов переключателя в Home Assistant, если установлено значение `true`. 📝" + }, + "max_brightness": { + "description": "Максимальный процент яркости. 💡" + }, + "sleep_rgb_color": { + "description": "Цвет RGB в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение \"rgb_color\"). 🌈" + }, + "use_defaults": { + "description": "Устанавливает значения по умолчанию, не указанные в этом вызове службы. Варианты: \"current\" (по умолчанию, сохраняются текущие значения), \"factory\" (сброс к документированным настройкам по умолчанию) или \"configuration\" (возврат к настройкам конфигурации по умолчанию). ⚙️" + }, + "sunset_time": { + "description": "Устанавливает фиксированное время (ЧЧ:ММ:СС) для заката. 🌇" + }, + "min_sunset_time": { + "description": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇" + }, + "sleep_brightness": { + "description": "Процент яркости света в режиме сна. 😴" + }, + "detect_non_ha_changes": { + "description": "Обнаруживает и останавливает адаптацию для изменений состояния, отличных от `light.turn_on`. Требуется включить `take_over_control`. 🕵️ Внимание: ⚠️ Некоторые индикаторы могут ошибочно указывать включенное состояние, что может привести к неожиданному включению света. Отключите эту функцию, если у вас возникнут такие проблемы." + }, + "sunrise_offset": { + "description": "Отрегулируйте время восхода солнца с положительным или отрицательным смещением в секундах. ⏰" + }, + "sleep_color_temp": { + "description": "Цветовая температура в режиме сна (используется, когда параметр «sleep_rgb_or_color_temp» имеет значение «color_temp») в Кельвинах. 😴" + }, + "min_color_temp": { + "description": "Самая теплая цветовая температура в Кельвинах. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Используйте либо «rgb_color», либо «color_temp» в режиме сна. 🌙" + }, + "turn_on_lights": { + "description": "Включать ли свет, который в данный момент выключен. 🔆" + }, + "initial_transition": { + "description": "Длительность первого плавного перехода, когда освещение переключается с «выключено» на «включено» в секундах. ⏲️" + }, + "entity_id": { + "description": "Сущность `Entity ID` переключателя `switch`. 📝" + }, + "take_over_control": { + "description": "Отключите адаптивное освещение, если другой источник вызывает `light.turn_on`, когда освещение включено и адаптируется. Обратите внимание, что это вызывает `homeassistant.update_entity` каждый `interval` интервал! 🔒" + }, + "sleep_transition": { + "description": "Длительность плавного перехода при переключении «спящего режима» в секундах. 😴" + }, + "autoreset_control_seconds": { + "description": "Автоматический сброс ручного управления через Х секунд. Установите значение 0, чтобы отключить. ⏲️" + }, + "adapt_delay": { + "description": "Время ожидания (в секундах) между включением света и применением адаптивного освещения. Может помочь избежать мерцаний. ⏲️" + }, + "only_once": { + "description": "Адаптировать освещение только тогда, когда оно включено («true») или продолжать его адаптировать («false»). 🔄" + }, + "separate_turn_on_commands": { + "description": "Используйте отдельные вызовы `light.turn_on` для цвета и яркости, требуется для некоторых типов освещения. 🔀" + }, + "prefer_rgb_color": { + "description": "Предпочитать ли настройку цвета RGB цветовой температуре света, когда это возможно. 🌈" + }, + "max_color_temp": { + "description": "Самая холодная цветовая температура в Кельвинах. ❄️" + }, + "sunset_offset": { + "description": "Отрегулируйте время заката с помощью положительного или отрицательного смещения в секундах. ⏰" + }, + "send_split_delay": { + "description": "Задержка (мс) между отдельными командами поворота `separate_turn_on_commands` для источников света, которые не поддерживают одновременную настройку яркости и цвета. ⏲️" + }, + "transition": { + "description": "Длительность плавного перехода при смене освещения, в секундах. 🕑" + } + }, + "description": "Измените переключателями настройки, которые вам подходят. Все параметры здесь такие же, как и в процессе настройки." + }, + "set_manual_control": { + "description": "Отметьте, контролируется ли свет вручную.", + "fields": { + "manual_control": { + "description": "Добавлять («true») или удалять («false») свет из списка «ручного упрваления». 🔒" + }, + "entity_id": { + "description": "Сущность `entity_id` переключателя для выбора ручного управления `manually controlled`. 📝" + }, + "lights": { + "description": "Сущность(и) `entity_id` источников света, если не указано, выбираются все источники света в переключателе. 💡" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/sk.json b/custom_components/adaptive_lighting/translations/sk.json new file mode 100644 index 00000000..733dff85 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/sk.json @@ -0,0 +1,213 @@ +{ + "options": { + "step": { + "init": { + "data": { + "lights": "svetlá: Zoznam svetiel (entity_id), ktoré majú byť ovládané (môže byť prázdny). 🌟", + "min_brightness": "min_brightness: Najnižší jas (v %). 💡", + "max_brightness": "max_brightness: Najvyšší jas (v %). 💡", + "min_color_temp": "min_color_temp: Najnižšia teplota svetla (v ˚K). 🔥", + "max_color_temp": "max_color_temp: Najvyššia teplota svetla (v ˚K). ❄️" + }, + "data_description": { + "interval": "Frekvencia s akou prispôsobovať svetlá (v sekundách). 🔄", + "transition": "Trvanie prechodu, keď sú svetlá zmenené (v sekundách). ⏲️", + "sleep_brightness": "Jas svetiel pri režime spánku (v %). 😴", + "sleep_color_temp": "Teplota svetla (v ˚K) v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `color_temp`). 😴" + }, + "title": "Nastavenia Adaptívneho osvetlenia", + "description": "Nastavte komponentu Adaptívneho osvetlenia. Názvy nastavení sú zhodné s názvami v súbore YAML. Ak ste túto položku nastavili už v YAML, tak tu sa nezobrazia žiadne možnosti nastavenia. Interaktívne grafy, ktoré zobrazujú vplyv nastavení, nájdete na [tejto webovej aplikácii]({webapp_url}). Ďalšie informácie nájdete v [oficiálnej dokumentácii]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Či preferovať nastavenie cez RGB než nastavením teploty svetla, ak je to možné. 🌈", + "transition_until_sleep": "transition_until_sleep: Keď je funkcia povolená, Adaptívne osvetlenie bude považovať nastavenia režimu spánku ako minimum, a na tieto hodnoty prejde po západe slnka. 🌙", + "take_over_control": "take_over_control: Ak sú svetlá zapnuté a prispôsobované a niečo zavolá službu `light.turn_on`, dôjde k vypnutiu Adaptívneho osvetlenia. Poznámka: Zapnutie tejto voľby spôsobí volanie služby `homeassistant.update_entity` každý `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Deteguje a zastaví prispôbovanie pre zmeny mimo `light.turn_on`. Vyžaduje zapnutie `take_over_control`. 🕵️ Upozornenie: ⚠️ Niektoré svetlá môžu falošne indikovať zapnutý stav, čo spôsobí, že sa svetlo neočakávane zapne. Ak narazíte na tento problém, funkciu vypnite.", + "only_once": "only_once: Prispôsobiť svetlá iba pri zapnutí (`true`) alebo prispôsobovať ich priebežne (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Len pri čistom zapnutí svetiel. Pri nastavení `true` prispôsobí Adaptívne osvetlenie svetlá len pri zavolaní služby `light.turn_on` bez parametrov jasu alebo teploty svetla. ❌🌈 Napríklad: zamedzí to prispôsobovaniu ak je aktivovaná scéna. Pri nastavení `false` dôjde k prispôsobeniu nezávisle na tom či sú parametre jasu alebo teploty svetla prítomné v`service_data`. Vyžaduje zapnutie `take_over_control`. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Použiť samostatné volania služby `light.turn_on` pre nastavenie teploty svetla a jasu (môže byť potrebné pre niektoré typy svetiel). 🔀", + "skip_redundant_commands": "skip_redundant_commands: Preskočiť odoslanie prispôsobovacích príkazov, ktorých cieľový stav je zhodný s posledným známym stavom. Nastavenie minimalizuje sieťovú prevádzku a v niektorých prípadoch môže zlepšiť odozvu prispôsobovania. 📉 Vypnite, pokiaľ skutočný stav svetiel prestáva odpovedať stavu zaznamenanom v HA.", + "intercept": "intercept: Zachytiť a prispôsobiť volania `light.turn_on`, aby došlo k okamžitému prispôsobeniu jasu a teploty svetla. 🏎️ Vypnite pre svetlá, ktoré nepodporujú `light.turn_on` s teplotou svetla a jasom zároveň.", + "multi_light_intercept": "multi_light_intercept: Zachytiť a prispôsobiť volanie služby`light.turn_on`, ktoré ovlyvňuje viacero svetiel. ➗⚠️ Toto môže spôsobiť rozdelenie jedného volania `light.turn_on`na viacero volaní, napr. ak sú svetlá pod rôznymi prepínačmi adaptívneho osvetlenia. Vyžaduje zapnutie `intercept`.", + "include_config_in_attributes": "include_config_in_attributes: Zobraziť všetky nastavenia ako atribúty prepínača v Home Assistant. 📝" + }, + "data_description": { + "initial_transition": "Trvanie prvého prechodu, keď sú svetlá zapnuté z `off` na `on` (v sekundách). ⏲️", + "sleep_rgb_or_color_temp": "V režime spánku použiť `\"rgb_color\"` alebo `\"color_temp\"`. 🌙", + "sleep_rgb_color": "Farba svetla RGB v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `rgb_color `). 🌈", + "sleep_transition": "Trvanie prechodu do alebo z režimu spánku (v sekundách). 😴", + "sunrise_time": "Nastaviť pevný čas (HH:MM:SS) pre východ slnka. 🌅", + "min_sunrise_time": "Nastavte najskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje neskorší východ slnka. 🌅", + "max_sunrise_time": "Nastavte najneskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje skorší východ slnka. 🌅", + "sunrise_offset": "Upravte čas východu slnka o sekundy vpred alebo vzad. ⏰", + "sunset_time": "Nastaviť pevný čas (HH:MM:SS) pre západ slnka. 🌇", + "min_sunset_time": "Nastavte najskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje neskorší západ slnka. 🌅", + "max_sunset_time": "Nastavte najneskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje skorší západ slnka. 🌅", + "sunset_offset": "Upravte čas západu slnka o sekundy vpred alebo vzad. ⏰", + "brightness_mode": "Výber režimu jasu. Možné hodnotu sú `default`, `linear` a `tanh` (používa `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignorované ak `brightness_mode='default'`) Čas na zvýšenie/zníženie jasu po udalosti/pred udalosťou východu/západu slnka. 📈📉", + "brightness_mode_time_light": "(Ignorovaný, ak `brightness_mode = 'predvolené') Trvanie v sekundách na rampu / vypnutie jasu po / pred východ slnka/sunset. 📈📉.", + "autoreset_control_seconds": "Automaticky ukončiť manuálne ovládanie po zadanom množtve sekúnd. Pre vypnutie nastavte 0. ⏲️", + "send_split_delay": "Pauza (v ms) medzi príkazmi pri zapnutom `separate_turn_on_commands` pre svetlá, ktoré nepodporujú súčasné nastavenie jasu a teploty svetla. ⏲️", + "adapt_delay": "Pauza (v sekundách) medzi zapnutím svetla a aplikáciou zmien Adaptívneho osvetlenia. Môže pomôcť zabrániť blikaniu. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Neplatné nastavenie", + "entity_missing": "V Home Assistant chýba jedno alebo viac vybraných svetiel" + } + }, + "title": "Adaptívne osvetlenie", + "config": { + "step": { + "user": { + "description": "Každá inštancia môže obsahovať viacero svetiel!", + "title": "Vyberte názov inštancie Adaptívneho osvetlenia", + "data": { + "name": "Názov" + } + } + }, + "abort": { + "already_configured": "Toto zariadenie už je nastavené" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sunrise_time": { + "description": "Nastaviť pevný čas (HH:MM:SS) pre východ slnka. 🌅" + }, + "sunset_time": { + "description": "Nastaviť pevný čas (HH:MM:SS) pre západ slnka. 🌇" + }, + "sleep_brightness": { + "description": "Jas svetiel pri režime spánku (v %). 😴" + }, + "sunrise_offset": { + "description": "Upravte čas východu slnka o sekundy vpred alebo vzad. ⏰" + }, + "max_sunrise_time": { + "description": "Nastavte najneskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje skorší východ slnka. 🌅" + }, + "sleep_color_temp": { + "description": "Teplota svetla (v ˚K) v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `color_temp`). 😴" + }, + "min_brightness": { + "description": "Najnižší jas (v %). 💡" + }, + "min_color_temp": { + "description": "Najnižšia teplota svetla (v ˚K). 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "V režime spánku použiť `\"rgb_color\"` alebo `\"color_temp\"`. 🌙" + }, + "turn_on_lights": { + "description": "Či sa majú zapnúť svetlá, ktoré sú momentálne vypnuté. 🔆" + }, + "initial_transition": { + "description": "Trvanie prvého prechodu, keď sú svetlá zapnuté z `off` na `on` (v sekundách). ⏲️" + }, + "entity_id": { + "description": "ID prepínača. 📝" + }, + "include_config_in_attributes": { + "description": "Zobraziť všetky nastavenia ako atribúty prepínača v Home Assistant. 📝" + }, + "max_brightness": { + "description": "Najvyšší jas (v %). 💡" + }, + "sleep_rgb_color": { + "description": "Farba svetla RGB v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `rgb_color `). 🌈" + }, + "take_over_control": { + "description": "Ak sú svetlá zapnuté a prispôsobované a niečo zavolá službu `light.turn_on`, dôjde k vypnutiu Adaptívneho osvetlenia. Poznámka: Zapnutie tejto voľby spôsobí volanie služby `homeassistant.update_entity` každý `interval`! 🔒" + }, + "sleep_transition": { + "description": "Trvanie prechodu do alebo z režimu spánku (v sekundách). 😴" + }, + "autoreset_control_seconds": { + "description": "Automaticky ukončiť manuálne ovládanie po zadanom množtve sekúnd. Pre vypnutie nastavte 0. ⏲️" + }, + "adapt_delay": { + "description": "Pauza (v sekundách) medzi zapnutím svetla a aplikáciou zmien Adaptívneho osvetlenia. Môže pomôcť zabrániť blikaniu. ⏲️" + }, + "only_once": { + "description": "Prispôsobiť svetlá len pri zapnutí (`true`) alebo prispôsobovať ich priebežne (`false`). 🔄" + }, + "use_defaults": { + "description": "Stanovuje predvolené hodnoty nie sú uvedené v tomto servisnom hovore. Možnosti: \"current\" (predvolené, zachováva aktuálne hodnoty), \"chôdzky na zdokumentované predvolené nastavenia), alebo \"konfigurácia\" (odkazy prepínať predvolené nastavenia). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Použiť samostatné volania služby `light.turn_on` pre nastavenie teploty svetla a jasu (môže byť potrebné pre niektoré typy svetiel). 🔀" + }, + "prefer_rgb_color": { + "description": "Či preferovať nastavenie cez RGB než nastavením teploty svetla, ak je to možné. 🌈" + }, + "max_color_temp": { + "description": "Najvyššia teplota svetla (v ˚K). ❄️" + }, + "sunset_offset": { + "description": "Upravte čas západu slnka o sekundy vpred alebo vzad. ⏰" + }, + "send_split_delay": { + "description": "Pauza (v ms) medzi príkazmi pri zapnutom `separate_turn_on_commands` pre svetlá, ktoré nepodporujú súčasné nastavenie jasu a teploty svetla. ⏲️" + }, + "transition": { + "description": "Trvanie prechodu, keď sú svetlá zmenené (v sekundách). ⏲️" + }, + "min_sunset_time": { + "description": "Nastavte najskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje neskorší západ slnka. 🌅" + }, + "detect_non_ha_changes": { + "description": "Detekuje a zastaví prispôsobovanie pre iné zmeny stavov než `light.turn_on`. Vyžaduje zapnuté `take_over_control`. 🕵️ Upozornenie: ⚠️ Niektoré svetlá môžu nesprávne indikovať stav 'on', čo môže spôsobiť neočakávané zapnutie svetiel. Vypnite toto nastavenie, ak sa takéto problémy objavia." + } + }, + "description": "Zmeňte ľubovoľné nastavenie prepínača. Všetky možnosti sú rovnaké ako v config flow." + }, + "apply": { + "fields": { + "entity_id": { + "description": "`entity_id` prepínača, na ktorý sa majú aplikovať zmeny. 📝" + }, + "adapt_brightness": { + "description": "Či prispôsobiť jas svetla. 🌞" + }, + "turn_on_lights": { + "description": "Či sa majú zapnúť svetlá, ktoré sú momentálne vypnuté. 🔆" + }, + "adapt_color": { + "description": "Či prispôsobiť teplotu svetla na podporovaných svetlách. 🌈" + }, + "prefer_rgb_color": { + "description": "Či preferovať nastavenie cez RGB než nastavením teploty svetla, ak je to možné. 🌈" + }, + "lights": { + "description": "Svetlo (alebo zoznam svetiel), na ktoré sa má nastavenie aplikovať. 💡" + }, + "transition": { + "description": "Trvanie prechodu, keď sú svetlá zmenené (v sekundách). ⏲️" + } + }, + "description": "Aplikuje na svetlá súčasné nastavenie Adaptívneho osvetlenia." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Či pridať (`true`) alebo odobrať (`false`) svetlo zo zoznamu \"manuálne ovládaných\". 🔒" + }, + "entity_id": { + "description": "`entity_id` prepínača u ktorého sa majú svetlá o(d)značiť ako \"manuálne ovládané\". 📝" + }, + "lights": { + "description": "subjekt_id(s) svietidiel, ak nie je špecifikované, všetky svetlá v prepínači sú vybrané. 💡" + } + }, + "description": "Či označiť svetlo ako \"manuálne ovládané\"." + } + } +} diff --git a/custom_components/adaptive_lighting/translations/sl.json b/custom_components/adaptive_lighting/translations/sl.json new file mode 100644 index 00000000..ae4e9a6e --- /dev/null +++ b/custom_components/adaptive_lighting/translations/sl.json @@ -0,0 +1,83 @@ +{ + "options": { + "step": { + "init": { + "data": { + "lights": "lights: Seznam entity_id-jev luči za nadzor (lahko je prazen). 🌟", + "min_brightness": "min_brightness: Odstotek najmanjše svetlosti. 💡", + "max_brightness": "max_brightness: Odstotek največeje svetlosti. 💡", + "min_color_temp": "min_color_temp: Najtoplejša barvna temperatura v Kelvinih. 🔥", + "max_color_temp": "max_color_temp: Najhladnejša barvna temperatura v kelvinih. ❄️" + }, + "data_description": { + "interval": "Pogostost prilagajanja luči, v sekundah. 🔄", + "transition": "Trajanje prehoda pri spreminjanju luči, v sekundah. 🕑", + "sleep_brightness": "Odstotek svetlosti luči v načinu spanja. 😴", + "sleep_color_temp": "Barvna temperatura v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljena na \"color_temp\") v Kelvinih. 😴" + }, + "title": "Nastavitve prilagodljive osvetlitve", + "description": "Konfigurirajte komponento Adaptive Lighting. Imena možnosti so usklajena z nastavitvami v YAML. Če ste ta vnos definirali v YAML, tukaj ne bodo prikazane nobene možnosti. Za interaktivne grafe, ki ponazarjajo učinke parametrov, obiščite to [spletno aplikacijo]({webapp_url}). Za dodatne podrobnosti glejte [uradno dokumentacijo]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Ali v primeru možnosti raje uporabiti prilagoditev RGB barve kot barvno temperaturo luči. 🌈", + "transition_until_sleep": "transition_until_sleep: Če je omogočeno, bo Adaptive Lighting obravnaval nastavitve spanja kot minimalne vrednosti in bo po zahodu sonca prehajal na te vrednosti. 🌙", + "take_over_control": "take_over_control: Onemogoči Adaptive Lighting, če drug vir pokliče \"light.turn_on\", ko so luči prižgane in se prilagajajo. Opozorilo: to ob vsakem intervalu kliče \"homeassistant.update_entity\"! 🔒", + "detect_non_ha_changes": "„detect_non_ha_changes: Zazna in ustavi prilagoditve za spremembe stanja, ki niso posledica \"light.turn_on\". Zahteva omogočeno \"take_over_control\". 🕵️ Pozor: ⚠️ Nekatere luči lahko nepravilno poročajo, da so prižgane, kar lahko povzroči nepričakovano vklapljanje. Onemogočite to funkcijo, če naletite na takšne težave.", + "only_once": "only_once: Prilagodi luči samo ob vklopu (true) ali pa jih še naprej prilagajaj (false). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ob začetnem vklopu luči. Če je nastavljeno na \"true\", AL prilagodi samo, če je \"light.turn_on\" klic brez podanih parametrov barve ali jakosti. ❌🌈 S tem se npr. prepreči prilagajanje pri aktivaciji scene. Če je \"false\", AL prilagodi ne glede na prisotnost barve ali jakosti v začetnih \"service_data\". Zahteva omogočeno \"take_over_control\". 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Uporabi ločene klice \"light.turn_on\" za barvo in jakost, kar je potrebno za nekatere tipe luči. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Preskoči pošiljanje prilagoditvenih ukazov, če je ciljano stanje že enako poznanemu stanju luči. Zmanjšuje omrežni promet in izboljšuje odzivnost prilagajanja v določenih situacijah. 📉 Onemogočite, če se fizična stanja luči ne ujemajo z zabeleženim stanjem v HA.", + "intercept": "intercept: Prestreza in prilagaja klice \"light.turn_on\" za takojšnjo prilagoditev barve in jakosti. 🏎️ Onemogočite za luči, ki ne podpirajo \"light.turn_on\" z barvo in svetlostjo.", + "multi_light_intercept": "multi_light_intercept: Prestreza in prilagaja klice \"light.turn_on\", ki ciljajo več luči. ➗⚠️ To lahko privede do razdelitve enega klica \"light.turn_on\" v več klicev, npr. ko so luči na različnih stikalih. Zahteva omogočeno \"intercept\".", + "include_config_in_attributes": "include_config_in_attributes: Ko je nastavljeno na \"true\", prikaže vse možnosti kot atribute stikala v Home Assistantu. 📝" + }, + "data_description": { + "initial_transition": "Trajanje prvega prehoda, ko se luči prižgejo (iz \"off\" v \"on\"), v sekundah. ⏲️", + "sleep_rgb_or_color_temp": "V načinu spanja uporabi \"rgb_color\" ali \"color_temp\". 🌙", + "sleep_rgb_color": "RGB barva v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljeno na \"rgb_color\"). 🌈", + "sleep_transition": "Trajanje prehoda, ko se preklopi način spanja, v sekundah. 😴", + "sunrise_time": "Nastavi fiksni čas (HH:MM:SS) sončnega vzhoda. 🌅", + "min_sunrise_time": "Nastavi najzgodnejši navidezni sončni vzhod (HH:MM:SS), dovoljuje kasnejše vzhode. 🌅", + "max_sunrise_time": "Nastavi najkasnejši virtualni sončni vzhod (HH:MM:SS), dovoljuje zgodnejše vzhode. 🌅", + "sunrise_offset": "Prilagodite čas sončnega vzhoda z pozitivnim ali negativnim zamikom v sekundah. ⏰", + "sunset_time": "Nastavite fiksni čas (HH:MM:SS) za sončni zahod. 🌇", + "min_sunset_time": "Nastavite najzgodnejši navidezni čas sončnega zahoda (HH:MM:SS), dovoljuje kasnejše sončne zahode. 🌇", + "max_sunset_time": "Nastavite najpoznejši navidezni čas sončnega zahoda (HH:MM:SS), dovoljuje zgodnejše sončne zahode. 🌇", + "sunset_offset": "Prilagodite čas sončnega zahoda s pozitivnim ali negativnim zamikom v sekundah. ⏰", + "brightness_mode": "Način upravljanja svetlosti. Možne vrednosti so \"default\", \"linear\" in \"tanh\" (uporablja \"brightness_mode_time_dark\" in \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Prezrto, če je \"brightness_mode='default'\") Trajanje v sekundah za postopno povečanje ali zmanjšanje svetlosti pred/po sončnem vzhodu/zahodu. 📈📉", + "brightness_mode_time_light": "(Prezrto, če je \"brightness_mode='default'\") Trajanje v sekundah za postopno povečanje ali zmanjšanje svetlosti po/pred sončnem vzhodu/zahodu. 📈📉", + "autoreset_control_seconds": "Samodejno ponastavi ročni nadzor po določenem številu sekund. Nastavite na 0, da onemogočite. ⏲️", + "send_split_delay": "Zamik (ms) med \"separate_turn_on_commands\" za luči, ki ne podpirajo hkratne nastavitve jakosti in barve. ⏲️" + } + } + } + } + } + }, + "config": { + "step": { + "user": { + "title": "Izberite ime za instanco Adaptive Lighting", + "description": "Vsaka instanca lahko vsebuje več luči!" + } + }, + "abort": { + "already_configured": "Naprava je že konfigurirana" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Prilagajaj luči samo kadar so prižgane (\"true\") ali konstantno jih prilagajaj (\"false\")" + }, + "max_sunrise_time": { + "description": "Nastavite najpoznejši navidezni čas sončnega vzhoda (HH:MM:SS), dovoljuje zgodnejše sončne vzhode. 🌅" + } + } + } + }, + "title": "Prilagodljiva osvetlitev" +} diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json new file mode 100644 index 00000000..e0b254e1 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -0,0 +1,233 @@ +{ + "title": "Adaptiv Ljussättning", + "config": { + "step": { + "user": { + "title": "Välj ett namn för Adaptiv Ljussättning", + "description": "Varje konfiguration kan innehålla flera ljuskällor!", + "data": { + "name": "Namn" + } + }, + "menu": { + "data": { + "action": "Åtgärd" + }, + "title": "Skapa eller duplicera", + "description": "Vill du skapa en ny instans eller duplicera en befintlig?" + } + }, + "abort": { + "already_configured": "Den här enheten är redan konfiguerad" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptiv Ljussättning Inställningar", + "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.", + "data": { + "lights": "lights, ljuskällor", + "interval": "interval, Tid mellan uppdateringar i sekunder", + "transition": "transition, i sekunder", + "min_brightness": "min_brightness, i %", + "max_brightness": "max_brightness, i procent %", + "min_color_temp": "min_color_temp, i Kelvin", + "max_color_temp": "max_color_temp, i Kelvin", + "sleep_brightness": "sleep_brightness, i %", + "sleep_color_temp": "sleep_color_temp, i Kelvin" + }, + "data_description": { + "interval": "Frekvens för att anpassa lamporna, i sekunder. 🔄", + "transition": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑", + "sleep_brightness": "Procent ljusstyrka för lampor i sovläge. 😴", + "sleep_color_temp": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras", + "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", + "transition_until_sleep": "transition_until_sleep: När aktiverat kommer Adaptive Lighting att behandla sömninställningarna som ett minimum och övergå till dessa värden efter solnedgången. 🌙", + "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)", + "sunrise_offset": "sunrise_offset, i +/- sekunder", + "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", + "sunset_offset": "sunset_offset, i +/- sekunder", + "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", + "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", + "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: När lampor först tänds. Om satt till \"true\", anpassar AL endast om \"light.turn_on\" anropas utan att ange färg eller ljusstyrka. ❌🌈 Detta förhindrar t.ex. anpassning när en scen aktiveras. Om \"false\" anpassas AL oavsett förekomsten av färg eller ljusstyrka i den initiala \"service_data\". \"takeover_control\" måste vara aktiverat. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", + "skip_redundant_commands": "skip_redundant_commands: Hoppa över att skicka anpassningskommandon vars måltillstånd redan är lika med lampans kända tillstånd. Minimerar nätverkstrafik och förbättrar anpassningsförmågan i vissa situationer. 📉 Inaktivera om lampans tillstånd blir osynkroniserade med HA:s registrerade tillstånd.", + "intercept": "intercept: Fånga upp och anpassa `light.turn_on`-anrop för att möjliggöra omedelbar anpassning av färg och ljusstyrka. 🏎️ Inaktivera för lampor som inte stöder `light.turn_on` med färg och ljusstyrka.", + "multi_light_intercept": "multi_light_intercept: Fånga upp och anpassa \"light.turn_on\"-anrop som riktar sig mot flera lampor. ➗⚠️ Detta kan resultera i att ett enda `light.turn_on`-anrop delas upp i flera anrop, t.ex. när lamporna är kopplade till olika strömbrytare. Kräver att \"intercept\" är aktiverat.", + "include_config_in_attributes": "include_config_in_attributes: Visa alla alternativ som attribut på strömbrytaren i Home Assistant när den är inställd på \"true\". 📝" + }, + "data_description": { + "initial_transition": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️", + "sleep_rgb_or_color_temp": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙", + "sleep_rgb_color": "RGB-färg i sovläge (används när \"sleep_rgb_or_color_temp\" är \"rgb_color\"). 🌈", + "sleep_transition": "Dröjsmål för övergång när \"sov läge\" slås på/av, i sekunder. 🕑", + "sunrise_time": "Ställ in en fast tid (TT:MM:SS) för soluppgången. 🌅", + "min_sunrise_time": "Ställ in den tidigaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör senare soluppgångar. 🌅", + "max_sunrise_time": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅", + "sunrise_offset": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰", + "sunset_time": "Ställ in en fast tid (TT:MM:SS) för solnedgången. 🌇", + "min_sunset_time": "Ställ in den tidigaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör senare solnedgångar. 🌇", + "max_sunset_time": "Ställ in den senaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör tidigare solnedgångar. 🌇", + "sunset_offset": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰", + "brightness_mode": "Ljusstyrkeinställing att använda. Möjliga värden är \"default\", \"linear\" och \"tanh\" (använder \"brightness_mode_time_dark\" och \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", + "brightness_mode_time_light": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", + "take_over_control_mode": "Anpassningspausläget när andra källor ändrar ljusstyrka och/eller färg på belysningen. `pause_all` pausar alltid både ljusstyrka och färganpassning. `pause_changed` pausar endast anpassningen av de ändrade attributen och fortsätter att anpassa oförändrade attribut, t.ex. fortsätter färganpassningen när endast ljusstyrkan har ändrats.", + "autoreset_control_seconds": "Nollställ automatiskt manuell kontroll efter ett antal sekunder. Sätt till 0 för at avaktivera. ⏲️", + "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️", + "adapt_delay": "Väntetid (sekunder) mellan lamptändning och Adaptiv Ljussättning tillämpar ändringar. Kan hjälpa till att undvika flimmer. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Ogiltlig inställning", + "entity_missing": "Ett valt ljus hittades inte" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Procent ljusstyrka för lampor i sovläge. 😴" + }, + "sunrise_offset": { + "description": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰" + }, + "sleep_color_temp": { + "description": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴" + }, + "entity_id": { + "description": "Enhets-ID för strömbrytaren. 📝" + }, + "sleep_transition": { + "description": "Dröjsmål för övergång när \"sov läge\" slås på/av, i sekunder. 🕑" + }, + "autoreset_control_seconds": { + "description": "Nollställ automatiskt manuell kontroll efter ett antal sekunder. Sätt till 0 för at avaktivera. ⏲️" + }, + "only_once": { + "description": "Anpassa lampor endast när de slås på ('true') eller fortsätt anpassa dem ('false'). 🔄" + }, + "max_color_temp": { + "description": "Kallaste färgtemperatur i Kelvin. ❄️" + }, + "sunset_offset": { + "description": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰" + }, + "send_split_delay": { + "description": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️" + }, + "transition": { + "description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑" + }, + "max_sunrise_time": { + "description": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅" + }, + "min_brightness": { + "description": "Minimal ljusstyrka i procent. 💡" + }, + "min_color_temp": { + "description": "Varmaste färgtemperaturen i Kelvin. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙" + }, + "turn_on_lights": { + "description": "Om att tända lampor som är för närvarande släckta. 🔆" + }, + "initial_transition": { + "description": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️" + }, + "sunrise_time": { + "description": "Ställ in en fast tid (TT:MM:SS) för soluppgången. 🌅" + }, + "include_config_in_attributes": { + "description": "Visa alla alternativ som attribut på strömbrytaren i Home Assistant när ”true”. 📝" + }, + "max_brightness": { + "description": "Maximal ljusstyrka i procent. 💡" + }, + "sleep_rgb_color": { + "description": "RGB-färg i sovläge (används när \"sleep_rgb_or_color_temp\" är \"rgb_color\"). 🌈" + }, + "adapt_delay": { + "description": "Väntetid (sekunder) mellan lamptändning och Adaptiv Ljussättning tillämpar ändringar. Kan hjälpa till att undvika flimmer. ⏲️" + }, + "separate_turn_on_commands": { + "description": "Använd separata `light.turn_on`anrop för färg och ljusstyrka, behövs för vissa lamptyper. 🔀" + }, + "prefer_rgb_color": { + "description": "Om att föredra RGB-färgjustering framför ljusfärgtemperatur när det är möjligt. 🌈" + }, + "sunset_time": { + "description": "Ställ in en fast tid (TT:MM:SS) för solnedgången. 🌇" + }, + "min_sunset_time": { + "description": "Ställ in den tidigaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör senare solnedgångar. 🌇" + }, + "detect_non_ha_changes": { + "description": "Upptäcker och stoppar anpassningar för tillståndsändringar som inte är \"light.turn_on\". Behöver \"takeover_control\" aktiverat. 🕵️ Varning: ⚠️ Vissa lampor kan felaktigt indikera ett \"på\"-läge, vilket kan resultera i att lamporna tänds oväntat. Inaktivera den här funktionen om du stöter på sådana problem." + }, + "take_over_control": { + "description": "Inaktivera Adaptive Ligting om en annan källa anropar 'light.turn_on' medan lamporna är tända och anpassas. Observera att detta anropar `homeassistant.update_entity` varje `intervall`! 🔒" + }, + "use_defaults": { + "description": "Ställer in standardvärden som inte anges i detta serviceanrop. Alternativ: \"current\" (standard, behåller nuvarande värden), \"factory\" (återställer till dokumenterade standardinställningar) eller \"configuration\" (återgår till strömbrytarens standardinställningar). ⚙️" + }, + "take_over_control_mode": { + "description": "Anpassningspausläget när andra källor ändrar ljusstyrka och/eller färg på belysningen. `pause_all` pausar alltid både ljusstyrka och färganpassning. `pause_changed` pausar endast anpassningen av de ändrade attributen och fortsätter att anpassa oförändrade attribut, t.ex. fortsätter färganpassningen när endast ljusstyrkan har ändrats." + } + }, + "description": "Ändra vilka inställningar du vill ha i strömbrytaren. All dessa inställningar är likadana som i config flow." + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "Enhets-ID för lampor. Om inget anges väljs alla lampor i strömbrytaren. 💡" + }, + "manual_control": { + "description": "Lägg till (\"true\") eller ta bort (\"false\") ljuset från listan \"manual_control\". 🔒" + }, + "entity_id": { + "description": "Strömbrytarens ”entity_id\" i vilken lampan ska (av)markeras som \"manuellt styrd\". 📝" + } + }, + "description": "Markera om en lampa är \"styrd manuellt\"." + }, + "apply": { + "description": "Tillämpar nuvarande Adaptiv Ljussätting inställningar till lampor.", + "fields": { + "lights": { + "description": "En lampa (eller en lamplista) till vilka inställningarna tillämpas." + }, + "transition": { + "description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑" + }, + "entity_id": { + "description": "\"entity_id\" för strömbrytaren med inställningarna som ska tillämpas. 📝" + }, + "adapt_brightness": { + "description": "Om lampans ljusstyrka ska anpassas. 🌞" + }, + "turn_on_lights": { + "description": "Om att tända lampor som är för närvarande släckta. 🔆" + }, + "adapt_color": { + "description": "Om färgen på lampor som stödjer ska anpassas. 🌈" + }, + "prefer_rgb_color": { + "description": "Om att föredra RGB-färgjustering framför ljusfärgtemperatur när det är möjligt. 🌈" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ta.json b/custom_components/adaptive_lighting/translations/ta.json new file mode 100644 index 00000000..39c6c918 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ta.json @@ -0,0 +1,210 @@ +{ + "services": { + "set_manual_control": { + "description": "ஒரு ஒளி 'கைமுறையாக கட்டுப்படுத்தப்பட்டதா' என்பதைக் குறிக்கவும்.", + "fields": { + "entity_id": { + "description": "சுவிட்சின் `நிறுவனம்_ஐடி`, இதில் (அன்) ஒளியை` கைமுறையாக கட்டுப்படுத்தப்படுகிறது 'என்று குறிக்கவும். ." + }, + "lights": { + "description": "விளக்குகளின் entity_id (கள்), குறிப்பிடப்படாவிட்டால், சுவிட்சில் உள்ள அனைத்து விளக்குகளும் தேர்ந்தெடுக்கப்படுகின்றன. ." + }, + "manual_control": { + "description": "\"கையேடு_ கன்ட்ரோல்\" பட்டியலிலிருந்து ஒளியை சேர்க்க வேண்டுமா அல்லது அகற்ற வேண்டுமா அல்லது அகற்ற வேண்டுமா அல்லது அகற்ற வேண்டுமா? ." + } + } + }, + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய தோன்றுகை நேரத்தை சரிசெய்யவும். ." + }, + "sunrise_time": { + "description": "சூரிய உதயத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். ." + }, + "entity_id": { + "description": "சுவிட்சின் நிறுவன ஐடி. ." + }, + "use_defaults": { + "description": "இந்த பணி அழைப்பில் குறிப்பிடப்படாத இயல்புநிலை மதிப்புகளை அமைக்கிறது. விருப்பங்கள்: \"நடப்பு\" (இயல்புநிலை, தற்போதைய மதிப்புகளைத் தக்க வைத்துக் கொள்கிறது), \"தொழிற்சாலை\" (ஆவணப்படுத்தப்பட்ட இயல்புநிலைகளுக்கு மீட்டமைக்கிறது) அல்லது \"உள்ளமைவு\" (கட்டமைப்பு இயல்புநிலைகளை மாற்றுவதற்கு மாற்றுகிறது). ." + }, + "include_config_in_attributes": { + "description": "`உண்மை` என அமைக்கப்பட்டிருக்கும் போது வீட்டு உதவியாளரின் சுவிட்சில் உள்ள அனைத்து விருப்பங்களையும் பண்புகளாகக் காட்டுங்கள். ." + }, + "turn_on_lights": { + "description": "தற்போது முடக்கப்பட்ட விளக்குகளை இயக்க வேண்டுமா. ." + }, + "initial_transition": { + "description": "விளக்குகள் `ஆஃப்` முதல்` ஆன் `வரை நொடிகளில் மாறும் போது முதல் மாற்றத்தின் காலம். ." + }, + "sleep_transition": { + "description": "\"தூக்க பயன்முறை\" நொடிகளில் மாற்றப்படும்போது மாற்றத்தின் காலம். ." + }, + "max_brightness": { + "description": "அதிகபட்ச ஒளி விழுக்காடு. ." + }, + "max_color_temp": { + "description": "கெல்வினில் குளிரான வண்ண வெப்பநிலை. ." + }, + "min_brightness": { + "description": "குறைந்தபட்ச ஒளி விழுக்காடு. ." + }, + "min_color_temp": { + "description": "கெல்வினில் வெப்பமான வண்ண வெப்பநிலை. ." + }, + "only_once": { + "description": "விளக்குகள் இயக்கப்படும்போது மட்டுமே (`உண்மை`) மாற்றியமைக்கும்போது அல்லது அவற்றைத் தழுவிக்கொள்ளுங்கள் (` பொய்`). ." + }, + "prefer_rgb_color": { + "description": "முடிந்தவரை ஒளி வண்ண வெப்பநிலையை விட RGB வண்ண சரிசெய்தலை விரும்பலாமா. ." + }, + "separate_turn_on_commands": { + "description": "சில ஒளி வகைகளுக்கு தேவைப்படும் வண்ணம் மற்றும் பிரகாசத்திற்கான தனித்தனி `லைட்.டர்ன்_ஓஎன்` அழைப்புகளைப் பயன்படுத்தவும். ." + }, + "send_split_delay": { + "description": "ஒரே நேரத்தில் ஒளி மற்றும் வண்ண அமைப்பை ஆதரிக்காத விளக்குகளுக்கு `தனி_டர்ன்_ஆன்_காமண்ட்ச்` இடையே நேரந்தவறுகை (எம்.எச்). ." + }, + "sleep_brightness": { + "description": "தூக்க பயன்முறையில் விளக்குகளின் ஒளி விழுக்காடு. ." + }, + "sleep_rgb_or_color_temp": { + "description": "தூக்க பயன்முறையில் `\" rgb_color \"` அல்லது `\" Color_Temp \"key ஐப் பயன்படுத்தவும். ." + }, + "sleep_rgb_color": { + "description": "தூக்க பயன்முறையில் RGB வண்ணம் (`SLEEP_RGB_OR_COLOR_TEMP`\" RGB_COLOR \"ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). ." + }, + "sleep_color_temp": { + "description": "ச்லீப் பயன்முறையில் வண்ண வெப்பநிலை (கெல்வினில் `SLEEP_RGB_OR_COLOR_TEMP` என்பது` color_temp` ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). ." + }, + "sunset_offset": { + "description": "வினாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய மறைவு நேரத்தை சரிசெய்யவும். ." + }, + "sunset_time": { + "description": "சூரிய அச்தமனத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். ." + }, + "max_sunrise_time": { + "description": "ஆரம்பகால சூரிய உதயத்தை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சூரிய தோன்றுகை நேரத்தை (HH: MM: SS) அமைக்கவும். ." + }, + "min_sunset_time": { + "description": "ஆரம்பகால மெய்நிகர் சூரிய மறைவு நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய அச்தமனங்களை அனுமதிக்கிறது. ." + }, + "take_over_control": { + "description": "விளக்குகள் இயக்கத்தில் இருக்கும்போது மற்றொரு சான்று `லைட்.டர்ன்_ஒன்` என்று அழைத்தால் தகவமைப்பு விளக்குகளை முடக்கு. இது `ஓமாசிச்டன்ட்.பிடேட்_என்டிட்டி` ஒவ்வொரு` இடைவெளியையும் 'என்று அழைக்கிறது என்பதை நினைவில் கொள்க! ." + }, + "detect_non_ha_changes": { + "description": "`விளக்கு அல்லாத. டர்ன்_ஓஎன்` மாநில மாற்றங்களுக்கான தழுவல்களைக் கண்டறிந்து நிறுத்துகிறது. `Take_over_control` இயக்கப்பட்டது. 🕵œ எச்சரிக்கை: ⚠œ சில விளக்குகள் ஒரு 'ஆன்' நிலையை பொய்யாகக் குறிக்கக்கூடும், இதனால் விளக்குகள் எதிர்பாராத விதமாக இயக்கப்படலாம். இதுபோன்ற சிக்கல்களை நீங்கள் சந்தித்தால் இந்த அம்சத்தை முடக்கு." + }, + "transition": { + "description": "விளக்குகள் மாறும்போது, நொடிகளில் மாற்றத்தின் காலம். ." + }, + "adapt_delay": { + "description": "லைட் டர்ன் மற்றும் தகவமைப்பு விளக்குகள் இடையே காத்திருப்பு நேரம் (விநாடிகள்) மாற்றங்களைப் பயன்படுத்துகிறது. ஒளிரும் தவிர்க்க உதவலாம். ." + }, + "autoreset_control_seconds": { + "description": "பல விநாடிகளுக்குப் பிறகு தானாகவே கையேடு கட்டுப்பாட்டை மீட்டமைக்கவும். முடக்க 0 என அமைக்கவும். ." + } + }, + "description": "சுவிட்சில் நீங்கள் விரும்பும் எந்த அமைப்புகளையும் மாற்றவும். இங்குள்ள அனைத்து விருப்பங்களும் கட்டமைப்பு ஓட்டத்தில் உள்ளதைப் போலவே இருக்கும்." + }, + "apply": { + "description": "தற்போதைய தகவமைப்பு லைட்டிங் அமைப்புகளை விளக்குகளுக்கு பயன்படுத்துகிறது.", + "fields": { + "entity_id": { + "description": "விண்ணப்பிக்க அமைப்புகளுடன் சுவிட்சின் `ENTITY_ID`. ." + }, + "lights": { + "description": "அமைப்புகளைப் பயன்படுத்த ஒரு ஒளி (அல்லது விளக்குகளின் பட்டியல்). ." + }, + "transition": { + "description": "விளக்குகள் மாறும்போது, நொடிகளில் மாற்றத்தின் காலம். ." + }, + "adapt_brightness": { + "description": "ஒளியின் பிரகாசத்தை மாற்றியமைக்க வேண்டுமா. ." + }, + "adapt_color": { + "description": "துணை விளக்குகளில் வண்ணத்தை மாற்றியமைக்க வேண்டுமா. ." + }, + "prefer_rgb_color": { + "description": "முடிந்தவரை ஒளி வண்ண வெப்பநிலையை விட RGB வண்ண சரிசெய்தலை விரும்பலாமா. ." + }, + "turn_on_lights": { + "description": "தற்போது முடக்கப்பட்ட விளக்குகளை இயக்க வேண்டுமா. ." + } + } + } + }, + "title": "தகவமைப்பு விளக்குகள்", + "config": { + "step": { + "user": { + "title": "தகவமைப்பு விளக்கு உதாரணத்திற்கு ஒரு பெயரைத் தேர்வுசெய்க", + "description": "ஒவ்வொரு நிகழ்விலும் பல விளக்குகள் இருக்கலாம்!" + } + }, + "abort": { + "already_configured": "இந்த சாதனம் ஏற்கனவே கட்டமைக்கப்பட்டுள்ளது" + } + }, + "options": { + "step": { + "init": { + "title": "தகவமைப்பு விளக்கு விருப்பங்கள்", + "description": "தகவமைப்பு விளக்கு கூறுகளை உள்ளமைக்கவும். விருப்பப் பெயர்கள் YAML அமைப்புகளுடன் சீரமைக்கப்படுகின்றன. இந்த உள்ளீட்டை நீங்கள் YAML இல் வரையறுத்திருந்தால், இங்கே எந்த விருப்பங்களும் தோன்றாது. அளவுரு விளைவுகளை நிரூபிக்கும் ஊடாடும் வரைபடங்களுக்கு, [இந்த வலை பயன்பாடு]({webapp_url}) ஐப் பார்வையிடவும். மேலும் விவரங்களுக்கு, [அதிகாரப்பூர்வ ஆவணங்கள்]({docs_url}) ஐப் பார்க்கவும்.", + "data": { + "lights": "விளக்குகள்: கட்டுப்படுத்தப்பட வேண்டிய ஒளி நிறுவனம்_டுகளின் பட்டியல் (காலியாக இருக்கலாம்). .", + "min_brightness": "min_brightness: குறைந்தபட்ச ஒளி விழுக்காடு. .", + "max_brightness": "அதிகபட்ச பிரகாசம்: அதிகபட்ச ஒளி விழுக்காடு. .", + "min_color_temp": "min_color_temp: கெல்வினில் வெப்பமான வண்ண வெப்பநிலை. .", + "max_color_temp": "MAX_COLOR_TEMP: கெல்வினில் குளிரான வண்ண வெப்பநிலை. ." + }, + "data_description": { + "interval": "விளக்குகளை மாற்றியமைக்க அதிர்வெண், நொடிகளில். .", + "transition": "விளக்குகள் மாறும்போது, நொடிகளில் மாற்றத்தின் காலம். .", + "sleep_brightness": "தூக்க பயன்முறையில் விளக்குகளின் ஒளி விழுக்காடு. .", + "sleep_color_temp": "ச்லீப் பயன்முறையில் வண்ண வெப்பநிலை (கெல்வினில் `SLEEP_RGB_OR_COLOR_TEMP` என்பது` color_temp` ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). ." + }, + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "bey_rgb_color: முடிந்தவரை ஒளி வண்ண வெப்பநிலையை விட RGB வண்ண சரிசெய்தலை விரும்பலாமா. .", + "transition_until_sleep": "Transition_until_sleep: இயக்கப்பட்டால், தகவமைப்பு விளக்குகள் தூக்க அமைப்புகளை குறைந்தபட்சமாகக் கருதும், சூரிய அச்தமனத்திற்குப் பிறகு இந்த மதிப்புகளுக்கு மாறும். .", + "take_over_control": "Take_over_control: விளக்குகள் இயக்கத்தில் இருக்கும்போது மற்றொரு சான்று `லைட்.டர்ன்_ஓஎன்` என்று அழைத்தால் தகவமைப்பு விளக்குகளை முடக்கு. இது `ஓமாசிச்டன்ட்.பிடேட்_என்டிட்டி` ஒவ்வொரு` இடைவெளியையும் 'என்று அழைக்கிறது என்பதை நினைவில் கொள்க! .", + "detect_non_ha_changes": "கண்டறிதல்_நான்_ஆ_சேஞ்ச்ச்: `விளக்கு அல்லாத. டர்ன்_ஓஎன்` மாநில மாற்றங்களுக்கான தழுவல்களைக் கண்டறிந்து நிறுத்துகிறது. `Take_over_control` இயக்கப்பட்டது. 🕵œ எச்சரிக்கை: ⚠œ சில விளக்குகள் ஒரு 'ஆன்' நிலையை பொய்யாகக் குறிக்கக்கூடும், இதனால் விளக்குகள் எதிர்பாராத விதமாக இயக்கப்படலாம். இதுபோன்ற சிக்கல்களை நீங்கள் சந்தித்தால் இந்த அம்சத்தை முடக்கு.", + "only_once": "மட்டும்_இன்: விளக்குகள் இயக்கப்படும்போது மட்டுமே (`உண்மை`) மாற்றியமைக்கும்போது அல்லது அவற்றைத் தழுவிக்கொள்ளுங்கள் (` தவறு`). .", + "adapt_only_on_bare_turn_on": "சரிசெய்_only_on_bare_turn_on: ஆரம்பத்தில் விளக்குகளை இயக்கும்போது. `உண்மை` என அமைக்கப்பட்டால், வண்ணம் அல்லது பிரகாசத்தைக் குறிப்பிடாமல்` லைட்.டர்ன்_ஓஎன்` செயல்படுத்தப்பட்டால் மட்டுமே அல் மாற்றியமைக்கிறது. ❌🌈 இது எ.கா., ஒரு காட்சியைச் செயல்படுத்தும்போது தழுவலைத் தடுக்கிறது. `தவறு` என்றால், ஆரம்ப` சேவை_டா` இல் நிறம் அல்லது ஒளி இருப்பதைப் பொருட்படுத்தாமல் AL மாற்றியமைக்கிறது. `Take_over_control` இயக்கப்பட்டது. . 🕵️", + "separate_turn_on_commands": "தனித்தனி_டர்ன்_ஆன்_காமண்ட்ச்: சில ஒளி வகைகளுக்கு தேவைப்படும் வண்ணம் மற்றும் பிரகாசத்திற்கான தனித்தனி `லைட்.டர்ன்_ஓஎன்` அழைப்புகளைப் பயன்படுத்தவும். .", + "skip_redundant_commands": "Skip_redundant_commands: தழுவல் கட்டளைகளை அனுப்புவதைத் தவிர்க்கவும், அதன் இலக்கு நிலை ஏற்கனவே ஒளியின் அறியப்பட்ட நிலைக்கு சமம். பிணையம் போக்குவரத்தை குறைக்கிறது மற்றும் சில சூழ்நிலைகளில் தழுவல் மறுமொழியை மேம்படுத்துகிறது. ஆ இன் பதிவு செய்யப்பட்ட நிலையுடன் இயற்பியல் ஒளி நிலைகள் ஒத்திசைவிலிருந்து வெளியேறினால் அது காணக்கூடியது.", + "intercept": "இடைமறிப்பு: உடனடி வண்ணம் மற்றும் பிரகாசமான தழுவலை செயல்படுத்த `ஒளி. Color வண்ணம் மற்றும் பிரகாசத்துடன் `ஒளி.", + "multi_light_intercept": "Mulli_light_intect: பல விளக்குகளை குறிவைக்கும் `light.turn_on` அழைப்புகளை இடைமறிக்கவும் மாற்றவும். ➗⚠œ இது ஒரு `லைட்.டர்ன்_ஒன்` அழைப்பை பல அழைப்புகளாக பிரிக்கக்கூடும், எ.கா., விளக்குகள் வெவ்வேறு சுவிட்சுகளில் இருக்கும்போது. இயக்கப்பட வேண்டும் `இடைமறிப்பு` தேவை.", + "include_config_in_attributes": "அடங்கும்_கான்ஃபிக்_இன்_அட்ரிபியூட்: `உண்மை` என அமைக்கப்பட்டிருக்கும் போது வீட்டு உதவியாளரின் சுவிட்சில் உள்ள பண்புகளாக அனைத்து விருப்பங்களையும் காட்டுங்கள். ." + }, + "data_description": { + "initial_transition": "விளக்குகள் `ஆஃப்` முதல்` ஆன் `வரை நொடிகளில் மாறும் போது முதல் மாற்றத்தின் காலம். .", + "sleep_rgb_or_color_temp": "தூக்க பயன்முறையில் `\" rgb_color \"` அல்லது `\" Color_Temp \"key ஐப் பயன்படுத்தவும். .", + "sleep_rgb_color": "தூக்க பயன்முறையில் RGB வண்ணம் (`SLEEP_RGB_OR_COLOR_TEMP`\" RGB_COLOR \"ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). .", + "sleep_transition": "\"தூக்க பயன்முறை\" நொடிகளில் மாற்றப்படும்போது மாற்றத்தின் காலம். .", + "sunrise_time": "சூரிய உதயத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "min_sunrise_time": "ஆரம்பகால மெய்நிகர் சூரிய உதய நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய உதயங்களை அனுமதிக்கிறது. .", + "max_sunrise_time": "ஆரம்பகால சூரிய உதயத்தை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சூரிய தோன்றுகை நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "sunrise_offset": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய தோன்றுகை நேரத்தை சரிசெய்யவும். .", + "sunset_time": "சூரிய அச்தமனத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "min_sunset_time": "ஆரம்பகால மெய்நிகர் சூரிய மறைவு நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய அச்தமனங்களை அனுமதிக்கிறது. .", + "max_sunset_time": "முந்தைய சூரிய அச்தமனங்களை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சன்செட் நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "sunset_offset": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய மறைவு நேரத்தை சரிசெய்யவும். .", + "brightness_mode": "பயன்படுத்த பிரகாசமான முறை. சாத்தியமான மதிப்புகள் `இயல்புநிலை`,` லீனியர்`, மற்றும் `டான்` (` பிரகாசம்_மோட்_ நேரம்_டார்க்` மற்றும் `பிரகாசம்_மோட்_மட்_லிட்` ஆகியவற்றைப் பயன்படுத்துகின்றன). .", + "brightness_mode_time_dark": ". .", + "brightness_mode_time_light": ". ..", + "autoreset_control_seconds": "பல விநாடிகளுக்குப் பிறகு தானாகவே கையேடு கட்டுப்பாட்டை மீட்டமைக்கவும். முடக்க 0 என அமைக்கவும். .", + "send_split_delay": "ஒரே நேரத்தில் ஒளி மற்றும் வண்ண அமைப்பை ஆதரிக்காத விளக்குகளுக்கு `தனி_டர்ன்_ஆன்_காமண்ட்ச்` இடையே நேரந்தவறுகை (எம்.எச்). .", + "adapt_delay": "லைட் டர்ன் மற்றும் தகவமைப்பு விளக்குகள் இடையே காத்திருப்பு நேரம் (விநாடிகள்) மாற்றங்களைப் பயன்படுத்துகிறது. ஒளிரும் தவிர்க்க உதவலாம். ." + } + } + } + } + }, + "error": { + "option_error": "தவறான விருப்பம்", + "entity_missing": "ஒன்று அல்லது அதற்கு மேற்பட்ட தேர்ந்தெடுக்கப்பட்ட ஒளி நிறுவனங்கள் வீட்டு உதவியாளரிடமிருந்து காணவில்லை" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/tr.json b/custom_components/adaptive_lighting/translations/tr.json new file mode 100644 index 00000000..d6f929f6 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/tr.json @@ -0,0 +1,210 @@ +{ + "title": "Akıllı Aydınlatma", + "options": { + "step": { + "init": { + "title": "Akıllı Aydınlatma seçenekleri", + "data": { + "lights": "`lights`: Kontrol edilecek ışıkların entity_id listesi (boş bırakılabilir). 🌟", + "min_brightness": "min_brightness: Minimum parlaklık yüzdesi.💡", + "max_brightness": "`max_brightness`: Maksimum parlaklık yüzdesi. 💡", + "min_color_temp": "`min_color_temp`: En yüksek (sıcak) renk sıcaklığı (Kelvin cinsinden). 🔥", + "max_color_temp": "`max_color_temp`: En düşük renk sıcaklığı (Kelvin cinsinden). ❄️" + }, + "data_description": { + "interval": "Işıkların uyarlanma sıklığı (saniye cinsinden). 🔄", + "transition": "Işıklar değişirken geçiş süresi (saniye cinsinden). 🕑", + "sleep_brightness": "Uyku modundayken ışıkların parlaklık yüzdesi 😴", + "sleep_color_temp": "Uyku modunda renk sıcaklığı ( `sleep_rgb_or_color_temp` `color_temp` olarak ayarlandığında kullanılır) Kelvin cinsinden. 😴" + }, + "description": "Bir Adaptive Lighting bileşenini yapılandırın. Seçenek adları YAML ayarlarıyla uyumludur. Eğer bu girdiyi YAML’da tanımladıysanız, burada seçenekler görünmez. \nParametrelerin etkilerini gösteren etkileşimli grafikler için [bu web uygulamasını]({webapp_url}) ziyaret edebilirsiniz. Daha fazla bilgi için [resmi dokümantasyona]({docs_url}) bakın.", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "`prefer_rgb_color`: Mümkünse ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈", + "transition_until_sleep": "`transition_until_sleep`: Etkinleştirildiğinde, Adaptive Lighting uyku ayarlarını minimum değer olarak kabul eder ve gün batımından sonra bu değerlere geçiş yapar. 🌙", + "take_over_control": "`take_over_control`: Işıklar açıkken ve uyarlanırken başka bir kaynaktan `light.turn_on` çağrılırsa Adaptive Lighting’i devre dışı bırakır. Dikkat: Bu işlem her `interval` süresinde `homeassistant.update_entity` çağrısı yapar! 🔒", + "detect_non_ha_changes": "`detect_non_ha_changes`: `light.turn_on` dışındaki durum değişikliklerini algılar ve uyarlamayı durdurur. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️\nDikkat: ⚠️ Bazı ışıklar yanlışlıkla “açık” durumunu bildirebilir, bu da ışıkların beklenmedik şekilde açılmasına yol açabilir. Böyle bir durumla karşılaşırsanız bu özelliği devre dışı bırakın.", + "only_once": "`only_once`: Işıkları yalnızca açıldıklarında mı uyarlasın (`true`), yoksa sürekli uyarlamaya devam mı etsin (`false`). 🔄", + "adapt_only_on_bare_turn_on": "Işıklar açıldığında geçerlidir. `true` olarak ayarlanırsa, eklenti yalnızca `light.turn_on` işlemi renk veya parlaklık belirtilmeden çağrıldığında uyarlama yapar (örneğin sahne etkinleştirmelerinde uyarlama yapılmaz). ❌🌈\n`false` olarak ayarlanırsa, renk veya parlaklık belirtilmiş olsa bile uyarlama yapılır. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️", + "separate_turn_on_commands": "`separate_turn_on_commands`: Renk ve parlaklık için ayrı `light.turn_on` çağrıları kullanır; bazı ışık türleri için gereklidir. 🔀", + "skip_redundant_commands": "`skip_redundant_commands`: Hedef durumu ışığın bilinen durumu ile aynı olan uyarlama komutlarını atlar. Ağ trafiğini azaltır ve bazı durumlarda uyarlamanın yanıt hızını artırır. 📉 \nFiziksel ışık durumları HA’daki kaydedilen durumla senkronize değilse devre dışı bırakın.", + "intercept": "`intercept`: `light.turn_on` çağrılarını yakalar ve renk ile parlaklığın anında uyarlanmasını sağlar. 🏎️ Renk ve parlaklığı desteklemeyen ışıklar için devre dışı bırakın.", + "multi_light_intercept": "`multi_light_intercept`: Birden fazla ışığı hedefleyen `light.turn_on` çağrılarını yakalar ve uyarlama yapar. ➗⚠️ Bu, örneğin ışıklar farklı anahtarlardaysa tek bir `light.turn_on` çağrısının birden fazla çağrıya bölünmesine yol açabilir. `intercept` etkin olmalıdır.", + "include_config_in_attributes": "`include_config_in_attributes`: `true` olarak ayarlandığında, tüm seçenekleri Home Assistant’ta anahtarın attribute’ları olarak gösterir. 📝" + }, + "data_description": { + "initial_transition": "Işıklar `off` durumundan `on` durumuna geçerken ilk geçişin süresi (saniye cinsinden). ⏲️", + "sleep_rgb_or_color_temp": "Uyku modunda `\"rgb_color\"` veya `\"color_temp\"` kullanın. 🌙", + "sleep_rgb_color": "Uyku modunda RGB renk ( `sleep_rgb_or_color_temp` \"rgb_color\" olarak ayarlandığında kullanılır). 🌈", + "sleep_transition": "“Uyku modu” açılıp kapatıldığında geçiş süresi (saniye cinsinden). 😴", + "sunrise_time": "Gün doğumu için sabit bir saat (SS:DD:YY) belirleyin. 🌅", + "min_sunrise_time": "En erken sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha geç gün doğumlarına izin verir. 🌅", + "max_sunrise_time": "En geç sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha erken gün doğumlarına izin verir. 🌅", + "sunrise_offset": "Gün doğumu saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰", + "sunset_time": "Gün batımı için sabit bir saat (SS:DD:YY) belirleyin. 🌇", + "min_sunset_time": "En erken sanal gün batımı saatini (SS:DD:YY) belirleyin; daha geç gün batımlarına izin verir. 🌇", + "max_sunset_time": "En geç sanal gün batımı saatini (SS:DD:YY) belirleyin; daha erken gün batımlarına izin verir. 🌇", + "sunset_offset": "Gün batımı saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰", + "brightness_mode": "Kullanılacak parlaklık modunu belirtir. Olası değerler: `default`, `linear` ve `tanh` (`brightness_mode_time_dark` ve `brightness_mode_time_light` ayarlarını kullanır). 📈", + "brightness_mode_time_dark": "(`brightness_mode='default'` ise göz ardı edilir) Gün doğumu/gün batımı öncesi/sonrası parlaklığı kademeli olarak artırma/azaltma süresi (saniye cinsinden). 📈📉", + "brightness_mode_time_light": "(`brightness_mode='default'` ise göz ardı edilir) Gün doğumu/gün batımı öncesi/sonrası parlaklığı kademeli olarak artırma/azaltma süresi (saniye cinsinden). 📈📉", + "autoreset_control_seconds": "Manuel kontrolü belirtilen saniye sonunda otomatik olarak sıfırlar. Devre dışı bırakmak için 0 olarak ayarlayın. ⏲️", + "send_split_delay": "Parlaklık ve renk ayarını aynı anda desteklemeyen ışıklar için `separate_turn_on_commands` arasındaki gecikme (ms). ⏲️", + "adapt_delay": "Işık açıldıktan sonra Adaptive Lighting’in değişiklikleri uygulamasına kadar bekleme süresi (saniye cinsinden). Titremeyi önlemeye yardımcı olabilir. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Geçersiz seçenek", + "entity_missing": "Seçilen bir veya birden fazla ışık entity’si Home Assistant’ta bulunamadı." + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Işıkları yalnızca açıkken (`true`) uyarla, veya sürekli olarak uyarlamaya devam et (`false`).🔄" + }, + "sunrise_offset": { + "description": "Gün doğumu saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰" + }, + "sunset_offset": { + "description": "Gün batımı saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰" + }, + "autoreset_control_seconds": { + "description": "Manuel kontrolü belirtilen saniye sonunda otomatik olarak sıfırlar. Devre dışı bırakmak için 0 olarak ayarlayın. ⏲️" + }, + "sleep_brightness": { + "description": "Uyku modundayken ışıkların parlaklık yüzdesi 😴" + }, + "max_color_temp": { + "description": "Kelvin cinsinden en düşük renk sıcaklığı. ❄️" + }, + "sleep_color_temp": { + "description": "Uyku modunda renk sıcaklığı ( `sleep_rgb_or_color_temp` `color_temp` olarak ayarlandığında kullanılır) Kelvin cinsinden. 😴" + }, + "send_split_delay": { + "description": "Parlaklık ve renk ayarını aynı anda desteklemeyen ışıklar için `separate_turn_on_commands` arasındaki gecikme (ms). ⏲️" + }, + "detect_non_ha_changes": { + "description": "`light.turn_on` dışındaki durum değişikliklerini algılar ve uyarlamayı durdurur. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️ \nDikkat: ⚠️ Bazı ışıklar yanlışlıkla “açık” durumunu bildirebilir, bu da ışıkların beklenmedik şekilde açılmasına neden olabilir. Böyle bir durumda bu özelliği devre dışı bırakın." + }, + "take_over_control": { + "description": "Işıklar açıkken ve uyarlanırken başka bir kaynaktan `light.turn_on` çağrılırsa Adaptive Lighting’i devre dışı bırakır. Dikkat: Bu işlem her `interval` süresinde `homeassistant.update_entity` çağrısı yapar! 🔒" + }, + "initial_transition": { + "description": "Işıklar `off` durumundan `on` durumuna geçerken ilk geçişin süresi (saniye cinsinden). ⏲️" + }, + "transition": { + "description": "Işıklar değişirken geçiş süresi (saniye cinsinden). 🕑" + }, + "sleep_transition": { + "description": "“Uyku modu” açılıp kapatıldığında geçiş süresi (saniye cinsinden). 😴" + }, + "entity_id": { + "description": "Anahtarın Entity ID’si. 📝" + }, + "max_brightness": { + "description": "Maksimum parlaklık yüzdesi.💡" + }, + "min_brightness": { + "description": "Minimum parlaklık yüzdesi.💡" + }, + "sleep_rgb_color": { + "description": "Uyku modunda RGB renk ( `sleep_rgb_or_color_temp` \"rgb_color\" olarak ayarlandığında kullanılır). 🌈" + }, + "sunrise_time": { + "description": "Gün doğumu için sabit bir saat (SS:DD:YY) belirleyin. 🌅" + }, + "sunset_time": { + "description": "Gün batımı için sabit bir saat (SS:DD:YY) belirleyin. 🌇" + }, + "use_defaults": { + "description": "Bu servis çağrısında belirtilmeyen varsayılan değerleri ayarlar. Seçenekler: \n- `current` (varsayılan, mevcut değerleri korur) \n- `factory` (belgelendirilmiş varsayılanlara sıfırlar) \n- `configuration` (anahtar yapılandırma varsayılanlarına döner) ⚙️" + }, + "min_sunset_time": { + "description": "En erken sanal gün batımı saatini (SS:DD:YY) belirleyin; daha geç gün batımlarına izin verir. 🌇" + }, + "max_sunrise_time": { + "description": "En geç sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha erken gün doğumlarına izin verir. 🌅" + }, + "include_config_in_attributes": { + "description": "`true` olarak ayarlandığında, tüm seçenekleri Home Assistant’ta anahtarın attribute’ları olarak gösterir. 📝" + }, + "sleep_rgb_or_color_temp": { + "description": "Uyku modunda `\"rgb_color\"` veya `\"color_temp\"` kullanın. 🌙" + }, + "separate_turn_on_commands": { + "description": "Renk ve parlaklık için ayrı `light.turn_on` çağrıları kullanın; bazı ışık türleri için gereklidir. 🔀" + }, + "min_color_temp": { + "description": "En yüksek (sıcak) renk sıcaklığı (Kelvin cinsinden). 🔥" + }, + "prefer_rgb_color": { + "description": "Mümkün olduğunda ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈" + }, + "turn_on_lights": { + "description": "Şu anda kapalı olan ışıkların açılıp açılmayacağını belirler. 🔆" + }, + "adapt_delay": { + "description": "Işık açıldıktan sonra Adaptive Lighting’in değişiklikleri uygulamasına kadar bekleme süresi (saniye cinsinden). Titremeyi önlemeye yardımcı olabilir. ⏲️" + } + }, + "description": "Anahtardaki tüm ayarları dilediğiniz gibi değiştirebilirsiniz. Buradaki seçeneklerin hepsi, yapılandırma akışındakilerle aynıdır." + }, + "apply": { + "fields": { + "lights": { + "description": "Ayarları bir veya birden fazla ışığa uygula.💡" + }, + "transition": { + "description": "Işıklar değişirken geçiş süresi (saniye cinsinden). 🕑" + }, + "entity_id": { + "description": "Uygulanacak ayarların bulunduğu anahtarın `entity_id`’si. 📝" + }, + "adapt_brightness": { + "description": "Işığın parlaklığının uyarlanıp uyarlanmayacağını belirler. 🌞" + }, + "adapt_color": { + "description": "Destekleyen ışıklarda rengin uyarlanıp uyarlanmayacağını belirler. 🌈" + }, + "prefer_rgb_color": { + "description": "Mümkün olduğunda ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈" + }, + "turn_on_lights": { + "description": "Şu anda kapalı olan ışıkların açılıp açılmayacağını belirler. 🔆" + } + }, + "description": "Şu anki Akıllı Işıklandırma ayarlarını ışıklara uygular." + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "Işıkların entity_id(leri). Belirtilmezse, anahtardaki tüm ışıklar seçilir. 💡" + }, + "entity_id": { + "description": "Işığın “manuel olarak kontrol edildiğini” işaretlemek veya kaldırmak için kullanılacak anahtarın `entity_id`’si. 📝" + }, + "manual_control": { + "description": "Işığı “manuel kontrol” listesinden eklemek (`true`) veya çıkarmak (`false`) için kullanılır. 🔒" + } + }, + "description": "Bir ışığın 'manuel olarak kontrol' edilip edilmediğini işaretleyin." + } + }, + "config": { + "step": { + "user": { + "title": "Akıllı ışıklandırma örneği için bir ad seçin.", + "description": "Her örnek birden fazla ışık içerebilir!" + } + }, + "abort": { + "already_configured": "Bu cihaz zaten ayarlanmış." + } + } +} diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json new file mode 100644 index 00000000..f3d66617 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/uk.json @@ -0,0 +1,233 @@ +{ + "title": "Адаптивне освітлення", + "config": { + "step": { + "user": { + "title": "Оберіть ім’я для екземпляра адаптивного освітлення", + "description": "Оберіть ім’я для цього екземпляра. Ви можете мати декілька екземплярів адаптивного освітлення, кожен може містити декілька приладів!", + "data": { + "name": "Ім’я" + } + }, + "menu": { + "data": { + "action": "Дія" + }, + "title": "Створити або дублювати", + "description": "Ви хочете створити новий екземпляр чи скопіювати існуючий?" + } + }, + "abort": { + "already_configured": "Цей пристрій вже налаштовано" + } + }, + "options": { + "step": { + "init": { + "title": "Опції адаптивного освітлення", + "description": "Всі налаштування компонента адаптивного освітлення. Назви опцій відповідають налаштуванням у YAML. Опції не відображаються, якщо ви вже визначили їх у компоненті adaptive_lighting вашої YAML-конфігурації.", + "data": { + "lights": "прилади", + "interval": "interval: Час між оновленнями перемикача. (секунди)", + "transition": "Час переходу, який застосовується до освітлення (секунди)", + "min_brightness": "min_brightness: Найнижча яскравість світла під час циклу. (%)", + "max_brightness": "max_brightness: Найвища яскравість світла під час циклу. (%)", + "min_color_temp": "min_color_temp: Найтепліший відтінок циклу кольорової температури. (%)", + "max_color_temp": "max_color_temp: Найхолодніший відтінок циклу кольорової температури. (Кельвін)", + "sleep_brightness": "sleep_brightness: Налаштування яскравості для Режиму сну. (%)", + "sleep_color_temp": "sleep_color_temp: Температура кольору для Режиму сну. (Кельвін)" + }, + "data_description": { + "interval": "Частота адаптації освітлення, у секундах. 🔄", + "transition": "Тривалість переходу, коли світло змінюється, у секундах. 🕑", + "sleep_brightness": "Відсоток яскравості світла в режимі сну. 😴", + "sleep_color_temp": "Колірна температура в режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення `color_temp`) у Кельвінах. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Коли прилад вимикається (off), вмикається (on), або змінює 'sleep_state'. (секунди)", + "prefer_rgb_color": "prefer_rgb_color: Використовувати 'rgb_color' замість 'color_temp', коли можливо.", + "transition_until_sleep": "transition_until_sleep: Коли активовано, адаптивне освітлення буде ставитись до налаштування сну як мінімум, переходячи до цих значень після заходу сонця. 🌙", + "sunrise_time": "sunrise_time: Ручний перезапис часу сходу сонця, якщо 'None', тоді використовується час сходу сонця у вашій локації (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Як за довго до(-) або після(+) визначати точку сходу сонця для циклу (+/- секунд)", + "sunset_time": "sunset_time: Ручний перезапис часу заходу сонця, якщо 'None', тоді використовується час заходу сонця у вашій локації (HH:MM:SS)", + "sunset_offset": "sunset_offset: Як за довго до(-) або після(+) визначати точку заходу сонця для циклу (+/- секунд)", + "take_over_control": "take_over_control: Якщо що-небудь, окрім Адаптивного освітлення, викликає 'light.turn_on', коли світло вже увімкнено, чи адаптувати освітлення допоки світло (або перемикач) перемкнеться (off -> on).", + "detect_non_ha_changes": "detect_non_ha_changes: виявляти всі зміни >10% до освітлення (включаючи ті, що зроблені поза HA), вимагає, щоб 'take_over_control' був включений (виклик 'homeassistant.update_entity' кожного оновлення 'interval'!)", + "only_once": "only_once: Адаптувати світло лише після початкового увімкнення.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: На початку вмикання світла. Якщо `true`, освітлення адаптується лише якщо `light.turn_on` викликано без вказання кольору чи яскравості. ❌🌈 Це, наприклад, запобігає адаптації, коли сцена активується. Якщо `false`, освітлення адаптується незалежно від наявності кольору чи яскравості у початковому `service_data`. Потребує ввімкнення `take_over_control`. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Окремі команди для кожного атрибута (колір, яскравість, тощо.) в 'light.turn_on' (необхідні для деяких приладів).", + "skip_redundant_commands": "skip_redundant_commands: Пропускати надсилання команд адаптації, цільовий стан яких вже дорівнює відомому стану освітлення. Мінімізує мережевий трафік і покращує швидкість реагування адаптації в деяких ситуаціях. 📉Вимкнути, якщо фізичний стан освітлення не синхронізується із записаним станом HA.", + "intercept": "intercept: Перехоплювати та адаптувати виклики увімкнення світла (`light.turn_on`), щоб увімкнути миттєву адаптацію кольору та яскравості. 🏎️ Вимкніть для світла, що не підтримує увімкнення світла (`light.turn_on`) з кольором та яскравістю.", + "multi_light_intercept": "multi_light_intercept: Перехоплення та адаптація викликів `light.turn_on`, які спрямовані на кілька світильників. ➗⚠️ Це може призвести до розділення одного виклику `light.turn_on` на кілька викликів, наприклад, коли світильники підключені до різних вимикачів. Потрібно ввімкнути `intercept`.", + "include_config_in_attributes": "Включити конфігурацію в атрибути (`include_config_in_attributes`): Показувати всі опції як атрибути на перемикачі в Home Assistant, якщо встановлено значення `true`. 📝" + }, + "data_description": { + "initial_transition": "Тривалість першого переходу, коли світло перемикається зі стану вимкнено `off` на увімкнено `on`, у секундах. ⏲️", + "sleep_rgb_or_color_temp": "Використовуйте `\"rgb_color\"` або `\"color_temp\"` у режимі сну. 🌙", + "sleep_rgb_color": "Колір RGB у режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення \"rgb_color\"). 🌈", + "sleep_transition": "Тривалість переходу, коли режим сну \"sleep mode\" увімкнено, у секундах. 😴", + "sunrise_time": "Встановіть фіксований час (ГГ:ХХ:СС) для сходу сонця. 🌅", + "min_sunrise_time": "Встановіть найраніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи пізніші сходи. 🌅", + "max_sunrise_time": "Встановіть найпізніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи більш ранні сходи сонця. 🌅", + "sunrise_offset": "Змінити час сходу сонця на +/- секунд. ⏰", + "sunset_time": "Встановіть фіксований час (ГГ:ХХ:СС) для заходу сонця. 🌇", + "min_sunset_time": "Встановіть найраніший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи пізніші заходи сонця. 🌇", + "max_sunset_time": "Встановіть найновіший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи більш ранні заходи сонця. 🌇", + "sunset_offset": "Змінити час заходу сонця на +/- секунд. ⏰", + "brightness_mode": "Режим яскравості для використання. Можливі значення: default (стандартний) , linear (лінійний) та tanh (гіперболічний тангенс) (використовує значення brightness_mode_time_dark та brightness_mode_time_light).", + "brightness_mode_time_dark": "(Ігнорується, якщо `brightness_mode='default'`) Тривалість у секундах для збільшення/зменшення яскравості до/після сходу/заходу сонця. 📈📉", + "brightness_mode_time_light": "(Ігнорується, якщо brightness_mode='default') Тривалість у секундах для збільшення/зменшення яскравості після/до сходу/заходу сонця. 📈📉.", + "take_over_control_mode": "Режим призупинення адаптації, коли інші джерела змінюють яскравість та/або колір світла. `pause_all` завжди призупиняє адаптацію як яскравості, так і кольору. `pause_changed` призупиняє адаптацію лише змінених атрибутів та продовжує адаптацію незмінних атрибутів, наприклад, продовжує адаптацію кольору, коли змінювалася лише яскравість.", + "autoreset_control_seconds": "Самочинно скидати ручне керування після кількох секунд. Встановіть 0, щоб вимкнути.", + "send_split_delay": "Затримка (мс) між `separate_turn_on_commands` (окремі команди увімкнення) для світла, що не підтримує одночасне налаштування яскравості та кольору. ⏲️", + "adapt_delay": "Час очікування (секунди) між увімкненням світла та застосуванням змін системою адаптивного освітлення. Може допомогти уникнути мерехтіння. ⏲️" + } + } + } + } + }, + "error": { + "option_error": "Хибна опція", + "entity_missing": "Вибраного світла в домашньому помічнику не знайшли" + } + }, + "services": { + "apply": { + "description": "Застосовує поточні налаштування Адаптивного освітлення до світильників.", + "fields": { + "lights": { + "description": "Світильник (або список світильників), до яких буде застосовано налаштування. 💡" + }, + "transition": { + "description": "Тривалість переходу, коли світло змінюється, у секундах. 🕑" + }, + "entity_id": { + "description": "`entity_id` перемикача з налаштуваннями, які потрібно застосувати. 📝" + }, + "adapt_brightness": { + "description": "Чи потрібно адаптувати яскравість світла. 🌞" + }, + "adapt_color": { + "description": "Чи адаптувати колір допоміжних ламп. 🌈" + }, + "prefer_rgb_color": { + "description": "Чи надавати перевагу налаштуванню кольору RGB над температурою кольору світла, коли це можливо. 🌈" + }, + "turn_on_lights": { + "description": "Чи вмикати світло, яке наразі вимкнене. 🔆" + } + } + }, + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "Змінити час сходу сонця на +/- секунд. ⏰" + }, + "sunset_offset": { + "description": "Змінити час заходу сонця на +/- секунд. ⏰" + }, + "autoreset_control_seconds": { + "description": "Самочинно скидати ручне керування після кількох секунд. Встановіть 0, щоб вимкнути." + }, + "only_once": { + "description": "Приладжувати освітлення тільки тоді, коли воно ввімкнено (`true`) чи продовжувати завжди (`false`). 🔄" + }, + "sleep_brightness": { + "description": "Відсоток яскравості світла в режимі сну. 😴" + }, + "take_over_control": { + "description": "Вимкнути Адаптивне освітлення, якщо інше джерело викликає `light.turn_on`, коли світло увімкнене та адаптується. Зауважте, що це викликає `homeassistant.update_entity` щокожного заданого інтервалу `interval`! 🔒" + }, + "entity_id": { + "description": "Ідентифікатор (ID) перемикача. 📝" + }, + "initial_transition": { + "description": "Тривалість першого переходу, коли світло перемикається зі стану вимкнено `off` на увімкнено `on`, у секундах. ⏲️" + }, + "sleep_transition": { + "description": "Тривалість переходу, коли режим сну \"sleep mode\" увімкнено, у секундах. 😴" + }, + "max_color_temp": { + "description": "Найхолодніша колірна температура в Кельвінах. ❄️" + }, + "max_brightness": { + "description": "Максимальний відсоток яскравості. 💡" + }, + "min_brightness": { + "description": "Мінімальний відсоток яскравості. 💡" + }, + "send_split_delay": { + "description": "Затримка (мс) між `separate_turn_on_commands` (окремі команди увімкнення) для світла, що не підтримує одночасне налаштування яскравості та кольору. ⏲️" + }, + "sleep_color_temp": { + "description": "Колірна температура в режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення `color_temp`) в Кельвінах. 😴" + }, + "detect_non_ha_changes": { + "description": "Виявляє та припиняє адаптації для змін стану, що не є `light.turn_on`. Потребує увімкнення `take_over_control`. 🕵️ Обережно: ⚠️ Деякі лампи можуть хибно вказувати на стан \"увімкнено\", що може призвести до неочікуваного ввімкнення ламп. Вимкніть цю функцію, якщо ви зіткнетеся з такими проблемами." + }, + "transition": { + "description": "Тривалість переходу, коли світло змінюється, у секундах. 🕑" + }, + "sleep_rgb_color": { + "description": "Колір RGB у режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення \"rgb_color\"). 🌈" + }, + "sunrise_time": { + "description": "Встановіть фіксований час (ГГ:ХХ:СС) для сходу сонця. 🌅" + }, + "sunset_time": { + "description": "Встановіть фіксований час (ГГ:ХХ:СС) для заходу сонця. 🌇" + }, + "use_defaults": { + "description": "Встановлює значення за замовчуванням, не вказані в цьому виклику служби. Параметри: «current» (за замовчуванням, зберігає поточні значення), «factory» (скидає до задокументованих значень за замовчуванням) або «configuration» (повертає до значень за замовчуванням конфігурації комутатора). ⚙️" + }, + "min_sunset_time": { + "description": "Встановіть найраніший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи пізніші заходи сонця. 🌇" + }, + "max_sunrise_time": { + "description": "Встановіть найпізніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи більш ранні сходи сонця. 🌅" + }, + "include_config_in_attributes": { + "description": "Показувати всі опції як атрибути перемикача в Домашньому помічнику, якщо встановлено значення `true`. 📝" + }, + "sleep_rgb_or_color_temp": { + "description": "Використовуйте `\"rgb_color\"` або `\"color_temp\"` у режимі сну. 🌙" + }, + "separate_turn_on_commands": { + "description": "Використовуйте окремі виклики `light.turn_on` для кольору та яскравості, що необхідно для деяких типів освітлення. 🔀" + }, + "adapt_delay": { + "description": "Час очікування (секунди) між увімкненням світла та застосуванням змін системою адаптивного освітлення. Може допомогти уникнути мерехтіння. ⏲️" + }, + "min_color_temp": { + "description": "Найтепліша колірна температура в Кельвінах. 🔥" + }, + "prefer_rgb_color": { + "description": "Чи надавати перевагу налаштуванню кольору RGB над температурою кольору світла, коли це можливо. 🌈" + }, + "turn_on_lights": { + "description": "Чи вмикати світло, яке наразі вимкнене. 🔆" + }, + "take_over_control_mode": { + "description": "Режим призупинення адаптації, коли інші джерела змінюють яскравість та/або колір світла. `pause_all` завжди призупиняє адаптацію як яскравості, так і кольору. `pause_changed` призупиняє адаптацію лише змінених атрибутів та продовжує адаптацію незмінних атрибутів, наприклад, продовжує адаптацію кольору, коли змінювалася лише яскравість." + } + }, + "description": "Змініть будь-які налаштування, які ви бажаєте, у цьому перемикачі. Усі опції тут такі ж, як і в поточному конфігураційному файлі." + }, + "set_manual_control": { + "description": "Позначте, чи світло \"керується вручну\".", + "fields": { + "lights": { + "description": "Ідентифікатор(и) світла (entity_id(s) of lights). Якщо не вказано, вибираються всі лампи у перемикачі. 💡" + }, + "entity_id": { + "description": "`entity_id` перемикача, в якому потрібно (зняти) позначку світла як `керованого вручну`. 📝" + }, + "manual_control": { + "description": "Додавати (\"true\") чи видаляти (\"false\") світло зі списку \"manual_control\". 🔒" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ur.json b/custom_components/adaptive_lighting/translations/ur.json new file mode 100644 index 00000000..f61e47b1 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ur.json @@ -0,0 +1,210 @@ +{ + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "نیند کے موڈ میں روشنی کی چمک کا فیصد. 😴" + }, + "detect_non_ha_changes": { + "description": "غیر light.turn_on ریاست کی تبدیلیوں کے لئے موافقت کا پتہ لگاتا ہے اور روکتا ہے۔ 'take_over_control' کو فعال کرنے کی ضرورت ہے۔ 🕵️ احتیاط: ⚠️ کچھ لائٹس غلط طور پر 'آن' حالت کی نشاندہی کر سکتی ہیں ، جس کے نتیجے میں لائٹس غیر متوقع طور پر آن ہوسکتی ہیں۔ اگر آپ کو اس طرح کے مسائل کا سامنا کرنا پڑتا ہے تو اس خصوصیت کو غیر فعال کریں۔" + }, + "sunrise_offset": { + "description": "طلوع آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰" + }, + "max_sunrise_time": { + "description": "تازہ ترین مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل طلوع آفتاب کی اجازت ملتی ہے۔ 🌅" + }, + "sleep_color_temp": { + "description": "کیلون میں نیند کے موڈ میں رنگ کا درجہ حرارت (جب 'sleep_rgb_or_color_temp' 'color_temp' ہوتا ہے) میں استعمال ہوتا ہے۔ 😴" + }, + "min_brightness": { + "description": "کم سے کم چمک کا فیصد. 💡" + }, + "min_color_temp": { + "description": "کیلون میں گرم ترین رنگ کا درجہ حرارت. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "نیند کے موڈ میں \"rgb_color\" یا \"color_temp\" کا استعمال کریں۔ 🌙" + }, + "turn_on_lights": { + "description": "کیا ان لائٹس کو آن کرنا ہے جو فی الحال بند ہیں۔ 🔆" + }, + "initial_transition": { + "description": "پہلی منتقلی کا دورانیہ جب لائٹس سیکنڈوں میں 'بند' سے 'آن' میں تبدیل ہوجاتی ہیں۔ ⏲️" + }, + "entity_id": { + "description": "سوئچ کی اینٹیٹی آئی ڈی۔ 📝" + }, + "sunrise_time": { + "description": "طلوع آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌅" + }, + "include_config_in_attributes": { + "description": "'سچ' پر سیٹ ہونے پر ہوم اسسٹنٹ میں سوئچ پر تمام اختیارات بطور خصوصیات دکھائیں۔ 📝" + }, + "max_brightness": { + "description": "زیادہ سے زیادہ چمک کا فیصد. 💡" + }, + "sleep_rgb_color": { + "description": "نیند کے موڈ میں آر جی بی رنگ (جب 'sleep_rgb_or_color_temp' \"rgb_color\" ہوتا ہے تو استعمال کیا جاتا ہے). 🌈" + }, + "take_over_control": { + "description": "اگر کوئی دوسرا ذریعہ 'light.turn_on' کا نام دیتا ہے تو ایڈاپٹو لائٹنگ کو غیر فعال کریں جب لائٹس آن ہیں اور اسے اپنایا جارہا ہے۔ نوٹ کریں کہ یہ ہر 'وقفے' کو 'homeassistant.update_entity' کہتا ہے! 🔒" + }, + "sleep_transition": { + "description": "منتقلی کا دورانیہ جب \"نیند کا موڈ\" سیکنڈوں میں طے کیا جاتا ہے۔ 😴" + }, + "autoreset_control_seconds": { + "description": "کئی سیکنڈ کے بعد دستی کنٹرول کو خود بخود ری سیٹ کریں۔ غیر فعال کرنے کے لئے 0 پر سیٹ کریں۔ ⏲️" + }, + "adapt_delay": { + "description": "لائٹ آن ہونے اور ایڈاپٹو لائٹنگ کے درمیان انتظار کا وقت (سیکنڈ) تبدیلیاں لاگو کرتا ہے۔ جھلکنے سے بچنے میں مدد مل سکتی ہے۔ ⏲️" + }, + "only_once": { + "description": "روشنیوں کو صرف اس وقت ڈھالیں جب وہ آن ہوں (`سچ`) یا انہیں ڈھالتے رہیں (`غلط`)۔ 🔄" + }, + "use_defaults": { + "description": "اس سروس کال میں متعین نہ ہونے والی ڈیفالٹ اقدار سیٹ کرتا ہے۔ اختیارات: \"موجودہ\" (ڈیفالٹ، موجودہ اقدار کو برقرار رکھتا ہے)، \"فیکٹری\" (دستاویزی ڈیفالٹ میں ری سیٹ) ، یا \"کنفیگریشن\" (سوئچ کنفگ ڈیفالٹس پر واپس آجاتا ہے)۔ ⚙️" + }, + "separate_turn_on_commands": { + "description": "رنگ اور چمک کے لئے علیحدہ 'light.turn_on' کا استعمال کریں ، جو کچھ روشنی کی اقسام کے لئے ضروری ہے۔ 🔀" + }, + "prefer_rgb_color": { + "description": "جب ممکن ہو تو روشنی کے رنگ کے درجہ حرارت پر آر جی بی رنگ ایڈجسٹمنٹ کو ترجیح دیں یا نہیں۔ 🌈" + }, + "max_color_temp": { + "description": "کیلون میں سرد ترین رنگ کا درجہ حرارت. ❄️" + }, + "sunset_offset": { + "description": "غروب آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰" + }, + "send_split_delay": { + "description": "ان روشنیوں کے لئے 'separate_turn_on_commands' کے درمیان تاخیر (ایم ایس) جو بیک وقت چمک اور رنگ کی ترتیب کی حمایت نہیں کرتی ہیں۔ ⏲️" + }, + "sunset_time": { + "description": "غروب آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌇" + }, + "transition": { + "description": "جب روشنیاں تبدیل ہوتی ہیں تو منتقلی کا دورانیہ ، سیکنڈوں میں۔ 🕑" + }, + "min_sunset_time": { + "description": "سب سے پہلے مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں غروب آفتاب کی اجازت ملتی ہے۔ 🌇" + } + }, + "description": "سوئچ میں آپ جو بھی ترتیبات چاہتے ہیں اسے تبدیل کریں۔ یہاں تمام اختیارات وہی ہیں جو کنفگ بہاؤ میں ہیں۔" + }, + "apply": { + "fields": { + "entity_id": { + "description": "لاگو کرنے کے لئے ترتیبات کے ساتھ سوئچ کا 'entity_id'۔ 📝" + }, + "adapt_brightness": { + "description": "کیا روشنی کی چمک کو ڈھالنا ہے۔ 🌞" + }, + "turn_on_lights": { + "description": "کیا ان لائٹس کو آن کرنا ہے جو فی الحال بند ہیں۔ 🔆" + }, + "adapt_color": { + "description": "کیا معاون لائٹس پر رنگ کو ڈھالنا ہے۔ 🌈" + }, + "prefer_rgb_color": { + "description": "جب ممکن ہو تو روشنی کے رنگ کے درجہ حرارت پر آر جی بی رنگ ایڈجسٹمنٹ کو ترجیح دیں یا نہیں۔ 🌈" + }, + "lights": { + "description": "ترتیبات کو لاگو کرنے کے لیے روشنی (یا لائٹس کی فہرست)۔ 💡" + }, + "transition": { + "description": "جب روشنیاں تبدیل ہوتی ہیں تو منتقلی کا دورانیہ ، سیکنڈوں میں۔ 🕑" + } + }, + "description": "روشنیوں پر موجودہ مطابقت پذیر روشنی کی ترتیبات کا اطلاق ہوتا ہے۔" + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "کیا روشنی کو \"manual_control\" کی فہرست سے شامل کرنا ہے (\"سچ\") یا (\"غلط\") ہٹانا ہے۔ 🔒" + }, + "entity_id": { + "description": "سوئچ کا 'entity_id' جس میں روشنی کو 'دستی طور پر کنٹرول' کے طور پر نشان زد کرنا ہے۔ 📝" + }, + "lights": { + "description": "لائٹس کے entity_id ، اگر واضح نہیں ہیں تو ، سوئچ میں موجود تمام لائٹس منتخب کی جاتی ہیں۔ 💡" + } + }, + "description": "نشان لگائیں کہ آیا روشنی کو 'دستی طور پر کنٹرول' کیا جاتا ہے۔" + } + }, + "options": { + "step": { + "init": { + "data_description": { + "interval": "روشنیوں کو سیکنڈوں میں ڈھالنے کی فریکوئنسی۔ 🔄", + "transition": "جب روشنیاں تبدیل ہوتی ہیں تو منتقلی کا دورانیہ ، سیکنڈوں میں۔ 🕑", + "sleep_brightness": "نیند کے موڈ میں روشنی کی چمک کا فیصد. 😴", + "sleep_color_temp": "کیلون میں نیند کے موڈ میں رنگ کا درجہ حرارت (جب 'sleep_rgb_or_color_temp' 'color_temp' ہوتا ہے) میں استعمال ہوتا ہے۔ 😴" + }, + "data": { + "lights": "لائٹس: کنٹرول کی جانے والی روشنی کے entity_ids کی فہرست (خالی ہوسکتی ہے). 🌟", + "min_brightness": "min_brightness: کم سے کم چمک کا فیصد. 💡", + "max_brightness": "max_brightness: زیادہ سے زیادہ چمک کا فیصد. 💡", + "min_color_temp": "min_color_temp: کیلون میں گرم ترین رنگ کا درجہ حرارت. 🔥", + "max_color_temp": "max_color_temp: کیلون میں سرد ترین رنگ کا درجہ حرارت۔ ❄️" + }, + "title": "مطابقت پذیر روشنی کے اختیارات", + "description": "ایک مطابقت پذیر لائٹنگ جزو تشکیل دیں۔ آپشن کے نام YAML کی ترتیبات کے ساتھ مطابقت رکھتے ہیں۔ اگر آپ نے YAML میں اس اندراج کی وضاحت کی ہے تو ، یہاں کوئی آپشن ظاہر نہیں ہوگا۔ انٹرایکٹو گراف کے لئے جو پیرامیٹر کے اثرات کو ظاہر کرتے ہیں ، ملاحظہ کریں [اس ویب ایپ]({webapp_url})۔ مزید تفصیلات کے لئے ، [سرکاری دستاویزات]({docs_url}) ملاحظہ کریں۔", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: جب ممکن ہو تو روشنی کے رنگ کے درجہ حرارت پر آر جی بی رنگ ایڈجسٹمنٹ کو ترجیح دیں یا نہیں۔ 🌈", + "transition_until_sleep": "transition_until_sleep: جب فعال کیا جاتا ہے تو ، ایڈاپٹو لائٹنگ نیند کی ترتیبات کو کم سے کم تصور کرے گی ، غروب آفتاب کے بعد ان اقدار میں منتقل ہوگی۔ 🌙", + "take_over_control": "take_over_control: اگر کوئی دوسرا ذریعہ 'light.turn_on' کا نام دیتا ہے تو ایڈاپٹو لائٹنگ کو غیر فعال کریں جب لائٹس آن ہیں اور اسے اپنایا جارہا ہے۔ نوٹ کریں کہ یہ ہر 'وقفے' کو 'homeassistant.update_entity' کہتا ہے! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: غیر light.turn_on ریاست کی تبدیلیوں کے لئے موافقت کا پتہ لگاتا ہے اور روکتا ہے۔ 'take_over_control' کو فعال کرنے کی ضرورت ہے۔ 🕵️ احتیاط: ⚠️ کچھ لائٹس غلط طور پر 'آن' حالت کی نشاندہی کر سکتی ہیں ، جس کے نتیجے میں لائٹس غیر متوقع طور پر آن ہوسکتی ہیں۔ اگر آپ کو اس طرح کے مسائل کا سامنا کرنا پڑتا ہے تو اس خصوصیت کو غیر فعال کریں۔", + "only_once": "only_once: لائٹس کو صرف اس وقت ڈھالیں جب وہ آن ہوں ('سچ') یا انہیں اپناتے رہیں ('جھوٹ')۔ 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: شروع میں لائٹس آن کرتے وقت۔ اگر 'true' پر سیٹ کیا جاتا ہے، AL صرف اس صورت میں موافق ہوتا ہے جب رنگ یا چمک کی وضاحت کیے بغیر 'light.turn_on' کو مدعو کیا جاتا ہے۔ ❌🌈 یہ مثال کے طور پر، کسی منظر کو چالو کرتے وقت موافقت کو روکتا ہے۔ اگر 'غلط'، AL ابتدائی `سروس_ڈیٹا` میں رنگ یا چمک کی موجودگی سے قطع نظر موافقت کرتا ہے۔ 'ٹیک_اوور_کنٹرول' کو فعال کرنے کی ضرورت ہے۔ 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: رنگ اور چمک کے لئے الگ الگ 'light.turn_on' کا استعمال کریں، جو کچھ روشنی کی اقسام کے لئے ضروری ہے. 🔀", + "skip_redundant_commands": "skip_redundant_commands: موافقت کے احکامات بھیجنے سے گریز کریں جن کی ہدف کی حالت پہلے سے ہی روشنی کی معلوم حالت کے برابر ہے۔ نیٹ ورک ٹریفک کو کم سے کم کرتا ہے اور کچھ حالات میں موافقت کی ذمہ داری کو بہتر بناتا ہے۔ 📉اگر جسمانی روشنی کی حالت یں ایچ اے کی ریکارڈ شدہ حالت کے ساتھ مطابقت سے باہر ہوجاتی ہیں تو غیر فعال کریں۔", + "intercept": "انٹرسیپٹ: 'light.turn_on' کالز کو فوری طور پر رنگ اور چمک کے مطابقت پذیری کو قابل بنانے کے لئے روکیں اور اپنائیں۔ 🏎️ ایسی روشنیوں کو غیر فعال کریں جو رنگ اور چمک کے ساتھ 'light.turn_on' کی حمایت نہیں کرتی ہیں۔", + "multi_light_intercept": "multi_light_intercept: 'light.turn_on' کالز کو روکیں اور ان کے مطابق ڈھالیں جو متعدد روشنیوں کو نشانہ بناتی ہیں۔ ➗⚠️ اس کے نتیجے میں ایک ہی 'light.turn_on' کال کو متعدد کالز میں تقسیم کیا جاسکتا ہے ، مثال کے طور پر ، جب لائٹس مختلف سوئچوں میں ہوتی ہیں۔ 'انٹرسیپٹ' کو فعال کرنے کی ضرورت ہے۔", + "include_config_in_attributes": "include_config_in_attributes: 'سچ' پر سیٹ ہونے پر ہوم اسسٹنٹ میں سوئچ پر خصوصیات کے طور پر تمام اختیارات دکھائیں۔ 📝" + }, + "data_description": { + "initial_transition": "پہلی منتقلی کا دورانیہ جب لائٹس سیکنڈوں میں 'بند' سے 'آن' میں تبدیل ہوجاتی ہیں۔ ⏲️", + "sleep_rgb_or_color_temp": "نیند کے موڈ میں \"rgb_color\" یا \"color_temp\" کا استعمال کریں۔ 🌙", + "sleep_rgb_color": "نیند کے موڈ میں آر جی بی رنگ (جب 'sleep_rgb_or_color_temp' \"rgb_color\" ہوتا ہے تو استعمال کیا جاتا ہے). 🌈", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sunrise_time": "طلوع آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌅", + "min_sunrise_time": "ابتدائی مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں طلوع آفتاب کی اجازت ملتی ہے۔ 🌅", + "max_sunrise_time": "تازہ ترین مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل طلوع آفتاب کی اجازت ملتی ہے۔ 🌅", + "sunrise_offset": "طلوع آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰", + "sunset_time": "غروب آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌇", + "min_sunset_time": "سب سے پہلے مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں غروب آفتاب کی اجازت ملتی ہے۔ 🌇", + "max_sunset_time": "تازہ ترین مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل غروب آفتاب کی اجازت ملتی ہے۔ 🌇", + "sunset_offset": "غروب آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰", + "brightness_mode": "استعمال کرنے کے لئے چمک کا موڈ۔ ممکنہ قدریں 'ڈیفالٹ'، 'لکیری' اور 'تن' ہیں ('brightness_mode_time_dark' اور 'brightness_mode_time_light' کا استعمال کرتی ہیں)۔ 📈", + "brightness_mode_time_dark": "(اگر 'brightness_mode='ڈیفالٹ') سورج طلوع ہونے سے پہلے / غروب آفتاب سے پہلے / بعد میں چمک کو بڑھانے کے لئے سیکنڈ میں دورانیہ۔ 📈📉", + "brightness_mode_time_light": "(اگر 'brightness_mode='ڈیفالٹ') سورج طلوع ہونے کے بعد / اس سے پہلے / غروب آفتاب سے پہلے چمک کو بڑھانے کے لئے سیکنڈوں میں دورانیہ۔ 📈📉.", + "autoreset_control_seconds": "کئی سیکنڈ کے بعد دستی کنٹرول کو خود بخود ری سیٹ کریں۔ غیر فعال کرنے کے لئے 0 پر سیٹ کریں۔ ⏲️", + "send_split_delay": "ان روشنیوں کے لئے 'separate_turn_on_commands' کے درمیان تاخیر (ایم ایس) جو بیک وقت چمک اور رنگ کی ترتیب کی حمایت نہیں کرتی ہیں۔ ⏲️", + "adapt_delay": "لائٹ آن ہونے اور ایڈاپٹو لائٹنگ کے درمیان انتظار کا وقت (سیکنڈ) تبدیلیاں لاگو کرتا ہے۔ جھلکنے سے بچنے میں مدد مل سکتی ہے۔ ⏲️" + } + } + } + } + }, + "error": { + "option_error": "غیر قانونی آپشن", + "entity_missing": "ہوم اسسٹنٹ سے ایک یا ایک سے زیادہ منتخب لائٹ ادارے غائب ہیں" + } + }, + "title": "مطابقت پذیر روشنی", + "config": { + "step": { + "user": { + "description": "ہر مثال میں متعدد روشنیاں ہوسکتی ہیں!", + "title": "ایڈاپٹو لائٹنگ مثال کے لئے ایک نام منتخب کریں" + } + }, + "abort": { + "already_configured": "یہ آلہ پہلے ہی تشکیل دیا گیا ہے" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/zh-Hans.json b/custom_components/adaptive_lighting/translations/zh-Hans.json new file mode 100644 index 00000000..1cebfdd0 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/zh-Hans.json @@ -0,0 +1,277 @@ +{ + "title": "自适应照明", + "config": { + "step": { + "user": { + "title": "为自适应照明实例选择一个名称", + "description": "每个实例可以包含多个灯光!", + "data": { + "name": "名称" + } + } + }, + "abort": { + "already_configured": "此设备已配置" + } + }, + "options": { + "step": { + "init": { + "title": "自适应照明选项", + "description": "配置自适应照明组件。选项名称与YAML设置对齐。如果在YAML中定义了此条目,则此处不会显示任何选项。有关演示参数影响的交互式图表,请访问[此Web应用程序]({webapp_url})。有关更多详细信息,请参阅[官方文档]({docs_url})。", + "data": { + "lights": "lights:要控制的灯光实体ID列表(可以为空)。🌟", + "interval": "频率(interval)", + "transition": "过渡(transition)", + "min_brightness": "min_brightness:最小亮度百分比。💡", + "max_brightness": "max_brightness:最大亮度百分比。💡", + "min_color_temp": "min_color_temp:最暖的色温,以开尔文为单位。🔥", + "max_color_temp": "max_color_temp:最冷的色温,以开尔文为单位。❄️", + "sleep_brightness": "睡眠模式亮度(sleep_brightness)", + "sleep_color_temp": "睡眠模式中的色温(sleep_color_temp)" + }, + "data_description": { + "interval": "调整灯光的频率,以秒为单位。🔄", + "transition": "灯光变化时的过渡持续时间,以秒为单位。🕑", + "sleep_brightness": "睡眠模式中的亮度百分比。😴", + "sleep_color_temp": "睡眠模式中的色温(当`sleep_rgb_or_color_temp`为`color_temp`时使用),以开尔文为单位。😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "初始过渡(initial_transition)", + "prefer_rgb_color": "prefer_rgb_color:在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈", + "sleep_rgb_or_color_temp": "睡眠模式RGB或色温(sleep_rgb_or_color_temp)", + "sleep_rgb_color": "睡眠模式中的RGB颜色(sleep_rgb_color)", + "sleep_transition": "睡眠模式过渡时间(sleep_transition)", + "transition_until_sleep": "transition_until_sleep:启用时,自适应照明将将睡眠设置视为最小值,在日落后过渡到这些值。🌙", + "sunrise_time": "日出时间(sunrise_time)", + "min_sunrise_time": "最早日出时间(min_sunrise_time)", + "max_sunrise_time": "最晚日出时间(max_sunrise_time)", + "sunrise_offset": "日出时间偏移(sunrise_offset)", + "sunset_time": "日落时间(sunset_time)", + "min_sunset_time": "最早日落时间(min_sunset_time)", + "max_sunset_time": "最晚日落时间(max_sunset_time)", + "sunset_offset": "日落时间偏移(sunset_offset)", + "brightness_mode": "亮度模式(brightness_mode)", + "brightness_mode_time_dark": "变暗时间(brightness_mode_time_dark)", + "brightness_mode_time_light": "变亮时间(brightness_mode_time_light)", + "take_over_control": "take_over_control: 如果在灯光处于开启并处于适应照明的状态时,另一个来源调用`light.turn_on`,则禁用自适应照明。请注意,这会在每个`interval`调用`homeassistant.update_entity`!🔒", + "detect_non_ha_changes": "detect_non_ha_changes: 检测非`light.turn_on`的状态更改,并停止自适应照明。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能会导致灯光意外打开。如果遇到此类问题,请禁用此功能。", + "autoreset_control_seconds": "自动重置时间(autoreset_control_seconds)", + "only_once": "only_once:仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on:当首次打开灯光时。如果设置为`true`,仅在没有指定颜色或亮度的情况下,AL才进行适应。❌🌈 例如,这可以防止在激活场景时进行适应。如果为`false`,则不考虑初始`service_data`中是否存在颜色或亮度,AL都会适应。需要启用`take_over_control`。🕵️", + "separate_turn_on_commands": "separate_turn_on_commands:为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀", + "send_split_delay": "指令发送间隔延迟(send_split_delay)", + "adapt_delay": "自适应照明延迟(adapt_delay)", + "skip_redundant_commands": "skip_redundant_commands:跳过目标状态已经等于灯光已知状态的自适应命令。在某些情况下,可以减少网络流量并提高适应响应性。📉如果物理灯光状态与HA的记录状态不同步,请禁用此功能。", + "intercept": "intercept:拦截并适应`light.turn_on`调用,以实现即时的颜色和亮度适应。🏎️ 对于不支持使用颜色和亮度进行`light.turn_on`的灯光,禁用此功能。", + "multi_light_intercept": "multi_light_intercept:拦截和适应针对多个灯光的`light.turn_on`调用。➗⚠️ 这可能会将单个`light.turn_on`调用拆分为多个调用,例如当灯光位于不同的开关中时。需要启用`intercept`。", + "include_config_in_attributes": "include_config_in_attributes:在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝" + }, + "data_description": { + "initial_transition": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️", + "sleep_rgb_or_color_temp": "在睡眠模式中使用“rgb_color”或“color_temp”。🌙", + "sleep_rgb_color": "睡眠模式中的RGB颜色(当`sleep_rgb_or_color_temp`为“rgb_color”时使用)。🌈", + "sleep_transition": "切换“睡眠模式”时的过渡持续时间,以秒为单位。😴", + "sunrise_time": "设置固定的日出时间(HH:MM:SS)。🌅", + "min_sunrise_time": "设置最早的虚拟日出时间(HH:MM:SS),允许更晚的日出。🌅", + "max_sunrise_time": "设置最晚的虚拟日出时间(HH:MM:SS),允许更早的日出。🌅", + "sunrise_offset": "以秒为单位的正负偏移调整日出时间。⏰", + "sunset_time": "设置固定的日落时间(HH:MM:SS)。🌇", + "min_sunset_time": "设置最早的虚拟日落时间(HH:MM:SS),允许更晚的日落。🌇", + "max_sunset_time": "设置最晚的虚拟日落时间(HH:MM:SS),允许更早的日落。🌇", + "sunset_offset": "以秒为单位的正负偏移调整日落时间。⏰", + "brightness_mode": "要使用的亮度模式。可能的值为`default`、`linear`和`tanh`(使用`brightness_mode_time_dark`和`brightness_mode_time_light`)。📈", + "brightness_mode_time_dark": "(如果`brightness_mode='default'`将被忽略)日出/日落之前/之后亮度逐渐增加/减少的持续时间,以秒为单位。📈📉", + "brightness_mode_time_light": "(如果`brightness_mode='default'`将被忽略)日出/日落之后/之前亮度逐渐增加/减少的持续时间,以秒为单位。📈📉。", + "autoreset_control_seconds": "在若干秒后自动重置手动控制。设置为0以禁用。⏲️", + "send_split_delay": "对于不支持同时设置亮度和颜色的灯光,`separate_turn_on_commands`之间的延迟时间(毫秒)。⏲️", + "adapt_delay": "灯光打开和自适应照明应用更改之间的等待时间(秒)。可能有助于避免闪烁。⏲️" + } + } + } + } + }, + "error": { + "option_error": "无效的选项", + "entity_missing": "一个或多个选择的灯光实体在Home Assistant中不存在" + } + }, + "services": { + "apply": { + "name": "应用", + "description": "将当前自适应照明设置应用于灯光。", + "fields": { + "entity_id": { + "description": "具有要应用设置的开关的`entity_id`。📝", + "name": "entity_id" + }, + "lights": { + "description": "要应用设置的灯光(或灯光列表)。💡", + "name": "lights" + }, + "transition": { + "description": "灯光变化时的过渡持续时间,以秒为单位。🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "是否调整灯光的亮度。🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "是否在支持的灯光上调整颜色。🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "是否打开当前关闭的灯光。🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "设置手动控制", + "description": "标记灯光是否为'手动控制'。", + "fields": { + "entity_id": { + "description": "要在其中(取消)标记灯光为“手动控制”的开关的`entity_id`。📝", + "name": "entity_id" + }, + "lights": { + "description": "如果未指定,则为灯光的entity_id(s),如果未指定,则选择开关中的所有灯光。💡", + "name": "lights" + }, + "manual_control": { + "description": "是否将灯光从“手动控制”列表中添加(“true”)或删除(“false”)。🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "更改开关设置", + "description": "在开关中更改您想要的任何设置。此处的所有选项与配置流中的选项相同。", + "fields": { + "entity_id": { + "description": "开关的实体ID。📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "设置未在此服务调用中指定的默认值。选项:“current”(默认值,保留当前值)、“factory”(重置为文档默认值)或“configuration”(恢复到开关配置默认值)。⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "是否打开当前关闭的灯光。🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "切换“睡眠模式”时的过渡持续时间,以秒为单位。😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "最大亮度百分比。💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "最低的色温,以开尔文为单位。❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "最小亮度百分比。💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "最高的色温,以开尔文为单位。🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "对于不支持同时设置亮度和颜色的灯光,`separate_turn_on_commands`之间的延迟时间(毫秒)。⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "睡眠模式中的亮度百分比。😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "在睡眠模式中使用“rgb_color”或“color_temp”。🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "睡眠模式中的RGB颜色(当`sleep_rgb_or_color_temp`为“rgb_color”时使用)。🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "睡眠模式中的色温(当`sleep_rgb_or_color_temp`为`color_temp`时使用),以开尔文为单位。😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "以秒为单位的正负偏移调整日出时间。⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "设置固定的日出时间(HH:MM:SS)。🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "以正负偏移秒调整日落时间。⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "设置固定的日落时间(HH:MM:SS)。🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "设置最晚的虚拟日出时间(HH:MM:SS),允许更早的日出。🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "设置最早的虚拟日落时间(HH:MM:SS),允许更晚的日落。🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "如果其他来源在灯光处于打开和正在适应状态时调用`light.turn_on`,则禁用自适应照明。请注意,这会每个`interval`调用`homeassistant.update_entity`!🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "检测并停止对非`light.turn_on`状态更改的适应。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能导致灯光意外打开。如果遇到此类问题,请禁用此功能。", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "灯光变化时的过渡持续时间,以秒为单位。🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "灯光打开和自适应照明应用更改之间的等待时间(秒)。可能有助于避免闪烁。⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "在若干秒后自动重置手动控制。设置为0以禁用。⏲️", + "name": "autoreset_control_seconds" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/zh_Hant.json b/custom_components/adaptive_lighting/translations/zh_Hant.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/zh_Hant.json @@ -0,0 +1 @@ +{} diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py deleted file mode 100644 index 8976e285..00000000 --- a/custom_components/circadian_lighting/__init__.py +++ /dev/null @@ -1,311 +0,0 @@ -""" -Circadian Lighting Component for Home-Assistant. - -This component calculates color temperature and brightness to synchronize -your color changing lights with perceived color temperature of the sky throughout -the day. This gives your environment a more natural feel, with cooler whites during -the midday and warmer tints near twilight and dawn. - -In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, -which is far brighter than starlight but won't reset your circadian rhythm or break down -too much rhodopsin in your eyes. - -Human circadian rhythms are heavily influenced by ambient light levels and -hues. Hormone production, brainwave activity, mood and wakefulness are -just some of the cognitive functions tied to cyclical natural light. -http://en.wikipedia.org/wiki/Zeitgeber - -Here's some further reading: - -http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm -http://en.wikipedia.org/wiki/Color_temperature - -Technical notes: I had to make a lot of assumptions when writing this app - * There are no considerations for weather or altitude, but does use your - hub's location to calculate the sun position. - * The component doesn't calculate a true "Blue Hour" -- it just sets the - lights to 2700K (warm white) until your hub goes into Night mode -""" - -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.light import ( - VALID_TRANSITION, ATTR_TRANSITION) -from homeassistant.const import ( - CONF_LATITUDE, CONF_LONGITUDE, CONF_ELEVATION, - SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) -from homeassistant.util import Throttle -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_sunrise, track_sunset, track_time_change -from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy, - color_xy_to_hs) -from homeassistant.util.dt import utcnow as dt_utcnow, as_local - -from datetime import datetime, timedelta - -VERSION = '1.0.5' - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'circadian_lighting' -CIRCADIAN_LIGHTING_PLATFORMS = ['sensor', 'switch'] -CIRCADIAN_LIGHTING_UPDATE_TOPIC = '{0}_update'.format(DOMAIN) -DATA_CIRCADIAN_LIGHTING = 'data_cl' - -CONF_MIN_CT = 'min_colortemp' -DEFAULT_MIN_CT = 2500 -CONF_MAX_CT = 'max_colortemp' -DEFAULT_MAX_CT = 5500 -CONF_SUNRISE_OFFSET = 'sunrise_offset' -CONF_SUNSET_OFFSET = 'sunset_offset' -CONF_SUNRISE_TIME = 'sunrise_time' -CONF_SUNSET_TIME = 'sunset_time' -CONF_INTERVAL = 'interval' -DEFAULT_INTERVAL = 300 -DEFAULT_TRANSITION = 60 - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, - vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION - }), -}, extra=vol.ALLOW_EXTRA) - -def setup(hass, config): - """Set up the Circadian Lighting component.""" - conf = config[DOMAIN] - min_colortemp = conf.get(CONF_MIN_CT) - max_colortemp = conf.get(CONF_MAX_CT) - sunrise_offset = conf.get(CONF_SUNRISE_OFFSET) - sunset_offset = conf.get(CONF_SUNSET_OFFSET) - sunrise_time = conf.get(CONF_SUNRISE_TIME) - sunset_time = conf.get(CONF_SUNSET_TIME) - - latitude = conf.get(CONF_LATITUDE, hass.config.latitude) - longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) - elevation = conf.get(CONF_ELEVATION, hass.config.elevation) - - load_platform(hass, 'sensor', DOMAIN, {}, config) - - interval = conf.get(CONF_INTERVAL) - transition = conf.get(ATTR_TRANSITION) - - cl = CircadianLighting(hass, min_colortemp, max_colortemp, - sunrise_offset, sunset_offset, sunrise_time, sunset_time, - latitude, longitude, elevation, - interval, transition) - - hass.data[DATA_CIRCADIAN_LIGHTING] = cl - - return True - -class CircadianLighting(object): - """Calculate universal Circadian values.""" - - def __init__(self, hass, min_colortemp, max_colortemp, - sunrise_offset, sunset_offset, sunrise_time, sunset_time, - latitude, longitude, elevation, - interval, transition): - self.hass = hass - self.data = {} - self.data['min_colortemp'] = min_colortemp - self.data['max_colortemp'] = max_colortemp - self.data['sunrise_offset'] = sunrise_offset - self.data['sunset_offset'] = sunset_offset - self.data['sunrise_time'] = sunrise_time - self.data['sunset_time'] = sunset_time - self.data['latitude'] = latitude - self.data['longitude'] = longitude - self.data['elevation'] = elevation - self.data['interval'] = interval - self.data['transition'] = transition - self.data['percent'] = self.calc_percent() - self.data['colortemp'] = self.calc_colortemp() - self.data['rgb_color'] = self.calc_rgb() - self.data['xy_color'] = self.calc_xy() - self.data['hs_color'] = self.calc_hs() - - self.update = Throttle(timedelta(seconds=interval))(self._update) - - if self.data['sunrise_time'] is not None: - track_time_change(self.hass, self._update, hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S"))) - else: - track_sunrise(self.hass, self._update, self.data['sunrise_offset']) - if self.data['sunset_time'] is not None: - track_time_change(self.hass, self._update, hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S"))) - else: - track_sunset(self.hass, self._update, self.data['sunset_offset']) - - def get_sunrise_sunset(self, date = None): - if self.data['sunrise_time'] is not None and self.data['sunset_time'] is not None: - if date is None: - utcdate = dt_utcnow() - date = as_local(utcdate) - sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) - sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) - solar_noon = sunrise + (sunset - sunrise)/2 - solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset)/2 - else: - import astral - location = astral.Location() - location.name = 'name' - location.region = 'region' - location.latitude = self.data['latitude'] - location.longitude = self.data['longitude'] - location.elevation = self.data['elevation'] - _LOGGER.debug("Astral location: " + str(location)) - if self.data['sunrise_time'] is not None: - if date is None: - utcdate = dt_utcnow() - date = as_local(utcdate) - sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) - else: - sunrise = location.sunrise(date) - if self.data['sunset_time'] is not None: - if date is None: - utcdate = dt_utcnow() - date = as_local(utcdate) - sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) - else: - sunset = location.sunset(date) - solar_noon = location.solar_noon(date) - solar_midnight = location.solar_midnight(date) - if self.data['sunrise_offset'] is not None: - sunrise = sunrise + self.data['sunrise_offset'] - if self.data['sunset_offset'] is not None: - sunset = sunset + self.data['sunset_offset'] - return { - SUN_EVENT_SUNRISE: sunrise, - SUN_EVENT_SUNSET: sunset, - 'solar_noon': solar_noon, - 'solar_midnight': solar_midnight - } - - def calc_percent(self): - utcnow = dt_utcnow() - now = as_local(utcnow) - _LOGGER.debug("now: " + str(now)) - - today_sun_times = self.get_sunrise_sunset(now) - _LOGGER.debug("today_sun_times: " + str(today_sun_times)) - - # Convert everything to epoch timestamps for easy calculation - now_seconds = now.timestamp() - sunrise_seconds = today_sun_times[SUN_EVENT_SUNRISE].timestamp() - sunset_seconds = today_sun_times[SUN_EVENT_SUNSET].timestamp() - solar_noon_seconds = today_sun_times['solar_noon'].timestamp() - solar_midnight_seconds = today_sun_times['solar_midnight'].timestamp() - - if now < today_sun_times[SUN_EVENT_SUNRISE]: # It's before sunrise (after midnight) - # Because it's before sunrise (and after midnight) sunset must have happend yesterday - yesterday_sun_times = self.get_sunrise_sunset(now - timedelta(days=1)) - _LOGGER.debug("yesterday_sun_times: " + str(yesterday_sun_times)) - sunset_seconds = yesterday_sun_times[SUN_EVENT_SUNSET].timestamp() - if today_sun_times['solar_midnight'] > today_sun_times[SUN_EVENT_SUNSET] and yesterday_sun_times['solar_midnight'] > yesterday_sun_times[SUN_EVENT_SUNSET]: - # Solar midnight is after sunset so use yesterdays's time - solar_midnight_seconds = yesterday_sun_times['solar_midnight'].timestamp() - elif now > today_sun_times[SUN_EVENT_SUNSET]: # It's after sunset (before midnight) - # Because it's after sunset (and before midnight) sunrise should happen tomorrow - tomorrow_sun_times = self.get_sunrise_sunset(now + timedelta(days=1)) - _LOGGER.debug("tomorrow_sun_times: " + str(tomorrow_sun_times)) - sunrise_seconds = tomorrow_sun_times[SUN_EVENT_SUNRISE].timestamp() - if today_sun_times['solar_midnight'] < today_sun_times[SUN_EVENT_SUNRISE] and tomorrow_sun_times['solar_midnight'] < tomorrow_sun_times[SUN_EVENT_SUNRISE]: - # Solar midnight is before sunrise so use tomorrow's time - solar_midnight_seconds = tomorrow_sun_times['solar_midnight'].timestamp() - - _LOGGER.debug("now_seconds: " + str(now_seconds)) - _LOGGER.debug("sunrise_seconds: " + str(sunrise_seconds)) - _LOGGER.debug("sunset_seconds: " + str(sunset_seconds)) - _LOGGER.debug("solar_midnight_seconds: " + str(solar_midnight_seconds)) - _LOGGER.debug("solar_noon_seconds: " + str(solar_noon_seconds)) - - # Figure out where we are in time so we know which half of the parabola to calculate - # We're generating a different sunset-sunrise parabola for before and after solar midnight - # because it might not be half way between sunrise and sunset - # We're also (obviously) generating a different parabola for sunrise-sunset - - # sunrise-sunset parabola - if now_seconds > sunrise_seconds and now_seconds < sunset_seconds: - h = solar_noon_seconds - k = 100 - # parabola before solar_noon - if now_seconds < solar_noon_seconds: - x = sunrise_seconds - # parabola after solar_noon - else: - x = sunset_seconds - y = 0 - - # sunset_sunrise parabola - elif now_seconds > sunset_seconds and now_seconds < sunrise_seconds: - h = solar_midnight_seconds - k = -100 - # parabola before solar_midnight - if now_seconds < solar_midnight_seconds: - x = sunset_seconds - # parabola after solar_midnight - else: - x = sunrise_seconds - y = 0 - - a = (y-k)/(h-x)**2 - percentage = a*(now_seconds-h)**2+k - - _LOGGER.debug("h: " + str(h)) - _LOGGER.debug("k: " + str(k)) - _LOGGER.debug("x: " + str(x)) - _LOGGER.debug("y: " + str(y)) - _LOGGER.debug("a: " + str(a)) - _LOGGER.debug("percentage: " + str(percentage)) - - return percentage - - def calc_colortemp(self): - if self.data['percent'] > 0: - return ((self.data['max_colortemp'] - self.data['min_colortemp']) * (self.data['percent'] / 100)) + self.data['min_colortemp'] - else: - return self.data['min_colortemp'] - - def calc_rgb(self): - return color_temperature_to_rgb(self.data['colortemp']) - - def calc_xy(self): - rgb = self.calc_rgb() - iR = rgb[0] - iG = rgb[1] - iB = rgb[2] - - return color_RGB_to_xy(iR, iG, iB) - - def calc_hs(self): - xy = self.calc_xy() - vX = xy[0] - vY = xy[1] - - return color_xy_to_hs(vX, vY) - - def _update(self, *args, **kwargs): - """Update Circadian Values.""" - self.data['percent'] = self.calc_percent() - self.data['colortemp'] = self.calc_colortemp() - self.data['rgb_color'] = self.calc_rgb() - self.data['xy_color'] = self.calc_xy() - self.data['hs_color'] = self.calc_hs() - dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) - _LOGGER.debug("Circadian Lighting Component Updated") diff --git a/custom_components/circadian_lighting/manifest.json b/custom_components/circadian_lighting/manifest.json deleted file mode 100644 index b45b39e2..00000000 --- a/custom_components/circadian_lighting/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "circadian_lighting", - "name": "Circadian Lighting", - "documentation": "https://github.com/claytonjn/hass-circadian_lighting", - "dependencies": [], - "codeowners": ["@claytonjn"], - "requirements": [] -} diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py deleted file mode 100644 index 3395142b..00000000 --- a/custom_components/circadian_lighting/sensor.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -Circadian Lighting Sensor for Home-Assistant. -""" - -DEPENDENCIES = ['circadian_lighting'] - -import logging - -from custom_components.circadian_lighting import DOMAIN, CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING - -from homeassistant.helpers.dispatcher import dispatcher_connect -from homeassistant.helpers.entity import Entity - -import datetime - -_LOGGER = logging.getLogger(__name__) - -ICON = 'mdi:theme-light-dark' - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Circadian Lighting sensor.""" - cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) - if cl: - cs = CircadianSensor(hass, cl) - add_devices([cs]) - - def update(call=None): - """Update component.""" - cl._update() - service_name = "values_update" - hass.services.register(DOMAIN, service_name, update) - return True - else: - return False - -class CircadianSensor(Entity): - """Representation of a Circadian Lighting sensor.""" - - def __init__(self, hass, cl): - """Initialize the Circadian Lighting sensor.""" - self._cl = cl - self._name = 'Circadian Values' - self._entity_id = 'sensor.circadian_values' - self._state = self._cl.data['percent'] - self._unit_of_measurement = '%' - self._icon = ICON - self._hs_color = self._cl.data['hs_color'] - self._attributes = self._cl.data - - """Register callbacks.""" - dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) - - @property - def entity_id(self): - """Return the entity ID of the sensor.""" - return self._entity_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def hs_color(self): - return self._hs_color - - @property - def device_state_attributes(self): - """Return the attributes of the sensor.""" - return dict((k,str(v) if isinstance(v, datetime.time) or isinstance(v, datetime.timedelta) else v) for k,v in self._attributes.items()) - - def update(self): - """Fetch new state data for the sensor. - - This is the only method that should fetch new data for Home Assistant. - """ - self._cl.update() - - def update_sensor(self): - if self._cl.data is not None: - self._state = self._cl.data['percent'] - self._hs_color = self._cl.data['hs_color'] - self._attributes = self._cl.data - _LOGGER.debug("Circadian Lighting Sensor Updated") \ No newline at end of file diff --git a/custom_components/circadian_lighting/services.yaml b/custom_components/circadian_lighting/services.yaml deleted file mode 100644 index e4f0b86b..00000000 --- a/custom_components/circadian_lighting/services.yaml +++ /dev/null @@ -1,3 +0,0 @@ -values_update: - description: Updates values for Circadian Lighting. - fields: diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py deleted file mode 100644 index 380e03fe..00000000 --- a/custom_components/circadian_lighting/switch.py +++ /dev/null @@ -1,336 +0,0 @@ -""" -Circadian Lighting Switch for Home-Assistant. -""" - -DEPENDENCIES = ['circadian_lighting', 'light'] - -import logging - -from custom_components.circadian_lighting import DOMAIN, CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_connect -from homeassistant.helpers.event import track_state_change -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.components.light import ( - is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, STATE_ON, - SERVICE_TURN_ON) -from homeassistant.util import slugify -from homeassistant.util.color import ( - color_RGB_to_xy, color_temperature_kelvin_to_mired, - color_temperature_to_rgb, color_xy_to_hs) - -_LOGGER = logging.getLogger(__name__) - -ICON = 'mdi:theme-light-dark' - -CONF_LIGHTS_CT = 'lights_ct' -CONF_LIGHTS_RGB = 'lights_rgb' -CONF_LIGHTS_XY = 'lights_xy' -CONF_LIGHTS_BRIGHT = 'lights_brightness' -CONF_DISABLE_BRIGHTNESS_ADJUST = 'disable_brightness_adjust' -CONF_MIN_BRIGHT = 'min_brightness' -DEFAULT_MIN_BRIGHT = 1 -CONF_MAX_BRIGHT = 'max_brightness' -DEFAULT_MAX_BRIGHT = 100 -CONF_SLEEP_ENTITY = 'sleep_entity' -CONF_SLEEP_STATE = 'sleep_state' -CONF_SLEEP_CT = 'sleep_colortemp' -CONF_SLEEP_BRIGHT = 'sleep_brightness' -CONF_DISABLE_ENTITY = 'disable_entity' -CONF_DISABLE_STATE = 'disable_state' - -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'circadian_lighting', - vol.Optional(CONF_NAME, default="Circadian Lighting"): cv.string, - vol.Optional(CONF_LIGHTS_CT): cv.entity_ids, - vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, - vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, - vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, - vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): cv.string, - vol.Optional(CONF_SLEEP_CT): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SLEEP_BRIGHT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): cv.string -}) - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Circadian Lighting switches.""" - cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) - if cl: - lights_ct = config.get(CONF_LIGHTS_CT) - lights_rgb = config.get(CONF_LIGHTS_RGB) - lights_xy = config.get(CONF_LIGHTS_XY) - lights_brightness = config.get(CONF_LIGHTS_BRIGHT) - disable_brightness_adjust = config.get(CONF_DISABLE_BRIGHTNESS_ADJUST) - name = config.get(CONF_NAME) - min_brightness = config.get(CONF_MIN_BRIGHT) - max_brightness = config.get(CONF_MAX_BRIGHT) - sleep_entity = config.get(CONF_SLEEP_ENTITY) - sleep_state = config.get(CONF_SLEEP_STATE) - sleep_colortemp = config.get(CONF_SLEEP_CT) - sleep_brightness = config.get(CONF_SLEEP_BRIGHT) - disable_entity = config.get(CONF_DISABLE_ENTITY) - disable_state = config.get(CONF_DISABLE_STATE) - cs = CircadianSwitch(hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, - disable_brightness_adjust, min_brightness, max_brightness, - sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, - disable_entity, disable_state) - add_devices([cs]) - - def update(call=None): - """Update lights.""" - cs.update_switch() - return True - else: - return False - - -class CircadianSwitch(SwitchDevice, RestoreEntity): - """Representation of a Circadian Lighting switch.""" - - def __init__(self, hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, - disable_brightness_adjust, min_brightness, max_brightness, - sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, - disable_entity, disable_state): - """Initialize the Circadian Lighting switch.""" - self.hass = hass - self._cl = cl - self._name = name - self._entity_id = "switch." + slugify("{} {}".format('circadian_lighting', name)) - self._state = None - self._icon = ICON - self._hs_color = None - self._attributes = {} - self._attributes['lights_ct'] = lights_ct - self._attributes['lights_rgb'] = lights_rgb - self._attributes['lights_xy'] = lights_xy - self._attributes['lights_brightness'] = lights_brightness - self._attributes['disable_brightness_adjust'] = disable_brightness_adjust - self._attributes['min_brightness'] = min_brightness - self._attributes['max_brightness'] = max_brightness - self._attributes['sleep_entity'] = sleep_entity - self._attributes['sleep_state'] = sleep_state - self._attributes['sleep_colortemp'] = sleep_colortemp - self._attributes['sleep_brightness'] = sleep_brightness - self._attributes['disable_entity'] = disable_entity - self._attributes['disable_state'] = disable_state - self._attributes['hs_color'] = self._hs_color - self._attributes['brightness'] = None - - self._lights = [] - if lights_ct != None: - self._lights += lights_ct - if lights_rgb != None: - self._lights += lights_rgb - if lights_xy != None: - self._lights += lights_xy - if lights_brightness != None: - self._lights += lights_brightness - - """Register callbacks.""" - dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_switch) - track_state_change(hass, self._lights, self.light_state_changed) - if self._attributes['sleep_entity'] is not None: - track_state_change(hass, self._attributes['sleep_entity'], self.sleep_state_changed) - - @property - def entity_id(self): - """Return the entity ID of the switch.""" - return self._entity_id - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if circadian lighting is on.""" - return self._state - - async def async_added_to_hass(self): - """Call when entity about to be added to hass.""" - # If not None, we got an initial value. - await super().async_added_to_hass() - if self._state is not None: - return - - state = await self.async_get_last_state() - self._state = state and state.state == STATE_ON - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def hs_color(self): - return self._hs_color - - @property - def device_state_attributes(self): - """Return the attributes of the switch.""" - return self._attributes - - def turn_on(self, **kwargs): - """Turn on circadian lighting.""" - self._state = True - - # Make initial update - self.update_switch() - - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn off circadian lighting.""" - self._state = False - self.schedule_update_ha_state() - self._hs_color = None - self._attributes['hs_color'] = self._hs_color - self._attributes['brightness'] = None - - def is_sleep(self): - return self._attributes['sleep_entity'] is not None and self.hass.states.get(self._attributes['sleep_entity']).state == self._attributes['sleep_state'] - - def calc_ct(self): - if self.is_sleep(): - _LOGGER.debug(self._name + " in Sleep mode") - return color_temperature_kelvin_to_mired(self._attributes['sleep_colortemp']) - else: - return color_temperature_kelvin_to_mired(self._cl.data['colortemp']) - - def calc_rgb(self): - if self.is_sleep(): - _LOGGER.debug(self._name + " in Sleep mode") - return color_temperature_to_rgb(self._attributes['sleep_colortemp']) - else: - return color_temperature_to_rgb(self._cl.data['colortemp']) - - def calc_xy(self): - return color_RGB_to_xy(*self.calc_rgb()) - - def calc_hs(self): - return color_xy_to_hs(*self.calc_xy()) - - def calc_brightness(self): - if self._attributes['disable_brightness_adjust'] is True: - return None - else: - if self.is_sleep(): - _LOGGER.debug(self._name + " in Sleep mode") - return self._attributes['sleep_brightness'] - else: - if self._cl.data['percent'] > 0: - return self._attributes['max_brightness'] - else: - return ((self._attributes['max_brightness'] - self._attributes['min_brightness']) * ((100+self._cl.data['percent']) / 100)) + self._attributes['min_brightness'] - - def update_switch(self, transition=None): - if self._cl.data is not None: - self._hs_color = self.calc_hs() - self._attributes['hs_color'] = self._hs_color - self._attributes['brightness'] = self.calc_brightness() - _LOGGER.debug(self._name + " Switch Updated") - - self.adjust_lights(self._lights, transition) - - def should_adjust(self): - if self._state is not True: - _LOGGER.debug(self._name + " off - not adjusting") - return False - elif self._cl.data is None: - _LOGGER.debug(self._name + " could not retrieve Circadian Lighting data") - return False - elif self._attributes['disable_entity'] is not None and self.hass.states.get(self._attributes['disable_entity']).state == self._attributes['disable_state']: - _LOGGER.debug(self._name + " disabled by " + str(self._attributes['disable_entity'])) - return False - else: - return True - - def adjust_lights(self, lights, transition=None): - if self.should_adjust(): - if transition == None: - transition = self._cl.data['transition'] - - brightness = (self._attributes['brightness'] / 100) * 255 if self._attributes['brightness'] is not None else None - - for light in lights: - """Set color of array of ct light.""" - if self._attributes['lights_ct'] is not None and light in self._attributes['lights_ct']: - mired = int(self.calc_ct()) - if is_on(self.hass, light): - service_data = {ATTR_ENTITY_ID: light} - if mired is not None: - service_data[ATTR_COLOR_TEMP] = int(mired) - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) - - """Set color of array of rgb light.""" - if self._attributes['lights_rgb'] is not None and light in self._attributes['lights_rgb']: - rgb = self.calc_rgb() - if is_on(self.hass, light): - service_data = {ATTR_ENTITY_ID: light} - if rgb is not None: - service_data[ATTR_RGB_COLOR] = rgb - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) - - """Set color of array of xy light.""" - if self._attributes['lights_xy'] is not None and light in self._attributes['lights_xy']: - x_val, y_val = self.calc_xy() - if is_on(self.hass, light): - service_data = {ATTR_ENTITY_ID: light} - if x_val is not None and y_val is not None: - service_data[ATTR_XY_COLOR] = [x_val, y_val] - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - service_data[ATTR_WHITE_VALUE] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " XY Adjusted - xy_color: [" + str(x_val) + ", " + str(y_val) + "], brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) - - """Set color of array of brightness light.""" - if self._attributes['lights_brightness'] is not None and light in self._attributes['lights_brightness']: - if is_on(self.hass, light): - service_data = {ATTR_ENTITY_ID: light} - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) - - def light_state_changed(self, entity_id, from_state, to_state): - self.adjust_lights([entity_id], 1) - - def sleep_state_changed(self, entity_id, from_state, to_state): - if to_state.state == self._attributes['sleep_state'] or from_state.state == self._attributes['sleep_state']: - self.update_switch(1) \ No newline at end of file diff --git a/custom_updater.json b/custom_updater.json deleted file mode 100644 index f2a32ea4..00000000 --- a/custom_updater.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "circadian_lighting": { - "updated_at": "2019-06-01", - "version": "1.0.5", - "local_location": "/custom_components/circadian_lighting/__init__.py", - "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", - "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", - "changelog": "https://github.com/claytonjn/hass-circadian_lighting/releases", - "resources": [ - "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/manifest.json", - "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/sensor.py", - "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/services.yaml", - "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/switch.py" - ] - } -} \ No newline at end of file diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..31586af9 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +adaptive-lighting.nijho.lt diff --git a/docs/advanced/brightness-modes.md b/docs/advanced/brightness-modes.md new file mode 100644 index 00000000..10408fb2 --- /dev/null +++ b/docs/advanced/brightness-modes.md @@ -0,0 +1,122 @@ +--- +icon: lucide/trending-up +--- + +# Brightness Modes + +Enhance your control over brightness transitions during sunrise and sunset with the `brightness_mode` option. + +## Available Modes + +Adaptive Lighting supports three brightness modes: + +| Mode | Description | +|------|-------------| +| `default` | Standard behavior based on sun position | +| `linear` | Linear ramp between min/max brightness | +| `tanh` | Smooth S-curve using hyperbolic tangent | + +## Detailed Explanation + + + + + + +
+Enhance your control over brightness transitions during sunrise and sunset with brightness_mode (click here to learn more 🧠). + +With Adaptive Lighting, you can set a `brightness_mode` to specify how the brightness changes during sunrise and sunset. The `brightness_mode` can be set to `"default"` ([as illustrated in other graphs above](#high_brightness-brightness)), `"linear"`, or `"tanh"`. If you choose to deviate from the `"default"` mode, you can adjust `brightness_mode_time_dark` and `brightness_mode_time_light` to further customize the lighting transitions. + +When `brightness_mode` is set to `"linear"`: + +- During **_sunset_**, the brightness begins to gradually decrease from `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches `min_brightness` at `time=sunset_time + brightness_mode_time_dark`. +- During **_sunrise_**, the brightness begins to gradually increase from `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches `max_brightness` at `time=sunrise_time + brightness_mode_time_light`. + +When `brightness_mode` is set to `"tanh"`, it uses the smooth transition of a [hyperbolic tangent function](https://mathworld.wolfram.com/HyperbolicTangent.html): + +- During **_sunset_**, the brightness starts to decrease from 95% of `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches 5% of `min_brightness` at `time=sunset_time + brightness_mode_time_dark`. +- During **_sunrise_**, the brightness starts to increase from 5% of `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches 95% of `max_brightness` at `time=sunrise_time + brightness_mode_time_light`. +
+ +Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark` in the text box. +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/15143580-13cd-4ab2-a603-89f2b7830afd) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/f61fdac9-6d47-48c9-84ed-cbb451d5de5d) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/e5fc5d27-3c37-4e3d-93d1-6e7cf4b48e7c) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/3dcbdc42-63c4-49df-8651-d2fae53dd08d) + +> Check out the interactive webapp on https://basnijholt.github.io/adaptive-lighting/ to play with the parameters and see how the brightness changes! + + + +## Configuration Parameters + +When using `linear` or `tanh` modes, you can fine-tune the transition with these parameters: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `brightness_mode_time_dark` | 900 (15 min) | Duration to ramp brightness before/after sunrise/sunset | +| `brightness_mode_time_light` | 3600 (1 hour) | Duration to ramp brightness after/before sunrise/sunset | + +## Example Configurations + +### Quick Transition (Linear) + +```yaml +adaptive_lighting: + - name: "Quick transitions" + lights: + - light.living_room + brightness_mode: linear + brightness_mode_time_dark: 600 # 10 minutes + brightness_mode_time_light: 1800 # 30 minutes +``` + +### Smooth Transition (Tanh) + +```yaml +adaptive_lighting: + - name: "Smooth transitions" + lights: + - light.bedroom + brightness_mode: tanh + brightness_mode_time_dark: 1200 # 20 minutes + brightness_mode_time_light: 3600 # 1 hour +``` + +## Graphs + +These graphs show how brightness changes throughout the day based on calculated values: + + + + + + +These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). + +### :sunny: Sun Position +![cl_percent|690x131](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/6/5/657ff98beb65a94598edeb4bdfd939095db1a22c.PNG) + +### :thermometer: Color Temperature +![cl_color_temp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) + +### :high_brightness: Brightness +![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) + +### While using `transition_until_sleep: true` +![image](https://user-images.githubusercontent.com/2219836/228949675-f9699624-8abc-466c-bb04-250ce0f495b8.png) + + + +## Interactive Simulator + +The best way to understand brightness modes is to experiment with the interactive simulator: + +
+ + Try the Simulator + +
+ +Adjust the `brightness_mode`, `brightness_mode_time_dark`, and `brightness_mode_time_light` parameters to see how they affect the brightness curve in real-time. diff --git a/docs/advanced/manual-control.md b/docs/advanced/manual-control.md new file mode 100644 index 00000000..4b0bfe51 --- /dev/null +++ b/docs/advanced/manual-control.md @@ -0,0 +1,201 @@ +--- +icon: lucide/hand +--- + +# Manual Control + +Adaptive Lighting is designed to work seamlessly with manual adjustments, detecting when you or another source changes light settings and pausing adaptation accordingly. + +## How It Works + + + + + + +Adaptive Lighting is designed to automatically detect when you or another source (e.g., automation) manually changes light settings 🕹️. +When this occurs, the affected light is marked as "manually controlled," and Adaptive Lighting will not make further adjustments until the light is turned off and back on or reset using the `adaptive_lighting.set_manual_control` service call. +This feature is available when `take_over_control` is enabled. + +Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. +The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. + +With `expand_light_groups: false`, manual control belongs to the group. A direct member change cannot pause adaptation for only that member; use group-level manual control or enable expansion for individual tracking. +Explicit member targets in Adaptive Lighting services stay individual targets and do not mark or command the whole group. +Changing expansion at runtime discards tracking and pending adaptation for targets no longer used by any profile. + +The Adaptive Lighting switch exposes these read-only attributes for its lights: + +- `manual_control`: lights with any attribute marked as manually controlled. +- `manual_control_brightness`: lights with brightness marked as manually controlled. +- `manual_control_color`: lights with color marked as manually controlled. + +These lists report manual-control flags. Actual adaptation also depends on `take_over_control_mode` and the brightness/color adaptation switches. For example, under the default `pause_all` mode, manually changing only brightness leaves `manual_control_color` empty while pausing both brightness and color adaptation. Under `pause_changed`, color can continue adapting. + +The attributes are absent when the Adaptive Lighting switch is off. Use a fallback when checking them in templates: + +```jinja +{{ 'light.bedroom' in (state_attr('switch.adaptive_lighting_bedroom', 'manual_control_brightness') or []) }} +``` + +> ⚠️ **_Caution: Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable `detect_non_ha_changes` if you encounter such issues._** + + + +## Configuration Options + +### take_over_control + +When enabled (default: `true`), Adaptive Lighting detects `light.turn_on` service calls that specify brightness or color values. If such a call is detected for a light that's already on, that light is marked as "manually controlled". + +```yaml +adaptive_lighting: + - name: "With manual control detection" + lights: + - light.living_room + take_over_control: true # default +``` + +### take_over_control_mode + +Controls how adaptation pauses when manual changes are detected: + +| Mode | Behavior | +|------|----------| +| `pause_all` | Pause both brightness and color adaptation (default) | +| `pause_changed` | Only pause adaptation of the changed attribute | + +```yaml +adaptive_lighting: + - name: "Selective pause" + lights: + - light.living_room + take_over_control: true + take_over_control_mode: pause_changed # Only pause changed attributes +``` + +### detect_non_ha_changes + +When enabled, Adaptive Lighting detects state changes made outside of Home Assistant by comparing the light's current state to its previously applied settings. + +> [!WARNING] +> **Use with caution.** Some lights may falsely report an "on" state, which could result in lights turning on unexpectedly. Disable this option if you encounter such issues. + +```yaml +adaptive_lighting: + - name: "Detect external changes" + lights: + - light.living_room + take_over_control: true + detect_non_ha_changes: true +``` + +### autoreset_control_seconds + +Automatically resets the manual control flag after a specified number of seconds. Set to `0` to disable (default). + +```yaml +adaptive_lighting: + - name: "Auto-reset after 2 hours" + lights: + - light.living_room + take_over_control: true + autoreset_control_seconds: 7200 # 2 hours +``` + +### adapt_only_on_bare_turn_on + +When enabled, Adaptive Lighting only adapts lights when `light.turn_on` is called without specifying brightness or color. This is useful when you want scenes to work without interference. + +```yaml +adaptive_lighting: + - name: "Respect scenes" + lights: + - light.living_room + take_over_control: true + adapt_only_on_bare_turn_on: true +``` + +### manual_control_on_external_turn_on + +When enabled, a turn-on without a state-change context matching the latest recorded Home Assistant `light.turn_on` is treated as manual control. This pauses brightness and color adaptation until manual control resets, rather than skipping just the first adjustment. The usual off/on, explicit reset, and configured timeout rules apply. A later unmatched turn-on marks the light manually controlled again. + +Manual-control flags are shared by profiles controlling the same light. Use the same turn-on policy on those profiles; mixed policies can allow an earlier profile to adapt before another marks the light manually controlled. + +Enable this if you want turn-ons from physical controls or native scenes to preserve their brightness and color. To adapt unmatched turn-ons, leave this disabled and enable `detect_non_ha_changes`. + +Its advantage over simply disabling `detect_non_ha_changes` is that the two behaviors are decoupled: you can keep `detect_non_ha_changes: true` to catch manual dimming of lights that are *already on*, while leaving unmatched turn-ons untouched. + +Adaptive Lighting cannot identify every physical versus Home Assistant source. Some integrations replace or omit the service context when they publish device state. In that case, even a Home Assistant turn-on does not match and this option treats it as external. + +```yaml +adaptive_lighting: + - name: "Respect physical switches and Lutron scenes" + lights: + - light.living_room + take_over_control: true + detect_non_ha_changes: true # still catch manual changes to already-on lights + manual_control_on_external_turn_on: true # leave unmatched off→on events unchanged +``` + +## Checking Manual Control Status + +You can see which lights are marked as manually controlled by checking the switch attributes: + +1. Go to **Developer Tools** → **States** +2. Find your Adaptive Lighting switch (e.g., `switch.adaptive_lighting_living_room`) +3. Look at the `manual_control` attribute - it lists all manually controlled lights + +## Resetting Manual Control + +### Via Service Call + +```yaml +service: adaptive_lighting.set_manual_control +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + manual_control: false # Resume adaptation +``` + +### By Turning Light Off and On + +Simply turning a light off and then back on will reset its manual control status. + +### Via Automation + +See [Automation Examples](../automation-examples.md) for automation recipes that automatically reset manual control. + +## Events + +When a light is marked as manually controlled, Adaptive Lighting fires an event: + +**Event type:** `adaptive_lighting.manual_control` + +**Event data:** +```yaml +entity_id: light.living_room +switch: switch.adaptive_lighting_living_room +``` + +You can use this event to trigger automations: + +```yaml +automation: + - alias: "Notify on manual control" + trigger: + platform: event + event_type: adaptive_lighting.manual_control + action: + - service: notify.mobile_app + data: + message: "{{ trigger.event.data.entity_id }} was manually adjusted" +``` + +## Best Practices + +1. **Use Zigbee groups** when controlling multiple bulbs together - this ensures consistent manual control detection +2. **Set reasonable autoreset times** if you want lights to eventually resume adaptation +3. **Use `pause_changed` mode** if you only adjust brightness or color individually +4. **Disable `detect_non_ha_changes`** if you experience unexpected light turn-ons diff --git a/docs/advanced/sleep-mode.md b/docs/advanced/sleep-mode.md new file mode 100644 index 00000000..9d90c854 --- /dev/null +++ b/docs/advanced/sleep-mode.md @@ -0,0 +1,57 @@ +--- +icon: lucide/moon +--- + +# Sleep Mode + +Sleep mode is a special operating mode that sets your lights to minimal brightness and very warm color, perfect for winding down at night without disrupting your circadian rhythm. + +## Activating Sleep Mode + +Each Adaptive Lighting configuration creates a sleep mode switch: + +``` +switch.adaptive_lighting_sleep_mode_ +``` + +Turn it on to activate sleep mode: + +```yaml +service: switch.turn_on +target: + entity_id: switch.adaptive_lighting_sleep_mode_living_room +``` + +Sleep mode stays active until this switch is turned off. It does not turn off +automatically at sunrise, and Home Assistant restores its previous state after a +restart. Use an automation, such as the sleep-mode blueprint linked under +Automation Examples, when you want the switch to follow a schedule or helper. + +If lights unexpectedly use `sleep_brightness` or `sleep_color_temp` during the +day, first check that the sleep-mode switch is off. While the main Adaptive +Lighting switch is on, it reports the current calculated `brightness_pct` and +`color_temp_kelvin` targets, including the sleep settings while sleep mode is on. +You can compare these attributes with the physical light state. They are `None` +when the main switch is off. In debug logs, +`initial_sleep=True` describes an internal delay before sending a command; it does +not mean that sleep mode is active. + +## Configuration Options + +Sleep mode is configured through the main Adaptive Lighting configuration. See the [Configuration](../configuration.md) page for the full options table. The sleep-related options are: + +| Option | Default | Description | +|--------|---------|-------------| +| `sleep_brightness` | 1 | Brightness percentage in sleep mode | +| `sleep_rgb_or_color_temp` | `color_temp` | Use `rgb_color` or `color_temp` in sleep mode | +| `sleep_color_temp` | 1000 | Color temperature in Kelvin for sleep mode | +| `sleep_rgb_color` | `[255, 56, 0]` | RGB color for sleep mode | +| `sleep_transition` | 1 | Transition duration in seconds | +| `transition_until_sleep` | false | Gradually transition to sleep settings after sunset | + +## Automation Examples + +See [Automation Examples](../automation-examples.md) for sleep mode automation recipes, including: + +- Toggle sleep mode using an `input_boolean` +- Set sunrise/sunset based on alarm time diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 00000000..d055aca3 Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css new file mode 100644 index 00000000..c00450ee --- /dev/null +++ b/docs/assets/stylesheets/extra.css @@ -0,0 +1,46 @@ +/* Adaptive Lighting Documentation Custom Styles */ + +/* Hero section on index page */ +.md-content__inner > h1:first-child { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +/* Logo in header - make it slightly larger */ +.md-header__button.md-logo img { + height: 1.8rem; +} + +/* Simulator link button styling */ +.simulator-link { + display: inline-block; + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, var(--md-primary-fg-color), var(--md-accent-fg-color)); + color: white !important; + text-decoration: none; + border-radius: 2rem; + font-weight: 600; + font-size: 1.1rem; + transition: transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.simulator-link:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); + text-decoration: none; +} + +/* Code block improvements */ +.highlight code { + font-size: 0.85rem; +} + +/* Table improvements */ +.md-typeset table:not([class]) { + font-size: 0.8rem; +} + +.md-typeset table:not([class]) th { + font-weight: 700; +} diff --git a/docs/automation-examples.md b/docs/automation-examples.md new file mode 100644 index 00000000..87ca3aa1 --- /dev/null +++ b/docs/automation-examples.md @@ -0,0 +1,471 @@ +--- +icon: lucide/bot +--- + +# Automation Examples + +Real-world automation examples showing how to integrate Adaptive Lighting with your Home Assistant setup. + + + + + + + +Replace every entity ID below with the IDs from your Home Assistant instance. Fresh Adaptive Lighting profiles use child IDs such as `switch.adaptive_lighting_living_room_sleep_mode`; profiles created before the device-based entity change may retain older IDs. + +Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`. + +Five examples also have blueprints with selectors, so you can configure them without editing YAML: + +| Blueprint | Purpose | Import | +| --- | --- | --- | +| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fsleep_mode.yaml) | +| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fturn_off_at_minimum.yaml) | +| [Pause at minimum](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) | Pause brightness through manual control, using its existing reset behavior. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fmanual_control_at_minimum.yaml) | +| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fschedule_profile.yaml) | +| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fdaylight_limit.yaml) | + +Click a blueprint's import badge, confirm the import in Home Assistant, then create an automation and select your entities. You can also copy its source link into **Settings → Automations & scenes → Blueprints → Import Blueprint**. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations. + +`change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused. + +
+Automatically reset manual control after one hour. + +Use the built-in timeout so every new manual change renews a single timer for that light: + +```yaml +adaptive_lighting: + - name: "Living Room" + lights: + - light.living_room + autoreset_control_seconds: 3600 +``` + +This is a top-level `configuration.yaml` example. The timer clears manual control and immediately readapts a light when both it and the Adaptive Lighting switch are on. + +
+ +
+Toggle multiple Adaptive Lighting switches to "sleep mode" using an input_boolean.sleep_mode. + +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml). Select an input boolean and the sleep-mode switches it should control. + +```yaml +- alias: "Adaptive lighting: toggle 'sleep mode'" + mode: restart + trigger: + - platform: state + entity_id: input_boolean.sleep_mode + - platform: homeassistant + event: start # apply the helper's restored state + variables: + sleep_mode: "{{ states('input_boolean.sleep_mode') }}" + conditions: + - condition: template + value_template: "{{ sleep_mode in ['on', 'off'] }}" + actions: + - action: "switch.turn_{{ sleep_mode }}" + target: + entity_id: + - switch.adaptive_lighting_living_room_sleep_mode + - switch.adaptive_lighting_bedroom_sleep_mode +``` + +
+ +
+Turn a light off when its adaptive brightness target reaches the minimum. + +Prefer a form over editing YAML? Import the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) in Home Assistant under **Settings → Automations & scenes → Blueprints → Import Blueprint**. Select your profile, its matching adapt brightness switch, one light managed by that profile, and its minimum brightness percentage. Create one automation per light. If you change the profile's minimum later, update the automation too. The blueprint and YAML example below have the same behavior. + +The Adaptive Lighting switch already exposes its calculated `brightness_pct` target. Use its state changes to choose a power policy in an automation; no custom event is needed. This example assumes `min_brightness: 1`. Change `minimum_pct` to match your profile, and replace the switch and light entity IDs with your own. + +The comparison uses the same rounded 0–255 brightness as an adaptation command. Comparing floating-point percentages for exact equality can miss the minimum between updates. This detects the calculated target reaching its minimum command, not the bulb finishing a transition or reaching its physical dimming limit. + +```yaml +- alias: "Adaptive lighting: turn off at minimum brightness" + mode: single + triggers: + - trigger: state + entity_id: switch.adaptive_lighting_living_room + attribute: brightness_pct + conditions: + - condition: state + entity_id: + - switch.adaptive_lighting_living_room + - switch.adaptive_lighting_living_room_adapt_brightness + state: "on" + - condition: template + value_template: >- + {% set minimum_pct = 1 %} + {% set minimum = (minimum_pct * 255 / 100) | round(0) %} + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: light.living_room + state: "on" + - condition: template + value_template: >- + {{ 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control') or []) }} + actions: + - action: light.turn_off + target: + entity_id: light.living_room +``` + +This runs once when a valid target crosses down into the minimum range. It skips lights currently marked as manually controlled, does not repeatedly turn them off while the target remains low, and does not turn them back on later. Startup or re-enabling the profile while already at the minimum is not a new crossing. Sleep mode can also cause a crossing if its brightness is at or below the chosen minimum. Changing sleep mode clears manual control by default; set `reset_manual_control_on_sleep_mode_change: false` if you want to preserve it. For a bedtime-only policy, trigger directly on the sleep-mode switch changing to `on` instead. + +
+ +
+Pause brightness at the minimum using manual control. + +Use the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) to mark an individual light's brightness as manually controlled when the calculated target reaches its minimum. The light stays on and the adaptation switches stay enabled. Set `take_over_control_mode: pause_changed` on the profile to keep adapting color; the default `pause_all` pauses both attributes. + +Select the profile, its adapt-brightness switch, and a light managed by it. Match the minimum percentage to the profile's `min_brightness`. This YAML example assumes `min_brightness: 1`; change `minimum_pct` and the entity IDs to match your setup. + +```yaml +- alias: "Adaptive lighting: pause brightness at minimum" + mode: single + variables: + minimum_pct: 1 + minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}" + triggers: + - trigger: state + entity_id: switch.adaptive_lighting_living_room + attribute: brightness_pct + conditions: + - condition: state + entity_id: + - switch.adaptive_lighting_living_room + - switch.adaptive_lighting_living_room_adapt_brightness + state: "on" + - condition: template + value_template: >- + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: light.living_room + state: "on" + - condition: template + value_template: >- + {{ 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }} + actions: + - variables: + light_session: "{{ states.light.living_room.last_changed.isoformat() }}" + profile_session: "{{ states.switch.adaptive_lighting_living_room.last_changed.isoformat() }}" + brightness_session: "{{ states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() }}" + - wait_template: >- + {% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %} + {{ not is_state('light.living_room', 'on') + or not is_state('switch.adaptive_lighting_living_room', 'on') + or not is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on') + or states.light.living_room.last_changed.isoformat() != light_session + or states.switch.adaptive_lighting_living_room.last_changed.isoformat() != profile_session + or states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() != brightness_session + or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum + or 'light.living_room' in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) + or (state_attr('light.living_room', 'brightness') | float(256)) <= minimum }} + timeout: "00:05:00" + continue_on_timeout: false + - condition: template + value_template: >- + {% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %} + {{ is_state('light.living_room', 'on') + and is_state('switch.adaptive_lighting_living_room', 'on') + and is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on') + and states.light.living_room.last_changed.isoformat() == light_session + and states.switch.adaptive_lighting_living_room.last_changed.isoformat() == profile_session + and states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() == brightness_session + and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum + and (state_attr('light.living_room', 'brightness') | float(256)) <= minimum + and 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }} + - action: adaptive_lighting.set_manual_control + data: + entity_id: switch.adaptive_lighting_living_room + lights: light.living_room + manual_control: >- + {{ true if 'light.living_room' in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_color') or []) + else 'brightness' }} +``` + +The comparison uses the rounded 0–255 target, so it does not depend on sampling an exact floating-point minimum. It waits up to five minutes for the light to report that minimum before marking manual control, so the final dimming command can complete. Reported brightness does not prove physical fade completion. If the light, profile, or adapt-brightness switch is toggled, the target rises, brightness is marked manually controlled elsewhere, or brightness never reaches the minimum, the attempt is abandoned. Lights that cannot report the configured minimum will not be paused. Existing manual color flags are preserved, and lights whose brightness is already manually controlled are left alone. Manual-control state is shared for lights managed by multiple profiles, so their existing takeover policies still apply. + +The usual resets apply: turning the light off, the configured `autoreset_control_seconds` timeout, clearing manual control through its service, and existing profile/sleep-switch reset behavior. After a reset, normal adaptation can increase brightness again. This runs once per downward crossing; resetting while the target remains at its minimum does not immediately mark the light again. Startup at the minimum is not a crossing either. + +This pauses further dimming as well as brightening. To pause brightness immediately after a brightness change made through Home Assistant, use `take_over_control_mode: pause_changed` with `take_over_control: true`; that needs no additional automation. + +
+ +
+Set sunrise and sunset from an alarm. + +Call this script from your alarm automation. It sets one Adaptive Lighting profile's sunrise to the current time and its sunset to 12 hours later on the local clock. + +```yaml +script: + set_adaptive_lighting_alarm_times: + alias: "Adaptive lighting: set times from alarm" + variables: + alarm_time: '{{ now().strftime("%H:%M:%S") }}' + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_alarm_lights + sunrise_time: "{{ alarm_time }}" + sunset_time: > + {{ (strptime(alarm_time, "%H:%M:%S") + timedelta(hours=12)) + .strftime("%H:%M:%S") }} +``` + +
+ +
+Use a Schedule helper as a step-based custom lighting profile. + +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml). Select the main profile switch and your Schedule helper. + +Create a [Schedule helper](https://www.home-assistant.io/integrations/schedule/) named `Adaptive Lighting Profile`. Add time blocks with Additional data like this: + +```yaml +brightness_pct: 20 +color_temp_kelvin: 2500 +``` + +Use different values for each block. The automation below applies the active block whenever the schedule state or its attributes change. Setting both brightness limits and both color temperature limits to the same value keeps each block at its setpoint. Outside a block, the configured Adaptive Lighting settings are restored. + +```yaml +- alias: "Adaptive lighting: apply scheduled profile" + triggers: + - trigger: state + entity_id: schedule.adaptive_lighting_profile + - trigger: homeassistant + event: start + actions: + - choose: + - conditions: + - condition: state + entity_id: schedule.adaptive_lighting_profile + state: "on" + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + min_brightness: > + {{ state_attr('schedule.adaptive_lighting_profile', 'brightness_pct') | int(1) }} + max_brightness: > + {{ state_attr('schedule.adaptive_lighting_profile', 'brightness_pct') | int(1) }} + min_color_temp: > + {{ state_attr('schedule.adaptive_lighting_profile', 'color_temp_kelvin') | int(2000) }} + max_color_temp: > + {{ state_attr('schedule.adaptive_lighting_profile', 'color_temp_kelvin') | int(2000) }} + default: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + use_defaults: configuration + mode: restart +``` + +This creates step changes at block boundaries. It does not interpolate between schedule points. Runtime settings also reset when Home Assistant restarts, so the startup trigger reapplies the active block. The default branch restores every configured setting; restore only the four fields explicitly if other automations also change runtime settings. + +
+ +
+Reduce daytime brightness when an illuminance sensor detects strong daylight. + +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml). Select the profile and sensor, then set the lux thresholds and brightness limits. The high lux threshold must exceed the low threshold; the blueprint does nothing if they are reversed or equal. + +Keep a low configured `min_brightness` for late night and let an automation lower `max_brightness` while the room has ample daylight. Use a sensor that is not significantly affected by the controlled lights to avoid a feedback loop. + +```yaml +- alias: "Adaptive lighting: limit brightness in daylight" + triggers: + - trigger: numeric_state + entity_id: sensor.living_room_illuminance + above: 300 + - trigger: numeric_state + entity_id: sensor.living_room_illuminance + below: 200 + - trigger: homeassistant + event: start + id: startup + actions: + - if: + - condition: trigger + id: startup + then: + - wait_template: > + {{ is_number(states('sensor.living_room_illuminance')) }} + timeout: "00:05:00" + continue_on_timeout: false + - choose: + - conditions: + - condition: numeric_state + entity_id: sensor.living_room_illuminance + above: 300 + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + max_brightness: 30 + - conditions: + - condition: numeric_state + entity_id: sensor.living_room_illuminance + below: 200 + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + max_brightness: 100 + mode: restart +``` + +The separate 200 and 300 lux thresholds add hysteresis. After a restart, the automation waits for a numeric sensor state before evaluating it. If the initial value is between the thresholds, Adaptive Lighting keeps its configured maximum. Replace `30` and `100` with your desired daytime limit and normal maximum. + +`min_brightness` and `max_brightness` are the solar-midnight and daytime endpoints of the brightness curve. Setting `min_brightness` higher than `max_brightness` is supported and creates an inverted curve that is brighter at night and dimmer during the day. If you only want a daytime limit, keep the reduced maximum at or above the configured minimum. + +
+ +
+Turn on Hue-controlled lights with the current Adaptive Lighting values. + +For a Hue button exposed to Home Assistant, call this script from the button automation. It turns on the listed lights directly with the current Adaptive Lighting brightness and color. + +```yaml +script: + living_room_adaptive_lighting: + alias: "Living room: adaptive lighting" + sequence: + - action: adaptive_lighting.apply + data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.living_room_ceiling + - light.living_room_table + turn_on_lights: true + transition: 0 +``` + +This requires Home Assistant to receive the button event. The one-shot `apply` call works while the main Adaptive Lighting switch is off, turns on the listed lights, and applies values even if a light is marked as manually controlled. It leaves the profile switch and manual-control state unchanged. + +Adaptive Lighting does not update scenes stored on the Hue Bridge, so scenes activated only inside Hue cannot use this script and retain Hue's operation when Home Assistant is unavailable. + +
+ +
+Use a fixed RGB stage before sleep mode. + +This script starts sleep mode with a fixed dim red color, waits 30 minutes, and then restores the configured Adaptive Lighting settings. The main profile switch and the light must already be on. + +```yaml +script: + adaptive_lighting_bedtime: + alias: "Adaptive lighting: bedtime" + mode: restart + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_bedroom + sleep_rgb_or_color_temp: rgb_color + sleep_rgb_color: [255, 56, 0] + sleep_brightness: 20 + - action: switch.turn_on + target: + entity_id: switch.adaptive_lighting_bedroom_sleep_mode + - delay: "00:30:00" + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_bedroom + use_defaults: configuration +``` + +The light must support RGB color. The first stage uses a fixed brightness rather than following the normal brightness curve. When sleep mode changes from off to on, the default `reset_manual_control_on_sleep_mode_change: true` returns manually controlled lights to Adaptive Lighting control so they receive the stage. If you disable that option, manually controlled lights remain paused. Restoring configuration defaults resets every runtime setting on this Adaptive Lighting switch, so restore only the sleep fields explicitly if other automations also change runtime settings. + +Stopping this script or reloading scripts during the delay prevents the final action, leaving the runtime overrides active. To recover, call `adaptive_lighting.change_switch_settings` for the profile with `use_defaults: configuration`. A Home Assistant restart reloads the configured settings. + +
+ +
+Run a fixed virtual day across midnight. + +Fixed virtual sunrise and sunset times can cross midnight. This configuration ramps an indoor garden from its minimum at 16:00 to its maximum at 22:00, then back to its minimum at 04:00. + +```yaml +adaptive_lighting: + - name: "Indoor Garden" + lights: + - light.indoor_garden + sunrise_time: "16:00:00" + sunset_time: "04:00:00" + min_brightness: 10 + max_brightness: 100 + brightness_mode: linear + brightness_mode_time_dark: 0 + brightness_mode_time_light: 21600 # 6 hours +``` + +Adaptive Lighting changes brightness and color while a light is on; it does not manage the light's power schedule. This separate automation turns the example light on and off: + +```yaml +- alias: "Indoor garden: power schedule" + triggers: + - trigger: time + at: "16:00:00" + id: turn_on + - trigger: time + at: "04:00:00" + id: turn_off + - trigger: homeassistant + event: start + id: startup + actions: + - choose: + - conditions: + - condition: trigger + id: turn_on + sequence: + - action: light.turn_on + target: + entity_id: light.indoor_garden + - conditions: + - condition: trigger + id: startup + - condition: time + after: "16:00:00" + before: "04:00:00" + sequence: + - action: light.turn_on + target: + entity_id: light.indoor_garden + default: + - action: light.turn_off + target: + entity_id: light.indoor_garden +``` + +Use `min_sunrise_time`, `max_sunrise_time`, `min_sunset_time`, or `max_sunset_time` instead when you want to constrain astronomical sunrise or sunset to an earliest or latest time rather than replace it. + +
+ + + +> [!TIP] +> **Have a useful automation?** Share your automation examples by [opening an issue](https://github.com/basnijholt/adaptive-lighting/issues) or submitting a pull request to the README. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..afd05721 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,147 @@ +--- +icon: lucide/settings +--- + +# Configuration + +Adaptive Lighting supports configuration through both YAML and the Home Assistant UI, with identical option names in both methods. + +## Basic Configuration + +The simplest setup uses the Home Assistant UI. Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting**. No `adaptive_lighting:` entry is needed in `configuration.yaml`. + +## YAML Configuration + +Alternatively, you can specify lights and options in `configuration.yaml`: + +```yaml +adaptive_lighting: + - name: "Living Room" + lights: + - light.living_room_ceiling + - light.living_room_lamp +``` + +## All Options + +All configuration options are listed below with their default values. These options work identically in both YAML and the UI. + + + + + + + +| Variable name | Description | Default | Type | +|:--------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `manual_control_on_external_turn_on` | Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `reset_manual_control_on_sleep_mode_change` | Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴 | `True` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `expand_light_groups` | Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets. | `True` | `bool` | + + + +## Full Configuration Example + + + + + + + +Full example: + +```yaml +# Example configuration.yaml entry +adaptive_lighting: +- name: "default" + lights: [] + prefer_rgb_color: false + transition: 45 + initial_transition: 1 + interval: 90 + min_brightness: 1 + max_brightness: 100 + min_color_temp: 2000 + max_color_temp: 5500 + sleep_brightness: 1 + sleep_color_temp: 1000 + sunrise_time: "08:00:00" # override the sunrise time + sunrise_offset: + sunset_time: + sunset_offset: 1800 # in seconds or '00:30:00' + take_over_control: true + detect_non_ha_changes: false + only_once: false + +``` + + + +## Multiple Configurations + +You can create multiple Adaptive Lighting configurations for different areas or use cases: + +```yaml +adaptive_lighting: + - name: "Daytime Spaces" + lights: + - light.living_room + - light.kitchen + - light.office + min_brightness: 30 + max_brightness: 100 + + - name: "Bedroom" + lights: + - light.bedroom_ceiling + - light.bedroom_lamp + min_brightness: 5 + max_brightness: 80 + sleep_brightness: 1 + sleep_color_temp: 1000 +``` + +## Related Topics + +- [Brightness Modes](advanced/brightness-modes.md) - Detailed explanation of brightness calculation modes +- [Sleep Mode](advanced/sleep-mode.md) - Sleep mode configuration +- [Manual Control](advanced/manual-control.md) - How manual control detection works diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..706a9640 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,102 @@ +--- +icon: lucide/rocket +--- + +# Getting Started + +This guide will help you install and configure Adaptive Lighting for the first time. + +## Prerequisites + +- [Home Assistant](https://www.home-assistant.io/) 2025.9.0 or newer +- [HACS](https://hacs.xyz/) (Home Assistant Community Store) installed + +## Installation + +### Via HACS (Recommended) + +1. Open HACS in your Home Assistant instance +2. Click on **Integrations** +3. Click the **+ Explore & Download Repositories** button +4. Search for "Adaptive Lighting" +5. Click **Download** +6. Restart Home Assistant + +Or use this button to open HACS directly: + +[![Open your Home Assistant instance and open the Adaptive Lighting integration inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=basnijholt&repository=adaptive-lighting&category=integration) + +### Manual Installation + +1. Download the latest release from [GitHub](https://github.com/basnijholt/adaptive-lighting/releases) +2. Extract the `adaptive_lighting` folder to your `config/custom_components/` directory +3. Restart Home Assistant + +## Configuration + +Choose one of two configuration methods: + +=== "Via UI" + + 1. Go to **Settings** → **Devices & Services** + 2. Click **+ Add Integration** + 3. Search for "Adaptive Lighting" + 4. Follow the setup wizard to name your Adaptive Lighting instance + 5. Find Adaptive Lighting and click **Configure** + 6. Select your lights and adjust the settings + + No `adaptive_lighting:` entry is needed in `configuration.yaml`. + +=== "Via YAML" + + Instances configured through YAML must be edited in YAML. + + ```yaml + adaptive_lighting: + - name: "Living Room" + lights: + - light.living_room_ceiling + - light.living_room_lamp + min_brightness: 20 + max_brightness: 100 + min_color_temp: 2200 + max_color_temp: 5500 + ``` + + Restart Home Assistant after changing the YAML configuration. + +## Basic YAML Configuration Example + +Here's a simple configuration to get you started: + +```yaml +adaptive_lighting: + - name: "Main Lights" + lights: + - light.living_room + - light.bedroom + - light.kitchen + transition: 30 + min_brightness: 10 + max_brightness: 100 + min_color_temp: 2000 + max_color_temp: 5500 +``` + +## Verifying Installation + +After configuration, you should see new switches in Home Assistant: + +- `switch.adaptive_lighting_main_lights` +- `switch.adaptive_lighting_sleep_mode_main_lights` +- `switch.adaptive_lighting_adapt_brightness_main_lights` +- `switch.adaptive_lighting_adapt_color_main_lights` + +Turn on `switch.adaptive_lighting_main_lights` to start adapting your lights! + +## Next Steps + +- [Configuration Reference](configuration.md) - Explore all available options +- [Services](services.md) - Learn about service calls for automations +- [Automation Examples](automation-examples.md) - See real-world automation recipes +- [Troubleshooting](troubleshooting.md) - Common issues and solutions diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..f01cbfa2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,74 @@ +--- +icon: lucide/sun +--- + +# Adaptive Lighting + +**Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting** + +
+ Adaptive Lighting Logo +
+ +[Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights based on the sun's position, while still allowing for manual control. + +
+ + Install via HACS + + + Try the Simulator + +
+ +By automatically adapting the settings of your lights throughout the day, Adaptive Lighting helps maintain your natural circadian rhythm, which can lead to improved sleep, mood, and overall well-being. Experience cooler color temperatures at noon, gradually transitioning to warmer colors at sunset and sunrise. + +## Features + + + + + + + +When initially turning on a light that is controlled by Adaptive Lighting, the `light.turn_on` service call is intercepted, and the light's brightness and color are automatically adjusted based on the sun's position. +After that, the light's brightness and color are automatically adjusted at a regular interval. + +Adaptive Lighting provides four switches (using "living_room" as an example component name): + +- `switch.adaptive_lighting_living_room`: Turn Adaptive Lighting on or off and view current light settings through its attributes. +- `switch.adaptive_lighting_sleep_mode_living_room`: Activate "sleep mode" 😴 and set custom sleep_brightness and sleep_color_temp. +- `switch.adaptive_lighting_adapt_brightness_living_room`: Enable or disable brightness adaptation 🔆 for supported lights. +- `switch.adaptive_lighting_adapt_color_living_room`: Enable or disable color adaptation 🌈 for supported lights. + + + +## Quick Start + +1. **Install via HACS**: Search for "Adaptive Lighting" in the [Home Assistant Community Store](https://hacs.xyz/) +2. **Add the integration**: Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting**, then name your instance +3. **Configure**: Open Adaptive Lighting, click **Configure**, select your lights, and adjust the settings. No YAML entry is needed. + +[Get Started →](getting-started.md){ .md-button .md-button--primary } +[View All Options →](configuration.md){ .md-button } + +## How It Works + +Adaptive Lighting provides four switches for each configuration (using "living_room" as an example): + +| Switch | Purpose | +|--------|---------| +| `switch.adaptive_lighting_living_room` | Main on/off control | +| `switch.adaptive_lighting_sleep_mode_living_room` | Activate sleep mode | +| `switch.adaptive_lighting_adapt_brightness_living_room` | Enable/disable brightness adaptation | +| `switch.adaptive_lighting_adapt_color_living_room` | Enable/disable color adaptation | + +## Interactive Simulator + +Visualize how Adaptive Lighting will work with your settings using the interactive simulator: + +
+ + Try the Interactive Simulator + +
diff --git a/docs/overrides/partials/integrations/analytics/custom.html b/docs/overrides/partials/integrations/analytics/custom.html new file mode 100644 index 00000000..c5e49c15 --- /dev/null +++ b/docs/overrides/partials/integrations/analytics/custom.html @@ -0,0 +1,6 @@ + + + diff --git a/docs/run_markdown_code_runner.py b/docs/run_markdown_code_runner.py new file mode 100755 index 00000000..3a4b59f8 --- /dev/null +++ b/docs/run_markdown_code_runner.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# ruff: noqa: T201, S603, S607 +"""Update all markdown files that use markdown-code-runner for auto-generation. + +Run from repo root: uv run python docs/run_markdown_code_runner.py +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def find_markdown_files_with_code_blocks(docs_dir: Path) -> list[Path]: + """Find all markdown files containing markdown-code-runner markers.""" + files_with_code = [] + for md_file in docs_dir.rglob("*.md"): + content = md_file.read_text() + if "" in content: + files_with_code.append(md_file) + return sorted(files_with_code) + + +def run_markdown_code_runner(files: list[Path], repo_root: Path) -> bool: + """Run markdown-code-runner on all files. Returns True if all succeeded.""" + if not files: + print("No files with CODE:START markers found.") + return True + + print(f"Found {len(files)} file(s) with auto-generated content:") + for f in files: + print(f" - {f.relative_to(repo_root)}") + print() + + all_success = True + for file in files: + rel_path = file.relative_to(repo_root) + print(f"Updating {rel_path}...", end=" ", flush=True) + result = subprocess.run( + ["markdown-code-runner", str(file)], + check=False, + capture_output=True, + text=True, + ) + if result.returncode == 0: + print("✓") + else: + print("✗") + print(f" Error: {result.stderr}") + all_success = False + + return all_success + + +def main() -> int: + """Main entry point.""" + repo_root = Path(__file__).parent.parent + + # Process docs/ files and README.md + files = find_markdown_files_with_code_blocks(repo_root / "docs") + readme = repo_root / "README.md" + if readme.exists() and "" in readme.read_text(): + files.append(readme) + + success = run_markdown_code_runner(files, repo_root) + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/see-also.md b/docs/see-also.md new file mode 100644 index 00000000..5b06fcae --- /dev/null +++ b/docs/see-also.md @@ -0,0 +1,73 @@ +--- +icon: lucide/external-link +--- + +# See Also + +Resources, tutorials, and related projects for Adaptive Lighting. + +## Tutorials & Articles + + + + + + + +- [*Sleep better with Adaptive Lighting in Home Assistant*](https://wartner.io/sleep-better-with-adaptive-lightning-in-home-assistant/) by Florian Wartner on 2023-02-23 (blog post 📜) +- [*Automatic smart light brightness and color based on the sun*](https://www.youtube.com/watch?v=Rg3zI1Oyk3c) by Home Automation Guy on 2022-08-31 (YouTube video 📺) +- [*Adaptive Lighting Blew My Mind in Home Assistant - How to set it up*](https://www.youtube.com/watch?v=c1cnccmgl3k) by Smart Home Junkie on 2022-06-26 (YouTube video 📺) + + + +## Interactive Tools + +### Adaptive Lighting Simulator + +Visualize how different settings affect your lighting throughout the day: + +
+ + Open the Simulator + +
+ +The simulator lets you: + +- Adjust all configuration parameters in real-time +- See how brightness and color temperature change throughout the day +- Visualize the effects of different `brightness_mode` settings +- Test sleep mode transitions + +## Related Projects + +### Circadian Lighting + +Adaptive Lighting was initially inspired by [hass-circadian_lighting](https://github.com/claytonjn/hass-circadian_lighting) by @claytonjn, but has since been entirely rewritten and expanded with many new features. + +## Official Documentation + +- [Home Assistant Documentation (PR Preview)](https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/) +- [Home Assistant Community Store (HACS)](https://hacs.xyz/) + +## Community + +- [Home Assistant Community Forums](https://community.home-assistant.io/) +- [Home Assistant Discord](https://discord.gg/home-assistant) +- [Reddit r/homeassistant](https://www.reddit.com/r/homeassistant/) + +## Contributing + +Interested in contributing to Adaptive Lighting? + +- [GitHub Repository](https://github.com/basnijholt/adaptive-lighting) +- [Issue Tracker](https://github.com/basnijholt/adaptive-lighting/issues) +- [Translation via Weblate](https://hosted.weblate.org/engage/adaptive-lighting/) + +### Translation + +Help translate Adaptive Lighting into your language on [Hosted Weblate](https://hosted.weblate.org/engage/adaptive-lighting/). No programming knowledge required! + + +Translation status + diff --git a/docs/services.md b/docs/services.md new file mode 100644 index 00000000..6258017a --- /dev/null +++ b/docs/services.md @@ -0,0 +1,203 @@ +--- +icon: lucide/zap +--- + +# Services + +Adaptive Lighting provides three services for programmatic control, allowing you to integrate with automations and scripts. + +## adaptive_lighting.apply + +Applies the current Adaptive Lighting settings to lights on demand. Useful for forcing an immediate update or applying settings to lights that aren't in the regular adaptation cycle. +Provide a switch in `entity_id`, a list of `lights`, or both. + +### Parameters + + + + + + + +| Service data attribute | Description | Required | Type | +|:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ❌ | list of `entity_id`s | +| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | +| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | +| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | +| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | + + + +### Example Usage + +```yaml +# Apply current settings to specific lights +service: adaptive_lighting.apply +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + - light.desk_lamp + turn_on_lights: false +``` + +```yaml +# Force apply with custom transition +service: adaptive_lighting.apply +data: + entity_id: switch.adaptive_lighting_bedroom + transition: 5 + adapt_brightness: true + adapt_color: true +``` + +--- + +## adaptive_lighting.set_manual_control + +Marks or unmarks a light as "manually controlled". When a light is marked as manually controlled, Adaptive Lighting will not adjust it until the manual control flag is cleared. +Provide a switch in `entity_id`, a list of `lights`, or both. + +### Parameters + + + + + + + +| Service data attribute | Description | Required | Type | +|:-------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------| +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ❌ | list of `entity_id`s | +| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | +| `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` | + + + +### Example Usage + +```yaml +# Remove manual control from a light (resume adaptation) +service: adaptive_lighting.set_manual_control +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + manual_control: false +``` + +```yaml +# Mark a light as manually controlled (pause adaptation) +service: adaptive_lighting.set_manual_control +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + manual_control: true +``` + +```yaml +# Only pause brightness adaptation, continue color adaptation +service: adaptive_lighting.set_manual_control +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + manual_control: brightness +``` + +--- + +## adaptive_lighting.change_switch_settings + + + + + + + +`adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. + +> [!WARNING] +> These settings will **not** be written to your config and will be reset on restart of Home Assistant! You can see the current settings in the `switch.adaptive_lighting_XXX` attributes if `include_config_in_attributes` is enabled. + +| Service data attribute | Required | Description | +| --------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `use_defaults` | ❌ | (default: `current` for current settings) Choose from `factory`, `configuration`, or `current` to reset variables not being set with this service call. `current` leaves them as they are, `configuration` resets to initial startup values, `factory` resets to default values listed in the documentation. | +| **all other keys** (except the ones in the table below ⚠️) | ❌ | See the table below for disallowed keys. | + +The following keys are disallowed: + +| **DISALLOWED** service data | Description | +| --------------------------- | ----------------------------------------------------------------------------------------------- | +| `entity_id` | You cannot change the switch's `entity_id`, as it has already been registered. | +| `lights` | You may call `adaptive_lighting.apply` with your lights or create a new config instead. | +| `name` | You can rename your switch's display name in Home Assistant's UI. | +| `interval` | The interval is used only once when the config loads. A config change and restart are required. | + + + +### Example Usage + +```yaml +# Temporarily change color temperature range +service: adaptive_lighting.change_switch_settings +data: + entity_id: switch.adaptive_lighting_living_room + min_color_temp: 2500 + max_color_temp: 4000 +``` + +```yaml +# Override sunrise time for the day +service: adaptive_lighting.change_switch_settings +data: + entity_id: switch.adaptive_lighting_bedroom + sunrise_time: "07:00:00" + use_defaults: current +``` + +```yaml +# Reset to configuration defaults +service: adaptive_lighting.change_switch_settings +data: + entity_id: switch.adaptive_lighting_living_room + use_defaults: configuration +``` + +--- + +## Events + +Adaptive Lighting also fires events that you can use in automations. + +### adaptive_lighting.manual_control + +Fired when a light is marked as "manually controlled" due to a detected manual change. + +**Event Data:** + +| Attribute | Description | +|-----------|-------------| +| `entity_id` | The light that was marked as manually controlled | +| `switch` | The Adaptive Lighting switch entity | + +### Example Automation + +```yaml +automation: + - alias: "Log manual control events" + trigger: + platform: event + event_type: adaptive_lighting.manual_control + action: + - service: notify.mobile_app + data: + title: "Adaptive Lighting" + message: "{{ trigger.event.data.entity_id }} was manually controlled" +``` + +See [Automation Examples](automation-examples.md) for more use cases. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..f0596563 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,133 @@ +--- +icon: lucide/life-buoy +--- + +# Troubleshooting + +This guide covers common issues and their solutions when using Adaptive Lighting. + +## Enable Debug Logging + + + + + + + +Encountering issues? Enable debug logging in your `configuration.yaml`: + +```yaml +logger: + default: warning + logs: + custom_components.adaptive_lighting: debug +``` + +After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). + +For support, use Home Assistant's **Download diagnostics** action on the +Adaptive Lighting config entry. The download is an on-demand snapshot of the +profile's current switches and currently tracked light targets. It does not +refresh group membership or predict targets a disabled profile would use after +being enabled. It does not create live sensors; existing switch attributes +remain the interface for automations. +The reported last adaptation values are the shared manager's latest retained +value for each attribute. They can come from different commands and do not +represent one sent command or the current desired state. + + + +## Common Problems & Solutions + + + + + + + +#### :bulb: Lights Only Adapt After Reloading + +If lights stop adapting after you turn them on with a physical switch or a Zigbee-bound remote, check the Adaptive Lighting switch's `manual_control` attribute. With `take_over_control: true` and `detect_non_ha_changes: false`, a turn-on without a matching Home Assistant `light.turn_on` call marks the light as manually controlled. Reloading clears that state, but the next physical turn-on can trigger it again. + +To adapt these turn-ons while still detecting later manual changes, enable `detect_non_ha_changes` and leave `manual_control_on_external_turn_on` disabled. This requires the light integration to report its state reliably. If you want Adaptive Lighting to keep adapting regardless of manual changes, disable `take_over_control` along with the options that require it: `detect_non_ha_changes`, `adapt_only_on_bare_turn_on`, and `manual_control_on_external_turn_on`. + +This explains the physical-switch case in [#1056](https://github.com/basnijholt/adaptive-lighting/issues/1056), but not every report in that thread. If the light is not listed in `manual_control`, include diagnostics and debug logs from the failed turn-on when reporting it. Lights returning from `unavailable` after a power cut are a separate case from an `off` to `on` state change. + +#### :bulb: Lights Not Responding or Turning On by Themselves + +Adaptive Lighting sends more commands to lights than a typical human user would. If your light control network is unhealthy, you may experience: + +- Laggy manual commands (e.g., turning lights on or off). +- Unresponsive lights. +- Home Assistant reporting incorrect light states, causing Adaptive Lighting to inadvertently turn lights back on. + +Most issues that appear to be caused by Adaptive Lighting are actually due to unrelated problems. +Addressing these issues will significantly improve your Home Assistant experience. + +In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action. +To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`. + +To keep detecting manual changes to lights that are already on while leaving unmatched `off` to `on` state events unchanged, enable `manual_control_on_external_turn_on`. Matching uses the exact context of the most recently recorded `light.turn_on` call. Some integrations replace or omit that context, so Adaptive Lighting cannot distinguish every physical versus Home Assistant turn-on source. + +#### :signal_strength: WiFi Networks + +Ensure your light bulbs have a strong WiFi connection. If the signal strength is less than -70dBm, the connection may be weak and prone to dropping messages. + +#### :spider_web: Zigbee, Z-Wave, and Other Mesh Networks + +Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). +Most modern lights function as routers, very early models may not. +If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. +Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. +Smart plugs can be an affordable way to add more routers to your network. + +For most Zigbee networks, **using groups is essential for optimal performance**. +For example, if you want to use Adaptive Lighting in a hallway with six bulbs, adding each bulb individually to the Adaptive Lighting configuration could overwhelm the network with commands. +Instead, create a group in your Zigbee software (not a regular Home Assistant group) and add that single group to the Adaptive Lighting configuration. +This sends a single broadcast command to adjust all bulbs, improving response times and keeping the bulbs in sync. + +As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. +Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. + +> :warning: **If you control lights individually, `manual_control` cannot behave correctly! If you need to control lights individually as well, use a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/).** + +When mixing group types, avoid nesting: do not add integration-level groups (e.g., Zigbee2MQTT groups) to a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/) that is managed by Adaptive Lighting, and do not nest Home Assistant Light Groups inside each other. +Adaptive Lighting cannot expand an integration-level group into its member lights, and nested groups make it unpredictable which entity Adaptive Lighting tracks and adapts, which can prevent lights from being adapted at all (see [#1378](https://github.com/basnijholt/adaptive-lighting/issues/1378)). +Instead, add the individual light entities or a single Zigbee group directly to the Adaptive Lighting configuration. +Also note that bulbs turned on via a Zigbee group broadcast may briefly flash their last (cached) brightness and color before the adapted values arrive; this happens inside the bulbs and cannot be prevented by Home Assistant or Adaptive Lighting. + +#### :rainbow: Light Colors Not Matching + +Bulbs from different manufacturers or models may have varying color temperature specifications. For instance, if you have two Adaptive Lighting configurations—one with only Philips Hue White Ambiance bulbs and another with a mix of Philips Hue White Ambiance and Sengled bulbs—the Philips Hue bulbs may appear to have different color temperatures despite having identical settings. + +To resolve this: + +1. Include only bulbs of the same make and model in a single Adaptive Lighting configuration. +2. Rearrange bulbs so that different color temperatures are not visible simultaneously. + +#### :bulb: Bulb-Specific Issues + +These lights are known to exhibit disadvantageous behaviour due to firmware bugs, insufficient functionality, or hardware limitations: + +- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26) + - Unexpected turn-ons: If Adaptive Lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off during that time, it may turn back on after approximately 10 seconds to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs indicating the cause of the light turning on. To fix this, set a much shorter `transition` time, such as 1 second. + - Heat sensitivity: Additionally, these bulbs may perform poorly in enclosed "dome" style ceiling lights, particularly when hot. While most LEDs (even non-smart ones) state in the fine print that they do not support working in enclosed fixtures, in practice, more expensive bulbs like Philips Hue generally perform better. To resolve this issue, move the problematic bulbs to open-air fixtures. +- Ikea Tradfri bulbs/drivers (and related Ikea smart light products) + - Unsupported simultaneous transition of brightness and color: When receiving such a command, they switch the brightness instantly and only transition the color. To get smooth transitions of both brightness and color, enable `separate_turn_on_commands`. + - Unresponsiveness during color transitions: No other commands are processed during an ongoing color transition, e.g., turn-off commands are ignored and lights stay on despite being reported as off to Home Assistant. The default config with long transitions thus results in long periods of unresponsiveness. To work around this, disable transitions by setting `transition` to `0`, and increase the adaptation frequency by setting `interval` to a short time, e.g., `15` seconds, to retain the impression of smooth continuous adaptations. Keeping the `initial_transition` is recommended for a smooth fade-in (lights are usually not turned off momentarily after being turned on, in which case a short period of unresponsiveness is tolerable). +- [Lonsonho ZB-RGBCW](https://www.zigbee2mqtt.io/devices/ZB-RGBCW.html#lonsonho-zb-rgbcw) + - Some Zigbee2MQTT/eWeLight firmware combinations do not turn the bulb on when the initial `light.turn_on` call includes brightness or color, although later adjustments work. Disable `intercept` for affected bulbs. + + + +## Getting Help + +If you're still having issues: + +1. Enable debug logging and capture the relevant logs +2. Open an issue on [GitHub](https://github.com/basnijholt/adaptive-lighting/issues) +3. Include: + - Your configuration (redact sensitive info) + - Debug logs + - Home Assistant version + - Description of expected vs actual behavior diff --git a/hacs.json b/hacs.json new file mode 100644 index 00000000..0887a9a7 --- /dev/null +++ b/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "Adaptive Lighting", + "render_readme": true, + "homeassistant": "2025.9.0" +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..29bee618 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "adaptive-lighting" +version = "1.32.0" +description = "Automatically adjust brightness and color of lights based on the sun position" +readme = "README.md" +license = "Apache-2.0" +requires-python = ">=3.12" + +[dependency-groups] +docs = [ + "astral", + "homeassistant", + "markdown-code-runner>=2.7.0", + "markdown-gfm-admonition", + "pandas", + "shinylive", + "tabulate", + "ulid-transform", + "voluptuous", + "zensical", +] +dev = [ + "mypy", + "pre-commit", + "ruff", +] + +[tool.hatch.build.targets.wheel] +packages = ["custom_components/adaptive_lighting"] + +[tool.ruff] +target-version = "py312" +line-length = 88 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "UP", # pyupgrade + "RUF", # Ruff-specific rules + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.lint.isort] +known-first-party = ["adaptive_lighting"] + +[tool.mypy] +python_version = "3.12" +warn_return_any = true +warn_unused_configs = true +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" diff --git a/scripts/develop b/scripts/develop new file mode 100755 index 00000000..ea36af03 --- /dev/null +++ b/scripts/develop @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -ex + +cd "$(dirname "$0")/.." + +# Create config dir if not present +if [[ ! -d "${PWD}/config" ]]; then + mkdir -p "${PWD}/config" + hass --config "${PWD}/config" --script ensure_config +fi + +# Set the path to custom_components +## This let's us have the structure we want /custom_components/adaptive_lighting +## while at the same time have Home Assistant configuration inside /config +## without resulting to symlinks. +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +uv run hass --config "${PWD}/config" --debug diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 00000000..eadfe510 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +uv run pre-commit run --all-files diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies new file mode 100755 index 00000000..0a96dd77 --- /dev/null +++ b/scripts/setup-dependencies @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -ex +cd "$(dirname "$0")/.." + +# Remove mypy-dev from requirements_test.txt since the maintainer deletes old versions from PyPI. +# We'll install the latest version separately below. +# See: https://github.com/cdce8p/mypy-dev/issues/62 +grep -v '^mypy-dev' core/requirements_test.txt > core/requirements_test.txt.tmp && mv core/requirements_test.txt.tmp core/requirements_test.txt + +uv pip install -r core/requirements.txt +uv pip install -r core/requirements_test.txt + +# HA 2026.4+ imports aiohasupervisor from tests/components/conftest.py +# but pins it in requirements_test_all.txt instead of requirements_test.txt. +# HA 2026.8+ removed requirements_test_all.txt (home-assistant/core#171530); +# the pin lives in requirements_all.txt there. +aiohasupervisor_req="" +if [[ -f core/requirements_test_all.txt ]]; then + aiohasupervisor_req="$(grep -m1 '^aiohasupervisor' core/requirements_test_all.txt || true)" +elif [[ -f core/requirements_all.txt ]]; then + aiohasupervisor_req="$(grep -m1 '^aiohasupervisor' core/requirements_all.txt || true)" +fi +if [[ -n "${aiohasupervisor_req}" ]]; then + uv pip install "${aiohasupervisor_req}" +fi + +# HA 2026.4+ validates service translations through translations/en.json. +# The core checkout keeps English source strings in strings.json, so seed en.json +# in the temporary CI checkout before tests load integrations. +for strings_file in core/homeassistant/components/*/strings.json; do + [[ -f "${strings_file}" ]] || continue + translations_dir="$(dirname "${strings_file}")/translations" + en_translation="${translations_dir}/en.json" + if [[ ! -f "${en_translation}" ]]; then + mkdir -p "${translations_dir}" + cp "${strings_file}" "${en_translation}" + fi +done + +uv pip install -e core/ +uv pip install ulid-transform # this is in Adaptive-lighting's manifest.json +uv pip install $(python3 test_dependencies.py) + +# Install the latest mypy-dev (not pinned since old versions get deleted from PyPI) +uv pip install --upgrade mypy-dev + +# Workaround for aiodns/pycares compatibility issue +# See: https://github.com/aio-libs/aiodns/issues/214 +uv pip install --upgrade aiodns diff --git a/scripts/setup-devcontainer b/scripts/setup-devcontainer new file mode 100755 index 00000000..31ad774e --- /dev/null +++ b/scripts/setup-devcontainer @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -ex +cd "$(dirname "$0")/.." + +# Clone only if the folder doesn't exist +if [[ ! -d "core" ]]; then + git clone --depth 1 --branch dev https://github.com/home-assistant/core.git +fi + +pip install \ + colorlog \ + pip \ + ruff \ + uv + +pip cache purge + +uv venv --clear --python 3.14.2 +./scripts/setup-dependencies +./scripts/setup-symlinks +uv run pre-commit install-hooks diff --git a/scripts/setup-symlinks b/scripts/setup-symlinks new file mode 100755 index 00000000..af95b831 --- /dev/null +++ b/scripts/setup-symlinks @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -ex +cd "$(dirname "$0")/.." + +# '-n' keeps a re-run idempotent: without it 'ln -fs' follows an existing +# symlink and creates the new link *inside* the target directory, leaving a +# stray 'tests/tests' and 'custom_components/adaptive_lighting/adaptive_lighting' +# in the working tree. + +# Link custom components +cd core/homeassistant/components/ +ln -fsn ../../../custom_components/adaptive_lighting adaptive_lighting +cd - + +# Link tests +cd core/tests/components/ +ln -fsn ../../../tests/ adaptive_lighting +cd - diff --git a/scripts/update-generated-content b/scripts/update-generated-content new file mode 100755 index 00000000..1fed3178 --- /dev/null +++ b/scripts/update-generated-content @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -ex + +cd "$(dirname "$0")/.." + +uv sync --group docs +uv pip install -e . +uv run python docs/run_markdown_code_runner.py +uv run python .github/update-services.py +uv run python .github/update-strings.py diff --git a/scripts/update-test-matrix.py b/scripts/update-test-matrix.py new file mode 100755 index 00000000..fea69423 --- /dev/null +++ b/scripts/update-test-matrix.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Update the pytest workflow matrix with latest HA Core versions. + +This script fetches the latest Home Assistant Core release versions from GitHub +and updates the pytest workflow matrix to test against them. + +Usage: + python scripts/update-test-matrix.py +""" + +from __future__ import annotations + +import json +import re +import urllib.request +from pathlib import Path + +# Keep the latest stable release month and the preceding 12 monthly release lines. +# Update this explicit floor when dropping another supported release month. +MIN_VERSION = (2025, 9) + + +def get_ha_core_versions() -> list[str]: + """Fetch latest stable HA Core versions from GitHub API.""" + all_tags = [] + page = 1 + + # Paginate through all tags to ensure we get older versions too + while True: + url = f"https://api.github.com/repos/home-assistant/core/tags?per_page=100&page={page}" + with urllib.request.urlopen(url) as response: + tags = json.loads(response.read().decode()) + + if not tags: + break + + all_tags.extend(tags) + + # Check if we've gone far enough back + # Stop if we've found versions older than our minimum + oldest_in_page = None + for t in tags: + if re.match(r"^\d+\.\d+\.\d+$", t["name"]): + parts = t["name"].split(".") + year, month = int(parts[0]), int(parts[1]) + if oldest_in_page is None or (year, month) < oldest_in_page: + oldest_in_page = (year, month) + + if oldest_in_page and oldest_in_page < MIN_VERSION: + break + + page += 1 + if page > 10: # Safety limit + break + + # Filter to stable releases only (no beta/rc) + stable_pattern = re.compile(r"^\d+\.\d+\.\d+$") + versions = [t["name"] for t in all_tags if stable_pattern.match(t["name"])] + + # Group by minor version and get latest patch for each + latest: dict[str, str] = {} + for version in versions: + parts = version.split(".") + year, month = int(parts[0]), int(parts[1]) + # Only include versions >= MIN_VERSION + if (year, month) >= MIN_VERSION: + minor_key = f"{parts[0]}.{parts[1]}" + # Keep the one with highest patch number + if minor_key not in latest: + latest[minor_key] = version + else: + current_patch = int(latest[minor_key].split(".")[2]) + new_patch = int(parts[2]) + if new_patch > current_patch: + latest[minor_key] = version + + # Sort by version + return sorted(latest.values(), key=lambda v: [int(x) for x in v.split(".")]) + + +def get_python_version(ha_version: str) -> str: + """Determine Python version based on HA Core version.""" + parts = ha_version.split(".") + year, month = int(parts[0]), int(parts[1]) + # 2025.9 through 2026.2 use Python 3.13. + # 2026.3+ uses Python 3.14. + if year > 2026 or (year == 2026 and month >= 3): + return "3.14.2" + return "3.13" + + +def generate_matrix_yaml(versions: list[str]) -> str: + """Generate the YAML matrix include block.""" + lines = [] + for version in versions: + python_ver = get_python_version(version) + lines.append(f' - core-version: "{version}"') + lines.append(f' python-version: "{python_ver}"') + # Add dev version + lines.append(' - core-version: "dev"') + lines.append(' python-version: "3.14.2"') + return "\n".join(lines) + + +def update_workflow_file(workflow_path: Path, new_matrix: str) -> bool: + """Update the workflow file with new matrix. Returns True if changed.""" + content = workflow_path.read_text() + + # Pattern to match the matrix include block + # Matches from "include:" to just before " steps:" + pattern = re.compile( + r"( include:\n)(.*?)( steps:)", + re.DOTALL, + ) + + def replacer(match: re.Match) -> str: + return f"{match.group(1)}{new_matrix}\n{match.group(3)}" + + new_content = pattern.sub(replacer, content) + + if new_content == content: + return False + + workflow_path.write_text(new_content) + return True + + +def main() -> None: + """Main entry point.""" + print("Fetching latest HA Core versions...") # noqa: T201 + versions = get_ha_core_versions() + print(f"Found {len(versions)} versions: {', '.join(versions)}") # noqa: T201 + + print("\nGenerating matrix...") # noqa: T201 + matrix = generate_matrix_yaml(versions) + print(matrix) # noqa: T201 + + workflow_path = Path(__file__).parent.parent / ".github/workflows/pytest.yaml" + print(f"\nUpdating {workflow_path}...") # noqa: T201 + + if update_workflow_file(workflow_path, matrix): + print("Workflow updated successfully!") # noqa: T201 + else: + print("No changes needed.") # noqa: T201 + + +if __name__ == "__main__": + main() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..a82d84b8 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,15 @@ +[isort] +force_sort_within_sections=True +profile=black + +[flake8] +ignore = E203, E266, W503 +max-line-length = 100 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +per-file-ignores = + code_example.py: E402, E501 + +[tool:pytest] +testpaths = tests +asyncio_mode = auto diff --git a/test_dependencies.py b/test_dependencies.py new file mode 100644 index 00000000..029e1beb --- /dev/null +++ b/test_dependencies.py @@ -0,0 +1,48 @@ +"""Extracts the dependencies of the components required for testing.""" + +from collections import defaultdict +from pathlib import Path + +deps = defaultdict(list) +components, packages = [], [] + +requirements = Path("core") / "requirements_test_all.txt" +if not requirements.exists(): + # Removed from HA core in 2026.8 (home-assistant/core#171530); the same + # per-integration annotations live in requirements_all.txt. + requirements = Path("core") / "requirements_all.txt" + +with requirements.open() as f: + lines = f.readlines() + +for line in lines: + line = line.strip() # noqa: PLW2901 + + if line.startswith("# homeassistant."): + if components and packages: + for component in components: + deps[component].extend(packages) + components, packages = [], [] + components.append(line.split("# homeassistant.")[1]) + elif components and line: + packages.append(line) + +# The last batch of components and packages +if components and packages: + for component in components: + deps[component].extend(packages) + +required = [ + "components.recorder", + "components.mqtt", + "components.zeroconf", + "components.http", + "components.stream", + "components.conversation", # only available after HA≥2023.2 + "components.cloud", + "components.ffmpeg", # needed since 2024.1 +] +to_install = [package for r in required for package in deps[r]] +to_install.append("flaky") + +print(" ".join(to_install)) # noqa: T201 diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..97c609c9 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,60 @@ +# Developer notes for the tests directory + +To run the tests, check out the [CI configuration](../.github/workflows/pytest.yaml) to see how they are executed in the CI pipeline. +Alternatively, you can use the provided Docker image to run the tests locally or run them with VS Code directly in the dev container. + +## Coverage reports + +Open a `pytest` workflow run in GitHub Actions to see line and branch coverage in each job's summary. Download its `coverage--py` artifact for the XML and JSON reports and the browsable HTML report. After extracting it, open `htmlcov/index.html` to inspect missing lines and branches. Supported stable Home Assistant versions require at least 89% line coverage and 80% branch coverage. The `dev` job reports coverage without enforcing these floors. + +Coverage measures executed code, not whether assertions would catch a bug. Add tests for observable behavior: emitted light commands, final states, manual-control events, and timer expiry. The integration suite runs inside Home Assistant with simulated lights; it does not establish physical-device behavior. It also does not execute every documentation generator included in the package's coverage total. + +The tests in `test_automation_examples.py` load YAML directly from `README.md` and execute it through Home Assistant's automation and script engines. Edit the README source when changing those examples, then run `./scripts/update-generated-content` to update the documentation pages. + +## Prerequisites + +Before running tests with Docker, you need a local Home Assistant core checkout with symlinks: + +```bash +# Clone HA core (one-time setup) +git clone --depth 1 https://github.com/home-assistant/core.git core + +# Setup symlinks (one-time setup) +./scripts/setup-symlinks +``` + +## Running tests with Docker + +Navigate to the `adaptive-lighting` repo folder and execute the following command. + +**Important:** Mount the entire repo (`-v $(pwd):/app`), not individual directories, or the symlinks will break. + +Linux / MacOS / Windows PowerShell: +```bash +docker run -v ${PWD}:/app basnijholt/adaptive-lighting:latest +``` + +- In windows command prompt, the command is: + ```bash + docker run -v %cd%:/app basnijholt/adaptive-lighting:latest + ``` + +This command will download the Docker image from [the adaptive-lighting Docker Hub repo](https://hub.docker.com/r/basnijholt/adaptive-lighting) and run the tests. + +If you prefer to build the image yourself, use the following command: + +```bash +docker build -t basnijholt/adaptive-lighting:latest --no-cache --progress=plain . +``` + +This might be necessary if the image on Docker Hub is outdated or if the [`test_dependencies.py`](../test_dependencies.py) file is updated. + +## Passing arguments to pytest + +You can pass arguments to pytest by appending them to the command: + +For example, to run the tests with a custom log format, use the following command (this also gets rid of the captured stderr output): + +```bash +docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest --show-capture=log --log-format="%(asctime)s %(levelname)-8s %(name)s:%(filename)s:%(lineno)s %(message)s" --log-date-format="%H:%M:%S" tests/components/adaptive_lighting/ +``` diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..5ae9fe68 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the Adaptive Lighting integration.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..cce77d96 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,26 @@ +"""Pytest configuration for adaptive-lighting tests.""" + +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_template_deprecation_issue(): + """Mock the template deprecation issue creation. + + The template component's legacy platform syntax creates deprecation + issues that require translations. Since adaptive-lighting tests use + template lights as test fixtures (not testing the template integration + itself), we mock the issue creation to avoid translation validation errors. + """ + # Patch the create_legacy_template_issue function in the template helpers + # to be a no-op when called for the deprecated_legacy_templates issue + try: + with patch( + "homeassistant.components.template.helpers.create_legacy_template_issue", + ): + yield + except (ImportError, ModuleNotFoundError, AttributeError): + # Older HA versions don't have this function + yield diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py new file mode 100644 index 00000000..e0ec07de --- /dev/null +++ b/tests/test_adaptation_utils.py @@ -0,0 +1,603 @@ +"""Tests for Adaptive Lighting utils.""" + +from unittest.mock import Mock + +import pytest +from homeassistant.components.adaptive_lighting.adaptation_utils import ( + LightControlAttributes, + ServiceData, + _create_service_call_data_iterator, + _has_relevant_service_data_attributes, + _remove_redundant_attributes, + _split_service_call_data, + get_light_control_attributes, + has_brightness_attribute, + has_color_attribute, + has_effect_attribute, + manual_control_event_attribute_to_flags, + prepare_adaptation_data, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP_KELVIN, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_TRANSITION, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import Context, State + + +@pytest.mark.parametrize( + ("input_data", "expected_data_list"), + [ + ( + {"foo": 1}, + [], + ), + ( + {ATTR_BRIGHTNESS: 10}, + [{ATTR_BRIGHTNESS: 10}], + ), + ( + {ATTR_COLOR_TEMP_KELVIN: 3500}, + [{ATTR_COLOR_TEMP_KELVIN: 3500}], + ), + ( + {ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}, + [{ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}], + ), + ( + {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500}, + [{ATTR_BRIGHTNESS: 10}, {ATTR_COLOR_TEMP_KELVIN: 3500}], + ), + ( + {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 2}, + [ + {ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 1}, + {ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 1}, + ], + ), + ( + {ATTR_TRANSITION: 1}, + [], + ), + ], + ids=[ + "remove irrelevant attributes", + "brightness only yields one service call", + "color only yields one service call", + "include entity ID", + "brightness and color are split into two with brightness first", + "transition time is distributed among service calls", + "ignore transition time without service calls", + ], +) +async def test_split_service_call_data(input_data, expected_data_list): + """Test splitting of service call data.""" + assert _split_service_call_data(input_data) == expected_data_list + + +@pytest.mark.parametrize( + ("service_data", "state", "service_data_expected"), + [ + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON), + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 10}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 13}), + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 230, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 229}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 230, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 227}), + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 230, ATTR_TRANSITION: 2}, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 5500, + ATTR_TRANSITION: 2, + }, + State("light.test", STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 5495}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 5500, + ATTR_TRANSITION: 2, + }, + State("light.test", STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 5524}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 6500, + ATTR_TRANSITION: 2, + }, + State("light.test", STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 6494}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 5500, + ATTR_TRANSITION: 2, + }, + State("light.test", STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 5400}), + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 5500, + ATTR_TRANSITION: 2, + }, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_HS_COLOR: (30.0, 40.0)}, + State("light.test", STATE_ON, {ATTR_HS_COLOR: (30.0, 40.0)}), + {ATTR_ENTITY_ID: "light.test"}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: None}), + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}, + ), + ], + ids=[ + "pass all attributes on empty state", + "remove attributes whose values equal the state", + "keep attributes whose values differ from the state", + "remove brightness within quantization tolerance (0-99 device scale)", + "keep brightness outside quantization tolerance", + "remove color temp within one mired (round-converting integration)", + "remove color temp within one mired (floor-converting HA core helpers)", + "remove color temp within one mired (6500 K)", + "keep color temp more than one mired away", + "remove non-numeric attributes on exact equality", + "keep attribute when state value is None", + ], +) +async def test_remove_redundant_attributes( + service_data: ServiceData, + state: State | None, + service_data_expected: ServiceData, +): + """Test filtering of service data.""" + assert _remove_redundant_attributes(service_data, state) == service_data_expected + + +@pytest.mark.parametrize( + ("service_data", "expected_relevant"), + [ + ( + {ATTR_ENTITY_ID: "light.test"}, + False, + ), + ( + {ATTR_TRANSITION: 2}, + False, + ), + ( + {ATTR_BRIGHTNESS: 10}, + True, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + True, + ), + ], +) +async def test_has_relevant_service_data_attributes( + service_data: ServiceData, + expected_relevant: bool, +): + """Test the determination of relevancy of service data.""" + assert _has_relevant_service_data_attributes(service_data) == expected_relevant + + +@pytest.mark.parametrize( + ("service_datas", "filter_by_state", "service_datas_expected"), + [ + ( + [{ATTR_ENTITY_ID: "light.test"}], + False, + [{ATTR_ENTITY_ID: "light.test"}], + ), + ( + [{ATTR_ENTITY_ID: "light.test"}, {ATTR_ENTITY_ID: "light.test2"}], + False, + [{ATTR_ENTITY_ID: "light.test"}, {ATTR_ENTITY_ID: "light.test2"}], + ), + ( + [{ATTR_ENTITY_ID: "light.test"}], + True, + [], + ), + ( + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}], + True, + [], + ), + ( + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 15}], + True, + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 15}], + ), + ( + [ + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 15}, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, + ], + True, + [ + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 15}, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, + ], + ), + ( + [ + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, + ], + True, + [ + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, + ], + ), + ( + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}], + True, + [], + ), + ], + ids=[ + "single item passed through without filtering", + "two items passed through without filtering", + "filter removes item without relevant attributes", + "filter removes item with relevant attribute that equals the state", + "filter keeps item with relevant attribute that is different from state", + "filter keeps two items with relevant attributes that are different from state", + "filter removes item that equals state and keeps items that differs from state", + "filter removes item with relevant attribute within tolerance of the state", + ], +) +async def test_create_service_call_data_iterator( + service_datas: list[ServiceData], + filter_by_state: bool, + service_datas_expected: list[ServiceData], + hass_states_mock, +): + """Test the generator function for correct enumeration and filtering.""" + generated_service_datas = [ + data + async for data in _create_service_call_data_iterator( + hass_states_mock, + service_datas, + filter_by_state, + ) + ] + + assert generated_service_datas == service_datas_expected + assert ( + hass_states_mock.states.get.call_count == 0 + if not filter_by_state + else len(service_datas) + ) + + +@pytest.mark.parametrize( + ( + "service_data", + "split", + "filter_by_state", + "service_datas_expected", + "sleep_time_expected", + ), + [ + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + False, + False, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + ], + 1.2, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + True, + False, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + }, + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + ], + 0.7, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + False, + True, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + ], + 1.2, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + True, + True, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + ], + 0.7, + ), + ], + ids=[ + "service data passed through", + "service data split", + "service data filtered", + "service data split and filtered", + ], +) +async def test_prepare_adaptation_data( + hass_states_mock, + service_data, + split, + filter_by_state, + service_datas_expected, + sleep_time_expected, +): + """Test creation of correct service data objects.""" + data = prepare_adaptation_data( + hass_states_mock, + "test.entity", + Context(id="test-id"), + 1, + 0.2, + service_data, + split, + filter_by_state, + force=False, + ) + + generated_service_datas = [item async for item in data.service_call_datas] + + assert data.entity_id == "test.entity" + assert data.context.id == "test-id" + assert data.sleep_time == sleep_time_expected + assert generated_service_datas == service_datas_expected + + +@pytest.fixture(name="hass_states_mock") +def fixture_hass_states_mock(): + """Mocks a HA state machine which returns a mock state.""" + hass = Mock() + hass.states.get.return_value = Mock(attributes={ATTR_BRIGHTNESS: 10}) + return hass + + +@pytest.mark.parametrize( + ("attribute", "expected_str", "has_any", "has_none", "has_all"), + [ + (LightControlAttributes.NONE, "NONE", False, True, False), + (LightControlAttributes.BRIGHTNESS, "BRIGHTNESS", True, False, False), + (LightControlAttributes.COLOR, "COLOR", True, False, False), + ( + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + "BRIGHTNESS|COLOR", + True, + False, + True, + ), + ( + LightControlAttributes.ALL, + "BRIGHTNESS|COLOR", + True, + False, + True, + ), + ], +) +def test_light_control_attribute_flags( + attribute: LightControlAttributes, + expected_str: str, + has_any: bool, + has_none: bool, + has_all: bool, +): + """Test helper methods and string conversion for the light attribute flag.""" + assert str(attribute) == expected_str + assert attribute.has_any() is has_any + assert attribute.has_none() is has_none + assert attribute.has_all() is has_all + + +@pytest.mark.parametrize( + ("manual_control_attribute", "expected_flag"), + [ + (True, LightControlAttributes.ALL), + (False, LightControlAttributes.NONE), + ("brightness", LightControlAttributes.BRIGHTNESS), + ("color", LightControlAttributes.COLOR), + ("unsupported", LightControlAttributes.NONE), + ], +) +def test_manual_control_event_attribute_to_flags( + manual_control_attribute: bool | str, + expected_flag: LightControlAttributes, +): + """Test mapping of manual control events to attribute flags.""" + assert ( + manual_control_event_attribute_to_flags(manual_control_attribute) + == expected_flag + ) + + +@pytest.mark.parametrize( + ("service_data", "expected"), + [ + ({ATTR_BRIGHTNESS: 125}, True), + ({ATTR_BRIGHTNESS_PCT: 50}, True), + ({ATTR_BRIGHTNESS_PCT: 50, ATTR_COLOR_TEMP_KELVIN: 3500}, True), + ({ATTR_BRIGHTNESS_PCT: 50, "unknown": "foo"}, True), + ({ATTR_COLOR_TEMP_KELVIN: 3500}, False), + ({}, False), + ], +) +def test_has_brightness_attribute(service_data: ServiceData, expected: bool): + """Test detection of brightness attributes in service data.""" + assert has_brightness_attribute(service_data) is expected + + +@pytest.mark.parametrize( + ("service_data", "expected"), + [ + ({ATTR_HS_COLOR: (10, 20)}, True), + ({ATTR_COLOR_TEMP_KELVIN: 5000}, True), + ({ATTR_COLOR_TEMP_KELVIN: 5000, ATTR_BRIGHTNESS: 125}, True), + ({ATTR_COLOR_TEMP_KELVIN: 5000, "unknown": "foo"}, True), + ({ATTR_BRIGHTNESS: 125}, False), + ({}, False), + ], +) +def test_has_color_attribute(service_data: ServiceData, expected: bool): + """Test detection of color attributes in service data.""" + assert has_color_attribute(service_data) is expected + + +@pytest.mark.parametrize( + ("service_data", "expected"), + [ + ({ATTR_EFFECT: "colorloop"}, True), + ({ATTR_FLASH: "short"}, True), + ({ATTR_EFFECT: "colorloop", ATTR_FLASH: "short"}, True), + ({ATTR_EFFECT: "colorloop", "unknown": "foo"}, True), + ({}, False), + ], +) +def test_has_effect_attribute(service_data: ServiceData, expected: bool): + """Test detection of effect attributes in service data.""" + assert has_effect_attribute(service_data) is expected + + +@pytest.mark.parametrize( + ("service_data", "expected_flags"), + [ + ({ATTR_BRIGHTNESS: 1}, LightControlAttributes.BRIGHTNESS), + ({ATTR_HS_COLOR: (1, 2)}, LightControlAttributes.COLOR), + ( + {ATTR_BRIGHTNESS: 1, ATTR_HS_COLOR: (1, 2)}, + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + ), + ( + {ATTR_EFFECT: "colorloop"}, + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + ), + ( + {ATTR_FLASH: "short"}, + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + ), + ({}, LightControlAttributes.NONE), + ], +) +def test_get_light_control_attributes( + service_data: ServiceData, + expected_flags: LightControlAttributes, +): + """Test determination of light control attributes.""" + assert get_light_control_attributes(service_data) == expected_flags + + +@pytest.mark.parametrize( + ("already_applied", "expected"), + [ + ( + LightControlAttributes.BRIGHTNESS, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 3448, + ATTR_TRANSITION: 1, + }, + ], + ), + ( + LightControlAttributes.COLOR, + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 171, ATTR_TRANSITION: 1}], + ), + (LightControlAttributes.ALL, []), + ], +) +async def test_remaining_split_commands_preserve_transition( + hass, + already_applied, + expected, +): + """Removing the shared command must not redistribute its transition time.""" + data = prepare_adaptation_data( + hass, + "light.test", + Context(), + transition=2, + split_delay=0.1, + service_data={ + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 171, + ATTR_COLOR_TEMP_KELVIN: 3448, + ATTR_TRANSITION: 2, + }, + split=True, + filter_by_state=False, + force=False, + already_applied=already_applied, + ) + assert [command async for command in data.service_call_datas] == expected + assert data.sleep_time == 1.1 diff --git a/tests/test_automation_examples.py b/tests/test_automation_examples.py new file mode 100644 index 00000000..28bd130a --- /dev/null +++ b/tests/test_automation_examples.py @@ -0,0 +1,1370 @@ +"""Execute the automation examples published in README.md.""" + +from __future__ import annotations + +import asyncio +import re +import shutil +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest +import yaml +from homeassistant.components import automation, script +from homeassistant.components.adaptive_lighting.adaptation_utils import ( + LightControlAttributes, +) +from homeassistant.components.adaptive_lighting.const import ( + CONF_AUTORESET_CONTROL, + CONF_BRIGHTNESS_MODE, + CONF_BRIGHTNESS_MODE_TIME_DARK, + CONF_BRIGHTNESS_MODE_TIME_LIGHT, + CONF_INITIAL_TRANSITION, + CONF_LIGHTS, + CONF_MAX_BRIGHTNESS, + CONF_MAX_COLOR_TEMP, + CONF_MIN_BRIGHTNESS, + CONF_MIN_COLOR_TEMP, + CONF_NAME, + CONF_SLEEP_BRIGHTNESS, + CONF_SLEEP_COLOR_TEMP, + CONF_SLEEP_RGB_OR_COLOR_TEMP, + CONF_SUNRISE_TIME, + CONF_SUNSET_TIME, + CONF_TAKE_OVER_CONTROL_MODE, + CONF_TRANSITION, + DOMAIN, +) +from homeassistant.components.blueprint.models import Blueprint +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, +) +from homeassistant.components.light import ( + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_CALL_SERVICE, + EVENT_STATE_CHANGED, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import CoreState, Event, HomeAssistant, State, callback +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util +from homeassistant.util import yaml as yaml_util + +from tests.common import async_fire_time_changed + +from .test_switch import setup_switch + +if TYPE_CHECKING: + from collections.abc import Callable + +README = Path(__file__).resolve().parents[1] / "README.md" + + +def _yaml_documents(summary: str) -> list[object]: + """Load every YAML fence from one README details block.""" + readme = README.read_text() + details = re.search( + rf"{re.escape(summary)}(.*?)
", + readme, + flags=re.DOTALL, + ) + assert details is not None, f"README example not found: {summary}" + blocks = re.findall(r"```yaml\n(.*?)```", details.group(1), flags=re.DOTALL) + assert blocks, f"README example has no YAML: {summary}" + return [yaml.safe_load(block) for block in blocks] + + +async def _setup_automation(hass: HomeAssistant, config: object) -> None: + """Load an extracted automation through Home Assistant.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + {automation.DOMAIN: config}, + ) + await hass.async_block_till_done() + + +async def _setup_script(hass: HomeAssistant, config: dict) -> None: + """Load an extracted script through Home Assistant.""" + assert await async_setup_component(hass, script.DOMAIN, config) + await hass.async_block_till_done() + + +async def _setup_template_lights( + hass: HomeAssistant, + names: list[str], +) -> None: + """Create color-temperature and RGB lights with documented entity IDs.""" + lights = [ + { + "name": name, + "unique_id": name.lower().replace(" ", "_"), + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_rgb": None, + } + for name in names + ] + assert await async_setup_component( + hass, + "template", + {"template": {"light": lights}}, + ) + await hass.async_block_till_done() + + +def _state_waiter( + hass: HomeAssistant, + entity_id: str, + predicate: Callable[[State], bool], +) -> tuple[asyncio.Future[State], Callable[[], None]]: + """Return a future that resolves when an entity state matches a predicate.""" + future = hass.loop.create_future() + + @callback + def state_changed(event: Event) -> None: + new_state = event.data.get("new_state") + if ( + not future.done() + and new_state is not None + and new_state.entity_id == entity_id + and predicate(new_state) + ): + future.set_result(new_state) + + remove_listener = hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed) + return future, remove_listener + + +def _prepare_hass_startup(hass: HomeAssistant) -> None: + """Reset the standard running test fixture to exercise a real HA start.""" + hass.set_state(CoreState.not_running) + + +def _blueprint_config(hass, tmp_path, filename, inputs, alias): + """Install an actual published blueprint for Home Assistant to load.""" + relative_path = f"adaptive_lighting/{filename}" + hass.config.config_dir = str(tmp_path) + destination = tmp_path / "blueprints" / "automation" / relative_path + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(README.parent / "blueprints" / "automation" / filename, destination) + return { + "alias": alias, + "use_blueprint": {"path": relative_path, "input": inputs}, + } + + +@pytest.fixture(params=["yaml", "blueprint"]) +def published_automation(hass: HomeAssistant, tmp_path: Path, request): + """Use the published YAML or blueprint with the same behavioral assertions.""" + + def config(summary, filename, inputs): + yaml_config = _yaml_documents(summary)[-1] + if request.param == "yaml": + return yaml_config + return _blueprint_config( + hass, + tmp_path, + filename, + inputs, + yaml_config[0]["alias"], + ) + + return config + + +@pytest.mark.parametrize( + "path", + sorted((README.parent / "blueprints" / "automation").glob("*.yaml")), + ids=lambda path: path.stem, +) +def test_published_blueprint_schema(path: Path) -> None: + """Validate every published blueprint with Home Assistant's own schema.""" + blueprint = Blueprint( + yaml_util.load_yaml(str(path)), + expected_domain=automation.DOMAIN, + schema=automation.config.AUTOMATION_BLUEPRINT_SCHEMA, + ) + assert blueprint.validate() is None + + +@pytest.fixture(params=["yaml", "blueprint", "blueprint-custom-minimum"]) +def minimum_automation_config(hass: HomeAssistant, tmp_path: Path, request): + """Run the same behavior checks against both published formats.""" + if request.param == "yaml": + return _yaml_documents( + "Turn a light off when its adaptive brightness target reaches the minimum.", + )[0] + inputs = { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness", + "light_entity": "light.living_room", + } + if request.param == "blueprint-custom-minimum": + inputs["minimum_pct"] = 10 + return _blueprint_config( + hass, + tmp_path, + "turn_off_at_minimum.yaml", + inputs, + "Turn off at minimum", + ) + + +@pytest.mark.parametrize("manual_control", [False, True]) +@pytest.mark.parametrize("trigger_kind", ["interval", "sleep"]) +@patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + new=dt_util.utcnow, +) +async def test_minimum_brightness_power_automation( + hass: HomeAssistant, + freezer, + manual_control: bool, + trigger_kind: str, + minimum_automation_config, +) -> None: + """Catch exact-float comparisons, repeated power actions, or lost manual control.""" + minimum = ( + minimum_automation_config.get("use_blueprint", {}) + .get("input", {}) + .get("minimum_pct", 1) + if isinstance(minimum_automation_config, dict) + else 1 + ) + freezer.move_to(datetime(2026, 9, 6, 18, 58, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + await _setup_template_lights(hass, ["Living Room"]) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room", ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.living_room"], + CONF_MIN_BRIGHTNESS: minimum, + CONF_MAX_BRIGHTNESS: 100, + CONF_BRIGHTNESS_MODE: "linear", + CONF_BRIGHTNESS_MODE_TIME_DARK: timedelta(hours=1), + CONF_BRIGHTNESS_MODE_TIME_LIGHT: timedelta(hours=1), + CONF_SUNRISE_TIME: "06:00:00", + CONF_SUNSET_TIME: "18:00:00", + CONF_TRANSITION: 0, + CONF_INITIAL_TRANSITION: 0, + }, + ) + if manual_control: + await hass.services.async_call( + DOMAIN, + "set_manual_control", + {ATTR_ENTITY_ID: adaptive_switch.entity_id, "manual_control": True}, + blocking=True, + ) + await _setup_automation(hass, minimum_automation_config) + off_calls = [] + + @callback + def record_off(event: Event) -> None: + if ( + event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_OFF + ): + off_calls.append(event.data["service_data"]) + + hass.bus.async_listen(EVENT_CALL_SERVICE, record_off) + assert hass.states.get("light.living_room").state == STATE_ON + assert adaptive_switch.extra_state_attributes["brightness_pct"] > minimum + 1 + + # The curve is above the minimum, but rounds to the same brightness command. + freezer.move_to(datetime(2026, 9, 6, 18, 59, 50, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + if trigger_kind == "sleep": + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.adaptive_lighting_living_room_sleep_mode"}, + blocking=True, + ) + else: + await adaptive_switch._async_update_at_interval_action() + await hass.async_block_till_done() + if trigger_kind == "sleep": + assert adaptive_switch.extra_state_attributes["brightness_pct"] == 1 + else: + assert ( + minimum + < adaptive_switch.extra_state_attributes["brightness_pct"] + < minimum + 0.2 + ) + # The default sleep-mode policy clears manual control before publishing its target. + should_turn_off = not manual_control or trigger_kind == "sleep" + assert hass.states.get("light.living_room").state == ( + STATE_OFF if should_turn_off else STATE_ON + ) + assert len(off_calls) == int(should_turn_off) + + # Further target changes inside the minimum command range do not retrigger. + freezer.move_to(datetime(2026, 9, 6, 19, 1, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + await adaptive_switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert adaptive_switch.extra_state_attributes["brightness_pct"] == ( + 1 if trigger_kind == "sleep" else minimum + ) + assert len(off_calls) == int(should_turn_off) + + if should_turn_off: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("light.living_room").state == STATE_ON + assert len(off_calls) == 1 + + +@pytest.mark.parametrize("previous", [None, "unknown", "unavailable"]) +async def test_minimum_brightness_ignores_missing_previous_target( + hass: HomeAssistant, + previous: str | None, + minimum_automation_config, +) -> None: + """A missing target must not become a numeric crossing during recovery.""" + await _setup_template_lights(hass, ["Living Room"]) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room"}, + blocking=True, + ) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.living_room"], + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 1, + CONF_TRANSITION: 0, + CONF_INITIAL_TRANSITION: 0, + }, + ) + await _setup_automation(hass, minimum_automation_config) + attributes = dict(hass.states.get(adaptive_switch.entity_id).attributes) + assert attributes["brightness_pct"] == 1 + if previous is None: + hass.states.async_remove(adaptive_switch.entity_id) + else: + hass.states.async_set( + adaptive_switch.entity_id, + previous, + {**attributes, "brightness_pct": previous}, + ) + await hass.async_block_till_done() + hass.states.async_set(adaptive_switch.entity_id, STATE_ON, attributes) + await hass.async_block_till_done() + assert hass.states.get("light.living_room").state == STATE_ON + + +@pytest.mark.parametrize("previous_manual", [None, "color", "brightness"]) +@patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + new=dt_util.utcnow, +) +async def test_minimum_manual_control_lifecycle( + hass: HomeAssistant, + freezer, + published_automation, + previous_manual: str | None, +) -> None: + """Pause at the calculated floor, preserve other flags, and reset on off/on.""" + freezer.move_to(datetime(2026, 9, 6, 18, 58, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + config = published_automation( + "Pause brightness at the minimum using manual control.", + "manual_control_at_minimum.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness", + "light_entity": "light.living_room", + }, + ) + await _setup_template_lights(hass, ["Living Room"]) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room", ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + _, profile = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.living_room"], + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 100, + CONF_MIN_COLOR_TEMP: 3000, + CONF_MAX_COLOR_TEMP: 3000, + CONF_BRIGHTNESS_MODE: "linear", + CONF_BRIGHTNESS_MODE_TIME_DARK: timedelta(hours=1), + CONF_BRIGHTNESS_MODE_TIME_LIGHT: timedelta(hours=1), + CONF_SUNRISE_TIME: "06:00:00", + CONF_SUNSET_TIME: "18:00:00", + CONF_TRANSITION: 0, + CONF_INITIAL_TRANSITION: 0, + CONF_TAKE_OVER_CONTROL_MODE: "pause_changed", + }, + ) + if previous_manual: + await hass.services.async_call( + DOMAIN, + "set_manual_control", + {ATTR_ENTITY_ID: profile.entity_id, "manual_control": previous_manual}, + blocking=True, + ) + await _setup_automation(hass, config) + manual_calls = [] + + @callback + def record_manual_call(event: Event) -> None: + if ( + event.data["domain"] == DOMAIN + and event.data["service"] == "set_manual_control" + ): + manual_calls.append(event.data["service_data"]) + + hass.bus.async_listen(EVENT_CALL_SERVICE, record_manual_call) + freezer.move_to(datetime(2026, 9, 6, 18, 59, 50, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + await profile._async_update_at_interval_action() + await hass.async_block_till_done() + flags = profile.manager.get_manual_control_attributes("light.living_room") + assert LightControlAttributes.BRIGHTNESS in flags + assert (LightControlAttributes.COLOR in flags) is (previous_manual == "color") + assert len(manual_calls) == (0 if previous_manual == "brightness" else 1) + paused_brightness = hass.states.get("light.living_room").attributes[ATTR_BRIGHTNESS] + assert profile.is_on + assert profile.adapt_brightness_switch.is_on + if previous_manual != "brightness": + assert paused_brightness == 3 + + freezer.move_to(datetime(2026, 9, 6, 19, 1, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + await profile._async_update_at_interval_action() + await hass.async_block_till_done() + assert len(manual_calls) == (0 if previous_manual == "brightness" else 1) + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 100, + CONF_MAX_BRIGHTNESS: 100, + CONF_MIN_COLOR_TEMP: 5000, + CONF_MAX_COLOR_TEMP: 5000, + }, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.living_room") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == paused_brightness + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx( + 3000 if previous_manual == "color" else 5000, + abs=5, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.living_room"}, + blocking=True, + ) + await hass.async_block_till_done() + assert not profile.manager.get_manual_control_attributes("light.living_room") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("light.living_room").attributes[ATTR_BRIGHTNESS] == 255 + + # Another crossing can pause again, but clearing it at the floor must stick. + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 1, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert profile.manager.get_manual_control_attributes("light.living_room") + await hass.services.async_call( + DOMAIN, + "set_manual_control", + {ATTR_ENTITY_ID: profile.entity_id, "manual_control": False}, + blocking=True, + ) + await profile._async_update_at_interval_action() + await hass.async_block_till_done() + assert not profile.manager.get_manual_control_attributes("light.living_room") + + if previous_manual is None: + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + CONF_AUTORESET_CONTROL: 1, + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 1, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert profile.manager.get_manual_control_attributes("light.living_room") + cleared, remove_listener = _state_waiter( + hass, + profile.entity_id, + lambda state: state.attributes.get("manual_control_brightness") == [], + ) + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass, dt_util.utcnow()) + await asyncio.wait_for(cleared, timeout=2) + remove_listener() + await hass.async_block_till_done() + assert not profile.manager.get_manual_control_attributes("light.living_room") + + +@pytest.mark.parametrize( + "abort_entity", + [ + "light.living_room", + "switch.adaptive_lighting_living_room", + "switch.adaptive_lighting_living_room_adapt_brightness", + ], +) +async def test_minimum_manual_control_aborts_when_disabled( + hass: HomeAssistant, + published_automation, + abort_entity: str, +) -> None: + """Abandon a pending wait immediately when the light or profile is disabled.""" + config = published_automation( + "Pause brightness at the minimum using manual control.", + "manual_control_at_minimum.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness", + "light_entity": "light.living_room", + }, + ) + await _setup_template_lights(hass, ["Living Room"]) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room", ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + _, profile = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.living_room"], + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_TAKE_OVER_CONTROL_MODE: "pause_changed", + }, + ) + await _setup_automation(hass, config) + waiting, remove_listener = _state_waiter( + hass, + "automation.adaptive_lighting_pause_brightness_at_minimum", + lambda state: state.attributes.get("current") == 1, + ) + state = hass.states.get(profile.entity_id) + hass.states.async_set( + profile.entity_id, + STATE_ON, + {**state.attributes, "brightness_pct": 1}, + ) + await asyncio.wait_for(waiting, timeout=1) + remove_listener() + assert not profile.manager.get_manual_control_attributes("light.living_room") + stopped, remove_stopped = _state_waiter( + hass, + "automation.adaptive_lighting_pause_brightness_at_minimum", + lambda state: state.attributes.get("current") == 0, + ) + await hass.services.async_call( + abort_entity.split(".", maxsplit=1)[0], + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: abort_entity}, + blocking=True, + ) + try: + await asyncio.wait_for(stopped, timeout=1) + finally: + remove_stopped() + if stopped.cancelled(): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "automation.adaptive_lighting_pause_brightness_at_minimum", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert not profile.manager.get_manual_control_attributes("light.living_room") + assert ( + hass.states.get( + "automation.adaptive_lighting_pause_brightness_at_minimum", + ).attributes["current"] + == 0 + ) + + await hass.services.async_call( + abort_entity.split(".", maxsplit=1)[0], + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: abort_entity}, + blocking=True, + ) + await profile._async_update_at_interval_action() + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 1, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + profile.manager.get_manual_control_attributes("light.living_room") + == LightControlAttributes.BRIGHTNESS + ) + + +async def test_schedule_profile_executes_blocks_and_restore( + hass: HomeAssistant, + published_automation, +) -> None: + """Catch ignored attribute changes, incomplete restore, or switch coupling.""" + summary = "Use a Schedule helper as a step-based custom lighting profile." + automation_config = published_automation( + summary, + "schedule_profile.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "schedule_entity": "schedule.adaptive_lighting_profile", + }, + ) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.manually_controlled"], + CONF_MIN_BRIGHTNESS: 8, + CONF_MAX_BRIGHTNESS: 88, + CONF_MIN_COLOR_TEMP: 2100, + CONF_MAX_COLOR_TEMP: 5100, + }, + ) + adaptive_switch.manager.set_manual_control_attributes( + "light.manually_controlled", + LightControlAttributes.BRIGHTNESS, + ) + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_OFF, + ) + await _setup_automation(hass, automation_config) + + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_ON, + {"brightness_pct": 20, "color_temp_kelvin": 2500}, + ) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.min_brightness == 20 + assert adaptive_switch._sun_light_settings.max_brightness == 20 + assert adaptive_switch._sun_light_settings.min_color_temp == 2500 + assert adaptive_switch._sun_light_settings.max_color_temp == 2500 + + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_ON, + {"brightness_pct": 60, "color_temp_kelvin": 4000}, + ) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.min_brightness == 60 + assert adaptive_switch._sun_light_settings.max_brightness == 60 + assert adaptive_switch._sun_light_settings.min_color_temp == 4000 + assert adaptive_switch._sun_light_settings.max_color_temp == 4000 + + hass.states.async_set("schedule.adaptive_lighting_profile", STATE_OFF) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.min_brightness == 8 + assert adaptive_switch._sun_light_settings.max_brightness == 88 + assert adaptive_switch._sun_light_settings.min_color_temp == 2100 + assert adaptive_switch._sun_light_settings.max_color_temp == 5100 + assert adaptive_switch.manager.manual_control["light.manually_controlled"] + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: adaptive_switch.entity_id}, + blocking=True, + ) + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_ON, + {"brightness_pct": 35, "color_temp_kelvin": 2750}, + ) + await hass.async_block_till_done() + assert adaptive_switch.is_on is False + assert adaptive_switch._sun_light_settings.min_brightness == 35 + assert adaptive_switch._sun_light_settings.max_color_temp == 2750 + + +async def test_schedule_profile_reapplies_at_startup( + hass: HomeAssistant, + published_automation, +) -> None: + """Verify startup applies the already-active schedule block.""" + _prepare_hass_startup(hass) + summary = "Use a Schedule helper as a step-based custom lighting profile." + automation_config = published_automation( + summary, + "schedule_profile.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "schedule_entity": "schedule.adaptive_lighting_profile", + }, + ) + _, adaptive_switch = await setup_switch(hass, {CONF_NAME: "Living Room"}) + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_ON, + {"brightness_pct": 20, "color_temp_kelvin": 2500}, + ) + await _setup_automation(hass, automation_config) + + await hass.async_start() + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.min_brightness == 20 + assert adaptive_switch._sun_light_settings.max_brightness == 20 + assert adaptive_switch._sun_light_settings.min_color_temp == 2500 + assert adaptive_switch._sun_light_settings.max_color_temp == 2500 + + +async def test_lux_profile_executes_hysteresis( + hass: HomeAssistant, + published_automation, +) -> None: + """Catch missing threshold actions or changes inside the dead band.""" + summary = ( + "Reduce daytime brightness when an illuminance sensor detects strong daylight." + ) + automation_config = published_automation( + summary, + "daylight_limit.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "illuminance_sensor": "sensor.living_room_illuminance", + }, + ) + _, adaptive_switch = await setup_switch( + hass, + {CONF_NAME: "Living Room", CONF_MAX_BRIGHTNESS: 80}, + ) + hass.states.async_set("sensor.living_room_illuminance", "250") + await _setup_automation(hass, automation_config) + + hass.states.async_set("sensor.living_room_illuminance", "350") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 30 + + hass.states.async_set("sensor.living_room_illuminance", "250") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 30 + + hass.states.async_set("sensor.living_room_illuminance", "150") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 100 + + hass.states.async_set("sensor.living_room_illuminance", "250") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 100 + + +async def test_lux_profile_executes_unknown_recovery_at_startup( + hass: HomeAssistant, + published_automation, +) -> None: + """Catch a startup hang or failure to recover from an unknown sensor.""" + _prepare_hass_startup(hass) + summary = ( + "Reduce daytime brightness when an illuminance sensor detects strong daylight." + ) + automation_config = published_automation( + summary, + "daylight_limit.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "illuminance_sensor": "sensor.living_room_illuminance", + }, + ) + _, adaptive_switch = await setup_switch( + hass, + {CONF_NAME: "Living Room", CONF_MAX_BRIGHTNESS: 80}, + ) + hass.states.async_set("sensor.living_room_illuminance", "unknown") + await _setup_automation(hass, automation_config) + waiting, remove_listener = _state_waiter( + hass, + "automation.adaptive_lighting_limit_brightness_in_daylight", + lambda state: state.attributes.get("current") == 1, + ) + + await hass.async_start() + await asyncio.wait_for(waiting, timeout=1) + remove_listener() + assert adaptive_switch._sun_light_settings.max_brightness == 80 + + hass.states.async_set("sensor.living_room_illuminance", "350") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 30 + + +@pytest.mark.parametrize(("high_lux", "low_lux"), [(400, 250), (200, 300), (200, 200)]) +async def test_daylight_blueprint_custom_inputs( + hass: HomeAssistant, + tmp_path: Path, + high_lux: int, + low_lux: int, +) -> None: + """Use selected entities and limits; invalid threshold order must do nothing.""" + config = _blueprint_config( + hass, + tmp_path, + "daylight_limit.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_office", + "illuminance_sensor": "sensor.office_illuminance", + "high_lux": high_lux, + "low_lux": low_lux, + "daylight_maximum": 20, + "normal_maximum": 70, + }, + "Custom daylight", + ) + _, adaptive_switch = await setup_switch( + hass, + {CONF_NAME: "Office", CONF_MAX_BRIGHTNESS: 80}, + ) + hass.states.async_set("sensor.office_illuminance", "300") + await _setup_automation(hass, config) + assert hass.states.get("automation.custom_daylight") is not None + valid_thresholds = high_lux > low_lux + for lux, expected in [(500, 20), (300, 20), (100, 70)]: + hass.states.async_set("sensor.office_illuminance", str(lux)) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == ( + expected if valid_thresholds else 80 + ) + + +async def test_hue_script_applies_current_values_to_fresh_profile_targets( + hass: HomeAssistant, +) -> None: + """Catch an invalid script wrapper or a one-shot apply that skips off lights.""" + summary = "Turn on Hue-controlled lights with the current Adaptive Lighting values." + script_config = _yaml_documents(summary)[0] + await _setup_template_lights( + hass, + ["Living Room Ceiling", "Living Room Table"], + ) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: [ + "light.living_room_ceiling", + "light.living_room_table", + ], + CONF_SUNRISE_TIME: "06:00:00", + CONF_SUNSET_TIME: "18:00:00", + CONF_MIN_BRIGHTNESS: 10, + CONF_MAX_BRIGHTNESS: 80, + CONF_MIN_COLOR_TEMP: 2000, + CONF_MAX_COLOR_TEMP: 5000, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + }, + ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: adaptive_switch.entity_id}, + blocking=True, + ) + adaptive_switch.manager.set_manual_control_attributes( + "light.living_room_ceiling", + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + ) + await _setup_script(hass, script_config) + + noon = datetime(2026, 9, 6, 12, tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(UTC) + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=noon, + ): + await hass.services.async_call( + script.DOMAIN, + "living_room_adaptive_lighting", + blocking=True, + ) + await hass.async_block_till_done() + + for entity_id in ("light.living_room_ceiling", "light.living_room_table"): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 204 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 5000 + assert adaptive_switch.is_on is False + assert adaptive_switch.manager.manual_control["light.living_room_ceiling"] + + +async def test_rgb_bedtime_script_applies_stage_then_restores_configuration( + hass: HomeAssistant, +) -> None: + """Catch a wrong fresh child ID, delayed first stage, or missing restoration.""" + summary = "Use a fixed RGB stage before sleep mode." + script_config = _yaml_documents(summary)[0] + await _setup_template_lights(hass, ["Bedroom"]) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Bedroom", + CONF_LIGHTS: ["light.bedroom"], + CONF_SLEEP_BRIGHTNESS: 7, + CONF_SLEEP_COLOR_TEMP: 2300, + CONF_SLEEP_RGB_OR_COLOR_TEMP: "color_temp", + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + }, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + adaptive_switch.manager.set_manual_control_attributes( + "light.bedroom", + LightControlAttributes.ALL, + ) + await _setup_script(hass, script_config) + light_staged, remove_light_listener = _state_waiter( + hass, + "light.bedroom", + lambda state: ( + state.attributes.get(ATTR_BRIGHTNESS) == 51 + and state.attributes.get(ATTR_RGB_COLOR) == (255, 56, 0) + ), + ) + sleep_enabled, remove_sleep_listener = _state_waiter( + hass, + "switch.adaptive_lighting_bedroom_sleep_mode", + lambda state: state.state == STATE_ON, + ) + + await hass.services.async_call( + script.DOMAIN, + "adaptive_lighting_bedtime", + blocking=False, + ) + await asyncio.wait_for(asyncio.gather(light_staged, sleep_enabled), timeout=1) + remove_light_listener() + remove_sleep_listener() + # Let the script continue from the completed switch action into its delay. + await asyncio.sleep(0) + sleep_state = hass.states.get("switch.adaptive_lighting_bedroom_sleep_mode") + assert sleep_state is not None + assert sleep_state.state == STATE_ON + assert adaptive_switch._sun_light_settings.sleep_brightness == 20 + assert adaptive_switch._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" + assert adaptive_switch._sun_light_settings.sleep_rgb_color == [255, 56, 0] + assert not adaptive_switch.manager.manual_control["light.bedroom"] + bedroom_state = hass.states.get("light.bedroom") + assert bedroom_state is not None + assert bedroom_state.attributes[ATTR_BRIGHTNESS] == 51 + assert bedroom_state.attributes[ATTR_RGB_COLOR] == (255, 56, 0) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.sleep_brightness == 7 + assert adaptive_switch._sun_light_settings.sleep_rgb_or_color_temp == "color_temp" + assert adaptive_switch._sun_light_settings.sleep_color_temp == 2300 + bedroom_state = hass.states.get("light.bedroom") + assert bedroom_state is not None + assert bedroom_state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx( + 2300, + abs=5, + ) + + +async def test_fixed_virtual_day_curve_and_power_automation( + hass: HomeAssistant, + freezer, +) -> None: + """Catch a broken cross-midnight curve or power-trigger branch.""" + summary = "Run a fixed virtual day across midnight." + integration_config, automation_config = _yaml_documents(summary) + await _setup_template_lights(hass, ["Indoor Garden"]) + assert isinstance(integration_config, dict) + assert await async_setup_component(hass, DOMAIN, integration_config) + await hass.async_block_till_done() + entry = hass.config_entries.async_entries(DOMAIN)[0] + adaptive_switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + + expected_brightness = { + datetime(2026, 9, 6, 16, tzinfo=dt_util.DEFAULT_TIME_ZONE): 10, + datetime(2026, 9, 6, 22, tzinfo=dt_util.DEFAULT_TIME_ZONE): 100, + datetime(2026, 9, 7, 4, tzinfo=dt_util.DEFAULT_TIME_ZONE): 10, + } + for now, expected in expected_brightness.items(): + assert adaptive_switch._sun_light_settings.brightness_pct( + now, + False, + ) == pytest.approx( + expected, + ) + + freezer.move_to( + datetime(2026, 9, 6, 15, 59, tzinfo=dt_util.DEFAULT_TIME_ZONE), + ) + await _setup_automation(hass, automation_config) + await hass.async_start() + async_fire_time_changed( + hass, + datetime(2026, 9, 6, 16, tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(UTC), + ) + await hass.async_block_till_done() + garden_state = hass.states.get("light.indoor_garden") + assert garden_state is not None + assert garden_state.state == STATE_ON + + async_fire_time_changed( + hass, + datetime(2026, 9, 7, 4, tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(UTC), + ) + await hass.async_block_till_done() + garden_state = hass.states.get("light.indoor_garden") + assert garden_state is not None + assert garden_state.state == STATE_OFF + + +@pytest.mark.parametrize( + ("start_hour", "expected_state"), + [(10, STATE_OFF), (22, STATE_ON)], +) +async def test_fixed_virtual_day_reconciles_power_at_startup( + hass: HomeAssistant, + freezer, + start_hour: int, + expected_state: str, +) -> None: + """Catch a power schedule that misses a trigger while HA is offline.""" + _prepare_hass_startup(hass) + summary = "Run a fixed virtual day across midnight." + _, automation_config = _yaml_documents(summary) + await _setup_template_lights(hass, ["Indoor Garden"]) + await _setup_automation(hass, automation_config) + freezer.move_to( + datetime(2026, 9, 6, start_hour, tzinfo=dt_util.DEFAULT_TIME_ZONE), + ) + + await hass.async_start() + await hass.async_block_till_done() + + garden_state = hass.states.get("light.indoor_garden") + assert garden_state is not None + assert garden_state.state == expected_state + + +async def test_autoreset_manual_control_uses_one_renewable_timer( + hass: HomeAssistant, +) -> None: + """Validate the documented built-in timeout and its renewal behavior.""" + summary = "Automatically reset manual control after one hour." + integration_config = _yaml_documents(summary)[0] + await _setup_template_lights(hass, ["Living Room"]) + assert isinstance(integration_config, dict) + assert await async_setup_component(hass, DOMAIN, integration_config) + await hass.async_block_till_done() + entry = hass.config_entries.async_entries(DOMAIN)[0] + adaptive_switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + assert adaptive_switch._auto_reset_manual_control_time == 3600 + + service_data = { + ATTR_ENTITY_ID: adaptive_switch.entity_id, + CONF_LIGHTS: ["light.living_room"], + "manual_control": True, + } + await hass.services.async_call( + DOMAIN, + "set_manual_control", + service_data, + blocking=True, + ) + first_timer = adaptive_switch.manager.auto_reset_manual_control_timers[ + "light.living_room" + ] + first_task = first_timer.task + + await hass.services.async_call( + DOMAIN, + "set_manual_control", + service_data, + blocking=True, + ) + renewed_timer = adaptive_switch.manager.auto_reset_manual_control_timers[ + "light.living_room" + ] + await asyncio.sleep(0) + assert renewed_timer is first_timer + assert renewed_timer.task is not first_task + assert first_task is not None + assert first_task.cancelled() + assert adaptive_switch.manager.manual_control["light.living_room"] + + await renewed_timer.callback() + assert not adaptive_switch.manager.manual_control["light.living_room"] + + +async def test_sleep_toggle_uses_fresh_profile_entity_ids( + hass: HomeAssistant, + published_automation, +) -> None: + """Execute state triggers against fresh child entity IDs.""" + summary = ( + 'Toggle multiple Adaptive Lighting switches to "sleep mode" using an ' + "input_boolean.sleep_mode." + ) + automation_config = published_automation( + summary, + "sleep_mode.yaml", + { + "sleep_helper": "input_boolean.sleep_mode", + "sleep_switches": [ + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ], + }, + ) + assert await async_setup_component( + hass, + "input_boolean", + {"input_boolean": {"sleep_mode": {}}}, + ) + await setup_switch(hass, {CONF_NAME: "Living Room"}) + await setup_switch(hass, {CONF_NAME: "Bedroom"}) + await _setup_automation(hass, automation_config) + + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + for entity_id in ( + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + for entity_id in ( + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + +async def test_sleep_toggle_tracks_rapid_helper_changes( + hass: HomeAssistant, + published_automation, +) -> None: + """Keep every sleep switch synchronized when the helper changes rapidly.""" + summary = ( + 'Toggle multiple Adaptive Lighting switches to "sleep mode" using an ' + "input_boolean.sleep_mode." + ) + sleep_switches = ( + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ) + automation_config = published_automation( + summary, + "sleep_mode.yaml", + { + "sleep_helper": "input_boolean.sleep_mode", + "sleep_switches": list(sleep_switches), + }, + ) + assert await async_setup_component( + hass, + "input_boolean", + {"input_boolean": {"sleep_mode": {}}}, + ) + await setup_switch(hass, {CONF_NAME: "Living Room"}) + await setup_switch(hass, {CONF_NAME: "Bedroom"}) + await _setup_automation(hass, automation_config) + + for _ in range(10): + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + + for entity_id in sleep_switches: + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + +async def test_sleep_toggle_applies_restored_state_at_startup( + hass: HomeAssistant, + published_automation, +) -> None: + """Verify startup applies the input boolean's restored state.""" + _prepare_hass_startup(hass) + summary = ( + 'Toggle multiple Adaptive Lighting switches to "sleep mode" using an ' + "input_boolean.sleep_mode." + ) + automation_config = published_automation( + summary, + "sleep_mode.yaml", + { + "sleep_helper": "input_boolean.sleep_mode", + "sleep_switches": [ + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ], + }, + ) + assert await async_setup_component( + hass, + "input_boolean", + {"input_boolean": {"sleep_mode": {}}}, + ) + await setup_switch(hass, {CONF_NAME: "Living Room"}) + await setup_switch(hass, {CONF_NAME: "Bedroom"}) + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await _setup_automation(hass, automation_config) + + await hass.async_start() + await hass.async_block_till_done() + state = hass.states.get("switch.adaptive_lighting_living_room_sleep_mode") + assert state is not None + assert state.state == STATE_ON + + +async def test_alarm_script_updates_its_profile_once( + hass: HomeAssistant, + freezer, +) -> None: + """Execute the alarm script and verify one profile update with a 12-hour day.""" + summary = "Set sunrise and sunset from an alarm." + script_config = _yaml_documents(summary)[0] + freezer.move_to( + datetime(2026, 11, 1, 0, 30, tzinfo=dt_util.DEFAULT_TIME_ZONE), + ) + _, adaptive_switch = await setup_switch(hass, {CONF_NAME: "Alarm Lights"}) + await _setup_script(hass, script_config) + calls = [] + + def record_service_call(event) -> None: + if ( + event.data["domain"] == DOMAIN + and event.data["service"] == "change_switch_settings" + ): + calls.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, record_service_call) + await hass.services.async_call( + script.DOMAIN, + "set_adaptive_lighting_alarm_times", + blocking=True, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + sunrise = adaptive_switch._sun_light_settings.sunrise_time + sunset = adaptive_switch._sun_light_settings.sunset_time + assert sunrise is not None + assert sunset is not None + sunrise_seconds = sunrise.hour * 3600 + sunrise.minute * 60 + sunrise.second + sunset_seconds = sunset.hour * 3600 + sunset.minute * 60 + sunset.second + assert (sunset_seconds - sunrise_seconds) % (24 * 3600) == 12 * 3600 diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py new file mode 100644 index 00000000..e7f0a21a --- /dev/null +++ b/tests/test_color_and_brightness.py @@ -0,0 +1,514 @@ +import datetime as dt +import zoneinfo + +import astral.sun +import pytest +from astral import LocationInfo +from astral.location import Location +from homeassistant.components.adaptive_lighting.color_and_brightness import ( + _POLAR_SUN_EVENT_OFFSET, + SunEvent, + SunEvents, + SunLightSettings, + clamp, +) + +# Create a mock astral location object (its `.observer` is passed to `SunEvents`) +location = Location(LocationInfo()) + +LAT_LONG_TZS = [ + (52.379189, 4.899431, "Europe/Amsterdam"), + (32.87336, -117.22743, "US/Pacific"), + (60, 50, "GMT"), + (60, 50, "UTC"), +] + + +@pytest.fixture(params=LAT_LONG_TZS) +def tzinfo_and_location(request): + lat, long, timezone = request.param + tzinfo = zoneinfo.ZoneInfo(timezone) + location = Location( + LocationInfo( + name="name", + region="region", + timezone=timezone, + latitude=lat, + longitude=long, + ), + ) + return tzinfo, location + + +def test_replace_time(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_observer=location.observer, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + + new_time = dt.time(5, 30) + datetime = dt.datetime(2022, 1, 1) + replaced_time_utc = sun_events._replace_time(datetime.date(), new_time) + assert replaced_time_utc.astimezone(tzinfo).time() == new_time + + +def test_sunrise_without_offset(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + + sun_events = SunEvents( + name="test", + astral_observer=location.observer, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + date = dt.datetime(2022, 1, 1).date() + result = sun_events.sunrise(date) + assert result == location.sunrise(date) + + +def test_sun_position_no_fixed_sunset_and_sunrise(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_observer=location.observer, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + date = dt.datetime(2022, 1, 1).date() + sunset = location.sunset(date) + position = sun_events.sun_position(sunset) + assert position == 0 + sunrise = location.sunrise(date) + position = sun_events.sun_position(sunrise) + assert position == 0 + noon = location.noon(date) + position = sun_events.sun_position(noon) + assert position == 1 + midnight = location.midnight(date) + position = sun_events.sun_position(midnight) + assert position == -1 + + +def test_sun_position_fixed_sunset_and_sunrise(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_observer=location.observer, + sunrise_time=dt.time(6, 0), + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=dt.time(18, 0), + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + date = dt.datetime(2022, 1, 1).date() + sunset = sun_events.sunset(date) + position = sun_events.sun_position(sunset) + assert position == 0 + sunrise = sun_events.sunrise(date) + position = sun_events.sun_position(sunrise) + assert position == 0 + noon, midnight = sun_events.noon_and_midnight(date) + position = sun_events.sun_position(noon) + assert position == 1 + position = sun_events.sun_position(midnight) + assert position == -1 + + +def test_noon_and_midnight(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_observer=location.observer, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + date = dt.datetime(2022, 1, 1) + noon, midnight = sun_events.noon_and_midnight(date) + assert noon == location.noon(date) + assert midnight == location.midnight(date) + + +def test_sun_events(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_observer=location.observer, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + + date = dt.datetime(2022, 1, 1) + events = sun_events.sun_events(date) + assert len(events) == 4 + assert (SunEvent.SUNRISE, location.sunrise(date).timestamp()) in events + + +def test_prev_and_next_events(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_observer=location.observer, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + datetime = dt.datetime(2022, 1, 1, 10, 0) + after_sunrise = sun_events.sunrise(datetime.date()) + dt.timedelta(hours=1) + prev_event, next_event = sun_events.prev_and_next_events(after_sunrise) + assert prev_event[0] == SunEvent.SUNRISE + assert next_event[0] == SunEvent.NOON + + +def test_closest_event(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_observer=location.observer, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + datetime = dt.datetime(2022, 1, 1, 6, 0) + sunrise = sun_events.sunrise(datetime.date()) + event_name, ts = sun_events.closest_event(sunrise) + assert event_name == SunEvent.SUNRISE + assert ts == location.sunrise(sunrise.date()).timestamp() + + +def _make_brightness_settings( + tzinfo, + location, + *, + min_brightness, + max_brightness, + brightness_mode, +): + """Build a SunLightSettings with only the fields brightness_pct() needs.""" + return SunLightSettings( + name="test", + astral_observer=location.observer, + adapt_until_sleep=False, + max_brightness=max_brightness, + max_color_temp=6500, + min_brightness=min_brightness, + min_color_temp=2000, + sleep_brightness=1, + sleep_rgb_or_color_temp="color_temp", + sleep_color_temp=2000, + sleep_rgb_color=(255, 56, 0), + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + brightness_mode_time_dark=dt.timedelta(minutes=30), + brightness_mode_time_light=dt.timedelta(minutes=30), + brightness_mode=brightness_mode, + timezone=tzinfo, + ) + + +def test_clamp_handles_inverted_bounds(): + """A user can intentionally set min_brightness > max_brightness for an + inverted timescale (#1421, e.g. a porch light that should be brighter at + night than during the day). clamp() must still bound the value between + whichever of the two is actually smaller/larger, not silently collapse + to `minimum` for every input the way `max(minimum, min(value, maximum))` + does when minimum > maximum. + """ + assert clamp(50, 100, 15) == 50 + assert clamp(0, 100, 15) == 15 + assert clamp(200, 100, 15) == 100 + + +def test_clamp_normal_bounds_unaffected(): + """The ordinary min <= max case must keep behaving exactly as before.""" + assert clamp(50, 0, 100) == 50 + assert clamp(-10, 0, 100) == 0 + assert clamp(150, 0, 100) == 100 + + +@pytest.mark.parametrize("brightness_mode", ["linear", "tanh"]) +def test_brightness_pct_varies_with_inverted_brightness_bounds( + tzinfo_and_location, + brightness_mode, +): + """#1421: with min_brightness > max_brightness, linear/tanh modes got + stuck returning min_brightness for every sample, because the final + `clamp(brightness, self.min_brightness, self.max_brightness)` call + collapsed to `minimum` regardless of the computed value. Sampling a few + points around sunrise must show the brightness actually move instead of + being pinned to one value. + """ + tzinfo, location = tzinfo_and_location + settings = _make_brightness_settings( + tzinfo, + location, + min_brightness=100, + max_brightness=15, + brightness_mode=brightness_mode, + ) + + sunrise = location.sunrise(dt.datetime(2022, 6, 1).date()) + samples = [ + settings.brightness_pct( + sunrise + dt.timedelta(minutes=offset), + is_sleep=False, + ) + for offset in (-20, -10, 0, 10, 20) + ] + + assert len({round(value) for value in samples}) > 1, samples + assert all(15 <= value <= 100 for value in samples), samples + + +# Tromsø, Norway (69.6°N) has polar night (Nov-Jan) and midnight sun (May-Jul). +TROMSO = Location( + LocationInfo( + name="Tromsø", + region="Norway", + timezone="Europe/Oslo", + latitude=69.6489, + longitude=18.9551, + ), +) +POLAR_NIGHT_DATE = dt.date(2026, 1, 7) +MIDNIGHT_SUN_DATE = dt.date(2026, 7, 7) +MCMURDO = Location( + LocationInfo( + name="McMurdo Station", + region="Antarctica", + timezone="Antarctica/McMurdo", + latitude=-77.8419, + longitude=166.6863, + ), +) + + +def _polar_sun_events(location=TROMSO, **kwargs): + defaults = { + "name": "test", + "astral_observer": location.observer, + "sunrise_time": None, + "min_sunrise_time": None, + "max_sunrise_time": None, + "sunset_time": None, + "min_sunset_time": None, + "max_sunset_time": None, + "timezone": zoneinfo.ZoneInfo(location.timezone), + } + return SunEvents(**{**defaults, **kwargs}) + + +def test_polar_night_synthesizes_short_day(): + # `astral` cannot compute sunrise/sunset (the sun never rises), see #1485 + with pytest.raises(ValueError): # noqa: PT011 + astral.sun.sunrise(TROMSO.observer, POLAR_NIGHT_DATE) + sun_events = _polar_sun_events() + noon = astral.sun.noon(TROMSO.observer, POLAR_NIGHT_DATE) + assert sun_events.sunrise(POLAR_NIGHT_DATE) == noon - _POLAR_SUN_EVENT_OFFSET + assert sun_events.sunset(POLAR_NIGHT_DATE) == noon + _POLAR_SUN_EVENT_OFFSET + + +def test_midnight_sun_synthesizes_short_night(): + # `astral` cannot compute sunrise/sunset (the sun never sets), see #1485 + with pytest.raises(ValueError): # noqa: PT011 + astral.sun.sunset(TROMSO.observer, MIDNIGHT_SUN_DATE) + sun_events = _polar_sun_events() + midnight = astral.sun.midnight(TROMSO.observer, MIDNIGHT_SUN_DATE) + next_midnight = astral.sun.midnight( + TROMSO.observer, + MIDNIGHT_SUN_DATE + dt.timedelta(days=1), + ) + assert sun_events.sunrise(MIDNIGHT_SUN_DATE) == midnight + _POLAR_SUN_EVENT_OFFSET + assert ( + sun_events.sunset(MIDNIGHT_SUN_DATE) == next_midnight - _POLAR_SUN_EVENT_OFFSET + ) + + +@pytest.mark.parametrize( + ("date", "midnight_sun"), + [(dt.date(2026, 1, 7), True), (dt.date(2026, 7, 7), False)], +) +def test_polar_fallback_handles_southern_hemisphere(date, midnight_sun): + sun_events = _polar_sun_events(MCMURDO) + noon = astral.sun.noon(MCMURDO.observer, date) + midnight = astral.sun.midnight(MCMURDO.observer, date) + next_midnight = astral.sun.midnight(MCMURDO.observer, date + dt.timedelta(days=1)) + + if midnight_sun: + assert sun_events.sunrise(date) == midnight + _POLAR_SUN_EVENT_OFFSET + assert sun_events.sunset(date) == next_midnight - _POLAR_SUN_EVENT_OFFSET + else: + assert sun_events.sunrise(date) == noon - _POLAR_SUN_EVENT_OFFSET + assert sun_events.sunset(date) == noon + _POLAR_SUN_EVENT_OFFSET + + +def test_boundary_day_with_real_sunrise_and_synthetic_sunset(): + # At the start of the midnight sun period, `astral` computes a real + # sunrise for this date but raises for sunset (this exact date depends on + # astral's numerics). The synthetic sunset must stay consistent with the + # nearly 24-hour day instead of collapsing into a polar-night day. + date = dt.date(2026, 5, 18) + astral.sun.sunrise(TROMSO.observer, date) # does not raise + with pytest.raises(ValueError): # noqa: PT011 + astral.sun.sunset(TROMSO.observer, date) + sun_events = _polar_sun_events() + day_length = sun_events.sunset(date) - sun_events.sunrise(date) + assert day_length > dt.timedelta(hours=22) + + +@pytest.mark.parametrize("date", [POLAR_NIGHT_DATE, MIDNIGHT_SUN_DATE]) +def test_sun_position_on_polar_days(date): + sun_events = _polar_sun_events() + datetime = dt.datetime(date.year, date.month, date.day, tzinfo=dt.timezone.utc) + noon, midnight = sun_events.noon_and_midnight(datetime) + assert sun_events.sun_position(noon) == 1 + assert sun_events.sun_position(midnight) == -1 + assert sun_events.sun_position(sun_events.sunrise(date)) == 0 + assert sun_events.sun_position(sun_events.sunset(date)) == 0 + + +def test_polar_night_min_max_times_shape_the_synthetic_day(): + # The (min/max)_(sunrise/sunset)_time options apply on top of the + # synthetic sun events, so users can still shape their schedule. + sun_events = _polar_sun_events( + max_sunrise_time=dt.time(9, 0), + min_sunset_time=dt.time(17, 0), + timezone=dt.timezone.utc, + ) + expected_sunrise = dt.datetime(2026, 1, 7, 9, 0, tzinfo=dt.timezone.utc) + expected_sunset = dt.datetime(2026, 1, 7, 17, 0, tzinfo=dt.timezone.utc) + assert sun_events.sunrise(POLAR_NIGHT_DATE) == expected_sunrise + assert sun_events.sunset(POLAR_NIGHT_DATE) == expected_sunset + + +@pytest.mark.parametrize("date", [POLAR_NIGHT_DATE, MIDNIGHT_SUN_DATE]) +@pytest.mark.parametrize( + ("sunrise_offset", "sunset_offset"), + [ + (dt.timedelta(hours=-20), dt.timedelta(hours=-20)), + (dt.timedelta(hours=-20), dt.timedelta(hours=20)), + (dt.timedelta(hours=20), dt.timedelta(hours=-20)), + (dt.timedelta(hours=20), dt.timedelta(hours=20)), + ], +) +def test_polar_offsets_cannot_invert_event_order( + date, + sunrise_offset, + sunset_offset, +): + sun_events = _polar_sun_events( + sunrise_offset=sunrise_offset, + sunset_offset=sunset_offset, + ) + + events = dict( + sun_events.sun_events(dt.datetime.combine(date, dt.time(), tzinfo=dt.UTC)), + ) + midnight = dt.datetime.fromtimestamp(events[SunEvent.MIDNIGHT], tz=dt.UTC) + next_midnight = astral.sun.midnight(TROMSO.observer, date + dt.timedelta(days=1)) + noon = dt.datetime.fromtimestamp(events[SunEvent.NOON], tz=dt.UTC) + sunrise = dt.datetime.fromtimestamp(events[SunEvent.SUNRISE], tz=dt.UTC) + sunset = dt.datetime.fromtimestamp(events[SunEvent.SUNSET], tz=dt.UTC) + + assert midnight < sunrise < noon < sunset < next_midnight + + +def test_polar_fallback_applies_offsets_within_solar_anchors(): + offset = dt.timedelta(minutes=15) + plain = _polar_sun_events() + shifted = _polar_sun_events( + sunrise_offset=offset, + sunset_offset=offset, + ) + + assert ( + shifted.sunrise(MIDNIGHT_SUN_DATE) - plain.sunrise(MIDNIGHT_SUN_DATE) == offset + ) + assert shifted.sunset(MIDNIGHT_SUN_DATE) - plain.sunset(MIDNIGHT_SUN_DATE) == offset + + +def test_sun_position_all_year_in_polar_region(): + # Covers the transitions into and out of polar night and midnight sun; + # `sun_position` internally validates the order of the sun events. + sun_events = _polar_sun_events() + datetime = dt.datetime(2026, 1, 1, tzinfo=dt.timezone.utc) + end = dt.datetime(2027, 1, 1, tzinfo=dt.timezone.utc) + while datetime < end: + position = sun_events.sun_position(datetime) + assert -1 <= position <= 1 + datetime += dt.timedelta(hours=8) + + +@pytest.mark.parametrize("date", [POLAR_NIGHT_DATE, MIDNIGHT_SUN_DATE]) +def test_brightness_and_color_on_polar_days(date): + settings = SunLightSettings( + name="test", + astral_observer=TROMSO.observer, + adapt_until_sleep=False, + max_brightness=100, + max_color_temp=5500, + min_brightness=30, + min_color_temp=2000, + sleep_brightness=1, + sleep_rgb_or_color_temp="color_temp", + sleep_color_temp=1000, + sleep_rgb_color=(255, 56, 0), + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + brightness_mode_time_dark=dt.timedelta(hours=1), + brightness_mode_time_light=dt.timedelta(hours=1), + timezone=zoneinfo.ZoneInfo("Europe/Oslo"), + ) + datetime = dt.datetime(date.year, date.month, date.day, tzinfo=dt.timezone.utc) + noon, midnight = settings.sun.noon_and_midnight(datetime) + at_noon = settings.brightness_and_color(noon, is_sleep=False) + assert at_noon["brightness_pct"] == 100 + assert at_noon["color_temp_kelvin"] == 5500 + at_midnight = settings.brightness_and_color(midnight, is_sleep=False) + assert at_midnight["brightness_pct"] == 30 + assert at_midnight["color_temp_kelvin"] == 2000 diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py new file mode 100644 index 00000000..addc460c --- /dev/null +++ b/tests/test_config_flow.py @@ -0,0 +1,371 @@ +"""Test Adaptive Lighting config flow.""" + +import json + +import pytest +import voluptuous as vol + +try: + from probatio import to_field_list +except ImportError: + from voluptuous_serialize import convert as to_field_list +from homeassistant.components.adaptive_lighting.const import ( + BASIC_OPTIONS, + CONF_EXPAND_LIGHT_GROUPS, + CONF_INITIAL_TRANSITION, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + CONF_SUNRISE_TIME, + CONF_SUNSET_TIME, + DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + DEFAULT_NAME, + DOMAIN, + NONE_STR, + VALIDATION_TUPLES, +) +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResultType, section +from homeassistant.helpers import config_validation as cv + +from tests.common import MockConfigEntry + +DEFAULT_DATA = {key: default for key, default, _ in VALIDATION_TUPLES} + +# Split DEFAULT_DATA into basic and advanced for section-based input +BASIC_DATA = {key: value for key, value in DEFAULT_DATA.items() if key in BASIC_OPTIONS} +ADVANCED_DATA = { + key: value for key, value in DEFAULT_DATA.items() if key not in BASIC_OPTIONS +} + + +def _schema_defaults(schema: vol.Schema) -> dict[str, object]: + """Return the defaults from a voluptuous schema.""" + return { + key.schema: key.default() if callable(key.default) else key.default + for key in schema.schema + } + + +def _advanced_section(result) -> section: + """Return the advanced options section from a flow result.""" + advanced = result["data_schema"].schema["advanced"] + assert isinstance(advanced, section) + return advanced + + +async def test_flow_manual_configuration(hass): + """Test that config flow works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["handler"] == "adaptive_lighting" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_NAME: "living room"}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "living room" + + +async def test_import_success(hass): + """Test import step is successful.""" + data = DEFAULT_DATA.copy() + data[CONF_NAME] = DEFAULT_NAME + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data=data, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + for key, value in data.items(): + assert result["data"][key] == value + + +async def test_options(hass): + """Test updating options with collapsible sections.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + options={}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + # Build input with advanced options nested in "advanced" section + advanced_data = ADVANCED_DATA.copy() + advanced_data[CONF_INITIAL_TRANSITION] = 23 + advanced_data[CONF_EXPAND_LIGHT_GROUPS] = False + advanced_data[CONF_SUNRISE_TIME] = NONE_STR + advanced_data[CONF_SUNSET_TIME] = NONE_STR + basic_data = {**BASIC_DATA, "min_brightness": 12} + user_input = { + **basic_data, + "advanced": advanced_data, + } + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=user_input, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + + # Verify flattened data is saved correctly + expected_data = {**basic_data, **advanced_data} + for key, value in expected_data.items(): + assert result["data"][key] == value + + assert "advanced" not in result["data"] + + # Starting the flow again must load the saved flat options into both parts + # of the sectioned form. + result = await hass.config_entries.options.async_init(entry.entry_id) + assert _schema_defaults(result["data_schema"])["min_brightness"] == 12 + assert ( + _schema_defaults(_advanced_section(result).schema)[CONF_INITIAL_TRANSITION] + == 23 + ) + + +async def test_options_schema_has_each_setting_once(hass): + """Test that basic and advanced options partition all settings.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME, "interval": 120, "min_brightness": 7}, + options={"min_brightness": 12}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + schema = result["data_schema"].schema + advanced = _advanced_section(result) + + assert advanced.options == {"collapsed": True} + assert ( + _schema_defaults(advanced.schema)[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] + is DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON + ) + assert {key.schema for key in schema if key.schema != "advanced"} == BASIC_OPTIONS + assert {key.schema for key in advanced.schema.schema} == set( + DEFAULT_DATA, + ) - BASIC_OPTIONS + assert _schema_defaults(result["data_schema"])["interval"] == 120 + assert _schema_defaults(result["data_schema"])["min_brightness"] == 12 + + serialized_schema = to_field_list( + result["data_schema"], + custom_serializer=cv.custom_serializer, + ) + json.dumps(serialized_schema) + serialized_advanced = next( + field for field in serialized_schema if field["name"] == "advanced" + ) + assert serialized_advanced["type"] == "expandable" + assert serialized_advanced["expanded"] is False + assert {field["name"] for field in serialized_advanced["schema"]} == set( + DEFAULT_DATA, + ) - BASIC_OPTIONS + + +@pytest.mark.parametrize("lights", [[], ["light.missing"]]) +async def test_incorrect_options(hass, lights): + """Test updating incorrect options in advanced section.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + options={}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + # Build input with invalid advanced options nested in section + advanced_data = ADVANCED_DATA.copy() + advanced_data[CONF_SUNRISE_TIME] = "yolo" + advanced_data[CONF_SUNSET_TIME] = "yolo" + basic_data = {**BASIC_DATA, "min_brightness": 12, "lights": lights} + user_input = { + **basic_data, + "advanced": advanced_data, + } + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=user_input, + ) + # Should show form with errors + assert result["type"] == FlowResultType.FORM + expected_errors = {"base": "option_error"} + if lights: + expected_errors["lights"] = "entity_missing" + assert result["errors"] == expected_errors + assert _schema_defaults(result["data_schema"])["lights"] == lights + assert _schema_defaults(result["data_schema"])["min_brightness"] == 12 + assert ( + _schema_defaults(_advanced_section(result).schema)[CONF_SUNRISE_TIME] == "yolo" + ) + + +async def test_import_twice(hass): + """Test importing twice.""" + data = DEFAULT_DATA.copy() + data[CONF_NAME] = DEFAULT_NAME + for _ in range(2): + _ = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data=data, + ) + + +async def test_options_flow_for_yaml_import(hass): + """Test that options flow for YAML-imported entries shows empty form. + + When a config entry is imported from YAML (source=SOURCE_IMPORT), + the options flow should show an empty form since the user should + modify the YAML configuration directly, not through the UI. + """ + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + source=SOURCE_IMPORT, + options={}, + ) + entry.add_to_hass(hass) + + # For YAML imports, the switch setup requires the unique_id to be in + # hass.data[DOMAIN]["__yaml__"], otherwise it deletes the entry. + # This simulates what async_step_import does. + hass.data.setdefault(DOMAIN, {}).setdefault("__yaml__", set()).add(entry.unique_id) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + # For YAML imports, the options flow shows an empty form (data_schema=None) + # This is intentional - users should modify YAML, not UI + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result.get("data_schema") is None + assert result["description_placeholders"] == { + "docs_url": "https://github.com/basnijholt/adaptive-lighting#readme", + "webapp_url": "https://basnijholt.github.io/adaptive-lighting", + } + + +async def test_menu_shown_when_entries_exist(hass): + """Test that menu step is shown when existing entries exist.""" + # Create an existing entry + entry = MockConfigEntry( + domain=DOMAIN, + title="existing", + data={CONF_NAME: "existing"}, + options={"min_brightness": 10}, + ) + entry.add_to_hass(hass) + + # Start a new config flow - should show menu + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "menu" + + +async def test_menu_create_new_instance(hass): + """Test creating a new instance through the menu.""" + # Create an existing entry + entry = MockConfigEntry( + domain=DOMAIN, + title="existing", + data={CONF_NAME: "existing"}, + options={"min_brightness": 10}, + ) + entry.add_to_hass(hass) + + # Start config flow - shows menu + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + ) + assert result["step_id"] == "menu" + + # Choose to create new instance + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"action": "new"}, + ) + + # Should show name form + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Enter name + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_NAME: "new instance"}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "new instance" + # New instance should have no options (not duplicated) + assert result["options"] == {} + + +async def test_menu_duplicate_instance(hass): + """Test duplicating an existing instance through the menu.""" + # Create an existing entry with custom options + source_options = {"min_brightness": 20, "max_brightness": 80} + entry = MockConfigEntry( + domain=DOMAIN, + title="source", + data={CONF_NAME: "source"}, + options=source_options, + ) + entry.add_to_hass(hass) + + # Start config flow - shows menu + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + ) + assert result["step_id"] == "menu" + + # Choose to duplicate existing entry + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"action": entry.entry_id}, + ) + + # Should show name form + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Enter name for duplicated instance + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_NAME: "duplicated"}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "duplicated" + # Duplicated instance should have copied options + assert result["options"] == source_options diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py new file mode 100644 index 00000000..9c96b295 --- /dev/null +++ b/tests/test_diagnostics.py @@ -0,0 +1,372 @@ +"""Tests for Adaptive Lighting diagnostics.""" + +import json +from copy import deepcopy +from unittest.mock import patch + +import pytest +from homeassistant.components.adaptive_lighting.adaptation_utils import ( + AdaptationData, + LightControlAttributes, + _create_service_call_data_iterator, +) +from homeassistant.components.adaptive_lighting.const import ( + ATTR_ADAPTIVE_LIGHTING_MANAGER, + CONF_AUTORESET_CONTROL, + CONF_INTERCEPT, + CONF_MANUAL_CONTROL, + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, +) +from homeassistant.components.adaptive_lighting.diagnostics import ( + async_get_config_entry_diagnostics, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + SERVICE_TURN_ON, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SERVICE_DATA, + CONF_LIGHTS, + CONF_NAME, + EVENT_CALL_SERVICE, + EVENT_STATE_CHANGED, + STATE_OFF, + STATE_UNAVAILABLE, +) +from homeassistant.core import State + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + +from .test_switch import ( + ENTITY_LIGHT_1, + ENTITY_LIGHT_2, + ENTITY_LIGHT_3, + setup_lights, +) + + +@pytest.fixture +async def cleanup_diagnostics(hass): + """Cancel integration tasks created by diagnostics fixtures.""" + yield + manager = hass.data.get(DOMAIN, {}).get(ATTR_ADAPTIVE_LIGHTING_MANAGER) + if manager is None: + return + for timer in manager.auto_reset_manual_control_timers.values(): + timer.cancel() + for timer in manager.transition_timers.values(): + timer.cancel() + for task in manager.adaptation_tasks: + task.cancel() + + +async def _setup_entry(hass, name, lights, **data): + """Set up a real Adaptive Lighting config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: name, + CONF_LIGHTS: lights, + CONF_INTERCEPT: False, + **data, + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + return entry, hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + + +async def test_config_entry_diagnostics_reports_allowlisted_current_facts( + hass, + hass_client, + cleanup_diagnostics, +): + """Diagnostics report current selected-profile facts without identifiers.""" + await setup_lights(hass) + entry, switch = await _setup_entry( + hass, + "Private Upstairs Profile", + [ENTITY_LIGHT_1, ENTITY_LIGHT_2], + **{CONF_AUTORESET_CONTROL: 60}, + ) + other_entry, _ = await _setup_entry( + hass, + "Private Basement Profile", + [ENTITY_LIGHT_3], + ) + + await switch.adapt_color_switch.async_turn_off() + await switch.sleep_mode_switch.async_turn_on() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_MANUAL_CONTROL: "brightness", + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [ENTITY_LIGHT_2], + CONF_MANUAL_CONTROL: "color", + }, + blocking=True, + ) + hass.states.async_set( + ENTITY_LIGHT_2, + STATE_UNAVAILABLE, + {"friendly_name": "Private Bedside Lamp", "room": "Private Bedroom"}, + ) + await hass.async_block_till_done() + + manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + assert manager.get_manual_control_attributes(ENTITY_LIGHT_1) == ( + LightControlAttributes.BRIGHTNESS + ) + manager.last_service_data[ENTITY_LIGHT_1] = { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 123, + ATTR_COLOR_TEMP_KELVIN: 3456, + ATTR_RGB_COLOR: (12, 34, 56), + ATTR_TRANSITION: 4.5, + "context_id": "private-context-id", + "friendly_name": "Private Bedside Lamp", + } + manager.last_service_data.pop(ENTITY_LIGHT_2, None) + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert result["loaded"] is True + assert result["profile_switches"] == { + "profile": True, + "adapt_brightness": True, + "adapt_color": False, + "sleep_mode": True, + } + assert result["manager_fact_scope"] == "global_shared_across_profiles" + assert list(result["lights"]) == ["light_1", "light_2"] + assert result["lights"]["light_1"]["state"] == "on" + assert result["lights"]["light_1"]["global_manager_manual_control"] == { + "brightness": True, + "color": False, + } + assert result["lights"]["light_1"][ + "global_manager_autoreset_seconds" + ] == pytest.approx(60, abs=2) + assert result["lights"]["light_1"]["global_manager_last_adaptation_values"] == { + ATTR_BRIGHTNESS: 123, + ATTR_COLOR_TEMP_KELVIN: 3456, + ATTR_RGB_COLOR: [12, 34, 56], + ATTR_TRANSITION: 4.5, + } + assert result["lights"]["light_2"] == { + "state": STATE_UNAVAILABLE, + "global_manager_manual_control": { + "brightness": False, + "color": True, + }, + "global_manager_autoreset_seconds": pytest.approx(60, abs=2), + "global_manager_last_adaptation_values": None, + } + + serialized = json.dumps(result, sort_keys=True) + for sensitive_value in ( + entry.entry_id, + other_entry.entry_id, + ENTITY_LIGHT_1, + ENTITY_LIGHT_2, + ENTITY_LIGHT_3, + "Private Upstairs Profile", + "Private Basement Profile", + "Private Bedside Lamp", + "Private Bedroom", + "private-context-id", + ): + assert sensitive_value not in serialized + + +async def test_diagnostics_labels_accumulated_partial_adaptation_values( + hass, + cleanup_diagnostics, +): + """Diagnostics do not describe merged per-attribute history as one command.""" + await setup_lights(hass) + entry, switch = await _setup_entry( + hass, + "Private Profile", + [ENTITY_LIGHT_1], + ) + commands = [ + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 100, + ATTR_RGB_COLOR: (12, 34, 56), + ATTR_TRANSITION: 2, + }, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 180}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_COLOR_TEMP_KELVIN: 3500}, + ] + call_events = [] + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, call_events.append) + + await switch._execute_adaptation_calls( + AdaptationData( + entity_id=ENTITY_LIGHT_1, + context=switch.create_context("diagnostics_test"), + sleep_time=0, + service_call_datas=_create_service_call_data_iterator( + hass, + commands, + filter_by_state=False, + ), + force=True, + max_length=len(commands), + attributes=LightControlAttributes.ALL, + ), + ) + await hass.async_block_till_done() + remove_listener() + + actual_commands = [ + event.data[ATTR_SERVICE_DATA] + for event in call_events + if event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + ] + assert actual_commands == commands + + result = await async_get_config_entry_diagnostics(hass, entry) + light = result["lights"]["light_1"] + assert "global_manager_last_sent_target" not in light + assert light["global_manager_last_adaptation_values"] == { + ATTR_BRIGHTNESS: 180, + ATTR_COLOR_TEMP_KELVIN: 3500, + ATTR_RGB_COLOR: [12, 34, 56], + ATTR_TRANSITION: 2, + } + + +async def test_diagnostics_preserves_restored_off_profile_tracked_group(hass): + """Diagnostics report tracked targets without refreshing late groups.""" + await setup_lights(hass) + group = "light.private_late_group" + members = [ENTITY_LIGHT_1, ENTITY_LIGHT_2] + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=State("switch.restored", STATE_OFF), + ): + entry, switch = await _setup_entry( + hass, + "Private Restored Profile", + [group], + ) + assert not switch.is_on + assert switch.lights == [group] + + hass.states.async_set( + group, + STATE_UNAVAILABLE, + {ATTR_ENTITY_ID: members, "friendly_name": "Private Late Group"}, + ) + await hass.async_block_till_done() + manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + manager_lights_before = set(manager.lights) + reset_times_before = dict(manager.auto_reset_manual_control_times) + + result = await async_get_config_entry_diagnostics(hass, entry) + + assert result["lights"] == { + "light_1": { + "state": STATE_UNAVAILABLE, + "global_manager_manual_control": { + "brightness": False, + "color": False, + }, + "global_manager_autoreset_seconds": None, + "global_manager_last_adaptation_values": None, + }, + } + assert switch.lights == [group] + assert manager.lights == manager_lights_before + assert manager.auto_reset_manual_control_times == reset_times_before + assert group not in json.dumps(result) + + +async def test_diagnostics_handles_missing_states_and_unload_without_side_effects( + hass, +): + """Diagnostics normalize states and never change live integration state.""" + await setup_lights(hass) + entry, switch = await _setup_entry( + hass, + "Private Profile", + [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3, "light.private_missing"], + ) + hass.states.async_set(ENTITY_LIGHT_2, STATE_OFF) + hass.states.async_set(ENTITY_LIGHT_3, STATE_UNAVAILABLE) + await hass.async_block_till_done() + + manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + manual_control_before = dict(manager.manual_control) + last_service_data_before = deepcopy(manager.last_service_data) + timers_before = dict(manager.auto_reset_manual_control_timers) + switch_states_before = ( + switch.is_on, + switch.adapt_brightness_switch.is_on, + switch.adapt_color_switch.is_on, + switch.sleep_mode_switch.is_on, + ) + service_events = [] + state_events = [] + remove_service_listener = hass.bus.async_listen( + EVENT_CALL_SERVICE, + service_events.append, + ) + remove_state_listener = hass.bus.async_listen( + EVENT_STATE_CHANGED, + state_events.append, + ) + + result = await async_get_config_entry_diagnostics(hass, entry) + await hass.async_block_till_done() + remove_service_listener() + remove_state_listener() + + assert [light["state"] for light in result["lights"].values()] == [ + "on", + STATE_OFF, + STATE_UNAVAILABLE, + "missing", + ] + assert json.dumps(result) + assert not service_events + assert not state_events + assert manager.manual_control == manual_control_before + assert manager.last_service_data == last_service_data_before + assert manager.auto_reset_manual_control_timers == timers_before + assert ( + switch.is_on, + switch.adapt_brightness_switch.is_on, + switch.adapt_color_switch.is_on, + switch.sleep_mode_switch.is_on, + ) == switch_states_before + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert await async_get_config_entry_diagnostics(hass, entry) == {"loaded": False} diff --git a/tests/test_hass_utils.py b/tests/test_hass_utils.py new file mode 100644 index 00000000..4be4c88d --- /dev/null +++ b/tests/test_hass_utils.py @@ -0,0 +1,80 @@ +"""Tests for Adaptive Lighting HASS utils.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.adaptive_lighting.adaptation_utils import ServiceData +from homeassistant.components.adaptive_lighting.hass_utils import ( + setup_service_call_interceptor, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.const import SERVICE_TURN_ON +from homeassistant.core import ServiceCall +from homeassistant.util.read_only_dict import ReadOnlyDict + + +async def test_setup_service_call_interceptor(hass): + """Test setup and removal of service call interceptor.""" + service_func_mock = AsyncMock() + hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock) + + async def service_call(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {}, + blocking=True, + ) + + # Test if service is called + + await service_call() + assert service_func_mock.call_count == 1 + + # Test if interceptor is called after setup + + intercept_func_mock = AsyncMock() + remove_interceptor = setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + intercept_func_mock, + ) + + await service_call() + assert service_func_mock.call_count == 2 + assert intercept_func_mock.call_count == 1 + + # Test if interceptor is no longer called after removal + + remove_interceptor() + await service_call() + assert service_func_mock.call_count == 3 + assert intercept_func_mock.call_count == 1 + + +async def test_service_call_interceptor_data_manipulation(hass): + """Test service call data manipulation by service call interceptor.""" + service_func_mock = AsyncMock() + hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock) + + async def intercept_func(call: ServiceCall, data: ServiceData): + data["test1"] = "changed" + data["test2"] = "added" + + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + intercept_func, + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {"test1": "initial"}, + blocking=True, + ) + + (service_call,) = service_func_mock.call_args[0] + assert service_call.data == {"test1": "changed", "test2": "added"} + assert isinstance(service_call.data, ReadOnlyDict) diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 00000000..aaf23851 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,187 @@ +"""Tests for Adaptive Lighting integration.""" + +import pytest +import voluptuous.error +from homeassistant.components import adaptive_lighting +from homeassistant.components.adaptive_lighting.const import ( + CONF_LIGHTS, + DEFAULT_NAME, + SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, + SERVICE_SET_MANUAL_CONTROL, + UNDO_UPDATE_LISTENER, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import service +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_setup_with_config(hass): + """Test that we import the config and setup the integration.""" + config = { + adaptive_lighting.DOMAIN: { + adaptive_lighting.CONF_NAME: DEFAULT_NAME, + }, + } + assert await async_setup_component(hass, adaptive_lighting.DOMAIN, config) + assert adaptive_lighting.DOMAIN in hass.data + + +async def test_successful_config_entry(hass): + """Test that Adaptive Lighting is configured successfully.""" + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED + + assert UNDO_UPDATE_LISTENER in hass.data[adaptive_lighting.DOMAIN][entry.entry_id] + + +async def test_unload_entry(hass): + """Test removing Adaptive Lighting.""" + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.NOT_LOADED + assert adaptive_lighting.DOMAIN not in hass.data + + +async def test_services_survive_entry_unload_and_reload(hass): + """Test integration services remain registered across entry lifecycle.""" + assert await async_setup_component(hass, adaptive_lighting.DOMAIN, {}) + service_names = ( + SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, + SERVICE_SET_MANUAL_CONTROL, + ) + services = hass.services.async_services()[adaptive_lighting.DOMAIN] + assert SERVICE_APPLY in services + assert SERVICE_SET_MANUAL_CONTROL in services + if hasattr(service, "async_register_platform_entity_service"): + assert SERVICE_CHANGE_SWITCH_SETTINGS in services + else: + assert SERVICE_CHANGE_SWITCH_SETTINGS not in services + + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + registered = { + name: hass.services.async_services()[adaptive_lighting.DOMAIN][name] + for name in service_names + } + switch = hass.data[adaptive_lighting.DOMAIN][entry.entry_id][SWITCH_DOMAIN] + assert await hass.config_entries.async_unload(entry.entry_id) + + for name in service_names: + assert ( + hass.services.async_services()[adaptive_lighting.DOMAIN][name] + is registered[name] + ) + + with pytest.raises(ServiceValidationError, match="No Adaptive Lighting"): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_APPLY, + {ATTR_ENTITY_ID: switch.entity_id}, + blocking=True, + ) + + assert await hass.config_entries.async_setup(entry.entry_id) + for name in service_names: + assert ( + hass.services.async_services()[adaptive_lighting.DOMAIN][name] + is registered[name] + ) + + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {ATTR_ENTITY_ID: switch.entity_id}, + blocking=True, + ) + + +async def test_service_call_without_loaded_entry(hass): + """Test global services reject calls when no profile is loaded.""" + assert await async_setup_component(hass, adaptive_lighting.DOMAIN, {}) + + with pytest.raises(ServiceValidationError, match="No Adaptive Lighting"): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_APPLY, + {CONF_LIGHTS: ["light.test"]}, + blocking=True, + ) + + pending_entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: "pending"}, + ) + pending_entry.add_to_hass(hass) + hass.data[adaptive_lighting.DOMAIN] = {pending_entry.entry_id: {}} + with pytest.raises(ServiceValidationError, match="not found in any switch"): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_APPLY, + {CONF_LIGHTS: ["light.test"]}, + blocking=True, + ) + + +async def test_apply_rejects_unknown_light(hass): + """Test the apply service rejects an unknown light target.""" + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + with pytest.raises(ServiceValidationError, match="not found in any switch"): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_APPLY, + {CONF_LIGHTS: ["light.does_not_exist"]}, + blocking=True, + ) + + +async def test_change_switch_settings_requires_entity_target(hass): + """Test change_switch_settings rejects a missing entity target.""" + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + with pytest.raises( + voluptuous.error.MultipleInvalid, + match=r"must contain at least one of entity_id.*area_id", + ): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {}, + blocking=True, + ) diff --git a/tests/test_switch.py b/tests/test_switch.py new file mode 100644 index 00000000..a3f5b689 --- /dev/null +++ b/tests/test_switch.py @@ -0,0 +1,6346 @@ +"""Tests for Adaptive Lighting switches.""" + +# pylint: disable=protected-access +import asyncio +import contextlib +import datetime +import logging +from copy import deepcopy +from random import randint +from typing import Any +from unittest.mock import AsyncMock, Mock, patch + +import homeassistant.util.dt as dt_util +import pytest +import ulid_transform +import voluptuous.error +from flaky import flaky +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components.adaptive_lighting.adaptation_utils import ( + AdaptationData, + LightControlAttributes, + _create_service_call_data_iterator, +) +from homeassistant.components.adaptive_lighting.color_and_brightness import ( + lerp_color_hsv, +) +from homeassistant.components.adaptive_lighting.const import ( + ADAPT_BRIGHTNESS_SWITCH, + ADAPT_COLOR_SWITCH, + ATTR_ADAPT_BRIGHTNESS, + ATTR_ADAPT_COLOR, + ATTR_ADAPTIVE_LIGHTING_MANAGER, + CONF_ADAPT_DELAY, + CONF_ADAPT_ONLY_ON_BARE_TURN_ON, + CONF_ADAPT_UNTIL_SLEEP, + CONF_AUTORESET_CONTROL, + CONF_BRIGHTNESS_MODE, + CONF_BRIGHTNESS_MODE_TIME_DARK, + CONF_BRIGHTNESS_MODE_TIME_LIGHT, + CONF_DETECT_NON_HA_CHANGES, + CONF_EXPAND_LIGHT_GROUPS, + CONF_INITIAL_TRANSITION, + CONF_MANUAL_CONTROL, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + CONF_MAX_BRIGHTNESS, + CONF_MAX_COLOR_TEMP, + CONF_MIN_BRIGHTNESS, + CONF_MIN_COLOR_TEMP, + CONF_MULTI_LIGHT_INTERCEPT, + CONF_ONLY_ONCE, + CONF_PREFER_RGB_COLOR, + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + CONF_SEND_SPLIT_DELAY, + CONF_SEPARATE_TURN_ON_COMMANDS, + CONF_SKIP_REDUNDANT_COMMANDS, + CONF_SLEEP_RGB_OR_COLOR_TEMP, + CONF_SLEEP_TRANSITION, + CONF_SUNRISE_OFFSET, + CONF_SUNRISE_TIME, + CONF_SUNSET_TIME, + CONF_TAKE_OVER_CONTROL, + CONF_TAKE_OVER_CONTROL_MODE, + CONF_TRANSITION, + CONF_TURN_ON_LIGHTS, + CONF_USE_DEFAULTS, + DEFAULT_MAX_BRIGHTNESS, + DEFAULT_NAME, + DEFAULT_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_COLOR_TEMP, + DEFAULT_SLEEP_RGB_COLOR, + DOMAIN, + SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, + SERVICE_SET_MANUAL_CONTROL, + SLEEP_MODE_SWITCH, + UNDO_UPDATE_LISTENER, + TakeOverControlMode, +) +from homeassistant.components.adaptive_lighting.switch import ( + CONF_INTERCEPT, + AdaptiveLightingManager, + AdaptiveSwitch, + SimpleSwitch, + _attributes_have_changed, + _expand_light_groups, + _turn_off_transition, + color_difference_redmean, + create_context, + is_our_context, + is_our_context_id, + short_hash, + validate, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, + SERVICE_TURN_OFF, + ColorMode, + LightEntityFeature, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.template import light as template_light +from homeassistant.components.template.light import StateLightEntity as LightTemplate +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigEntryState +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, + ATTR_SERVICE_DATA, + ATTR_SUPPORTED_FEATURES, + CONF_LIGHTS, + CONF_NAME, + EVENT_CALL_SERVICE, + EVENT_STATE_CHANGED, + SERVICE_TOGGLE, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + EntityCategory, +) +from homeassistant.core import Context, CoreState, Event, HomeAssistant, State +from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import area_registry as ar +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity_platform import async_get_platforms +from homeassistant.setup import async_setup_component +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) + +from tests.common import MockConfigEntry +from tests.common import mock_area_registry as mock_ha_area_registry + +# HA 2026.6 removed the legacy `light: platform: template` YAML format +# (home-assistant/core#169615); use the modern `template:` format there. +LEGACY_TEMPLATE_LIGHTS = hasattr(template_light, "PLATFORM_SCHEMA") + +_LOGGER = logging.getLogger(__name__) + +SUNRISE = datetime.datetime( + year=2020, + month=10, + day=17, + hour=6, +) +SUNSET = datetime.datetime( + year=2020, + month=10, + day=17, + hour=22, +) + +LAT_LONG_TZS = [ + (39, -1, "Europe/Madrid"), + (60, 50, "GMT"), + (55, 13, "Europe/Copenhagen"), + (52.379189, 4.899431, "Europe/Amsterdam"), + (32.87336, -117.22743, "US/Pacific"), +] + +ENTITY_LIGHT_1 = "light.light_1" +ENTITY_LIGHT_2 = "light.light_2" +ENTITY_LIGHT_3 = "light.light_3" +_SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}" +ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}" +ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_sleep_mode" +ENTITY_ADAPT_BRIGHTNESS_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_adapt_brightness" +ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_adapt_color" + +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE + + +def create_random_context() -> str: + return Context(id=ulid_transform.ulid_now(), parent_id=None) + + +@pytest.fixture +def reset_time_zone(): + """Reset time zone.""" + yield + dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE + + +@pytest.fixture +async def cleanup(hass): + yield + manager: AdaptiveLightingManager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + for timer in manager.auto_reset_manual_control_timers.values(): + timer.cancel() + for timer in manager.transition_timers.values(): + timer.cancel() + for task in manager.adaptation_tasks: + task.cancel() + + +async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitch]: + """Create the switch entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: DEFAULT_NAME, + CONF_INTERCEPT: False, + **extra_data, + }, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + return entry, switch + + +async def setup_lights(hass: HomeAssistant, with_group: bool = False): + """Set up 3 light entities using the 'template' platform.""" + n = 3 if not with_group else 5 # last 2 will be put in a group + + group_platform = { + "platform": "group", + "entities": ["light.light_4", "light.light_5"], + "name": "Light Group", + "unique_id": "light_group", + "all": "false", + } + + if LEGACY_TEMPLATE_LIGHTS: + template_lights = { + f"light_{i}": { + "unique_id": f"light_{i}", + "friendly_name": f"light_{i}", + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_color": None, + } + for i in range(1, n + 1) + } + template_lights["light_3"]["supports_transition_template"] = True + platforms = [{"platform": "template", "lights": template_lights}] + if with_group: + platforms.append(group_platform) + await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: platforms}, + ) + else: + if with_group: + # Setting up `template` below also sets up the `light` domain, + # after which `async_setup_component(hass, LIGHT_DOMAIN, ...)` + # would be a no-op, so the group platform must be set up first. + await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: [group_platform]}, + ) + modern_lights = [ + { + "name": f"light_{i}", + "unique_id": f"light_{i}", + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_hs": None, + } + for i in range(1, n + 1) + ] + modern_lights[2]["supports_transition"] = "{{ true }}" + await async_setup_component( + hass, + "template", + {"template": {"light": modern_lights}}, + ) + await hass.async_block_till_done() + + if with_group: + state = hass.states.get("light.light_group") + assert state.attributes["entity_id"] == ["light.light_4", "light.light_5"] + + platform = async_get_platforms(hass, "template") + lights = list(platform[0].entities.values()) + + await lights[0].async_turn_on() + await lights[1].async_turn_on() + for light in lights[2:]: + await light.async_turn_off() + + for light in lights: + set_light_brightness(light, 255) + light._attr_color_temp = 250 + light.async_write_ha_state() + + assert all(hass.states.get(light.entity_id) is not None for light in lights) + return lights + + +def set_light_brightness(light: LightTemplate, brightness: int) -> None: + """Set brightness across Home Assistant template light internals.""" + if hasattr(light, "_brightness"): + light._brightness = brightness + light._attr_brightness = brightness + + +async def setup_lights_and_switch( + hass, + extra_conf=None, + all_lights: bool = False, +) -> tuple[AdaptiveSwitch, list[LightTemplate]]: + """Create switch and demo lights.""" + # Setup demo lights and turn on + lights_instances = await setup_lights(hass) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + + # Setup switch + lights = [ + ENTITY_LIGHT_1, + ENTITY_LIGHT_2, + ] + + if all_lights: + lights.append(ENTITY_LIGHT_3) + + assert all(hass.states.get(light) is not None for light in lights) + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: lights, + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_PREFER_RGB_COLOR: False, + CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp + **(extra_conf or {}), + }, + ) + await hass.async_block_till_done() + return switch, lights_instances + + +# see https://github.com/home-assistant/core/blob/dev/homeassistant/scripts/benchmark/__init__.py +# basically just search the repo for EVENT_STATE_CHANGED look for how it's fired. +def create_transition_events( + light: str, + state: State, + last: dict | None = None, + current: dict | None = None, + total_events: int = 4, +) -> list[dict]: + assert light is not None + all_events = [] + for i in range(1, total_events): + # Build basic event data. + attributes = {} + + # The first state change always has the context from our integration. + # That one will not be in all_events. + # It's very possible it stores the parent_id though. + # If it stores the parent_id in all situations, there's a great improvement + # that could added in future updates. + + # Simulate the events the bulb would send to HASS. + last_brightness = last.get(ATTR_BRIGHTNESS) or state[ATTR_BRIGHTNESS] + current_brightness = current.get(ATTR_BRIGHTNESS) + if ( + last_brightness + and current_brightness + and last_brightness != current_brightness + ): + diff = (current_brightness - last_brightness) * (i / total_events) + attributes[ATTR_BRIGHTNESS] = last_brightness + diff + elif current_brightness: + attributes[ATTR_BRIGHTNESS] = current_brightness + current_kelvin = current.get(ATTR_COLOR_TEMP_KELVIN) + last_kelvin = last.get(ATTR_COLOR_TEMP_KELVIN) or state[ATTR_COLOR_TEMP_KELVIN] + if last_kelvin and current_kelvin and last_kelvin != current_kelvin: + diff = (current_kelvin - last_kelvin) * (i / total_events) + attributes[ATTR_COLOR_TEMP_KELVIN] = last_kelvin + diff + elif current_kelvin: + attributes[ATTR_COLOR_TEMP_KELVIN] = current_kelvin + + # Pack event + event_data = { + ATTR_ENTITY_ID: light, + "old_state": State(light, "on", attributes=last), + "new_state": State( + light, + "on", + attributes=attributes, + context=create_random_context(), + ), + } + all_events.append(event_data) + return all_events + + +async def test_adaptive_lighting_switches(hass): + """Test switches created for adaptive_lighting integration.""" + entry, switch = await setup_switch(hass, {}) + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 + assert set(hass.states.async_entity_ids(SWITCH_DOMAIN)) == { + switch.entity_id, + switch.sleep_mode_switch.entity_id, + switch.adapt_color_switch.entity_id, + switch.adapt_brightness_switch.entity_id, + } + assert ATTR_ADAPTIVE_LIGHTING_MANAGER in hass.data[DOMAIN] + assert entry.entry_id in hass.data[DOMAIN] + assert len(hass.data[DOMAIN].keys()) == 2 + + data = hass.data[DOMAIN][entry.entry_id] + assert SLEEP_MODE_SWITCH in data + assert SWITCH_DOMAIN in data + assert ADAPT_COLOR_SWITCH in data + assert ADAPT_BRIGHTNESS_SWITCH in data + assert UNDO_UPDATE_LISTENER in data + + assert len(data.keys()) == 5 + + +def async_process_ha_core_config(hass, config): + """Set up the Home Assistant configuration.""" + try: + # ha >= "2023.11.0" + from homeassistant.core_config import async_process_ha_core_config + + return async_process_ha_core_config(hass, config) + except ModuleNotFoundError: + import homeassistant.config as config_util + + return config_util.async_process_ha_core_config(hass, config) + + +@pytest.mark.parametrize(("lat", "long", "timezone"), LAT_LONG_TZS) +async def test_adaptive_lighting_time_zones_with_default_settings( + hass, + lat, + long, + timezone, + reset_time_zone, # pylint: disable=redefined-outer-name +): + """Test setting up the Adaptive Lighting switches with different timezones.""" + await async_process_ha_core_config( + hass, + {"latitude": lat, "longitude": long, "time_zone": timezone, "country": "US"}, + ) + _, switch = await setup_switch(hass, {}) + # Shouldn't raise an exception ever + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + ) + + +@pytest.mark.parametrize(("lat", "long", "timezone"), LAT_LONG_TZS) +async def test_adaptive_lighting_time_zones_and_sun_settings( + hass, + lat, + long, + timezone, + reset_time_zone, # pylint: disable=redefined-outer-name +): + """Test setting up the Adaptive Lighting switches with different timezones. + + Also test the (sleep) brightness and color temperature settings. + """ + await async_process_ha_core_config( + hass, + {"latitude": lat, "longitude": long, "time_zone": timezone, "country": "US"}, + ) + _, switch = await setup_switch( + hass, + { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + }, + ) + + context = switch.create_context("test") # needs to be passed to update method + min_color_temp = switch._sun_light_settings.min_color_temp + + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunrise = sunrise - datetime.timedelta(hours=1) + after_sunrise = sunrise + datetime.timedelta(hours=1) + + async def patch_time_and_update(time): + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=time, + ): + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + # At sunset the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour before sunset the brightness should be max and color_temp + # not at the smallest value yet. + await patch_time_and_update(before_sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + + # One hour after sunset the brightness should be down + await patch_time_and_update(after_sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # At sunrise the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour before sunrise the brightness should smaller than max + # and color_temp at the min value. + await patch_time_and_update(before_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour after sunrise the brightness should be up + await patch_time_and_update(after_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + + # Turn on sleep mode which make the brightness and color_temp + # deterministic regardless of the time + await switch.sleep_mode_switch.async_turn_on() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == DEFAULT_SLEEP_COLOR_TEMP + + +async def test_light_settings(hass): + """Test that light settings are correctly applied.""" + switch, _ = await setup_lights_and_switch(hass) + lights = switch.lights + + # Turn on "sleep mode" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + light_states = [hass.states.get(light) for light in lights] + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == round( + 255 * switch._settings[ATTR_BRIGHTNESS_PCT] / 100, + ) + last_service_data = switch.manager.last_service_data[state.entity_id] + assert state.attributes[ATTR_BRIGHTNESS] == last_service_data[ATTR_BRIGHTNESS] + assert ( + state.attributes[ATTR_COLOR_TEMP_KELVIN] + == last_service_data[ATTR_COLOR_TEMP_KELVIN] + ) + + # Turn off "sleep mode" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + # Test with different times + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunrise = sunrise - datetime.timedelta(hours=1) + after_sunrise = sunrise + datetime.timedelta(hours=1) + + context = switch.create_context("test") # needs to be passed to update method + + async def patch_time_and_get_updated_states(time): + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=time, + ): + await switch._update_attrs_and_maybe_adapt_lights( + context=context, + transition=0, + force=True, + ) + await hass.async_block_till_done() + return [hass.states.get(light) for light in lights] + + def assert_expected_color_temp(state): + last_service_data = switch.manager.last_service_data[state.entity_id] + assert ( + state.attributes[ATTR_COLOR_TEMP_KELVIN] + == last_service_data[ATTR_COLOR_TEMP_KELVIN] + ) + + # At sunset the brightness should be max and color_temp at the smallest value + light_states = await patch_time_and_get_updated_states(sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour before sunset the brightness should be max and color_temp + # not at the smallest value yet. + light_states = await patch_time_and_get_updated_states(before_sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour after sunset the brightness should be down + light_states = await patch_time_and_get_updated_states(after_sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] < 255 + assert_expected_color_temp(state) + + # At sunrise the brightness should be max and color_temp at the smallest value + light_states = await patch_time_and_get_updated_states(sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour before sunrise the brightness should smaller than max + # and color_temp at the min value. + light_states = await patch_time_and_get_updated_states(before_sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] < 255 + assert_expected_color_temp(state) + + # One hour after sunrise the brightness should be up + light_states = await patch_time_and_get_updated_states(after_sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + +async def test_manager_not_tracking_untracked_lights(hass): + """Test that lights that are not in a Adaptive Lighting switch aren't tracked.""" + switch, _ = await setup_lights_and_switch(hass) + light = ENTITY_LIGHT_3 + assert light not in switch.lights + for state in [True, False]: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: light}, + blocking=True, + ) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + ) + await hass.async_block_till_done() + assert light not in switch.manager.lights + + +@pytest.mark.parametrize("adapt_only_on_bare_turn_on", [True, False]) +@pytest.mark.parametrize("proactive_service_call_adaptation", [True, False]) +async def test_manual_control( + hass, + adapt_only_on_bare_turn_on, + proactive_service_call_adaptation, +): + """Test the 'manual control' tracking.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_ADAPT_ONLY_ON_BARE_TURN_ON: adapt_only_on_bare_turn_on, + CONF_INTERCEPT: proactive_service_call_adaptation, + }, + ) + assert switch._take_over_control + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + context = switch.create_context("test") # needs to be passed to update method + manual_control = switch.manager.manual_control + + async def update(): + await switch._update_attrs_and_maybe_adapt_lights(context=context, transition=0) + await hass.async_block_till_done() + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, + blocking=True, + ) + _LOGGER.debug("Turn light %s, to %s", "on" if state else "off", kwargs) + await hass.async_block_till_done() + await update() + + async def turn_switch(state, entity_id): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + async def change_manual_control(set_to, extra_service_data=None): + if extra_service_data is None: + extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT_1]} + _LOGGER.debug(f"{switch.manager.manual_control=}") + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_MANUAL_CONTROL: set_to, + **extra_service_data, + }, + blocking=True, + ) + _LOGGER.debug(f"{switch.manager.manual_control=}") + _LOGGER.debug("Called set_manual_control with %s", set_to) + await hass.async_block_till_done() + await update() + _LOGGER.debug("End of change_manual_control") + + def increased_brightness(): + return max(1, (light._attr_brightness + 100) % 255) + + def increased_color_temp(): + return max( + (light._attr_color_temp + 100) % light.max_color_temp_kelvin, + light.min_color_temp_kelvin, + ) + + # Nothing is manually controlled + await update() + assert not manual_control[ENTITY_LIGHT_1] + # Call light.turn_on for ENTITY_LIGHT_1 + await turn_light(True, brightness=increased_brightness()) + # Check that ENTITY_LIGHT_1 is manually controlled + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + # Per-attribute state attributes should reflect this + state_attrs = hass.states.get(switch.entity_id).attributes + assert ENTITY_LIGHT_1 in state_attrs["manual_control"] + assert ENTITY_LIGHT_1 in state_attrs["manual_control_brightness"] + assert ENTITY_LIGHT_1 not in state_attrs["manual_control_color"] + # Test adaptive_lighting.set_manual_control + await change_manual_control(False) + # Check that ENTITY_LIGHT_1 is not manually controlled + assert not manual_control[ENTITY_LIGHT_1] + state_attrs = hass.states.get(switch.entity_id).attributes + assert ENTITY_LIGHT_1 not in state_attrs["manual_control"] + assert ENTITY_LIGHT_1 not in state_attrs["manual_control_brightness"] + assert ENTITY_LIGHT_1 not in state_attrs["manual_control_color"] + + # Check that toggling light off to on resets manual control + await change_manual_control(True) + assert manual_control[ENTITY_LIGHT_1] + await turn_light(False) + assert not manual_control[ENTITY_LIGHT_1], manual_control + await turn_light(True, brightness=increased_brightness()) + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + # Turning on from OFF with brightness: + # - With adapt_only_on_bare_turn_on=True: SHOULD mark as manually controlled (to preserve scenes) + # - With adapt_only_on_bare_turn_on=False: should NOT mark (fix for issue #1378) + if adapt_only_on_bare_turn_on: + assert ( + manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + ), manual_control + else: + assert not manual_control[ENTITY_LIGHT_1], manual_control + # Reset for next test + await turn_light(False) + await turn_light(True) + assert not manual_control[ENTITY_LIGHT_1], manual_control + # Now change brightness while ON - this should always be manual control + await turn_light(True, brightness=increased_brightness()) + assert ( + manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + ), manual_control + + # Toggling the main or sleep switch resets manual control by default. + for entity_id in [switch.entity_id, switch.sleep_mode_switch.entity_id]: + await change_manual_control(True) + assert manual_control[ENTITY_LIGHT_1] + await turn_switch(False, entity_id) + await turn_switch(True, entity_id) + assert not manual_control[ENTITY_LIGHT_1] + + # Check that manual control is still enabled if set while bulb is off. + # Test issue #37 + await turn_light(False) + await change_manual_control(True) + await turn_light(True) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL + + # Check that when 'adapt_brightness' is off, changing the brightness + # doesn't mark it as manually controlled but changing color_temp + # does + await turn_light(False) + await turn_light(True) # reset manually controlled status + assert not manual_control[ENTITY_LIGHT_1] + await switch.adapt_brightness_switch.async_turn_off() + await turn_light(True, brightness=increased_brightness()) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + mired_range = (light.min_color_temp_kelvin, light.max_color_temp_kelvin) + kelvin_range = ( + color_temperature_mired_to_kelvin(mired_range[1]), + color_temperature_mired_to_kelvin(mired_range[0]), + ) + ptp_kelvin = kelvin_range[1] - kelvin_range[0] + await turn_light( + True, + color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin, + ) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL + await switch.adapt_brightness_switch.async_turn_on() # turn on again + + # Check that when 'adapt_color' is off, changing the color + # doesn't mark it as manually controlled but changing brightness + # does + await turn_light(False) # reset manually controlled status + await turn_light(True) + assert not manual_control[ENTITY_LIGHT_1] + await switch.adapt_color_switch.async_turn_off() + await turn_light(True, color_temp_kelvin=increased_color_temp()) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR + await turn_light(True, brightness=increased_brightness()) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL + + # Check that when 'adapt_color' adapt_brightness are both off + # nothing marks it as manually controlled + await turn_light(False) # reset manually controlled status + await turn_light(True) + await switch.adapt_color_switch.async_turn_off() + await switch.adapt_brightness_switch.async_turn_off() + assert not manual_control[ENTITY_LIGHT_1] + await turn_light(True, color_temp_kelvin=increased_color_temp()) + await turn_light(True, brightness=increased_brightness()) + await turn_light( + True, + color_temp_kelvin=increased_color_temp(), + brightness=increased_brightness(), + ) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL + # Turn switches on again + await switch.adapt_color_switch.async_turn_on() + await switch.adapt_brightness_switch.async_turn_on() + + # Check that when no lights are specified, all are reset + await change_manual_control(True, {CONF_LIGHTS: switch.lights}) + assert all(manual_control[eid] for eid in switch.lights) + # do not pass "lights" so reset all + await change_manual_control(False, {}) + assert all(not manual_control[eid] for eid in switch.lights) + + # Turn off light and turn on using adaptive_lighting.apply + await turn_light(False) + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + assert not manual_control[ENTITY_LIGHT_1] + + # Check that manual control `True` sets all attributes + await change_manual_control(False) + assert not manual_control[ENTITY_LIGHT_1] + await change_manual_control(True) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL + state_attrs = hass.states.get(switch.entity_id).attributes + assert state_attrs["manual_control_brightness"] == [ENTITY_LIGHT_1] + assert state_attrs["manual_control_color"] == [ENTITY_LIGHT_1] + + # Check that manual control `False` unsets all attributes + await change_manual_control(False) + assert not manual_control[ENTITY_LIGHT_1] + + # Check that manual control attributes can be selectively set + await change_manual_control("brightness") + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + await change_manual_control("color") + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR + state_attrs = hass.states.get(switch.entity_id).attributes + assert state_attrs["manual_control_brightness"] == [] + assert state_attrs["manual_control_color"] == [ENTITY_LIGHT_1] + + +async def test_sleep_mode_does_not_emit_manual_control_event(hass): + """Sleep adaptation is internal; only an external change emits manual control.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_INTERCEPT: True, + CONF_SLEEP_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + events = [] + remove_listener = hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + for service, brightness in [(SERVICE_TURN_ON, 3), (SERVICE_TURN_OFF, 128)]: + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == brightness + assert hass.states.get(switch.entity_id).attributes["manual_control"] == [] + assert events == [] + + # Positive control: the same live listener must see a real manual change. + context = Context() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 77}, + context=context, + blocking=True, + ) + await hass.async_block_till_done() + remove_listener() + assert len(events) == 1 + assert events[0].context == context + assert events[0].data == { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + SWITCH_DOMAIN: switch.entity_id, + CONF_MANUAL_CONTROL: LightControlAttributes.BRIGHTNESS, + } + + +async def test_reload_cancels_old_manual_reset_and_keeps_service_tracking(hass): + """An unloaded timer must not re-adapt the replacement profile's light.""" + await setup_lights(hass) + entry, switch = await setup_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_AUTORESET_CONTROL: 60, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + await hass.async_block_till_done() + old_timer = switch.manager.auto_reset_manual_control_timers[ENTITY_LIGHT_1] + assert old_timer.is_running() + assert await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + assert not old_timer.is_running() + new_switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + + events = [] + remove_listener = hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 99}, + blocking=True, + ) + await hass.async_block_till_done() + remove_listener() + assert len(events) == 1 + assert events[0].data[SWITCH_DOMAIN] == new_switch.entity_id + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 99 + assert hass.states.get(new_switch.entity_id).attributes[ + "manual_control_brightness" + ] == [ENTITY_LIGHT_1] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_manual_control_expiry_does_not_adapt_disabled_profile(hass): + """Expiry clears ownership without sending light commands for a disabled profile.""" + switch, _ = await setup_lights_and_switch( + hass, + {CONF_LIGHTS: [ENTITY_LIGHT_1], CONF_AUTORESET_CONTROL: 1}, + ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: switch.entity_id}, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + {ATTR_ENTITY_ID: switch.entity_id, CONF_MANUAL_CONTROL: True}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + light_before = hass.states.get(ENTITY_LIGHT_1) + calls = [] + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, calls.append) + timer = switch.manager.auto_reset_manual_control_timers[ENTITY_LIGHT_1] + await timer.task + await hass.async_block_till_done() + remove_listener() + assert not timer.is_running() + assert hass.states.get(switch.entity_id).state == STATE_OFF + assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + assert hass.states.get(ENTITY_LIGHT_1) == light_before + assert not [event for event in calls if event.data["domain"] == LIGHT_DOMAIN] + + +@pytest.mark.parametrize("reset_on_sleep", [None, True, False]) +async def test_sleep_mode_manual_control_reset(hass, reset_on_sleep): + """Keep the old default and preserve manual brightness only when opted out.""" + options = {CONF_MIN_BRIGHTNESS: 50, CONF_MAX_BRIGHTNESS: 50} + if reset_on_sleep is not None: + options[CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE] = reset_on_sleep + switch, (light, *_) = await setup_lights_and_switch(hass, options) + + for service, adapted_brightness in [(SERVICE_TURN_ON, 3), (SERVICE_TURN_OFF, 128)]: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.manager.get_manual_control_attributes(light.entity_id) + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + if reset_on_sleep is False: + assert switch.manager.get_manual_control_attributes(light.entity_id) + assert hass.states.get(light.entity_id).attributes[ATTR_BRIGHTNESS] == 200 + else: + assert not switch.manager.get_manual_control_attributes(light.entity_id) + assert ( + hass.states.get(light.entity_id).attributes[ATTR_BRIGHTNESS] + == adapted_brightness + ) + + +async def test_sleep_mode_preserves_manual_control_and_cancels_old_adaptation(hass): + """A queued pre-sleep command must not overwrite a preserved manual setting.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + {CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE: False}, + ) + waiting = asyncio.Event() + release = asyncio.Event() + + async def pending_service_data(): + waiting.set() + await release.wait() + yield {ATTR_ENTITY_ID: light.entity_id, ATTR_BRIGHTNESS: 1} + + data = AdaptationData( + light.entity_id, + switch.create_context("test"), + 0, + pending_service_data(), + force=False, + max_length=1, + attributes=LightControlAttributes.BRIGHTNESS, + ) + task = asyncio.create_task(switch.execute_cancellable_adaptation_calls(data)) + await waiting.wait() + try: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + finally: + release.set() + await task + await hass.async_block_till_done() + assert switch.manager.get_manual_control_attributes(light.entity_id) + assert hass.states.get(light.entity_id).attributes[ATTR_BRIGHTNESS] == 200 + + +@flaky(max_runs=3, min_passes=1) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +async def test_auto_reset_manual_control(hass, mode): + switch, (light, *_) = await setup_lights_and_switch( + hass, + {CONF_AUTORESET_CONTROL: 0.1, CONF_TAKE_OVER_CONTROL_MODE: mode}, + ) + context = switch.create_context("test") # needs to be passed to update method + manual_control = switch.manager.manual_control + + async def update(): + await switch._update_attrs_and_maybe_adapt_lights(context=context, transition=0) + await hass.async_block_till_done() + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: light.entity_id, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + await update() + _LOGGER.debug( + "Turn light %s to state %s, to %s", + light.entity_id, + state, + kwargs, + ) + + _LOGGER.debug("Start test auto reset manual control") + await turn_light(True, brightness=1) + await turn_light(True, brightness=10) + assert manual_control[light.entity_id] == LightControlAttributes.BRIGHTNESS + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0 + ) + await update() + # The auto reset must also re-adapt the light right away, not only clear the + # flag. Collect the 'light.turn_on' calls made with the 'autoreset' context. + autoreset_calls: list[Event] = [] + + async def _on_call_service(event: Event) -> None: + if ( + event.data.get("domain") == LIGHT_DOMAIN + and event.data.get("service") == SERVICE_TURN_ON + and is_our_context(event.context, "autoreset") + ): + autoreset_calls.append(event) + + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, _on_call_service) + await asyncio.sleep(0.3) # Should be enough time for auto reset + await hass.async_block_till_done() + remove_listener() + assert not manual_control[light.entity_id], (light, manual_control) + assert ( + light.entity_id not in switch.extra_state_attributes["autoreset_time_remaining"] + ) + assert autoreset_calls, "auto reset did not re-adapt the light" + + # Do a couple of quick changes and check that light is not reset + for i in range(3): + _LOGGER.debug("Quick change %s", i) + await turn_light(True, brightness=(i + 1) * 20) + await asyncio.sleep(0.05) # Less than 0.1 + assert manual_control[light.entity_id] + + await update() + await asyncio.sleep(0.3) # Wait the auto reset time + assert not manual_control[light.entity_id] + + +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + ("attribute", "value", "next_value", "manual_attributes"), + [ + (ATTR_BRIGHTNESS, 10, 20, LightControlAttributes.BRIGHTNESS), + (ATTR_COLOR_TEMP_KELVIN, 2000, 2200, LightControlAttributes.COLOR), + ], +) +async def test_interval_adaptation_preserves_manual_control_timeout( + hass, + freezer, + cleanup, + intercept, + mode, + attribute, + value, + next_value, + manual_attributes, +): + """Adaptation must not postpone auto reset; another manual change must.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: intercept, + }, + ) + + async def change_manually(value): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, attribute: value}, + blocking=True, + ) + await hass.async_block_till_done() + + for manual_value in (value, next_value): + # A new external change to the already-manual axis restarts the timer. + await change_manually(manual_value) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + for elapsed in (90, 180): + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attributes + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][ + light.entity_id + ] + == 7200 - elapsed + ) + + # An external bare turn-on also keeps its existing timer restart behavior. + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + "manual_attribute", + [LightControlAttributes.BRIGHTNESS, LightControlAttributes.COLOR], +) +async def test_tracked_change_seeds_non_ha_baseline( + hass, + freezer, + cleanup, + intercept, + mode, + manual_attribute, +): + """A tracked service change must not be detected again by the next poll.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + }, + ) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + force=True, + transition=0, + ) + await hass.async_block_till_done() + + if manual_attribute == LightControlAttributes.BRIGHTNESS: + adaptive_value = light.brightness + attribute = ATTR_BRIGHTNESS + difference = 120 + else: + adaptive_value = light.color_temp_kelvin + attribute = ATTR_COLOR_TEMP_KELVIN + difference = 500 + assert adaptive_value is not None + manual_value = ( + adaptive_value - difference + if adaptive_value >= difference + else adaptive_value + difference + ) + + events = [] + hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + service_context = Context() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, attribute: manual_value}, + blocking=True, + context=service_context, + ) + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attribute + ) + assert len(events) == 1 + + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7110 + ) + assert len(events) == 1 + + if manual_attribute == LightControlAttributes.BRIGHTNESS: + set_light_brightness(light, adaptive_value) + else: + light._attr_color_temp_kelvin = adaptive_value + if hasattr(light, "_temperature"): + light._temperature = color_temperature_kelvin_to_mired(adaptive_value) + light.async_set_context(service_context) + light.async_write_ha_state() + await hass.async_block_till_done() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + assert len(events) == 2 + + +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + ("first_manual_attribute", "second_manual_attribute"), + [ + (LightControlAttributes.BRIGHTNESS, LightControlAttributes.COLOR), + (LightControlAttributes.COLOR, LightControlAttributes.BRIGHTNESS), + ], +) +async def test_unchanged_non_ha_change_preserves_manual_control_timeout( + hass, + freezer, + cleanup, + intercept, + mode, + first_manual_attribute, + second_manual_attribute, +): + """An unchanged physical state must not renew its manual-control timeout.""" + switch, lights = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + }, + ) + light = lights[0] + lights_by_entity = {item.entity_id: item for item in lights} + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + force=True, + transition=0, + ) + await hass.async_block_till_done() + + adaptive_brightness = light.brightness + assert adaptive_brightness is not None + adaptive_color_temp = light.color_temp_kelvin + assert adaptive_color_temp is not None + manual_values = { + LightControlAttributes.BRIGHTNESS: ( + adaptive_brightness - 120 + if adaptive_brightness >= 120 + else adaptive_brightness + 120 + ), + LightControlAttributes.COLOR: ( + adaptive_color_temp - 500 + if adaptive_color_temp >= 2500 + else adaptive_color_temp + 500 + ), + } + + def set_physical_state(attribute): + if attribute == LightControlAttributes.BRIGHTNESS: + set_light_brightness(light, manual_values[attribute]) + else: + color_temp_kelvin = manual_values[attribute] + light._attr_color_temp_kelvin = color_temp_kelvin + if hasattr(light, "_temperature"): + light._temperature = color_temperature_kelvin_to_mired( + color_temp_kelvin, + ) + + async def flush_physical_state(hass, entity_id): + lights_by_entity[entity_id].async_write_ha_state() + + with patch( + "homeassistant.components.adaptive_lighting.switch.async_update_entity", + new=AsyncMock(side_effect=flush_physical_state), + ): + set_physical_state(first_manual_attribute) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == first_manual_attribute + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == first_manual_attribute + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7110 + ) + + freezer.tick(90) + set_physical_state(second_manual_attribute) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == LightControlAttributes.ALL + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + "manual_attribute", + [LightControlAttributes.BRIGHTNESS, LightControlAttributes.COLOR], +) +async def test_apply_updates_non_ha_change_baseline( + hass, + freezer, + cleanup, + intercept, + mode, + manual_attribute, +): + """An adaptive apply must become the baseline for later physical changes.""" + switch, lights = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + }, + ) + light = lights[0] + lights_by_entity = {item.entity_id: item for item in lights} + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + force=True, + transition=0, + ) + await hass.async_block_till_done() + + adaptive_value = ( + light.brightness + if manual_attribute == LightControlAttributes.BRIGHTNESS + else light.color_temp_kelvin + ) + assert adaptive_value is not None + difference = 120 if manual_attribute == LightControlAttributes.BRIGHTNESS else 500 + manual_value = ( + adaptive_value - difference + if adaptive_value >= difference + else adaptive_value + difference + ) + + def set_physical_state(value=manual_value): + if manual_attribute == LightControlAttributes.BRIGHTNESS: + set_light_brightness(light, value) + else: + light._attr_color_temp_kelvin = value + if hasattr(light, "_temperature"): + light._temperature = color_temperature_kelvin_to_mired(value) + + async def flush_physical_state(hass, entity_id): + lights_by_entity[entity_id].async_write_ha_state() + + with patch( + "homeassistant.components.adaptive_lighting.switch.async_update_entity", + new=AsyncMock(side_effect=flush_physical_state), + ): + set_physical_state() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attribute + ) + + direction = 1 if manual_value < adaptive_value else -1 + # Legacy template lights round via mireds; 70 K keeps one reported step + # below 100 K and two steps above it across the configured range. + small_change = ( + 15 if manual_attribute == LightControlAttributes.BRIGHTNESS else 70 + ) + freezer.tick(90) + set_physical_state(manual_value + direction * small_change) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7110 + ) + + adapt_brightness = manual_attribute == LightControlAttributes.COLOR + adapt_color = manual_attribute == LightControlAttributes.BRIGHTNESS + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [light.entity_id], + ATTR_ADAPT_BRIGHTNESS: adapt_brightness, + ATTR_ADAPT_COLOR: adapt_color, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attribute + ) + + freezer.tick(90) + pre_apply_value = manual_value + direction * small_change * 2 + set_physical_state(pre_apply_value) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [light.entity_id], + ATTR_ADAPT_BRIGHTNESS: not adapt_brightness, + ATTR_ADAPT_COLOR: not adapt_color, + }, + blocking=True, + ) + await hass.async_block_till_done() + applied_value = ( + light.brightness + if manual_attribute == LightControlAttributes.BRIGHTNESS + else light.color_temp_kelvin + ) + assert applied_value != pre_apply_value + + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7110 + ) + + set_physical_state(pre_apply_value) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize("service_data", [{}, {ATTR_BRIGHTNESS: 20}]) +async def test_mixed_turn_on_restarts_manual_control_timeout( + hass, + freezer, + cleanup, + mode, + service_data, +): + """A mixed-target request must renew manual control on its skipped light.""" + switch, (manual_light, _, off_light) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: True, + }, + all_lights=True, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: manual_light.entity_id, ATTR_BRIGHTNESS: 10}, + blocking=True, + ) + await hass.async_block_till_done() + freezer.tick(90) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: [manual_light.entity_id, off_light.entity_id], + **service_data, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # HA dispatches the original external event before interception. It renews + # manual control even though the skipped target is replayed in our context. + assert hass.states.is_state(off_light.entity_id, STATE_ON) + assert is_our_context( + switch.manager.turn_on_event[manual_light.entity_id].context, + "skipped", + ) + assert ( + switch.manager.get_manual_control_attributes(manual_light.entity_id) + == LightControlAttributes.BRIGHTNESS + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][ + manual_light.entity_id + ] + == 7200 + ) + + +@pytest.mark.parametrize("intercept", [True, False]) +@pytest.mark.parametrize( + ("service_data", "brightness", "color"), + [ + ({ATTR_BRIGHTNESS: 200}, True, False), + ({"brightness_step": 5}, True, False), + ({ATTR_COLOR_TEMP_KELVIN: 4000}, False, True), + ({ATTR_BRIGHTNESS: 200, ATTR_COLOR_TEMP_KELVIN: 4000}, True, True), + ], +) +async def test_manual_control_state_updates_without_adaptation( + hass, + intercept, + service_data, + brightness, + color, +): + """Publish manual state on service changes and resets, without an interval tick.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + events = [] + hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, **service_data}, + blocking=True, + context=Context(), + ) + await hass.async_block_till_done() + attrs = hass.states.get(switch.entity_id).attributes + assert attrs["manual_control"] == [light.entity_id] + assert attrs["manual_control_brightness"] == ( + [light.entity_id] if brightness else [] + ) + assert attrs["manual_control_color"] == ([light.entity_id] if color else []) + assert len(events) == 1 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + attrs = hass.states.get(switch.entity_id).attributes + assert attrs["manual_control"] == [] + assert attrs["manual_control_brightness"] == [] + assert attrs["manual_control_color"] == [] + + +async def test_manual_control_state_updates_shared_switches(hass): + """Publish shared state on both profiles when one receives a service call.""" + switch, (light, *_) = await setup_lights_and_switch(hass) + _, other = await setup_switch( + hass, + {CONF_NAME: "other", CONF_LIGHTS: [light.entity_id]}, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [light.entity_id], + CONF_MANUAL_CONTROL: "brightness", + }, + blocking=True, + ) + await hass.async_block_till_done() + for profile in (switch, other): + attrs = hass.states.get(profile.entity_id).attributes + assert attrs["manual_control"] == [light.entity_id] + assert attrs["manual_control_brightness"] == [light.entity_id] + assert attrs["manual_control_color"] == [] + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + {ATTR_ENTITY_ID: other.entity_id, CONF_MANUAL_CONTROL: False}, + blocking=True, + ) + await hass.async_block_till_done() + for profile in (switch, other): + attrs = hass.states.get(profile.entity_id).attributes + assert attrs["manual_control"] == [] + assert attrs["manual_control_brightness"] == [] + assert attrs["manual_control_color"] == [] + + +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize( + ( + "brightness_takeover", + "color_takeover", + "color_enabled", + "color_mode", + "expected_brightness", + "expected_color_calls", + "event_profiles", + ), + [ + ( + True, + True, + True, + TakeOverControlMode.PAUSE_CHANGED, + 77, + 1, + ["brightness", "color"], + ), + ( + True, + True, + True, + TakeOverControlMode.PAUSE_ALL, + 77, + 0, + ["brightness", "color"], + ), + (False, True, True, TakeOverControlMode.PAUSE_CHANGED, 77, 1, ["color"]), + (True, False, True, TakeOverControlMode.PAUSE_CHANGED, 77, 1, ["brightness"]), + (False, False, True, TakeOverControlMode.PAUSE_CHANGED, 128, 1, []), + (False, True, False, TakeOverControlMode.PAUSE_CHANGED, 128, 0, []), + (True, True, False, TakeOverControlMode.PAUSE_CHANGED, 77, 0, ["brightness"]), + ], +) +async def test_shared_profiles_track_manual_brightness( + hass, + intercept, + brightness_takeover, + color_takeover, + color_enabled, + color_mode, + expected_brightness, + expected_color_calls, + event_profiles, +): + """Shared owners track manual service calls and apply each profile's pause mode.""" + await setup_lights(hass) + profiles = {} + for name, takeover, mode in ( + ("brightness", brightness_takeover, TakeOverControlMode.PAUSE_CHANGED), + ("color", color_takeover, color_mode), + ): + _, switch = await setup_switch( + hass, + { + CONF_NAME: name, + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_INTERCEPT: intercept, + CONF_TAKE_OVER_CONTROL: takeover, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + CONF_MIN_COLOR_TEMP: 4000, + CONF_MAX_COLOR_TEMP: 4000, + }, + ) + other_axis = ( + switch.adapt_color_switch + if name == "brightness" + else switch.adapt_brightness_switch + ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: other_axis.entity_id}, + blocking=True, + ) + profiles[name] = switch + if not color_enabled: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: profiles["color"].entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + events = [] + remove_events = hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + context = Context() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 77}, + context=context, + blocking=True, + ) + await hass.async_block_till_done() + calls = [] + remove_calls = hass.bus.async_listen(EVENT_CALL_SERVICE, calls.append) + for switch in profiles.values(): + if switch.is_on: + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + remove_calls() + remove_events() + + light_calls = [ + event.data["service_data"] + for event in calls + if event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + ] + assert ( + sum(ATTR_COLOR_TEMP_KELVIN in call for call in light_calls) + == expected_color_calls + ) + assert sum(ATTR_BRIGHTNESS in call for call in light_calls) == ( + expected_brightness == 128 + ) + assert ( + hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] + == expected_brightness + ) + # Independent profiles may publish their events in either order. + assert sorted(event.data[SWITCH_DOMAIN] for event in events) == sorted( + profiles[name].entity_id for name in event_profiles + ) + assert all(event.context == context for event in events) + assert all( + event.data[CONF_MANUAL_CONTROL] == LightControlAttributes.BRIGHTNESS + for event in events + ) + for switch in profiles.values(): + if not switch.is_on: + continue + attrs = hass.states.get(switch.entity_id).attributes + assert attrs["manual_control_brightness"] == ( + [ENTITY_LIGHT_1] if event_profiles else [] + ) + assert attrs["manual_control_color"] == [] + + +async def test_manual_control_state_ignores_incomplete_entries(hass): + """An entry awaiting platform setup must not break another profile's updates.""" + switch, (light, *_) = await setup_lights_and_switch(hass) + pending = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: "pending"}) + pending.add_to_hass(hass) + hass.data[DOMAIN][pending.entry_id] = {} + + switch.manager.set_manual_control_attributes(light.entity_id) + await hass.async_block_till_done() + assert hass.states.get(switch.entity_id).attributes["manual_control"] == [ + light.entity_id, + ] + + +async def test_adaptation_attribute_selection(hass): + """Test the 'manual control' tracking.""" + switch, (light, *_) = await setup_lights_and_switch(hass) + + # Assert default settings + assert switch._take_over_control + assert switch._take_over_control_mode == TakeOverControlMode.PAUSE_ALL + + # Check that PAUSE_ALL leads to adaptation of all attributes when none are manually controlled + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.ALL + ) + + # Check that PAUSE_ALL leads to no adaptation when a single attribute is manually controlled + switch.manager.add_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.BRIGHTNESS, + ) + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.BRIGHTNESS + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + # Check that PAUSE_ALL leads to no adaptation when all attributes are manually controlled + switch.manager.add_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.COLOR, + ) + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.ALL + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.NONE, + ) + + # Check that PAUSE_CHANGED leads to adaptation of all attributes when none are manually controlled + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.ALL + ) + + # Check that PAUSE_CHANGED leads to adaptation of the remaining non-manual attributes + switch.manager.add_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.BRIGHTNESS, + ) + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.BRIGHTNESS + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.COLOR + ) + + # Check that PAUSE_CHANGED leads to no adaptation when all attributes are manually controlled + switch.manager.add_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.COLOR, + ) + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.ALL + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + await switch.adapt_brightness_switch.async_turn_off() + + # Check that with adapt_brightness off and PAUSE_CHANGED, only color is adapted when none are manually controlled + switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.NONE, + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.COLOR + ) + + # Check that with adapt_brightness off and PAUSE_CHANGED, nothing is adapted when color is manually controlled + switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.COLOR, + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + # Check that with adapt_brightness off and PAUSE_ALL, only color is adapted when none are manually controlled + switch._take_over_control_mode = TakeOverControlMode.PAUSE_ALL + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.NONE, + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.COLOR + ) + + # Check that with adapt_brightness off and PAUSE_ALL, nothing is adapted when color is manually controlled + switch._take_over_control_mode = TakeOverControlMode.PAUSE_ALL + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.COLOR, + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + +async def test_apply_service(hass): + """Test adaptive_lighting.apply service.""" + switch, (_, _, light) = await setup_lights_and_switch(hass) + entity_id = light.entity_id + assert entity_id not in switch.lights + + def increased_brightness(): + return max(1, (light._attr_brightness + 100) % 255) + + def increased_color_temp(): + return max( + (light._attr_color_temp + 100) % light.max_color_temp_kelvin, + light.min_color_temp_kelvin, + ) + + async def change_light(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: increased_brightness(), + ATTR_COLOR_TEMP_KELVIN: increased_color_temp(), + }, + blocking=True, + ) + await hass.async_block_till_done() + + async def apply(**kwargs): + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [entity_id], + CONF_TURN_ON_LIGHTS: True, + **kwargs, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test turn on with defaults + assert hass.states.get(entity_id).state == STATE_OFF + await apply() + assert hass.states.get(entity_id).state == STATE_ON + await change_light() + + # Test only changing color + old_state = hass.states.get(entity_id).attributes + await apply(adapt_color=True, adapt_brightness=False) + new_state = hass.states.get(entity_id).attributes + assert old_state[ATTR_BRIGHTNESS] == new_state[ATTR_BRIGHTNESS] + assert old_state[ATTR_COLOR_TEMP_KELVIN] != new_state[ATTR_COLOR_TEMP_KELVIN] + + # Test only changing brightness + await change_light() + old_state = hass.states.get(entity_id).attributes + await apply(adapt_color=False, adapt_brightness=True) + new_state = hass.states.get(entity_id).attributes + assert old_state[ATTR_BRIGHTNESS] != new_state[ATTR_BRIGHTNESS] + assert old_state[ATTR_COLOR_TEMP_KELVIN] == new_state[ATTR_COLOR_TEMP_KELVIN] + + +async def test_apply_service_uses_each_switch_transition(hass): + """Test global apply resolves omitted transition for each profile.""" + await setup_lights(hass) + _, switch_1 = await setup_switch( + hass, + { + CONF_NAME: "switch 1", + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_INITIAL_TRANSITION: 3, + }, + ) + _, switch_2 = await setup_switch( + hass, + { + CONF_NAME: "switch 2", + CONF_LIGHTS: [ENTITY_LIGHT_2], + CONF_INITIAL_TRANSITION: 7, + }, + ) + + with ( + patch.object(switch_1, "_adapt_light", new=AsyncMock()) as adapt_1, + patch.object(switch_2, "_adapt_light", new=AsyncMock()) as adapt_2, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + {ATTR_ENTITY_ID: [switch_1.entity_id, switch_2.entity_id]}, + blocking=True, + ) + assert adapt_1.await_args.kwargs["transition"] == 3 + assert adapt_2.await_args.kwargs["transition"] == 7 + + adapt_1.reset_mock() + adapt_2.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: [switch_1.entity_id, switch_2.entity_id], + CONF_TRANSITION: 0, + }, + blocking=True, + ) + assert adapt_1.await_args.kwargs["transition"] == 0 + assert adapt_2.await_args.kwargs["transition"] == 0 + + +async def test_switch_off_on_off(hass): + """Test switch rapid off_on_off.""" + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + + async def update(): + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + transition=0, + ) + await hass.async_block_till_done() + + switch, _ = await setup_lights_and_switch(hass) + + for turn_light_state_at_end in [True, False]: + # Turn light on + await turn_light(True) + # Turn light off with transition + await turn_light(False, transition=1) + + assert not switch.manager.manual_control[ENTITY_LIGHT_1] + # Set state to on after a second (like happens IRL) + await asyncio.sleep(1e-3) + hass.states.async_set(ENTITY_LIGHT_1, STATE_ON) + # Set state to off after a second (like happens IRL) + await asyncio.sleep(1e-3) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF) + + # Now we test whether the sleep task is there + assert ENTITY_LIGHT_1 in switch.manager.sleep_tasks + sleep_task = switch.manager.sleep_tasks[ENTITY_LIGHT_1] + assert not sleep_task.cancelled() + + # A 'light.turn_on' event should cancel that task + await turn_light(turn_light_state_at_end) + await update() + state = hass.states.get(ENTITY_LIGHT_1).state + if turn_light_state_at_end: + assert sleep_task.cancelled() + assert state == STATE_ON + else: + assert state == STATE_OFF + + +def test_color_difference_redmean(): + """Test color_difference_redmean function.""" + for _ in range(10): + rgb_1 = (randint(0, 255), randint(0, 255), randint(0, 255)) + rgb_2 = (randint(0, 255), randint(0, 255), randint(0, 255)) + color_difference_redmean(rgb_1, rgb_2) + color_difference_redmean((0, 0, 0), (255, 255, 255)) + + +def test_attributes_have_changed(): + """Test _attributes_have_changed function.""" + attributes_1 = { + ATTR_BRIGHTNESS: 1, + ATTR_RGB_COLOR: (0, 0, 0), + ATTR_COLOR_TEMP_KELVIN: 100, + } + attributes_2 = { + ATTR_BRIGHTNESS: 100, + ATTR_RGB_COLOR: (255, 0, 0), + ATTR_COLOR_TEMP_KELVIN: 300, + } + kwargs = { + "light": "light.test", + "context": Context(), + } + assert not _attributes_have_changed( + old_attributes=attributes_1, + new_attributes=attributes_1, + **kwargs, + ) + for key, value in attributes_2.items(): + attrs = dict(attributes_1) + attrs[key] = value + assert _attributes_have_changed( + old_attributes=attributes_1, + new_attributes=attrs, + **kwargs, + ) + # Test color mode switches - feature added to detect external changes + # (e.g., when Hue scenes change light from color_temp to RGB mode) + # See: https://github.com/basnijholt/adaptive-lighting/issues/1275 + # + # All mode switches are now detected bidirectionally by checking original + # attributes BEFORE conversion in _has_color_mode_changed(). + _LOGGER.debug( + "Test switch from color_temp to rgb_color - should detect mode change", + ) + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)}, + **kwargs, + ) + _LOGGER.debug( + "Test switch from rgb_color to color_temp - should detect mode change", + ) + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, + **kwargs, + ) + _LOGGER.debug("Test switch from color_temp to color_xy - should detect mode change") + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)}, + **kwargs, + ) + _LOGGER.debug("Test switch from color_xy to color_temp - should detect mode change") + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, + **kwargs, + ) + + +async def test_state_change_handlers(hass): + """Test AdaptiveLightingManager's EVENT_STATE_CHANGED listener. + =============== + + Sequence of events: + 1. Transition from sleep mode to normal. + 2. Create simulated transition events for that adapt. + 3. Fire all simulated transition events. + 4. Assert all possible problems that would result. + Also tests significant changes. + """ + # Keep adaptive brightness distinct from the manual values 20, 40, and 50. + switch, (light, *_) = await setup_lights_and_switch( + hass, + {CONF_MIN_BRIGHTNESS: 50, CONF_MAX_BRIGHTNESS: 50}, + ) + context = switch.create_context("test") # needs to be passed to update method + + # [Config options]: + transition_used = 2 + total_events = 5 + + async def set_brightness(val: int): + # 'Unsafe' set but we know what we're doing. + hass.states.async_set( + ENTITY_LIGHT_1, + "on", + {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}, + ) + await hass.async_block_till_done() + # Call code in AdaptiveLightingManager + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + "new_state": { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + "state": "on", + ATTR_BRIGHTNESS: val, + }, + }, + ) + await hass.async_block_till_done() + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + + async def update(force: bool = False): + await switch._update_attrs_and_maybe_adapt_lights( + context=context, + force=force, + transition=0, + ) + await hass.async_block_till_done() + + # 1. Adapt to sleep without a transition. + # Should only be one state change. + _LOGGER.debug('test_state_change_handling: Turn on "sleep mode"') + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1) + assert len(switch.manager.our_last_state_on_change[ENTITY_LIGHT_1]) == 1 + assert not switch.manager.transition_timers.get(ENTITY_LIGHT_1) + last_service_data = deepcopy(switch.manager.last_service_data) + assert last_service_data.get(ENTITY_LIGHT_1) + + # 2 Adapt from sleep with a 'transition'. + await switch.sleep_mode_switch.async_turn_off() + await switch._update_attrs_and_maybe_adapt_lights( + context=context, + force=False, + transition=0, + ) + await hass.async_block_till_done() + current_service_data = switch.manager.last_service_data + assert current_service_data != last_service_data + + for light in switch.lights: + # current_service_data should have changed after the last update. + assert current_service_data.get(light) + assert last_service_data.get(light) + assert current_service_data[light] != last_service_data[light] + + # Test same context id events. + current_service_data[light][ATTR_TRANSITION] = transition_used + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + ATTR_ENTITY_ID: light, + "old_state": State(light, "on", attributes=last_service_data), + "new_state": State( + light, + "on", + attributes=current_service_data, + context=context, + ), + }, + ) + assert not switch.manager.transition_timers.get(light) + + # 2.3 Refire and overwrite the original state_changed event with our 'transition' + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + ATTR_ENTITY_ID: light, + "old_state": State(light, "on", attributes=last_service_data), + "new_state": State( + light, + "on", + attributes=current_service_data, + # We need to overwrite the old context_id + context=switch.create_context("test"), + ), + }, + ) + await hass.async_block_till_done() + # Assert our transition timer was created. + assert switch.manager.transition_timers.get(light) + # 2.5 Simulate a transition. There's no other way to do this in the demo. + events = create_transition_events( + light=light, + state=hass.states.get(light), + last=last_service_data[light], + current=current_service_data[light], + total_events=total_events, + ) + # 3. Fire simulated events for our AdaptiveLightingManager + for event in events: + _LOGGER.debug("Test EVENT_STATE_CHANGED listener") + hass.bus.async_fire(EVENT_STATE_CHANGED, event) + await hass.async_block_till_done() + # On real systems HA fires transition state changes every ~3 seconds. + # asyncio.sleep(3) + # 4. Assert the transition timer started and everything was filled. + listener = switch.manager + assert listener.our_last_state_on_change.get(ENTITY_LIGHT_1) + assert len(listener.our_last_state_on_change[ENTITY_LIGHT_1]) == total_events + assert listener.transition_timers.get(ENTITY_LIGHT_1) + + # 5. Execute some checks during a transition + _LOGGER.debug("Test detect_non_ha_changes:") + switch._take_over_control = True + assert switch._take_over_control + switch._detect_non_ha_changes = True + assert switch._detect_non_ha_changes + await asyncio.sleep(transition_used / 3) + # Ensure the timer still exists + timer = listener.transition_timers.get(ENTITY_LIGHT_1) + assert timer + assert timer.is_running() + last_service_data = deepcopy(current_service_data) + await update() + assert not switch.manager.manual_control[ENTITY_LIGHT_1] + await update() + assert not switch.manager.manual_control[ENTITY_LIGHT_1] + timer = listener.transition_timers.get(ENTITY_LIGHT_1) + assert timer + assert timer.is_running() + # Ensure the light did not adapt during the transition. + assert last_service_data == current_service_data + + # 6. Assert everything after the transition finishes. + await asyncio.sleep(transition_used) + assert listener.our_last_state_on_change.get(ENTITY_LIGHT_1) + assert len(listener.our_last_state_on_change[ENTITY_LIGHT_1]) == total_events + # Timer should be done and reset now. + # This is the assert that I can't fix. + timer = listener.transition_timers.get(ENTITY_LIGHT_1) + assert not timer or not timer.is_running() + + # build last service data + await update(force=False) + + # force=True should not reset manual control. + await turn_light(True, brightness=40) + await turn_light(True, brightness=20) + await update(force=False) + assert ( + switch.manager.manual_control[ENTITY_LIGHT_1] + == LightControlAttributes.BRIGHTNESS + ) + await update(force=True) + assert ( + switch.manager.manual_control[ENTITY_LIGHT_1] + == LightControlAttributes.BRIGHTNESS + ) + + # turn light off then on should reset manual control. + await turn_light(False) + await turn_light(True) + assert not switch.manager.manual_control[ENTITY_LIGHT_1] + + await turn_light(True, brightness=50) + _LOGGER.debug("Test: Brightness set to %s", 50) + + # On next update ENTITY_LIGHT_1 should be marked as manually controlled + await update(force=False) + assert switch.manager.last_service_data.get(ENTITY_LIGHT_1) is not None + assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1) is not None + assert ( + switch.manager.manual_control[ENTITY_LIGHT_1] + == LightControlAttributes.BRIGHTNESS + ) + + +def test_is_our_context(): + """Test is_our_context function.""" + context = create_context(DOMAIN, "test", 0) + assert is_our_context(context) + assert not is_our_context(None) + assert not is_our_context(Context()) + + +async def test_unload_switch(hass): + """Test removing Adaptive Lighting.""" + entry, switch = await setup_switch(hass, {}) + switch.manager.set_auto_reset_manual_control_times([ENTITY_LIGHT_1], 60) + switch.manager.set_manual_control_attributes(ENTITY_LIGHT_1) + timer = switch.manager.auto_reset_manual_control_timers[ENTITY_LIGHT_1] + assert timer.is_running() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert DOMAIN not in hass.data + assert not timer.is_running() + + +async def test_unload_cancels_pending_light_transition(hass): + """A real transition must not keep its timer alive after last profile unload.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_3], + CONF_INITIAL_TRANSITION: 60, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + calls = [] + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, calls.append) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + ) + await hass.async_block_till_done() + transition_calls = [ + event.data["service_data"] + for event in calls + if event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + and ATTR_TRANSITION in event.data["service_data"] + ] + assert len(transition_calls) == 1 + assert transition_calls[0][ATTR_TRANSITION] == 60 + timer = switch.manager.transition_timers[ENTITY_LIGHT_3] + assert timer.is_running() + entry = hass.config_entries.async_entries(DOMAIN)[0] + try: + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert DOMAIN not in hass.data + assert not timer.is_running() + assert not switch.manager.transition_timers + state = hass.states.get(ENTITY_LIGHT_3) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + finally: + remove_listener() + timer.cancel() + await asyncio.gather(timer.task, return_exceptions=True) + + +@pytest.mark.parametrize("state", [STATE_ON, STATE_OFF, None]) +async def test_restore_off_state(hass, state): + """Test that the 'off' and 'on' states are propoperly restored.""" + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=State(ENTITY_SWITCH, state) if state is not None else None, + ): + await hass.async_start() + await hass.async_block_till_done() + _, switch = await setup_switch(hass, {}) + if state == STATE_ON: + assert switch.is_on + elif state == STATE_OFF: + assert not switch.is_on + elif state is None: + assert switch.is_on + + for _switch, initial_state in [ + (switch.sleep_mode_switch, False), + (switch.adapt_brightness_switch, True), + (switch.adapt_color_switch, True), + ]: + if state == STATE_ON: + assert _switch.is_on + elif state == STATE_OFF: + assert not _switch.is_on + elif state is None: + if initial_state: + assert _switch.is_on + else: + assert not _switch.is_on + + +async def test_offset_too_large(hass): + """Test that update fails when the sunrise offset is too large. + + A 12-hour offset causes sun events to be out of order (e.g., sunrise after sunset), + which makes the adaptive lighting algorithm fail with a ValueError. + """ + _, switch = await setup_switch(hass, {CONF_SUNRISE_OFFSET: 3600 * 12}) + with pytest.raises(ValueError, match=r"sun events.*not in the expected order"): + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + ) + await hass.async_block_till_done() + + +async def test_turn_on_and_off_when_already_at_that_state(hass): + """Test 'switch.turn_on/off' when switch is on/off.""" + _, switch = await setup_switch(hass, {}) + + await switch.async_turn_on() + await hass.async_block_till_done() + await switch.async_turn_on() + await hass.async_block_till_done() + + await switch.async_turn_off() + await hass.async_block_till_done() + await switch.async_turn_off() + await hass.async_block_till_done() + + +async def test_async_update_at_interval_action(hass): + """Test '_async_update_at_interval_action' method.""" + _, switch = await setup_switch(hass, {}) + await switch._async_update_at_interval_action() + + +async def test_stagger_offset_deterministic_and_bounded(hass): + """Test switches get stable relative delays within the interval.""" + interval = datetime.timedelta(seconds=90) + + _, switch_a = await setup_switch(hass, {CONF_NAME: "switch_a"}) + _, switch_b = await setup_switch(hass, {CONF_NAME: "switch_b"}) + + offset_a_1 = switch_a._stagger_offset(interval) + offset_a_2 = switch_a._stagger_offset(interval) + assert offset_a_1 == offset_a_2 + + offset_b = switch_b._stagger_offset(interval) + assert offset_a_1 != offset_b + + for offset in (offset_a_1, offset_b): + assert datetime.timedelta(0) <= offset < interval + + +async def test_disable_cancels_pending_stagger(hass): + """Test disabling the switch cancels delayed interval registration.""" + switch_module = "homeassistant.components.adaptive_lighting.switch" + with ( + patch( + f"{switch_module}.AdaptiveSwitch._stagger_offset", + return_value=datetime.timedelta(seconds=10), + ) as mock_offset, + patch( + f"{switch_module}.async_track_time_interval", + return_value=lambda: None, + ) as mock_track_interval, + ): + _, switch = await setup_switch(hass, {}) + mock_offset.return_value = datetime.timedelta(seconds=0.05) + switch._update_time_interval_listener() + await switch.async_turn_off() + await asyncio.sleep(0.1) + + mock_track_interval.assert_not_called() + + +async def test_reconfigure_replaces_stagger_and_preserves_interval(hass): + """Test the replacement starts at offset + interval and keeps its cadence.""" + calls: list[float] = [] + two_calls = asyncio.Event() + loop = asyncio.get_running_loop() + stagger = datetime.timedelta(seconds=0.2) + + async def record_interval(_now=None): + calls.append(loop.time()) + if len(calls) == 2: + two_calls.set() + + with patch( + "homeassistant.components.adaptive_lighting.switch.AdaptiveSwitch._stagger_offset", + return_value=datetime.timedelta(seconds=10), + ) as mock_offset: + _, switch = await setup_switch(hass, {}) + switch._interval = datetime.timedelta(0) + mock_offset.return_value = stagger + effective_interval = ( + switch._interval + + datetime.timedelta(milliseconds=switch._send_split_delay) + + datetime.timedelta(seconds=0.5) + ) + + with patch.object( + switch, + "_async_update_at_interval_action", + side_effect=record_interval, + ): + switch._update_time_interval_listener() + await asyncio.sleep(0.02) + + replacement_started = loop.time() + switch._update_time_interval_listener() + await asyncio.wait_for(two_calls.wait(), timeout=2) + await switch.async_turn_off() + + first_delay = calls[0] - replacement_started + interval_seconds = effective_interval.total_seconds() + expected_first_delay = interval_seconds + stagger.total_seconds() + assert expected_first_delay - 0.1 <= first_delay < expected_first_delay + 0.5 + assert interval_seconds - 0.1 <= calls[1] - calls[0] < interval_seconds + 0.5 + + +@pytest.mark.parametrize("separate_turn_on_commands", (True, False)) +async def test_separate_turn_on_commands(hass, separate_turn_on_commands): + """Test 'separate_turn_on_commands' argument.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_SEPARATE_TURN_ON_COMMANDS: separate_turn_on_commands, + # Keep normal brightness distinct from sleep mode at any time of day. + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + # We just turn sleep mode on and off which should change the + # brightness and color. We don't test whether the number are exactly + # what we expect because we do this in other tests already, we merely + # check whether the brightness and color_temp change. + context = switch.create_context("test") # needs to be passed to update method + brightness = light.brightness + color_temp = light.color_temp_kelvin + await switch.sleep_mode_switch.async_turn_on() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + # TODO: figure out why `light.brightness` is not updating + attrs = hass.states.get(light.entity_id).attributes + sleep_brightness = attrs["brightness"] + sleep_color_temp = attrs["color_temp_kelvin"] + + assert sleep_brightness != brightness + assert sleep_color_temp != color_temp + + await switch.sleep_mode_switch.async_turn_off() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + attrs = hass.states.get(light.entity_id).attributes + brightness = attrs["brightness"] + color_temp = attrs["color_temp_kelvin"] + + assert sleep_brightness != brightness + assert sleep_color_temp != color_temp + + +def mock_area_registry( + hass: HomeAssistant, +) -> ar.AreaRegistry: + """Mock the Area Registry.""" + area = ar.AreaEntry( + aliases=set(), + floor_id="test-floor", + humidity_entity_id=None, + icon=None, + id="test-area", + name="Test Area", + picture=None, + temperature_entity_id=None, + ) + return mock_ha_area_registry(hass, {area.id: area}) + + +async def test_light_switch_in_specific_area(hass): + switch, (light, *_) = await setup_lights_and_switch(hass) + + mock_area_registry(hass) + + entity = entity_registry.async_get(hass).async_get_or_create( + LIGHT_DOMAIN, + "template", + light.unique_id, + ) + entity = entity_registry.async_get(hass).async_update_entity( + entity.entity_id, + area_id="test-area", + ) + _LOGGER.debug("test-area entity: %s", entity) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_AREA_ID: entity.area_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert light.entity_id in switch.manager.last_service_data + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_AREA_ID: entity.area_id}, + blocking=True, + ) + await hass.async_block_till_done() + + _LOGGER.debug( + "switch.manager.last_service_data: %s", + switch.manager.last_service_data, + ) + assert light.entity_id not in switch.manager.last_service_data + + +async def test_change_switch_settings_service(hass): + """Test adaptive_lighting.change_switch_settings service.""" + switch, (_, _, light) = await setup_lights_and_switch(hass) + entity_id = light.entity_id + assert entity_id not in switch.lights + + async def change_switch_settings(**kwargs): + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + { + ATTR_ENTITY_ID: switch.entity_id, + **kwargs, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test changing sunrise offset + assert switch._sun_light_settings.sunrise_offset.total_seconds() == 0 + await change_switch_settings(**{CONF_SUNRISE_OFFSET: 10}) + assert switch._sun_light_settings.sunrise_offset.total_seconds() == 10 + + # Test changing max brightness + assert switch._sun_light_settings.max_brightness == 100 + await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 50}) + assert switch._sun_light_settings.max_brightness == 50 + + # Test changing to illegal max brightness + with pytest.raises( + voluptuous.error.MultipleInvalid, + match="value must be at most 100", + ): + await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 5000}) + + # Change CONF_MIN_COLOR_TEMP, the factory default is 2000, but setup_lights_and_switch + # sets it to 2500 + assert switch._sun_light_settings.min_color_temp == 2500 + + # testing with "factory" should change it to 2000 + await change_switch_settings(**{CONF_USE_DEFAULTS: "factory"}) + assert switch._sun_light_settings.min_color_temp == 2000 + + # testing with "current" should not change things + await change_switch_settings(**{CONF_USE_DEFAULTS: "current"}) + assert switch._sun_light_settings.min_color_temp == 2000 + + # testing with "configuration" and setting a new value + await change_switch_settings( + **{CONF_USE_DEFAULTS: "configuration", CONF_MIN_COLOR_TEMP: 3000}, + ) + assert switch._sun_light_settings.min_color_temp == 3000 + + # testing with "configuration" should revert back to 2500 + await change_switch_settings(**{CONF_USE_DEFAULTS: "configuration"}) + assert switch._sun_light_settings.min_color_temp == 2500 + + +@pytest.mark.parametrize("target", ["entity", "area", "device", "all"]) +async def test_change_switch_settings_entity_targets(hass, device_registry, target): + """Test settings changes through Home Assistant entity targets.""" + _, switch = await setup_switch(hass, {}) + mock_area_registry(hass) + registry_entry = entity_registry.async_get(hass).async_get(switch.entity_id) + assert registry_entry is not None + assert registry_entry.device_id is not None + device_registry.async_update_device( + registry_entry.device_id, + area_id="test-area", + ) + service_data = { + "entity": {ATTR_ENTITY_ID: switch.entity_id}, + "area": {ATTR_AREA_ID: "test-area"}, + "device": {ATTR_DEVICE_ID: registry_entry.device_id}, + "all": {ATTR_ENTITY_ID: "all"}, + }[target] + + with patch.object( + switch, + "_set_changeable_settings", + wraps=switch._set_changeable_settings, + ) as set_settings: + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {**service_data, CONF_MAX_BRIGHTNESS: 50}, + blocking=True, + ) + + set_settings.assert_called_once() + assert switch._sun_light_settings.max_brightness == 50 + + +async def test_change_switch_settings_ignores_unknown_entity(hass): + """Test an unknown entity target does not change a loaded profile.""" + _, switch = await setup_switch(hass, {}) + + with patch.object(switch, "_set_changeable_settings") as set_settings: + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + { + ATTR_ENTITY_ID: "switch.does_not_exist", + CONF_MAX_BRIGHTNESS: 50, + }, + blocking=True, + ) + + set_settings.assert_not_called() + + +async def test_change_switch_settings_checks_entity_permissions( + hass, + hass_read_only_user, +): + """Test settings changes require permission to control the target entity.""" + _, switch = await setup_switch(hass, {}) + + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_MAX_BRIGHTNESS: 50, + }, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + + assert switch._sun_light_settings.max_brightness == DEFAULT_MAX_BRIGHTNESS + + +async def test_cancellable_service_calls_task(hass): + """Test the creation and execution of the task that wraps adaptation service calls.""" + light, *_ = await setup_lights(hass) + _, switch = await setup_switch(hass, {CONF_SEPARATE_TURN_ON_COMMANDS: True}) + context = switch.create_context("test") + + assert switch.manager.adaptation_tasks_color.get(light.entity_id) is None + + service_data = { + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 10, + ATTR_ENTITY_ID: light.entity_id, + } + adaptation_data = AdaptationData( + light.entity_id, + context, + 0, + _create_service_call_data_iterator(hass, [service_data], False), + force=False, + max_length=1, + attributes=LightControlAttributes.ALL, + ) + await switch.execute_cancellable_adaptation_calls(adaptation_data) + + task = switch.manager.adaptation_tasks_brightness.get(light.entity_id) + task2 = switch.manager.adaptation_tasks_color.get(light.entity_id) + assert task is task2 + assert task is not None + assert task.done() + + +async def test_service_calls_task_cancellation(hass): + """Tests if the task that wraps ongoing adaptation service calls gets cancelled.""" + _, switch = await setup_switch(hass, {}) + entity_id = "test_id" + + task = asyncio.ensure_future(asyncio.sleep(1)) + switch.manager.adaptation_tasks_brightness[entity_id] = task + + switch.manager.cancel_ongoing_adaptation_calls(entity_id) + + with contextlib.suppress(asyncio.CancelledError): + await task + + assert task.cancelled() + + +async def _turn_on_and_track_event_contexts( + hass: HomeAssistant, + context_id: str | Context, + entity_id, + return_full_events: bool = False, +): + context = context_id if isinstance(context_id, Context) else Context(id=context_id) + event_context_ids = [] + events = [] + + async def turn_on_off_event_listener(event: Event) -> None: + event_context_ids.append(event.context.id) + events.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, turn_on_off_event_listener) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + context=context, + ) + await hass.async_block_till_done() + if return_full_events: + return events + return event_context_ids + + +async def _admin_context(hass: HomeAssistant, context_id: str) -> Context: + """Create a user-originated context accepted by entity service checks.""" + user = await hass.auth.async_create_user(context_id, group_ids=[GROUP_ID_ADMIN]) + return Context( + id=context_id, + parent_id="automation_origin", + user_id=user.id, + ) + + +async def test_apply_service_context_links_to_origin(hass): + """The apply service links its light call to the originating service call.""" + switch, (_, _, light) = await setup_lights_and_switch(hass) + origin = await _admin_context(hass, "apply_origin") + events: list[Event] = [] + + async def listener(event: Event) -> None: + if ( + event.data.get("domain") == LIGHT_DOMAIN + and event.data.get("service") == SERVICE_TURN_ON + ): + events.append(event) + + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, listener) + try: + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [light.entity_id], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + context=origin, + ) + await hass.async_block_till_done() + finally: + remove_listener() + + assert len(events) == 1 + assert events[0].context.parent_id == origin.id + assert events[0].context.user_id is None + + +async def test_single_light_intercept_keeps_origin_context(hass): + """A directly intercepted call keeps the original context unchanged.""" + await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True) + origin = await _admin_context(hass, "single_intercept_origin") + + events = await _turn_on_and_track_event_contexts( + hass, + origin, + ENTITY_LIGHT_3, + return_full_events=True, + ) + + assert len(events) == 1 + assert events[0].context.id == origin.id + assert events[0].context.parent_id == origin.parent_id + assert events[0].context.user_id == origin.user_id + + +async def test_multi_profile_intercept_context_links_to_origin(hass): + """A secondary profile adaptation links its new call to the origin.""" + lights, _, _ = await setup_proactive_multiple_lights_two_switches(hass) + origin = await _admin_context(hass, "multi_profile_origin") + + events = await _turn_on_and_track_event_contexts( + hass, + origin, + lights[:2], + return_full_events=True, + ) + secondary_events = [event for event in events if ":ntrc:" in event.context.id] + + assert len(secondary_events) == 1 + assert secondary_events[0].context.parent_id == origin.id + assert secondary_events[0].context.user_id is None + + +async def test_skipped_light_context_links_to_origin(hass): + """A split call for an unmanaged light links its new call to the origin.""" + lights, _, _ = await setup_proactive_multiple_lights_two_switches(hass) + origin = await _admin_context(hass, "skipped_light_origin") + + events = await _turn_on_and_track_event_contexts( + hass, + origin, + [lights[0], lights[2]], + return_full_events=True, + ) + skipped_events = [event for event in events if ":skpp:" in event.context.id] + + assert len(skipped_events) == 1 + assert skipped_events[0].context.parent_id == origin.id + assert skipped_events[0].context.user_id is None + + +def _mock_sun_light_settings(switch: AdaptiveSwitch, settings: dict[str, Any]): + sun_light_settings_mock = Mock() + sun_light_settings_mock.get_settings = Mock(return_value=settings) + switch._sun_light_settings = sun_light_settings_mock + + +async def test_proactive_adaptation(hass): + """Validate that a proactive adaptation updates the original service call.""" + switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True) + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + + event_context_ids = await _turn_on_and_track_event_contexts( + hass, + "test_context", + ENTITY_LIGHT_3, + ) + + # Expect a single service call + assert len(event_context_ids) == 1 + assert event_context_ids == ["test_context"] + + # Expect adapted light state + state = hass.states.get(ENTITY_LIGHT_3) + # Sun light settings use %, state only contains absolute + assert state.attributes[ATTR_BRIGHTNESS] == 171 # == 67% + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 + + +async def test_proactive_adaptation_with_separate_commands(hass): + """Validate that a split proactive adaptation yields one additional service call.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_INTERCEPT: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + }, + True, + ) + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + + origin = await _admin_context(hass, "separate_commands_origin") + events = await _turn_on_and_track_event_contexts( + hass, + origin, + ENTITY_LIGHT_3, + return_full_events=True, + ) + # Wait for all adaptation tasks to complete + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + event_context_ids = [event.context.id for event in events] + + # Expect two service calls + assert len(event_context_ids) == 2, event_context_ids + assert event_context_ids[0] == origin.id + assert is_our_context_id(event_context_ids[1]) + assert events[1].context.parent_id == origin.id + assert events[1].context.user_id is None + + # Expect adapted light state + state = hass.states.get(ENTITY_LIGHT_3) + assert state.attributes[ATTR_BRIGHTNESS] == 171 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 + + +async def test_proactive_adaptation_toggle(hass): + """Validate that a proactive adaptation updates service calls which toggle a light on, + but not those which toggle off. + + This test is based on the fact that contexts of proactive adaptations are recorded. + """ + switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True) + + # Toggle ON + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + context=Context(id="test1"), + ) + + assert switch.manager.is_proactively_adapting("test1") + + # Toggle OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + context=Context(id="test2"), + ) + + assert not switch.manager.is_proactively_adapting("test2") + + +async def test_proactive_adaptation_transition_override(hass): + """Validate that transitions in service calls are preferred over the default transition.""" + switch, (_, _, light3) = await setup_lights_and_switch( + hass, + { + CONF_INTERCEPT: True, + CONF_INITIAL_TRANSITION: 123, + }, + True, + ) + with patch.object( + light3, + "async_turn_on", + wraps=light3.async_turn_on, + ) as patched_async_turn_on: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3, ATTR_TRANSITION: 456}, + blocking=True, + ) + await hass.async_block_till_done() + + # Assert that default is used when no transition is specified in service call + assert patched_async_turn_on.call_args_list, patched_async_turn_on.call_args_list + kwargs = patched_async_turn_on.call_args_list[0].kwargs + assert set({ATTR_TRANSITION: 123}.items()).issubset(kwargs.items()) + + # Assert that specified service call transition takes precedence over default + kwargs = patched_async_turn_on.call_args_list[1].kwargs + assert set({ATTR_TRANSITION: 456}.items()).issubset(kwargs.items()) + + # Cleanup + switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT_3) + + +async def setup_proactive_multiple_lights_two_switches(hass): + await setup_lights(hass) + # Setup switches + lights = [ + ENTITY_LIGHT_1, + ENTITY_LIGHT_2, + ENTITY_LIGHT_3, + ] + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: lights}, + blocking=True, + ) + defaults = { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_PREFER_RGB_COLOR: False, + CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp} + CONF_INTERCEPT: True, + } + _, switch1 = await setup_switch( + hass, + {CONF_NAME: "switch1", CONF_LIGHTS: [ENTITY_LIGHT_1], **defaults}, + ) + _, switch2 = await setup_switch( + hass, + {CONF_NAME: "switch2", CONF_LIGHTS: [ENTITY_LIGHT_2], **defaults}, + ) + assert hass.states.get(switch1.entity_id).state == STATE_ON + assert hass.states.get(switch2.entity_id).state == STATE_ON + assert all(hass.states.get(light).state == STATE_OFF for light in lights) + return lights, switch1, switch2 + + +async def test_proactive_multiple_lights_all_at_once(hass): + """Create switch and demo lights.""" + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") + # Setup demo lights and turn on + events = await _turn_on_and_track_event_contexts( + hass, + "test1", + lights, + return_full_events=True, + ) + assert len(events) == 3, events + + # Original turn_on call that is intercepted + assert events[0].context.id == "test1" + assert events[0].data["service_data"][ATTR_ENTITY_ID] == lights + + # The `has_intercepted` path + assert events[1].data["service_data"][ATTR_ENTITY_ID] == ENTITY_LIGHT_2 + assert ":ntrc:" in events[1].context.id + + # The skipped lights, the one not in a switch + assert events[2].data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3] + assert ":skpp:" in events[2].context.id + + assert switch1.manager.is_proactively_adapting("test1") + assert switch2.manager.is_proactively_adapting("test1") + + await hass.async_block_till_done() + + assert all(hass.states.get(light).state == STATE_ON for light in lights) + + # Turn on second time even though already on + events = await _turn_on_and_track_event_contexts( + hass, + "test2", + lights, + return_full_events=True, + ) + assert len(events) == 1, events + assert events[0].context.id == "test2" + + +async def test_proactive_multiple_lights_turn_on_non_managed_light(hass): + """Create switch and demo lights.""" + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + turn_ons = await _turn_on_and_track_event_contexts(hass, "test1", lights) + assert len(turn_ons) == 3, turn_ons + await hass.async_block_till_done() + assert all(hass.states.get(light).state == STATE_ON for light in lights) + + # Turn off ENTITY_LIGHT_3 (which is not in a switch), leaving the other two on + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + context=Context(id="test2"), + ) + + # Now turn on all lights again, which means the code gets to "if skipped: if not has_intercepted:" + turn_ons = await _turn_on_and_track_event_contexts(hass, "test2", ENTITY_LIGHT_3) + assert len(turn_ons) == 1, turn_ons + + +async def test_proactive_multiple_lights_turn_on_managed_lights_only(hass): + """Create switch and demo lights.""" + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") + # Setup demo lights and turn on + events = await _turn_on_and_track_event_contexts( + hass, + "test1", + lights[:-1], + return_full_events=True, + ) + assert len(events) == 2, events + + # Original turn_on call that is intercepted + assert events[0].context.id == "test1" + assert events[0].data["service_data"][ATTR_ENTITY_ID] == lights[:-1] + + # The `has_intercepted` path + assert events[1].data["service_data"][ATTR_ENTITY_ID] == ENTITY_LIGHT_2 + assert ":ntrc:" in events[1].context.id + assert ATTR_BRIGHTNESS in events[1].data["service_data"] + + +async def test_proactive_multiple_lights_one_switch_and_one_skipped(hass): + """Create switch and demo lights.""" + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + two_lights = [lights[0], lights[-1]] + _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") + # Setup demo lights and turn on + events = await _turn_on_and_track_event_contexts( + hass, + "test1", + two_lights, + return_full_events=True, + ) + assert len(events) == 2, events + + # Original turn_on call that is intercepted + assert events[0].context.id == "test1" + assert events[0].data["service_data"][ATTR_ENTITY_ID] == two_lights + + # The skipped lights, the one not in a switch + assert events[1].data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3] + assert ":skpp:" in events[1].context.id + + assert switch1.manager.is_proactively_adapting("test1") + assert switch2.manager.is_proactively_adapting("test1") + + await hass.async_block_till_done() + + assert all(hass.states.get(light).state == STATE_ON for light in two_lights) + + +async def test_two_switches_for_single_light(hass): + """Test the case where someone has two switches for a single light. + + One switch for brightness and another for color. + """ + extra_conf = {CONF_INTERCEPT: True} + switch1, (light1, *_) = await setup_lights_and_switch( + hass, + extra_conf | {CONF_NAME: "switch1"}, + all_lights=True, + ) + switch2, (light2, *_) = await setup_lights_and_switch( + hass, + extra_conf | {CONF_NAME: "switch2"}, + all_lights=True, + ) + assert light1.entity_id == light2.entity_id + + # One switch controls brightness the other color + await switch1.adapt_color_switch.async_turn_off() + await switch2.adapt_brightness_switch.async_turn_off() + + assert switch1.adapt_brightness_switch.is_on + assert switch2.adapt_color_switch.is_on + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + _LOGGER.debug("Turn light %s, to %s", state, kwargs) + + def increased_brightness(): + return max(1, (light1._attr_brightness + 100) % 255) + + def increased_color_temp(): + return max( + (light1._attr_color_temp + 100) % light1.max_color_temp_kelvin, + light1.min_color_temp_kelvin, + ) + + assert light1.is_on + await turn_light(True, brightness=increased_brightness()) + await turn_light(True, color_temp_kelvin=increased_color_temp()) + + attrs = hass.states.get(light1.entity_id).attributes + before_brightness = attrs[ATTR_BRIGHTNESS] + before_color_temp = attrs[ATTR_COLOR_TEMP_KELVIN] + + # Turn off "light1" + await turn_light(False) + + # Turn on "light1" + await turn_light(True) + + # Assert that the brightness and color temp have changed + attrs = hass.states.get(light1.entity_id).attributes + after_brightness = attrs[ATTR_BRIGHTNESS] + after_color_temp = attrs[ATTR_COLOR_TEMP_KELVIN] + assert before_brightness != after_brightness + assert before_color_temp != after_color_temp + + +async def test_adapt_until_sleep_and_rgb_colors(hass): + """Test setting up the Adaptive Lighting switches with different timezones. + + Also test the (sleep) brightness and color temperature settings. + """ + lat, long, timezone = (32.87336, -117.22743, "US/Pacific") + await async_process_ha_core_config( + hass, + {"latitude": lat, "longitude": long, "time_zone": timezone, "country": "US"}, + ) + switch, lights = await setup_lights_and_switch( + hass, + { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_ADAPT_UNTIL_SLEEP: True, + CONF_SLEEP_RGB_OR_COLOR_TEMP: "rgb_color", + }, + ) + + context = switch.create_context("test") # needs to be passed to update method + min_color_temp = switch._sun_light_settings.min_color_temp + + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunrise = sunrise - datetime.timedelta(hours=1) + after_sunrise = sunrise + datetime.timedelta(hours=1) + + async def patch_time_and_update(time): + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=time, + ): + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + # At sunset the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunset) + assert not switch._settings["force_rgb_color"] + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour before sunset the brightness should be max and color_temp + # not at the smallest value yet. + await patch_time_and_update(before_sunset) + assert not switch._settings["force_rgb_color"] + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1] + + # One hour after sunset the brightness should be down and use RGB + await patch_time_and_update(after_sunset) + assert switch._settings["force_rgb_color"] + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT_1] + + # At sunrise the brightness should be max and use Kelvin + await patch_time_and_update(sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1] + + # One hour before sunrise the brightness should smaller than max + # and use RGB + await patch_time_and_update(before_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT_1] + + # One hour after sunrise the brightness should be up and it should use Kelvin + await patch_time_and_update(after_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1] + + # Turn on sleep mode which make the brightness and color_temp + # deterministic regardless of the time + await switch.sleep_mode_switch.async_turn_on() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS + assert switch._settings["rgb_color"] == DEFAULT_SLEEP_RGB_COLOR + + +def test_lerp_color_hsv(): + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0) == (255, 0, 0) + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 1) == (0, 255, 0) + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0.5) == (255, 255, 0) + assert lerp_color_hsv((0, 0, 255), (255, 255, 255), 0.5) == (128, 255, 128) + + # Tests that the interpolation is consistent + for t in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]: + color = lerp_color_hsv((255, 0, 0), (0, 255, 0), t) + inverted_color = lerp_color_hsv((0, 255, 0), (255, 0, 0), 1 - t) + assert color == inverted_color + + with pytest.raises(AssertionError): + lerp_color_hsv((255, 0, 0), (0, 255, 0), 1.1) + + +async def test_expand_light_groups_waits_for_group_state(hass): + """Test expansion waits until a light group's state is available.""" + await setup_switch(hass, {}) + group = "light.pending_group" + members = ["light.light_1", "light.light_2"] + + assert _expand_light_groups(hass, [group]) == [group] + + hass.states.async_set(group, STATE_ON, {ATTR_ENTITY_ID: members}) + + assert _expand_light_groups(hass, [group]) == members + + +async def test_group_created_during_startup_tracks_manual_member_changes(hass): + """A profile loaded before its group must track members once HA starts.""" + hass.set_state(CoreState.not_running) + await setup_lights(hass) + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: ["light.light_group"], + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + group_entry = MockConfigEntry( + domain="group", + title="Light Group", + data={}, + options={"group_type": "light", "entities": [ENTITY_LIGHT_2, ENTITY_LIGHT_3]}, + ) + group_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(group_entry.entry_id) + await hass.async_start() + await hass.async_block_till_done() + + # Target the member directly. A still-unexpanded group would miss this call. + member = ENTITY_LIGHT_3 + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: member}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 128 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: member, ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + await hass.async_block_till_done() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 77 + assert hass.states.get(switch.entity_id).attributes[ + "manual_control_brightness" + ] == [member] + + +@pytest.mark.parametrize("proactive_service_call_adaptation", [True, False]) +@pytest.mark.parametrize("take_over_control", [True, False]) +@pytest.mark.parametrize("multi_light_intercept", [True, False]) +async def test_light_group( + hass, + proactive_service_call_adaptation, + take_over_control, + multi_light_intercept, + cleanup, +): + lights = await setup_lights(hass, with_group=True) + all_entity_ids = [light.entity_id for light in lights] + entity_ids = all_entity_ids[:3] # the last two are in the group + entity_ids.append("light.light_group") + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: entity_ids, + CONF_INTERCEPT: proactive_service_call_adaptation, + CONF_TAKE_OVER_CONTROL: take_over_control, + CONF_MULTI_LIGHT_INTERCEPT: multi_light_intercept, + }, + ) + await hass.async_block_till_done() + assert switch.is_on + assert all(eid in switch.lights for eid in all_entity_ids) + + # Set the brightness of the group twice, once to turn it on and once to + # trigger manual control + for _ in range(2): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS_PCT: 50}, + blocking=True, + ) + await hass.async_block_till_done() + + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + ) + await hass.async_block_till_done() + + if take_over_control: + assert switch.manager.manual_control["light.light_4"] + assert switch.manager.manual_control["light.light_5"] + else: + assert not switch.manager.manual_control["light.light_4"] + assert not switch.manager.manual_control["light.light_5"] + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert not switch.manager.manual_control["light.light_4"] + assert not switch.manager.manual_control["light.light_5"] + events = await _turn_on_and_track_event_contexts( + hass, + "testing", + "light.light_group", + return_full_events=True, + ) + if proactive_service_call_adaptation and multi_light_intercept: + await asyncio.gather(*switch.manager.adaptation_tasks) + # Both lights should be adapted via interception, so with the original context + # [ + # "testing", # original call light 4 + # "testing", # original call light 5 + # ] + + assert events[0].data["service_data"][ATTR_ENTITY_ID] == "light.light_group" + assert events[0].context.id == "testing" + assert events[1].data["service_data"][ATTR_ENTITY_ID] == [ + "light.light_4", + "light.light_5", + ] + assert events[1].context.id == "testing" + else: + assert events[0].data["service_data"][ATTR_ENTITY_ID] == "light.light_group" + assert events[0].context.id == "testing" + assert events[1].data["service_data"][ATTR_ENTITY_ID] == [ + "light.light_4", + "light.light_5", + ] + assert events[1].context.id == "testing" + e1 = events[2].data["service_data"][ATTR_ENTITY_ID] + e2 = events[3].data["service_data"][ATTR_ENTITY_ID] + assert (e1 == "light.light_4" and e2 == "light.light_5") or ( + e1 == "light.light_5" and e2 == "light.light_4" + ) + assert ":lght:" in events[2].context.id + assert ":lght:" in events[3].context.id + assert len(events) == 4 + assert not switch.manager.is_proactively_adapting(events[0].context.id) + assert not switch.manager.is_proactively_adapting(events[1].context.id) + + # Turn off all lights, and then turn on all lights + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: all_entity_ids}, + blocking=True, + ) + await hass.async_block_till_done() + + # This turns on light_1, light_2, light_3, light_group (which is light_4 and light_5) + # This should result in the intercepted adaptation of light_1, light_2, light_3 + # and skip the light_group first. Then on a second light.turn_on where the + # light_group is expanded, with a :skpp: context_id, this goes trhough another iteration, + # and then the light_group is adapted. + events = await _turn_on_and_track_event_contexts( + hass, + "testing", + entity_ids, + return_full_events=True, + ) + if proactive_service_call_adaptation and multi_light_intercept: + await asyncio.gather(*switch.manager.adaptation_tasks) + # Original call + assert events[0].data["service_data"][ATTR_ENTITY_ID] == [ + "light.light_1", + "light.light_2", + "light.light_3", + "light.light_group", + ] + assert events[0].context.id == "testing" + # Skipped call with light_group + assert events[1].data["service_data"][ATTR_ENTITY_ID] == ["light.light_group"] + assert ":skpp:" in events[1].context.id + # HA automatically forwarded call with light_group expanded with same context + assert events[2].data["service_data"][ATTR_ENTITY_ID] == [ + "light.light_4", + "light.light_5", + ] + assert ":skpp:" in events[2].context.id + assert len(events) == 3 + + +def _track_adaptive_light_calls(hass, *, ours_only=True): + """Capture commands emitted by Adaptive Lighting at the HA service boundary.""" + calls = [] + + def track(event): + if ( + event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + and (not ours_only or is_our_context(event.context)) + ): + calls.append(event.data["service_data"]) + + hass.bus.async_listen(EVENT_CALL_SERVICE, track) + return calls + + +async def _setup_group_switch(hass, **settings): + return await setup_switch( + hass, + { + CONF_LIGHTS: ["light.light_group"], + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + **settings, + }, + ) + + +@pytest.mark.parametrize("intercept", [False, True]) +async def test_light_group_expand_disabled_off_to_on(hass, intercept, cleanup): + """Group adaptation reaches members through the group on both turn-on paths.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch( + hass, + expand_light_groups=False, + intercept=intercept, + ) + calls = _track_adaptive_light_calls(hass, ours_only=False) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + await hass.async_block_till_done() + await asyncio.gather(*switch.manager.adaptation_tasks) + # HA emits the original service event before the interceptor changes its data. + # The group forwards the injected values to its real member entities. + expected_target = ( + ["light.light_4", "light.light_5"] if intercept else "light.light_group" + ) + assert any( + call[ATTR_ENTITY_ID] == expected_target and call.get(ATTR_BRIGHTNESS) == 128 + for call in calls + ), calls + for member in ["light.light_4", "light.light_5"]: + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 128 + + calls.clear() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert any(call[ATTR_ENTITY_ID] == "light.light_group" for call in calls) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + await hass.async_block_till_done() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert hass.states.get(switch.entity_id).attributes["manual_control"] == [ + "light.light_group", + ] + for member in ["light.light_4", "light.light_5"]: + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 77 + + +@pytest.mark.parametrize("expand", [False, True]) +@pytest.mark.parametrize("explicit_member", [False, True]) +async def test_group_apply_respects_target_policy( + hass, + expand, + explicit_member, + cleanup, +): + """Apply obeys profile expansion without widening an explicit member request.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch(hass, expand_light_groups=expand) + calls = _track_adaptive_light_calls(hass) + target = "light.light_4" if explicit_member else "light.light_group" + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [target], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + ) + await hass.async_block_till_done() + expected = ( + ["light.light_4"] + if explicit_member + else (["light.light_4", "light.light_5"] if expand else ["light.light_group"]) + ) + # HA also emits forwarded member calls; AL's own commands use a scalar target. + assert ( + sorted( + { + call[ATTR_ENTITY_ID] + for call in calls + if isinstance(call[ATTR_ENTITY_ID], str) + }, + ) + == expected + ) + assert hass.states.get("light.light_4").attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get("light.light_5").state == ( + STATE_OFF if explicit_member else STATE_ON + ) + + +@pytest.mark.parametrize("expand", [False, True]) +async def test_group_manual_control_service(hass, expand, cleanup): + """Group lookup and manual service use the same target policy, including reset.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch(hass, expand_light_groups=expand) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + {CONF_LIGHTS: ["light.light_group"], CONF_MANUAL_CONTROL: True}, + blocking=True, + ) + await hass.async_block_till_done() + expected = ["light.light_4", "light.light_5"] if expand else ["light.light_group"] + assert hass.states.get(switch.entity_id).attributes["manual_control"] == expected + for target in expected: + assert switch.manager.manual_control[target] == LightControlAttributes.ALL + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + {CONF_LIGHTS: ["light.light_group"], CONF_MANUAL_CONTROL: False}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(switch.entity_id).attributes["manual_control"] == [] + + +@pytest.mark.parametrize("shared_member", [False, True]) +async def test_group_runtime_expansion_restores_targets(hass, shared_member, cleanup): + """Changing expansion back and forth restores groups and retires member timers.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch(hass, autoreset_control_seconds=60) + if shared_member: + _, other = await _setup_group_switch( + hass, + name="member", + lights=["light.light_4"], + autoreset_control_seconds=60, + ) + await other.async_turn_off() + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + {ATTR_ENTITY_ID: switch.entity_id, CONF_TURN_ON_LIGHTS: True}, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: ["light.light_group"], + CONF_MANUAL_CONTROL: True, + }, + blocking=True, + ) + manager = switch.manager + old_timers = dict(manager.auto_reset_manual_control_timers) + assert len(old_timers) == 2 + calls = _track_adaptive_light_calls(hass) + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {ATTR_ENTITY_ID: switch.entity_id, CONF_EXPAND_LIGHT_GROUPS: False}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.lights == ["light.light_group"] + retained = {"light.light_4"} if shared_member else set() + assert manager.lights == {"light.light_group"} | retained + assert set(manager.auto_reset_manual_control_timers) == retained + for light, timer in old_timers.items(): + assert timer.is_running() == (light in retained) + assert ( + manager.manual_control.get("light.light_5", LightControlAttributes.NONE) + == LightControlAttributes.NONE + ) + assert any(call[ATTR_ENTITY_ID] == "light.light_group" for call in calls) + calls.clear() + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {ATTR_ENTITY_ID: switch.entity_id, CONF_EXPAND_LIGHT_GROUPS: True}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.lights == ["light.light_4", "light.light_5"] + assert manager.lights == {"light.light_4", "light.light_5"} + expected = ( + ["light.light_5"] if shared_member else ["light.light_4", "light.light_5"] + ) + assert sorted({call[ATTR_ENTITY_ID] for call in calls}) == expected + if shared_member: + assert manager.manual_control["light.light_4"] == LightControlAttributes.ALL + + +@pytest.mark.parametrize("expand", [False, True]) +@pytest.mark.parametrize("shared_target", [False, True]) +async def test_group_runtime_change_retires_delayed_events( + hass, + expand, + shared_target, + cleanup, +): + """Delayed reactive handlers must not command targets this profile retired.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch( + hass, + expand_light_groups=expand, + adapt_delay=0.1234, + ) + retired_targets = ( + {"light.light_4", "light.light_5"} if expand else {"light.light_group"} + ) + retained_target = "light.light_4" if expand else "light.light_group" + if shared_target: + _, other = await _setup_group_switch( + hass, + name="retained", + lights=[retained_target], + expand_light_groups=False, + ) + await other.async_turn_off() + + entered, release = asyncio.Event(), asyncio.Event() + original_sleep = asyncio.sleep + delayed_count = 0 + + async def controlled_sleep(delay, *args, **kwargs): + nonlocal delayed_count + if delay == 0.1234: + delayed_count += 1 + if delayed_count == (2 if expand else 1): + entered.set() + await release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + calls = _track_adaptive_light_calls(hass) + with patch.object(asyncio, "sleep", controlled_sleep): + try: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + await asyncio.wait_for(entered.wait(), timeout=2) + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_EXPAND_LIGHT_GROUPS: not expand, + }, + blocking=True, + ) + expected = ( + ["light.light_group"] if expand else ["light.light_4", "light.light_5"] + ) + assert switch.lights == expected + assert switch.manager.lights == set(expected) | ( + {retained_target} if shared_target else set() + ) + calls.clear() + finally: + release.set() + await hass.async_block_till_done() + + retired_calls = [ + call + for call in calls + if isinstance(call[ATTR_ENTITY_ID], str) + and call[ATTR_ENTITY_ID] in retired_targets + ] + assert ( + not retired_calls + ), f"Retired reactive handlers issued commands: {retired_calls}" + for member in ["light.light_4", "light.light_5"]: + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 128 + + +@pytest.mark.parametrize("trigger", ["turn_on", "autoreset"]) +async def test_group_mixed_profiles_preserve_tracking(hass, trigger, cleanup): + """Expanding one profile must not remove another profile's group tracking.""" + await setup_lights(hass, with_group=True) + _, proxy = await _setup_group_switch( + hass, + expand_light_groups=False, + autoreset_control_seconds=60, + detect_non_ha_changes=True, + ) + _, expanded = await _setup_group_switch( + hass, + name="expanded", + min_brightness=70, + max_brightness=70, + detect_non_ha_changes=True, + ) + calls = _track_adaptive_light_calls(hass) + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: expanded.entity_id, + CONF_LIGHTS: ["light.light_group"], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert "light.light_group" in proxy.manager.lights + calls.clear() + if trigger == "turn_on": + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + else: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: proxy.entity_id, + CONF_LIGHTS: ["light.light_group"], + CONF_MANUAL_CONTROL: True, + }, + blocking=True, + ) + timer = proxy.manager.auto_reset_manual_control_timers["light.light_group"] + timer.delay = 0 + timer.start() + await timer.task + await hass.async_block_till_done() + # Only the proxy profile may command the group; the expanded profile uses members. + assert { + call[ATTR_BRIGHTNESS] + for call in calls + if call[ATTR_ENTITY_ID] == "light.light_group" and ATTR_BRIGHTNESS in call + } == {128} + + +async def test_nested_group_apply_targets_leaves(hass, cleanup): + """Default expansion reaches nested leaves without sending commands to subgroups.""" + await setup_lights(hass, with_group=True) + hass.states.async_set( + "light.outer", + STATE_OFF, + {ATTR_ENTITY_ID: ["light.light_group", "light.light_3"]}, + ) + _, switch = await _setup_group_switch(hass, lights=["light.outer"]) + calls = _track_adaptive_light_calls(hass) + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + {CONF_LIGHTS: ["light.outer"], CONF_TURN_ON_LIGHTS: True}, + blocking=True, + ) + await hass.async_block_till_done() + assert sorted({call[ATTR_ENTITY_ID] for call in calls}) == [ + "light.light_3", + "light.light_4", + "light.light_5", + ] + assert switch.lights == ["light.light_3", "light.light_4", "light.light_5"] + + +def _state_changed_event(entity_id: str, ts: float, context: Context) -> Event: + return Event( + EVENT_STATE_CHANGED, + {"entity_id": entity_id}, + time_fired_timestamp=ts, + context=context, + ) + + +def _turn_on_service_event(entity_ids: list[str], ts: float, context: Context) -> Event: + return Event( + EVENT_CALL_SERVICE, + { + "domain": LIGHT_DOMAIN, + "service": SERVICE_TURN_ON, + "service_data": {ATTR_ENTITY_ID: entity_ids}, + }, + time_fired_timestamp=ts, + context=context, + ) + + +def _turn_off_service_event( + entity_ids: list[str], + ts: float, + context: Context, + transition: float | str | None, +) -> Event: + service_data = {ATTR_ENTITY_ID: entity_ids} + if transition is not None: + service_data[ATTR_TRANSITION] = transition + return Event( + EVENT_CALL_SERVICE, + { + "domain": LIGHT_DOMAIN, + "service": SERVICE_TURN_OFF, + "service_data": service_data, + }, + time_fired_timestamp=ts, + context=context, + ) + + +async def test_just_turned_off_group_context_reuse(hass, cleanup): + """Group 'off' → 'on' with a reused 'turn_off' context must still adapt. + + When a member of a light group is turned on (e.g., by a motion sensor + automation) while the group is off, the group turns on as a side effect, + but Home Assistant may reuse the context of the earlier 'turn_off' call + for the group's state change. `just_turned_off` used to treat this as a + polling artifact and cancel adaptation. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1378 + """ + await setup_lights(hass, with_group=True) + _, switch = await setup_switch(hass, {CONF_LIGHTS: ["light.light_group"]}) + await hass.async_block_till_done() + manager = switch.manager + + group = "light.light_group" + member = "light.light_4" + now = dt_util.utcnow().timestamp() + turn_off_context = Context() + + # The group was turned off 2 seconds ago... + manager.on_to_off_event[group] = _state_changed_event( + group, + now - 2, + turn_off_context, + ) + # ...then an automation turned on a member light with a fresh context... + manager.turn_on_event[member] = _turn_on_service_event( + [member], + now - 0.5, + Context(), + ) + # ...which turned the group back on, but HA reused the old turn_off context. + manager.off_to_on_event[group] = _state_changed_event( + group, + now, + turn_off_context, + ) + + # The member's turn_on explains the group's turn-on: adaptation must proceed. + assert not await manager.just_turned_off(group) + + # A member turn_on from *before* the group was turned off does not explain + # the group's turn-on: this must still be treated as a polling artifact. + manager.turn_on_event[member] = _turn_on_service_event( + [member], + now - 10, + Context(), + ) + assert await manager.just_turned_off(group) + + # Without any member turn_on event, the matching context IDs must still be + # treated as a polling artifact. + del manager.turn_on_event[member] + assert await manager.just_turned_off(group) + + +def _register_mixed_target_lights( + hass, + device_registry, + floor_registry, + label_registry, +): + """Assign the three test lights to mixed indirect HA targets.""" + floor = floor_registry.async_create("Upstairs") + area_registry = ar.async_get(hass) + upstairs_area = area_registry.async_create( + "Upstairs room", + floor_id=floor.floor_id, + ) + hall_area = area_registry.async_create("Hall") + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("test", "device-target")}, + ) + label = label_registry.async_create("Skipped light") + + registry = entity_registry.async_get(hass) + registry.async_update_entity(ENTITY_LIGHT_1, area_id=upstairs_area.id) + registry.async_update_entity(ENTITY_LIGHT_2, area_id=hall_area.id) + registry.async_update_entity( + ENTITY_LIGHT_3, + device_id=device.id, + labels={label.label_id}, + ) + return { + ATTR_FLOOR_ID: floor.floor_id, + ATTR_AREA_ID: hall_area.id, + ATTR_DEVICE_ID: device.id, + ATTR_LABEL_ID: label.label_id, + } + + +async def test_mixed_turn_off_targets_do_not_readapt_off_device_light( + hass, + device_registry, + floor_registry, + label_registry, + cleanup, +): + """A mixed-target turn-off must cover an already-off device light (#1069).""" + await setup_lights(hass) + targets = _register_mixed_target_lights( + hass, + device_registry, + floor_registry, + label_registry, + ) + targets.pop(ATTR_LABEL_ID) + + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3], + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: True, + CONF_INITIAL_TRANSITION: 0, + }, + ) + assert hass.states.is_state(ENTITY_LIGHT_3, STATE_OFF) + + turn_off_context = Context() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + { + **targets, + ATTR_TRANSITION: 10, + }, + blocking=True, + context=turn_off_context, + ) + await hass.async_block_till_done() + + calls = _track_adaptive_light_calls(hass) + off_state = hass.states.get(ENTITY_LIGHT_3) + assert off_state is not None + hass.states.async_set( + ENTITY_LIGHT_3, + STATE_ON, + off_state.attributes, + context=turn_off_context, + ) + await hass.async_block_till_done() + + assert not calls + + +async def test_intercept_replaces_all_mixed_target_selectors( + hass, + device_registry, + floor_registry, + label_registry, + cleanup, +): + """A narrowed intercepted call must not retain indirect target selectors.""" + lights = await setup_lights(hass) + targets = _register_mixed_target_lights( + hass, + device_registry, + floor_registry, + label_registry, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3], + }, + blocking=True, + ) + await setup_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_1, ENTITY_LIGHT_2], + CONF_INTERCEPT: True, + CONF_MULTI_LIGHT_INTERCEPT: True, + CONF_INITIAL_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + + with patch.object( + lights[2], + "async_turn_on", + wraps=lights[2].async_turn_on, + ) as skipped_turn_on: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {**targets, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get(ENTITY_LIGHT_2).attributes[ATTR_BRIGHTNESS] == 128 + skipped_turn_on.assert_awaited_once() + assert skipped_turn_on.call_args.kwargs[ATTR_BRIGHTNESS] == 200 + + +async def test_just_turned_off_same_automation_context(hass, cleanup): + """'turn_off' and 'turn_on' from one automation share a context. + + An automation calling 'light.turn_off' and later 'light.turn_on' reuses + its own context for both service calls, so the 'on' → 'off' and + 'off' → 'on' state changes have matching context IDs. The turn_on service + call must take precedence over the matching-context polling-artifact check. + """ + await setup_lights(hass) + _, switch = await setup_switch(hass, {CONF_LIGHTS: [ENTITY_LIGHT_1]}) + await hass.async_block_till_done() + manager = switch.manager + + now = dt_util.utcnow().timestamp() + automation_context = Context() + + manager.turn_off_event[ENTITY_LIGHT_1] = _turn_off_service_event( + [ENTITY_LIGHT_1], + now - 2, + automation_context, + transition=10, + ) + manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now - 2, + automation_context, + ) + manager.turn_on_event[ENTITY_LIGHT_1] = _turn_on_service_event( + [ENTITY_LIGHT_1], + now - 0.5, + automation_context, + ) + manager.off_to_on_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now, + automation_context, + ) + assert not await manager.just_turned_off(ENTITY_LIGHT_1) + + # A stale turn_on with an unrelated context does not explain the + # 'off' → 'on' state change: still a polling artifact. + manager.turn_on_event[ENTITY_LIGHT_1] = _turn_on_service_event( + [ENTITY_LIGHT_1], + now - 10, + Context(), + ) + assert await manager.just_turned_off(ENTITY_LIGHT_1) + + # A stale turn_on *sharing the automation's context* but fired before the + # 'on' → 'off' state change (i.e., 'turn_on' → delay → 'turn_off' in one + # automation run) does not explain the 'off' → 'on' state change either: + # `turn_on_event` entries are never cleaned up, so without the time bounds + # this would defeat the polling-artifact detection. + manager.turn_on_event[ENTITY_LIGHT_1] = _turn_on_service_event( + [ENTITY_LIGHT_1], + now - 10, + automation_context, + ) + assert await manager.just_turned_off(ENTITY_LIGHT_1) + + # A later physical turn-on has a fresh context and must not remain blocked by + # the old turn-off record after its transition window has elapsed. + manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now - 20, + automation_context, + ) + manager.turn_off_event[ENTITY_LIGHT_1] = _turn_off_service_event( + [ENTITY_LIGHT_1], + now - 20, + automation_context, + transition=10, + ) + manager.off_to_on_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now, + Context(), + ) + assert not await manager.just_turned_off(ENTITY_LIGHT_1) + + +@pytest.mark.parametrize( + ("transition", "window"), + [(10, 10), (10.0, 10), ("10", 10), ("10000", 6553), ("inf", 6553), (None, 5)], +) +async def test_just_turned_off_normalized_transition(hass, cleanup, transition, window): + """Both turn-off guards use coerced and clamped transition windows.""" + await setup_lights(hass) + _, switch = await setup_switch(hass, {CONF_LIGHTS: [ENTITY_LIGHT_1]}) + await hass.async_block_till_done() + manager = switch.manager + + now = dt_util.utcnow().timestamp() + context = Context() + other_context = Context() + + # Setting up the switch turns the light on, and that 'turn_on' would be read + # as the legitimate explanation for the 'off' → 'on' state changes below. + manager.turn_on_event.pop(ENTITY_LIGHT_1, None) + + def set_events(turn_off_ts: float, off_to_on_context: Context) -> None: + manager.turn_off_event[ENTITY_LIGHT_1] = _turn_off_service_event( + [ENTITY_LIGHT_1], + turn_off_ts, + context, + transition=transition, + ) + manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + turn_off_ts, + other_context, + ) + manager.off_to_on_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now, + off_to_on_context, + ) + + # A matching context is ignored within the normalized transition window. + set_events(now - window + 1, context) + assert await manager.just_turned_off(ENTITY_LIGHT_1) + + # Past that window the same shape must stop matching. + set_events(now - window - 1, context) + assert not await manager.just_turned_off(ENTITY_LIGHT_1) + + # `just_turned_off`'s own `max(transition, TURNING_OFF_DELAY)`: reached when + # the 'off' → 'on' state change carries a fresh context, so the check above + # returns early and the delay is computed from the 'on' → 'off' change. + manager.turn_off_event[ENTITY_LIGHT_1] = _turn_off_service_event( + [ENTITY_LIGHT_1], + now - window - 1, + context, + transition=transition, + ) + manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now - window - 1, + context, + ) + manager.off_to_on_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now, + Context(), + ) + assert not await manager.just_turned_off(ENTITY_LIGHT_1) + + +@pytest.mark.parametrize( + ("transition", "expected"), + [("2", 2.0), ("10000", 6553), ("inf", 6553), ("-2", 0), (None, None)], +) +async def test_turn_off_event_keeps_raw_transition(hass, cleanup, transition, expected): + """Normalize raw event data to the same transition used by the light service.""" + await setup_lights(hass) + _, switch = await setup_switch(hass, {CONF_LIGHTS: [ENTITY_LIGHT_1]}) + await hass.async_block_till_done() + manager = switch.manager + + service_data = {ATTR_ENTITY_ID: ENTITY_LIGHT_1} + if transition is not None: + service_data[ATTR_TRANSITION] = transition + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + service_data, + blocking=True, + ) + await hass.async_block_till_done() + + event = manager.turn_off_event[ENTITY_LIGHT_1] + assert event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) == transition + assert _turn_off_transition(event) == expected + + # A 'transition' that cannot be coerced is rejected by the schema, so it + # never reaches the listener. + manager.turn_off_event.pop(ENTITY_LIGHT_1) + with pytest.raises(voluptuous.error.MultipleInvalid): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_TRANSITION: "not-a-number"}, + blocking=True, + ) + await hass.async_block_till_done() + assert ENTITY_LIGHT_1 not in manager.turn_off_event + + +async def test_just_turned_off_group_context_reuse_end_to_end(hass, cleanup): + """A tracked member turn-on explains a group's reused OFF context (#1378).""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch( + hass, + lights=["light.light_group", "light.light_4"], + expand_light_groups=False, + detect_non_ha_changes=True, + ) + await hass.async_block_till_done() + manager = switch.manager + + group = "light.light_group" + member = "light.light_4" + assert member in manager.lights + assert group in manager.lights + + turn_off_context = Context() + # The group was turned off... + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + "entity_id": group, + "old_state": State(group, STATE_ON), + "new_state": State(group, STATE_OFF), + }, + context=turn_off_context, + ) + await hass.async_block_till_done() + assert group in manager.on_to_off_event + + # ...then an automation turned on a member light with a fresh context... + hass.bus.async_fire( + EVENT_CALL_SERVICE, + { + "domain": LIGHT_DOMAIN, + "service": SERVICE_TURN_ON, + "service_data": {ATTR_ENTITY_ID: [member]}, + }, + context=Context(), + ) + await hass.async_block_till_done() + assert member in manager.turn_on_event + + # ...which turned the group back on, but HA reused the old turn_off context. + calls = _track_adaptive_light_calls(hass) + state = hass.states.get(group) + hass.states.async_set(group, STATE_ON, state.attributes, context=turn_off_context) + await hass.async_block_till_done() + + # The real group command must survive polling-artifact detection. + assert any( + call[ATTR_ENTITY_ID] == group and call.get(ATTR_BRIGHTNESS) == 128 + for call in calls + ) + for entity_id in ["light.light_4", "light.light_5"]: + assert hass.states.get(entity_id).attributes[ATTR_BRIGHTNESS] == 128 + + +@pytest.mark.parametrize("brightness_mode", ["linear", "tanh"]) +@pytest.mark.parametrize(("dark", "light"), ([900, 1800], [1800, 900], [1800, 1800])) +async def test_brightness_mode(hass, brightness_mode, dark, light): + """Test brightness mode. + + We are not testing the "default" mode because that is tested in all other tests. + """ + is_symmetric = dark == light + _, switch = await setup_switch( + hass, + { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_BRIGHTNESS_MODE: brightness_mode, + CONF_BRIGHTNESS_MODE_TIME_DARK: datetime.timedelta(seconds=dark), + CONF_BRIGHTNESS_MODE_TIME_LIGHT: datetime.timedelta(seconds=light), + }, + ) + + context = switch.create_context("test") # needs to be passed to update method + min_brightness = switch._sun_light_settings.min_brightness + max_brightness = switch._sun_light_settings.max_brightness + brightness_range = max_brightness - min_brightness + brightness_event = min_brightness + brightness_range / 2 + dark = switch._sun_light_settings.brightness_mode_time_dark + light = switch._sun_light_settings.brightness_mode_time_light + + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunset = sunset - light + after_sunset = sunset + dark + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunrise = sunrise - dark + after_sunrise = sunrise + light + + light_brightness = ( + max_brightness + if brightness_mode == "linear" + else 0.95 * brightness_range + min_brightness + ) + dark_brightness = ( + min_brightness + if brightness_mode == "linear" + else 0.05 * brightness_range + min_brightness + ) + + def is_approx_equal(a, b): + return abs(a - b) < 0.01 + + async def patch_time_and_update(time): + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=time, + ): + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + if is_symmetric: + # At sunset the brightness should be 50% + await patch_time_and_update(sunset) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], brightness_event) + + # Before sunset the brightness should be max + await patch_time_and_update(before_sunset) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness) + + # After sunset the brightness should be dark_brightness + await patch_time_and_update(after_sunset) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], dark_brightness) + + if is_symmetric: + # At sunrise the brightness should be 50% + await patch_time_and_update(sunrise) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], brightness_event) + + # Before sunrise the brightness should be min + await patch_time_and_update(before_sunrise) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], dark_brightness) + + # After sunrise the brightness should be light_brightness + await patch_time_and_update(after_sunrise) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness) + + +async def test_simple_switch_initial_state_not_none(hass): + """Test that SimpleSwitch._state is not None after __init__. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1264 + + When an entity is disabled in Home Assistant, async_added_to_hass() is never + called. Previously, SimpleSwitch._state was initialized to None and only set + to True/False in async_added_to_hass(). This caused an infinite loop in + AdaptiveSwitch._setup_listeners() which waits for all SimpleSwitch._state + to be not None. + + The fix is to initialize _state to the initial_state value in __init__. + """ + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME}) + entry.add_to_hass(hass) + + # Create a SimpleSwitch without calling async_added_to_hass + # (simulating a disabled entity) + switch = SimpleSwitch( + which="Test", + initial_state=True, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + + # Before the fix: _state would be None, causing infinite loop + # After the fix: _state should be the initial_state value + assert switch._state is not None, ( + "SimpleSwitch._state should not be None after __init__. " + "This would cause an infinite loop in _setup_listeners when the entity is disabled." + ) + assert switch._state is True # Should be the initial_state value + + +async def test_simple_switch_state_after_async_added_to_hass(hass): + """Test that SimpleSwitch._state is properly set after async_added_to_hass. + + This ensures the fix for #1264 doesn't break normal entity initialization. + """ + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME}) + entry.add_to_hass(hass) + + # Create switches with different initial states + switch_true = SimpleSwitch( + which="Test True", + initial_state=True, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + switch_false = SimpleSwitch( + which="Test False", + initial_state=False, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + + # Verify initial state is set correctly + assert switch_true._state is True + assert switch_false._state is False + + # Call async_added_to_hass (simulating normal entity setup) + # Since there's no last state, it should use the initial_state + await switch_true.async_added_to_hass() + await switch_false.async_added_to_hass() + + # State should still be correct after async_added_to_hass + assert switch_true._state is True + assert switch_false._state is False + + +def test_attributes_have_changed_light_mode_switch(): + """Test detection of external light mode changes (color_temp vs rgb vs xy). + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1275 + + When a user activates a Hue Scene (or similar) via an external app, the light + may switch from color_temp mode to RGB/XY mode (or vice versa). This should be + detected as an external change so AL doesn't immediately override it. + + The _has_color_mode_changed() function checks the original attributes BEFORE + any conversion, enabling bidirectional mode change detection. + """ + context = Context() + base_kwargs = { + "light": "light.test", + "context": context, + } + kwargs_adapt_color = base_kwargs + + # color_temp → RGB + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + **kwargs_adapt_color, + ), "Should detect color_temp → RGB mode switch" + + # color_temp → XY + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + **kwargs_adapt_color, + ), "Should detect color_temp → XY mode switch" + + # RGB → color_temp + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_adapt_color, + ), "Should detect RGB → color_temp mode switch" + + # RGB → XY + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + **kwargs_adapt_color, + ), "Should detect RGB → XY mode switch" + + # XY → color_temp + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_adapt_color, + ), "Should detect XY → color_temp mode switch" + + # XY → RGB + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + **kwargs_adapt_color, + ), "Should detect XY → RGB mode switch" + + # No mode change - same type with same values shouldn't be detected + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_adapt_color, + ), "Same color_temp should not be detected as change" + + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + **kwargs_adapt_color, + ), "Same RGB should not be detected as change" + + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + **kwargs_adapt_color, + ), "Same XY should not be detected as change" + + +# Regression tests for bugs found in PR #1348 by @protyposis +# See: https://github.com/basnijholt/adaptive-lighting/pull/1348 + + +async def test_multi_light_intercept_prepares_adaptation_for_first_entity(hass): + """Test that adaptation data is prepared for the first entity, not the last. + + Regression test for a bug where `entity_id` from a for-loop was used after + the loop ended, causing `prepare_adaptation_data` to be called with only + the last entity's ID instead of the first. + + In `_service_interceptor_turn_on_single_light_handler`: + ```python + for entity_id in entity_ids: + self.clear_proactively_adapting(entity_id) + + adaptation_data = await switch.prepare_adaptation_data( + entity_id, # BUG: This uses the last entity_id from the loop! + transition, + ) + ``` + + The adaptation data should be prepared for the first entity in the list since + that's the one whose service call is being intercepted and modified. + + See: https://github.com/basnijholt/adaptive-lighting/pull/1348 + """ + switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True) + + # Turn off all lights first + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3]}, + blocking=True, + ) + await hass.async_block_till_done() + + # Mock prepare_adaptation_data to track which entity_id it's called with + original_prepare = switch.prepare_adaptation_data + called_with_entities = [] + + async def mock_prepare_adaptation_data(light, *args, **kwargs): + called_with_entities.append(light) + return await original_prepare(light, *args, **kwargs) + + switch.prepare_adaptation_data = mock_prepare_adaptation_data + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + + # Turn on multiple lights at once - this triggers the interceptor + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [ENTITY_LIGHT_1, ENTITY_LIGHT_2]}, + blocking=True, + ) + await hass.async_block_till_done() + + # The bug causes prepare_adaptation_data to be called with the LAST entity + # (ENTITY_LIGHT_2) instead of the FIRST entity (ENTITY_LIGHT_1) + assert len(called_with_entities) >= 1, "prepare_adaptation_data should be called" + + # The first call should be for ENTITY_LIGHT_1 (the first entity in the list) + # since the intercepted service call will apply to all entities in entity_ids + # BUG: Currently this fails because entity_id is ENTITY_LIGHT_2 (the last one) + assert called_with_entities[0] == ENTITY_LIGHT_1, ( + f"prepare_adaptation_data should be called with the first entity " + f"({ENTITY_LIGHT_1}), but was called with {called_with_entities[0]}. " + f"This indicates the bug where the last entity from the for-loop is used." + ) + + +async def test_skipped_lights_context_not_from_arbitrary_switch(hass): + """Test that context for skipped lights uses manager, not an arbitrary switch. + + Regression test for a bug where the context for skipped lights was created + using `switch.create_context("skipped")` where `switch` was from the last + iteration of a for-loop, which had no relationship to the skipped lights. + + The fix uses `self.create_context("skipped")` on the AdaptiveLightingManager + instead, which uses "manager" as the context name. + + See: https://github.com/basnijholt/adaptive-lighting/pull/1348 + """ + # Setup two switches with different lights + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + + # Turn on all three lights at once: + # - ENTITY_LIGHT_1 is in switch1 + # - ENTITY_LIGHT_2 is in switch2 + # - ENTITY_LIGHT_3 is not in any switch (will be skipped) + events = await _turn_on_and_track_event_contexts( + hass, + "test_skipped_context", + lights, + return_full_events=True, + ) + + # Find the skipped event (contains ":skpp:" in context) + skipped_events = [e for e in events if ":skpp:" in e.context.id] + assert ( + len(skipped_events) == 1 + ), f"Expected 1 skipped event, got {len(skipped_events)}" + + skipped_event = skipped_events[0] + skipped_context_id = skipped_event.context.id + + # Extract the name_hash from the context + # Context format: {timestamp}:{al}:{name_hash}:{which_short}:{index} + context_parts = skipped_context_id.split(":") + assert len(context_parts) >= 4, f"Unexpected context format: {skipped_context_id}" + + # The context should still be recognized as ours + assert is_our_context_id(skipped_context_id), "Skipped context should be recognized" + assert is_our_context_id( + skipped_context_id, + "skipped", + ), "Skipped context should have 'skipped' marker" + + # Verify the skipped lights are the ones not in any switch + assert skipped_event.data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3] + + # After the fix, the context should use "manager" as the name, not a switch name. + # The name_hash is the 3rd segment (index 2) in the context ID. + name_hash_in_context = context_parts[2] + expected_manager_hash = short_hash("manager") + assert name_hash_in_context == expected_manager_hash, ( + f"Skipped context should use 'manager' hash ({expected_manager_hash}), " + f"but got {name_hash_in_context}. This indicates the context is still " + f"being created from an arbitrary switch instead of the manager." + ) + + +async def test_automation_turn_on_from_off_not_marked_as_manual_control(hass): + """Test that turning on a light from OFF via automation is not marked as manual control. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1378 + + When an automation turns on a light from OFF state with brightness/color attributes, + the light should NOT be marked as manually controlled. Adaptive Lighting should + adapt the light normally. + + The bug in v1.30.0 was that `update_manually_controlled_from_event` was called for + ALL `light.turn_on` events, not just when the light was already ON. This caused + lights turned on by automations to be incorrectly marked as "manually controlled". + """ + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_TAKE_OVER_CONTROL: True, + CONF_DETECT_NON_HA_CHANGES: False, + }, + ) + + # Ensure light is OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + + # Verify light is not manually controlled + assert not switch.manager.manual_control.get( + ENTITY_LIGHT_1, + ), "Light should not be manually controlled before test" + + # Simulate an automation turning on the light with brightness + # This is an external call (not from AL) with brightness attribute + external_context = Context(id="automation_context_12345") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 255, + }, + blocking=True, + context=external_context, + ) + await hass.async_block_till_done() + + # The light should be ON + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + # CRITICAL: The light should NOT be marked as manually controlled! + # The bug in v1.30.0 would incorrectly mark this as manual control because + # the turn_on had a brightness attribute. + manual_control_attrs = switch.manager.manual_control.get(ENTITY_LIGHT_1) + assert not manual_control_attrs, ( + f"Bug confirmed: Light was incorrectly marked as manually controlled " + f"(attributes: {manual_control_attrs}) when turned on from OFF state. " + f"Lights turned on from OFF by automations should NOT be marked as " + f"manually controlled - only lights that were already ON and then had " + f"their brightness/color changed externally should be marked as such." + ) + + +@pytest.mark.parametrize("intercept", [True, False]) +async def test_manual_control_on_external_turn_on_allows_tracked_service_call( + hass, + intercept, +): + """Test a real HA turn-on remains eligible for initial adaptation.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 200}, + blocking=True, + context=Context(id=f"ha_turn_on_{intercept}"), + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_LIGHT_1) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + +@pytest.mark.parametrize("intercept", [True, False]) +@pytest.mark.parametrize( + ( + "manual_control_on_external_turn_on", + "detect_non_ha_changes", + "expected_manual_control", + "expected_adaptation", + ), + [ + (True, True, LightControlAttributes.ALL, False), + (True, False, LightControlAttributes.ALL, False), + (False, True, LightControlAttributes.NONE, True), + (False, False, LightControlAttributes.ALL, False), + ], +) +async def test_manual_control_on_external_turn_on_external_state_change( + hass, + freezer, + intercept, + manual_control_on_external_turn_on, + detect_non_ha_changes, + expected_manual_control, + expected_adaptation, +): + """Test an unmatched off-to-on state event follows the opt-in policy.""" + switch, _ = await setup_lights_and_switch( + hass, + { + "manual_control_on_external_turn_on": manual_control_on_external_turn_on, + CONF_DETECT_NON_HA_CHANGES: detect_non_ha_changes, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + external_attributes = dict(hass.states.get(ENTITY_LIGHT_1).attributes) + external_attributes[ATTR_BRIGHTNESS] = 200 + hass.states.async_set( + ENTITY_LIGHT_1, + STATE_OFF, + external_attributes, + context=Context(id=f"unmatched_turn_off_{intercept}"), + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + freezer.tick(6) + + hass.states.async_set( + ENTITY_LIGHT_1, + STATE_ON, + external_attributes, + context=Context(id=f"unmatched_turn_on_{intercept}"), + ) + await hass.async_block_till_done() + + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == expected_manual_control + ) + last_service_data = switch.manager.last_service_data.get(ENTITY_LIGHT_1) + if expected_adaptation: + assert last_service_data[ATTR_BRIGHTNESS] == 128 + else: + assert last_service_data is None + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 200 + + +@pytest.mark.parametrize("intercept", [True, False]) +async def test_manual_control_on_external_turn_on_keeps_non_ha_change_detection( + hass, + intercept, +): + """Test the option does not disable manual tracking for an on light.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + transition=0, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + + set_light_brightness(light, 200) + light.async_write_ha_state() + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + transition=0, + ) + await hass.async_block_till_done() + + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 200 + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.BRIGHTNESS + ) + + +@pytest.mark.parametrize("intercept", [True, False]) +async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, intercept): + """Test that adapt_only_on_bare_turn_on respects take_over_control_mode=PAUSE_CHANGED. + + When adapt_only_on_bare_turn_on=True and take_over_control_mode=PAUSE_CHANGED, + turning on a light from OFF with only brightness should: + 1. Mark ONLY brightness as manually controlled (not all attributes) + 2. Continue adapting color (since only brightness was specified) + + This test verifies the integration of #1356 (individual attribute tracking) + with adapt_only_on_bare_turn_on. Prior to the fix, the code would return early + after marking attributes as manually controlled, skipping all adaptation + including unspecified attributes like color. + + The test is parameterized with intercept=True/False to verify consistency + between the intercept path and the reactive (event-based) path. + """ + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_TAKE_OVER_CONTROL: True, + CONF_TAKE_OVER_CONTROL_MODE: TakeOverControlMode.PAUSE_CHANGED.value, + CONF_ADAPT_ONLY_ON_BARE_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: intercept, + }, + ) + + # Verify settings + assert switch._take_over_control + assert switch._take_over_control_mode == TakeOverControlMode.PAUSE_CHANGED + assert switch._adapt_only_on_bare_turn_on + + # Ensure light is OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + + # Clear any prior service data + switch.manager.last_service_data.pop(ENTITY_LIGHT_1, None) + + # Turn on light from OFF with only brightness (simulating a scene or automation) + external_context = Context(id="scene_turn_on_with_brightness") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 200, # Only brightness specified + }, + blocking=True, + context=external_context, + ) + await hass.async_block_till_done() + + # Light should be ON + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + # 1. Verify that ONLY brightness is marked as manually controlled + manual_control_attrs = switch.manager.manual_control.get(ENTITY_LIGHT_1) + assert manual_control_attrs == LightControlAttributes.BRIGHTNESS, ( + f"Expected only BRIGHTNESS to be marked as manually controlled, " + f"but got: {manual_control_attrs}. With adapt_only_on_bare_turn_on=True, " + f"only the attributes specified in the turn_on call should be marked." + ) + + # 2. Verify that color WAS adapted (last_service_data should have color_temp) + last_service_data = switch.manager.last_service_data.get(ENTITY_LIGHT_1) + assert last_service_data is not None, ( + "Bug: last_service_data is None, meaning adaptation was skipped entirely. " + "With PAUSE_CHANGED mode, color should still be adapted since only brightness " + "was marked as manually controlled." + ) + assert ATTR_COLOR_TEMP_KELVIN in last_service_data, ( + f"Bug: Color was not adapted. last_service_data={last_service_data}. " + f"With take_over_control_mode=PAUSE_CHANGED and only brightness marked " + f"as manually controlled, color_temp should still be adapted." + ) + + +@pytest.mark.parametrize( + ("repeat_bare_turn_on", "mode", "intercept"), + [ + (False, TakeOverControlMode.PAUSE_ALL, False), + (False, TakeOverControlMode.PAUSE_CHANGED, False), + (True, TakeOverControlMode.PAUSE_ALL, True), + (True, TakeOverControlMode.PAUSE_CHANGED, True), + ], + ids=[ + "direct-pause-all-reactive", + "direct-pause-changed-reactive", + "bare-turn-on-pause-all-intercept", + "bare-turn-on-pause-changed-intercept", + ], +) +async def test_detect_non_ha_changes_with_separate_turn_on_commands( + hass, + repeat_bare_turn_on, + mode, + intercept, +): + """Regression test for detect_non_ha_changes with separate_turn_on_commands. + + With separate_turn_on_commands=True, each adaptation cycle makes two sequential + light.turn_on calls (brightness, then color). If the second call overwrites + last_service_data instead of merging, brightness is dropped — and + _attributes_have_changed silently skips the brightness comparison, so a direct + Zigbee brightness change is never detected as manual control. + + A repeated bare light.turn_on from an automation must not hide the physical + change before the periodic adaptation path runs. + """ + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_TAKE_OVER_CONTROL: True, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_INTERCEPT: intercept, + }, + ) + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 50, + ATTR_COLOR_TEMP_KELVIN: 3000, + "force_rgb_color": False, + }, + ) + + context = switch.create_context("interval") + + async def update(force: bool = False): + await switch._update_attrs_and_maybe_adapt_lights( + context=context, + force=force, + transition=0, + ) + await hass.async_block_till_done() + + await update(force=True) + + al_brightness = light.brightness + assert al_brightness is not None + al_color_temp = light.color_temp_kelvin + assert al_color_temp is not None + switch.manager.manual_control[ENTITY_LIGHT_1] = LightControlAttributes.NONE + + manual_brightness = ( + al_brightness - 120 if al_brightness >= 120 else al_brightness + 120 + ) + set_light_brightness(light, manual_brightness) + + if repeat_bare_turn_on: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 50, + ATTR_COLOR_TEMP_KELVIN: 4000, + "force_rgb_color": False, + }, + ) + + async def _flush_attr_state(hass, entity_id): + """Mimic a ZHA attribute report: write current hardware state to HA.""" + light.async_write_ha_state() + + with patch( + "homeassistant.components.adaptive_lighting.switch.async_update_entity", + new=AsyncMock(side_effect=_flush_attr_state), + ): + await update(force=False) + await update(force=False) + + assert ( + light.brightness == manual_brightness + ), f"AL overrode manual brightness {manual_brightness} with {light.brightness}" + expected_color_temp = ( + 4000 if mode == TakeOverControlMode.PAUSE_CHANGED else al_color_temp + ) + assert light.color_temp_kelvin == expected_color_temp + + +async def test_fresh_install_entity_ids(hass): + """Test the entity ids a new install gets with device-relative naming.""" + _, switch = await setup_switch(hass, {}) + + assert switch.entity_id == ENTITY_SWITCH + assert switch.sleep_mode_switch.entity_id == ENTITY_SLEEP_MODE_SWITCH + assert switch.adapt_brightness_switch.entity_id == ENTITY_ADAPT_BRIGHTNESS_SWITCH + assert switch.adapt_color_switch.entity_id == ENTITY_ADAPT_COLOR_SWITCH + + +async def test_existing_entity_ids_are_preserved(hass): + """Test an install predating this change keeps its entity ids. + + The unique ids are unchanged, so the entity registry must keep the + classic `..._sleep_mode_` id instead of renaming the entity. + """ + classic_entity_id = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" + assert classic_entity_id != ENTITY_SLEEP_MODE_SWITCH + + registry = entity_registry.async_get(hass) + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + f"{DEFAULT_NAME}_sleep_mode", + suggested_object_id=classic_entity_id.split(".", 1)[1], + ) + + _, switch = await setup_switch(hass, {}) + + assert switch.sleep_mode_switch.entity_id == classic_entity_id + + +def test_validate_ui_options_win_over_stale_data(): + """A UI-configured entry's `options` (from the options flow) must win. + + `data` for a `SOURCE_USER` entry either only holds the entry name, or - + for entries created before `data`/`options` were split - a stale + snapshot from initial setup. Either way, a later change made through + the options flow (stored in `options`) must not be silently discarded + by that stale/legacy `data`. + """ + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={CONF_NAME: DEFAULT_NAME, CONF_LIGHTS: ["light.a"]}, + options={CONF_LIGHTS: ["light.a", "light.b"]}, + ) + + result = validate(entry) + + assert result[CONF_LIGHTS] == ["light.a", "light.b"] + + +def test_validate_yaml_data_wins_over_stray_options(): + """A YAML-imported entry's `data` must keep winning over `options`. + + YAML configuration is the source of truth for a `SOURCE_IMPORT` entry, + so any leftover `options` (e.g. from a UI setup that predates the YAML + import) must not override it. + """ + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={CONF_NAME: DEFAULT_NAME, CONF_LIGHTS: ["light.a"]}, + options={CONF_LIGHTS: ["light.b"]}, + ) + + result = validate(entry) + + assert result[CONF_LIGHTS] == ["light.a"] + + +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TOGGLE]) +@pytest.mark.parametrize("explicit", [False, True], ids=["area", "direct"]) +@pytest.mark.parametrize("managed", [False, True], ids=["unmanaged", "managed"]) +@pytest.mark.parametrize( + "registry_settings", + [ + {}, + {"entity_category": EntityCategory.CONFIG}, + {"entity_category": EntityCategory.DIAGNOSTIC}, + {"hidden_by": entity_registry.RegistryEntryHider.USER}, + ], + ids=["normal", "config", "diagnostic", "hidden"], +) +async def test_intercept_preserves_area_target_exclusions( + hass: HomeAssistant, + service: str, + explicit: bool, + managed: bool, + registry_settings: dict[str, Any], +): + """Area calls exclude hidden/categorized lights; direct calls honor them.""" + await setup_lights(hass) + mock_area_registry(hass) + registry = entity_registry.async_get(hass) + lights = [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3] + for light in lights: + registry.async_update_entity(light, area_id="test-area") + registry.async_update_entity(ENTITY_LIGHT_3, **registry_settings) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: lights}, + blocking=True, + ) + await hass.async_block_till_done() + await setup_switch( + hass, + { + CONF_LIGHTS: ( + [ENTITY_LIGHT_1, ENTITY_LIGHT_3] if managed else [ENTITY_LIGHT_1] + ), + CONF_INTERCEPT: True, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + assert all(hass.states.get(light).state == STATE_OFF for light in lights) + + target = {ATTR_ENTITY_ID: lights} if explicit else {ATTR_AREA_ID: "test-area"} + await hass.services.async_call(LIGHT_DOMAIN, service, target, blocking=True) + await hass.async_block_till_done() + + # Both normal lights turn on; only the managed one gets adaptive brightness. + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get(ENTITY_LIGHT_2).state == STATE_ON + assert hass.states.get(ENTITY_LIGHT_2).attributes.get(ATTR_BRIGHTNESS) != 128 + target_state = hass.states.get(ENTITY_LIGHT_3) + if registry_settings and not explicit: + assert target_state.state == STATE_OFF + else: + assert target_state.state == STATE_ON + if managed: + assert target_state.attributes[ATTR_BRIGHTNESS] == 128 + else: + assert target_state.attributes.get(ATTR_BRIGHTNESS) != 128 + + +@pytest.mark.parametrize("split", [False, True]) +@pytest.mark.parametrize("target_group", [False, True]) +@pytest.mark.parametrize("skip_redundant", [False, True]) +async def test_multi_light_intercept_adapts_every_member( + hass, + split, + target_group, + skip_redundant, + cleanup, +): + """Each member receives brightness and color, including split follow-up calls.""" + lights = await setup_lights(hass, with_group=True) + # The second member already has target brightness; its color still needs work. + set_light_brightness(lights[4], 171) + lights[4].async_write_ha_state() + members = ["light.light_4", "light.light_5"] + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: members, + CONF_INTERCEPT: True, + CONF_MULTI_LIGHT_INTERCEPT: True, + CONF_SEPARATE_TURN_ON_COMMANDS: split, + CONF_SKIP_REDUNDANT_COMMANDS: skip_redundant, + CONF_INITIAL_TRANSITION: 0, + }, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + events = await _turn_on_and_track_event_contexts( + hass, + "multi_light_split", + "light.light_group" if target_group else members, + return_full_events=True, + ) + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + + if split: + color_targets = { + event.data["service_data"][ATTR_ENTITY_ID] + for event in events + if ATTR_COLOR_TEMP_KELVIN in event.data["service_data"] + } + assert color_targets == set(members) + for entity_id in members: + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 171 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 + + +@pytest.mark.parametrize("physical_off", [False, True]) +async def test_split_command_stays_off_after_turn_off(hass, physical_off): + """An actual OFF between brightness and color cancels the pending command.""" + switch, _ = await setup_lights_and_switch( + hass, + {CONF_INTERCEPT: True, CONF_SEPARATE_TURN_ON_COMMANDS: True}, + all_lights=True, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + events = await _turn_on_and_track_event_contexts( + hass, + "split_then_off", + ENTITY_LIGHT_3, + return_full_events=True, + ) + assert len(events) == 1 + assert hass.states.get(ENTITY_LIGHT_3).state == STATE_ON + if physical_off: + # Device reports may retain the context of the preceding turn-on. + state = hass.states.get(ENTITY_LIGHT_3) + hass.states.async_set( + ENTITY_LIGHT_3, + STATE_OFF, + state.attributes, + context=Context(id="split_then_off"), + ) + else: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + ) + await hass.async_block_till_done() + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + + turn_on_events = [ + event for event in events if event.data["service"] == SERVICE_TURN_ON + ] + assert len(turn_on_events) == 1 + assert hass.states.get(ENTITY_LIGHT_3).state == STATE_OFF + + +@pytest.mark.parametrize( + ("brightness_only_member", "initial_transition", "shared_transition"), + [(0, 0, 0), (1, 0, 0), (0, 0.4, 0.4), (1, 0.4, 0.2)], +) +async def test_multi_light_split_with_brightness_only_member( + hass, + brightness_only_member, + initial_transition, + shared_transition, + cleanup, +): + """Each member gets its color command after the shared brightness transition.""" + lights = await setup_lights(hass, with_group=True) + members = ["light.light_4", "light.light_5"] + light = lights[3 + brightness_only_member] + # Legacy YAML support outlived the old entity storage fields. + if hasattr(light, "_supported_color_modes"): + light._supported_color_modes = {ColorMode.BRIGHTNESS} + light._color_mode = ColorMode.BRIGHTNESS + else: + light._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + light._attr_color_mode = ColorMode.BRIGHTNESS + light.async_write_ha_state() + for member in lights[3:5]: + member._attr_supported_features |= LightEntityFeature.TRANSITION + member.async_write_ha_state() + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: members, + CONF_INTERCEPT: True, + CONF_MULTI_LIGHT_INTERCEPT: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_INITIAL_TRANSITION: initial_transition, + CONF_SEND_SPLIT_DELAY: 50, + }, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + call_times = [] + loop = asyncio.get_running_loop() + + async def record_call(event): + if event.data["domain"] == LIGHT_DOMAIN: + call_times.append(loop.time()) + + hass.bus.async_listen(EVENT_CALL_SERVICE, record_call) + events = await _turn_on_and_track_event_contexts( + hass, + "mixed_split", + members, + return_full_events=True, + ) + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + color_targets = [ + event.data["service_data"][ATTR_ENTITY_ID] + for event in events + if ATTR_COLOR_TEMP_KELVIN in event.data["service_data"] + ] + assert color_targets == [members[1 - brightness_only_member]] + assert len(call_times) == 2 + # Leave room for event dispatch without accepting overlapping transitions. + assert call_times[1] - call_times[0] >= shared_transition + 0.05 - 0.02 + for entity_id in members: + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 171 + assert ( + hass.states.get(members[1 - brightness_only_member]).attributes[ + ATTR_COLOR_TEMP_KELVIN + ] + == 3448 + ) + + +@pytest.mark.parametrize("off_member", [0, 1]) +@pytest.mark.parametrize("physical_off", [False, True]) +async def test_multi_light_split_cancels_only_member_turned_off( + hass, + off_member, + physical_off, + cleanup, +): + """An OFF member stays off while the other finishes its color adaptation.""" + await setup_lights(hass, with_group=True) + members = ["light.light_4", "light.light_5"] + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: members, + CONF_INTERCEPT: True, + CONF_MULTI_LIGHT_INTERCEPT: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_INITIAL_TRANSITION: 0, + }, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + resume = asyncio.Event() + original_execute = switch._execute_adaptation_calls + + async def wait_before_followup(data): + await resume.wait() + await original_execute(data) + + with patch.object(switch, "_execute_adaptation_calls", new=wait_before_followup): + events = await _turn_on_and_track_event_contexts( + hass, + "member_off", + members, + return_full_events=True, + ) + off_entity = members[off_member] + state = hass.states.get(off_entity) + assert state.state == STATE_ON + if physical_off: + hass.states.async_set( + off_entity, + STATE_OFF, + state.attributes, + context=Context(id="member_off"), + ) + else: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: off_entity}, + blocking=True, + ) + await hass.async_block_till_done() + resume.set() + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + + color_targets = [ + event.data["service_data"][ATTR_ENTITY_ID] + for event in events + if ATTR_COLOR_TEMP_KELVIN in event.data["service_data"] + ] + assert color_targets == [members[1 - off_member]] + assert hass.states.get(off_entity).state == STATE_OFF + other_state = hass.states.get(members[1 - off_member]) + assert other_state.state == STATE_ON + assert other_state.attributes[ATTR_BRIGHTNESS] == 171 + assert other_state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 + + +@pytest.mark.parametrize( + "off_action", + [SERVICE_TURN_OFF, SERVICE_TOGGLE, "physical", "transition"], +) +async def test_forced_split_apply_stays_off(hass, off_action, cleanup): + """Even forced apply must cancel its remaining color command after OFF.""" + switch, _ = await setup_lights_and_switch( + hass, + {CONF_INTERCEPT: True, CONF_SEPARATE_TURN_ON_COMMANDS: True}, + all_lights=True, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + events = [] + light_on = asyncio.Event() + waiting_for_color = asyncio.Event() + resume = asyncio.Event() + calls_read = 0 + original_next = AdaptationData.next_service_call_data + + async def next_with_color_barrier(data): + nonlocal calls_read + calls_read += 1 + if calls_read == 2: + waiting_for_color.set() + await resume.wait() + return await original_next(data) + + async def track_service(event): + if event.data["domain"] == LIGHT_DOMAIN: + events.append(event) + + async def track_state(event): + if ( + event.data[ATTR_ENTITY_ID] == ENTITY_LIGHT_3 + and event.data["new_state"] is not None + and event.data["new_state"].state == STATE_ON + ): + light_on.set() + + hass.bus.async_listen(EVENT_CALL_SERVICE, track_service) + hass.bus.async_listen(EVENT_STATE_CHANGED, track_state) + with patch.object( + AdaptationData, + "next_service_call_data", + new=next_with_color_barrier, + ): + applying = asyncio.create_task( + hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [ENTITY_LIGHT_3], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + ), + ) + await asyncio.wait_for(waiting_for_color.wait(), timeout=1) + await asyncio.wait_for(light_on.wait(), timeout=1) + if off_action == "physical": + state = hass.states.get(ENTITY_LIGHT_3) + hass.states.async_set( + ENTITY_LIGHT_3, + STATE_OFF, + state.attributes, + context=state.context, + ) + else: + service_data = {ATTR_ENTITY_ID: ENTITY_LIGHT_3} + if off_action == "transition": + service_data[ATTR_TRANSITION] = 1 + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF if off_action == "transition" else off_action, + service_data, + blocking=True, + ) + await hass.async_block_till_done() + resume.set() + await applying + await hass.async_block_till_done() + + turn_on_events = [ + event for event in events if event.data["service"] == SERVICE_TURN_ON + ] + assert len(turn_on_events) == 1 + assert ATTR_BRIGHTNESS in turn_on_events[0].data["service_data"] + assert ATTR_COLOR_TEMP_KELVIN not in turn_on_events[0].data["service_data"] + assert hass.states.get(ENTITY_LIGHT_3).state == STATE_OFF + + +@pytest.mark.parametrize("intercept", [False, True]) +async def test_shared_profiles_keep_independent_sun_schedules( + hass, + intercept, + reset_time_zone, +): + """A later color sunrise keeps running after manual brightness takeover.""" + await hass.config.async_set_time_zone("UTC") + await setup_lights(hass) + profiles = [] + for name, sunrise, sunset in (("brightness", 6, 18), ("color", 10, 22)): + _, switch = await setup_switch( + hass, + { + CONF_NAME: name, + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_INTERCEPT: intercept, + CONF_TAKE_OVER_CONTROL_MODE: TakeOverControlMode.PAUSE_CHANGED, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_SUNRISE_TIME: datetime.time(sunrise), + CONF_SUNSET_TIME: datetime.time(sunset), + CONF_MIN_BRIGHTNESS: 10, + CONF_MAX_BRIGHTNESS: 90, + CONF_MIN_COLOR_TEMP: 2000, + CONF_MAX_COLOR_TEMP: 6000, + }, + ) + other_axis = ( + switch.adapt_color_switch + if name == "brightness" + else switch.adapt_brightness_switch + ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: other_axis.entity_id}, + blocking=True, + ) + profiles.append(switch) + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=datetime.datetime.fromisoformat("2026-09-05T08:00:00+00:00"), + ): + for switch in profiles: + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + morning = hass.states.get(ENTITY_LIGHT_1) + assert morning.attributes[ATTR_BRIGHTNESS] > 26 + assert morning.attributes[ATTR_COLOR_TEMP_KELVIN] == 2000 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + await hass.async_block_till_done() + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=datetime.datetime.fromisoformat("2026-09-05T12:00:00+00:00"), + ): + for switch in profiles: + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + noon = hass.states.get(ENTITY_LIGHT_1) + assert noon.attributes[ATTR_BRIGHTNESS] == 77 + assert noon.attributes[ATTR_COLOR_TEMP_KELVIN] > 2000 + + +@pytest.mark.parametrize("via_unavailable", [False, True]) +async def test_split_adaptation_cancelled_after_physical_off( + hass, + monkeypatch, + via_unavailable, +): + """Pending split commands must not resurrect a physically switched-off light.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_DETECT_NON_HA_CHANGES: True, + CONF_ONLY_ONCE: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_SEND_SPLIT_DELAY: 1234, + CONF_INITIAL_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + state = hass.states.get(ENTITY_LIGHT_1) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes) + await hass.async_block_till_done() + # Isolate the split-command lifetime from the separate turn-off debounce. + monkeypatch.setattr( + switch.manager, + "just_turned_off", + AsyncMock(return_value=False), + ) + entered, release = asyncio.Event(), asyncio.Event() + original_sleep = asyncio.sleep + + async def controlled_sleep(delay, *args, **kwargs): + if delay == 1.234: + entered.set() + await release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + monkeypatch.setattr(asyncio, "sleep", controlled_sleep) + calls = _track_adaptive_light_calls(hass) + hass.states.async_set(ENTITY_LIGHT_1, STATE_ON, state.attributes) + await asyncio.wait_for(entered.wait(), 2) + assert len(calls) == 1 + if via_unavailable: + hass.states.async_set(ENTITY_LIGHT_1, STATE_UNAVAILABLE, state.attributes) + await original_sleep(0) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes) + await original_sleep(0) + release.set() + await hass.async_block_till_done() + assert len(calls) == 1, f"Physical OFF resurrected by split command: {calls}" + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + + +@pytest.mark.parametrize("remaining_profile", [False, True]) +async def test_profile_unloaded_during_adapt_delay( + hass, + monkeypatch, + remaining_profile, +): + """A removed profile must not send commands after its adaptation delay.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_DETECT_NON_HA_CHANGES: True, + CONF_ONLY_ONCE: True, + CONF_ADAPT_DELAY: 0.1234, + }, + ) + if remaining_profile: + _, other = await setup_switch( + hass, + { + CONF_NAME: "remaining", + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_ONLY_ONCE: True, + CONF_INITIAL_TRANSITION: 0, + }, + ) + await other.async_turn_off() + state = hass.states.get(ENTITY_LIGHT_1) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes) + await hass.async_block_till_done() + monkeypatch.setattr( + switch.manager, + "just_turned_off", + AsyncMock(return_value=False), + ) + entered, release = asyncio.Event(), asyncio.Event() + original_sleep = asyncio.sleep + + async def controlled_sleep(delay, *args, **kwargs): + if delay == 0.1234: + entered.set() + await release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + monkeypatch.setattr(asyncio, "sleep", controlled_sleep) + calls = _track_adaptive_light_calls(hass) + hass.states.async_set(ENTITY_LIGHT_1, STATE_ON, state.attributes) + await asyncio.wait_for(entered.wait(), 2) + entry = hass.config_entries.async_entries(DOMAIN)[0] + await hass.config_entries.async_unload(entry.entry_id) + calls.clear() + release.set() + await hass.async_block_till_done() + assert calls == [] + if remaining_profile: + await other.async_turn_on() + await other._update_attrs_and_maybe_adapt_lights( + context=other.create_context("test"), + lights=[ENTITY_LIGHT_1], + force=True, + ) + await hass.async_block_till_done() + assert calls + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + +async def test_profile_unloaded_during_split_delay(hass, monkeypatch): + """Removed profiles must not send remaining split commands.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_DETECT_NON_HA_CHANGES: True, + CONF_ONLY_ONCE: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_SEND_SPLIT_DELAY: 1234, + CONF_INITIAL_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + state = hass.states.get(ENTITY_LIGHT_1) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes) + await hass.async_block_till_done() + # Isolate the split-command lifetime from the separate turn-off debounce. + monkeypatch.setattr( + switch.manager, + "just_turned_off", + AsyncMock(return_value=False), + ) + entered, release = asyncio.Event(), asyncio.Event() + original_sleep = asyncio.sleep + + async def controlled_sleep(delay, *args, **kwargs): + if delay == 1.234: + entered.set() + await release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + monkeypatch.setattr(asyncio, "sleep", controlled_sleep) + calls = _track_adaptive_light_calls(hass) + hass.states.async_set(ENTITY_LIGHT_1, STATE_ON, state.attributes) + await asyncio.wait_for(entered.wait(), 2) + assert len(calls) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert await hass.config_entries.async_unload(entry.entry_id) + release.set() + await hass.async_block_till_done() + assert len(calls) == 1 + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + +@pytest.mark.parametrize("unload_before_split", [False, True]) +async def test_unloaded_polling_profile_preserves_other_split_adaptation( + hass, + monkeypatch, + unload_before_split, +): + """A removed profile resuming a poll must not cancel another profile's work.""" + switch, _ = await setup_lights_and_switch(hass, {CONF_ONLY_ONCE: True}) + _, other = await setup_switch( + hass, + { + CONF_NAME: "remaining", + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_ONLY_ONCE: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_SEND_SPLIT_DELAY: 1234, + CONF_INITIAL_TRANSITION: 0, + }, + ) + poll_entered, poll_release = asyncio.Event(), asyncio.Event() + split_entered, split_release = asyncio.Event(), asyncio.Event() + original_update = switch.manager.update_manually_controlled_from_untracked_change + original_sleep = asyncio.sleep + + async def delayed_update(profile, *args, **kwargs): + if profile is switch: + poll_entered.set() + await poll_release.wait() + await original_update(profile, *args, **kwargs) + + async def controlled_sleep(delay, *args, **kwargs): + if delay == 1.234: + split_entered.set() + await split_release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + monkeypatch.setattr( + switch.manager, + "update_manually_controlled_from_untracked_change", + delayed_update, + ) + monkeypatch.setattr(asyncio, "sleep", controlled_sleep) + calls = _track_adaptive_light_calls(hass) + polling = hass.async_create_task( + switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + lights=[ENTITY_LIGHT_1], + force=True, + ), + ) + await asyncio.wait_for(poll_entered.wait(), 2) + entry = hass.config_entries.async_entries(DOMAIN)[0] + if unload_before_split: + assert await hass.config_entries.async_unload(entry.entry_id) + adapting = hass.async_create_task( + other._adapt_light( + ENTITY_LIGHT_1, + other.create_context("test"), + 0, + force=True, + ), + ) + await asyncio.wait_for(split_entered.wait(), 2) + assert len(calls) == 1 + if not unload_before_split: + assert await hass.config_entries.async_unload(entry.entry_id) + poll_release.set() + await polling + split_release.set() + await adapting + await hass.async_block_till_done() + assert len(calls) == 2 + assert ATTR_COLOR_TEMP_KELVIN in calls[-1] diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..56d3d2af --- /dev/null +++ b/uv.lock @@ -0,0 +1,6079 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] + +[[package]] +name = "acme" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "josepy", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pyopenssl", version = "24.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pyrfc3339", marker = "python_full_version < '3.13'" }, + { name = "pytz", marker = "python_full_version < '3.13'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "setuptools", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/34/d6d7064afabe02f0043e5015aab664b67fc8bf049e81ac9220526afed3ce/acme-3.0.1.tar.gz", hash = "sha256:2f4ae207c8a6791a2bc74cd18d60274766f483c2059145b0142cbb43e761331c", size = 91828, upload-time = "2024-11-14T19:13:31.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/7d/2daaaacf8dea7012fc680e4c1cf5dad426b6cc97296ba5b6431d313b83ab/acme-3.0.1-py3-none-any.whl", hash = "sha256:6b5f88681ead76f8c7de313ac6c7ee1c567fdcc61d48cfad2f5cb3606778529b", size = 95884, upload-time = "2024-11-14T19:13:08.565Z" }, +] + +[[package]] +name = "acme" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "josepy", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyopenssl", version = "25.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyrfc3339", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pytz", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/6a/c94be0e8ee157f3c7721844863589c627c40bfa10d8838faeb6b28b59bb2/acme-3.2.0.tar.gz", hash = "sha256:e11d0ccf43ec19244ada40df1dc4ca49c9ce407749f3771d2cefe0674e206d84", size = 92875, upload-time = "2025-02-11T21:35:57.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/1c/da759f277879f5b8aa0b6355689e091224b8af500c6983a1c8fb66bad5b0/acme-3.2.0-py3-none-any.whl", hash = "sha256:201b118d12426f746d936efc61706d30dc2f9e2635aebab0c86ec7f80eca5f30", size = 97444, upload-time = "2025-02-11T21:35:25.465Z" }, +] + +[[package]] +name = "acme" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "josepy", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "pyopenssl", version = "25.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "pyrfc3339", marker = "python_full_version >= '3.13.2'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/f6/897be0abeb0e64f0e6136a8a6369a54d2a603a44cb7a411f6d77dbafb4ac/acme-5.1.0.tar.gz", hash = "sha256:7b97820857d9baffed98bca50ab82bb6a636e447865d7a013a7bdd7972f03cda", size = 89982, upload-time = "2025-10-07T17:30:38.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/0b/4d0421412bb063f4393ae7ebf3a9a6fde621aed187a1140ccf7f9e22b823/acme-5.1.0-py3-none-any.whl", hash = "sha256:80e9c315d82302bb97279f4516ff31230d29195ab9d4a6c9411ceec20481b61e", size = 94151, upload-time = "2025-10-07T17:30:15.994Z" }, +] + +[[package]] +name = "adaptive-lighting" +version = "1.32.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "ruff" }, +] +docs = [ + { name = "astral" }, + { name = "homeassistant", version = "2024.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "homeassistant", version = "2025.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "homeassistant", version = "2026.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "markdown-code-runner" }, + { name = "markdown-gfm-admonition" }, + { name = "pandas" }, + { name = "shinylive" }, + { name = "tabulate" }, + { name = "ulid-transform", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "ulid-transform", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ulid-transform", version = "1.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "zensical" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "ruff" }, +] +docs = [ + { name = "astral" }, + { name = "homeassistant" }, + { name = "markdown-code-runner", specifier = ">=2.7.0" }, + { name = "markdown-gfm-admonition" }, + { name = "pandas" }, + { name = "shinylive" }, + { name = "tabulate" }, + { name = "ulid-transform" }, + { name = "voluptuous" }, + { name = "zensical" }, +] + +[[package]] +name = "aiodns" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "pycares", marker = "python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/84/41a6a2765abc124563f5380e76b9b24118977729e25a84112f8dfb2b33dc/aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72", size = 7823, upload-time = "2024-03-31T11:27:30.639Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/14/13c65b1bd59f7e707e0cc0964fbab45c003f90292ed267d159eeeeaa2224/aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5", size = 5735, upload-time = "2024-03-31T11:27:28.615Z" }, +] + +[[package]] +name = "aiodns" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "pycares", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2f/9d1ee4f937addda60220f47925dac6c6b3782f6851fd578987284a8d2491/aiodns-3.6.1.tar.gz", hash = "sha256:b0e9ce98718a5b8f7ca8cd16fc393163374bc2412236b91f6c851d066e3324b6", size = 15143, upload-time = "2025-12-11T12:53:07.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/e3/9f777774ebe8f664bcd564f9de3936490a16effa82a969372161c9b0fb21/aiodns-3.6.1-py3-none-any.whl", hash = "sha256:46233ccad25f2037903828c5d05b64590eaa756e51d12b4a5616e2defcbc98c7", size = 7975, upload-time = "2025-12-11T12:53:06.387Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohasupervisor" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "mashumaro", marker = "python_full_version < '3.13'" }, + { name = "orjson", version = "3.10.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/c8/7eed41d183ae5290761b101651baf688014c4ecf4e398311dd2255a0e6b0/aiohasupervisor-0.2.1.tar.gz", hash = "sha256:a1242165fd255796c961dadfbb88fc1f0d45f8441f8af10f42899ab478b1cbd9", size = 34806, upload-time = "2024-10-31T11:25:15.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/48/3a96f8b3510d82260d0912929ef5337262659688659e1e2e87386deec8e8/aiohasupervisor-0.2.1-py3-none-any.whl", hash = "sha256:b1e17c916bc1cec13611f74851305c8901525612ac6261e6aba2b225f2b15016", size = 33420, upload-time = "2024-10-31T11:25:14.611Z" }, +] + +[[package]] +name = "aiohasupervisor" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "mashumaro", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "orjson", version = "3.10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/23/eceea174c1d827adea8a8b23f1428454157288fd58e6a9231e8861a45383/aiohasupervisor-0.3.0.tar.gz", hash = "sha256:91bf0b051f28582196f900a31c9bcbebec6de9e3ed1a32a2947a892c04748ce2", size = 40542, upload-time = "2025-02-05T14:41:08.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/09/98c83e4a20ae951d49720caeb6daf784293498fab0450af5b2236ca0c079/aiohasupervisor-0.3.0-py3-none-any.whl", hash = "sha256:f85b45c80ee24b381523e5a84a39f962f25e72c90026a3dcef2becea1d7f5501", size = 38550, upload-time = "2025-02-05T14:41:06.838Z" }, +] + +[[package]] +name = "aiohasupervisor" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "mashumaro", marker = "python_full_version >= '3.13.2'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e0/f8865efa28ce22e44e3526f18654c7a69a6f0d0e8523e2aaf743f2798fd8/aiohasupervisor-0.3.3.tar.gz", hash = "sha256:24e268f58f37f9d8dafadba2ef9d860292ff622bc6e78b1ca4ef5e5095d1bbc8", size = 44696, upload-time = "2025-10-01T14:55:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/97/b811d22148e7227e6f02a1f0f13f60d959bb163c806feab853544da07c3e/aiohasupervisor-0.3.3-py3-none-any.whl", hash = "sha256:bc185dbb81bb8ec6ba91b5512df7fd3bf99db15e648b20aed3f8ce7dc3203f1f", size = 40486, upload-time = "2025-10-01T14:55:56.52Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.11.11" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohappyeyeballs", marker = "python_full_version < '3.13'" }, + { name = "aiosignal", marker = "python_full_version < '3.13'" }, + { name = "attrs", version = "24.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "frozenlist", marker = "python_full_version < '3.13'" }, + { name = "multidict", marker = "python_full_version < '3.13'" }, + { name = "propcache", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618, upload-time = "2024-12-18T21:20:50.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666, upload-time = "2024-12-18T21:18:49.254Z" }, + { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057, upload-time = "2024-12-18T21:18:51.375Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996, upload-time = "2024-12-18T21:18:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367, upload-time = "2024-12-18T21:18:55.053Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989, upload-time = "2024-12-18T21:18:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265, upload-time = "2024-12-18T21:19:00.174Z" }, + { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841, upload-time = "2024-12-18T21:19:02.3Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317, upload-time = "2024-12-18T21:19:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416, upload-time = "2024-12-18T21:19:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514, upload-time = "2024-12-18T21:19:12.154Z" }, + { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095, upload-time = "2024-12-18T21:19:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611, upload-time = "2024-12-18T21:19:18.849Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576, upload-time = "2024-12-18T21:19:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363, upload-time = "2024-12-18T21:19:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666, upload-time = "2024-12-18T21:19:26.425Z" }, + { url = "https://files.pythonhosted.org/packages/49/d1/d8af164f400bad432b63e1ac857d74a09311a8334b0481f2f64b158b50eb/aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9", size = 697982, upload-time = "2024-12-18T21:19:28.454Z" }, + { url = "https://files.pythonhosted.org/packages/92/d1/faad3bf9fa4bfd26b95c69fc2e98937d52b1ff44f7e28131855a98d23a17/aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194", size = 460662, upload-time = "2024-12-18T21:19:31.077Z" }, + { url = "https://files.pythonhosted.org/packages/db/61/0d71cc66d63909dabc4590f74eba71f91873a77ea52424401c2498d47536/aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f", size = 452950, upload-time = "2024-12-18T21:19:33.108Z" }, + { url = "https://files.pythonhosted.org/packages/07/db/6d04bc7fd92784900704e16b745484ef45b77bd04e25f58f6febaadf7983/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104", size = 1665178, upload-time = "2024-12-18T21:19:36.556Z" }, + { url = "https://files.pythonhosted.org/packages/54/5c/e95ade9ae29f375411884d9fd98e50535bf9fe316c9feb0f30cd2ac8f508/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff", size = 1717939, upload-time = "2024-12-18T21:19:40.081Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/1e7d5c5daea9e409ed70f7986001b8c9e3a49a50b28404498d30860edab6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3", size = 1775125, upload-time = "2024-12-18T21:19:43.578Z" }, + { url = "https://files.pythonhosted.org/packages/5d/66/890987e44f7d2f33a130e37e01a164168e6aff06fce15217b6eaf14df4f6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1", size = 1677176, upload-time = "2024-12-18T21:19:46.239Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dc/e2ba57d7a52df6cdf1072fd5fa9c6301a68e1cd67415f189805d3eeb031d/aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4", size = 1603192, upload-time = "2024-12-18T21:19:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9e/8d08a57de79ca3a358da449405555e668f2c8871a7777ecd2f0e3912c272/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d", size = 1618296, upload-time = "2024-12-18T21:19:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/56/51/89822e3ec72db352c32e7fc1c690370e24e231837d9abd056490f3a49886/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87", size = 1616524, upload-time = "2024-12-18T21:19:52.542Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/e2e6d9398f462ffaa095e84717c1732916a57f1814502929ed67dd7568ef/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2", size = 1685471, upload-time = "2024-12-18T21:19:54.683Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5f/6bb976e619ca28a052e2c0ca7b0251ccd893f93d7c24a96abea38e332bf6/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12", size = 1715312, upload-time = "2024-12-18T21:19:56.824Z" }, + { url = "https://files.pythonhosted.org/packages/79/c1/756a7e65aa087c7fac724d6c4c038f2faaa2a42fe56dbc1dd62a33ca7213/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5", size = 1672783, upload-time = "2024-12-18T21:19:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/73/ba/a6190ebb02176c7f75e6308da31f5d49f6477b651a3dcfaaaca865a298e2/aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d", size = 410229, upload-time = "2024-12-18T21:20:02.469Z" }, + { url = "https://files.pythonhosted.org/packages/b8/62/c9fa5bafe03186a0e4699150a7fed9b1e73240996d0d2f0e5f70f3fdf471/aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99", size = 436081, upload-time = "2024-12-18T21:20:04.557Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.11.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiohappyeyeballs", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiosignal", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "attrs", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "frozenlist", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "multidict", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "propcache", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d9/1c4721d143e14af753f2bf5e3b681883e1f24b592c0482df6fa6e33597fa/aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8", size = 7676826, upload-time = "2025-04-02T02:17:44.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/38/100d01cbc60553743baf0fba658cb125f8ad674a8a771f765cdc155a890d/aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27", size = 704881, upload-time = "2025-04-02T02:16:09.26Z" }, + { url = "https://files.pythonhosted.org/packages/21/ed/b4102bb6245e36591209e29f03fe87e7956e54cb604ee12e20f7eb47f994/aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713", size = 464564, upload-time = "2025-04-02T02:16:10.781Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e1/a9ab6c47b62ecee080eeb33acd5352b40ecad08fb2d0779bcc6739271745/aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb", size = 456548, upload-time = "2025-04-02T02:16:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/80/ad/216c6f71bdff2becce6c8776f0aa32cb0fa5d83008d13b49c3208d2e4016/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321", size = 1691749, upload-time = "2025-04-02T02:16:14.304Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ea/7df7bcd3f4e734301605f686ffc87993f2d51b7acb6bcc9b980af223f297/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e", size = 1736874, upload-time = "2025-04-02T02:16:16.538Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/c7724b9c87a29b7cfd1202ec6446bae8524a751473d25e2ff438bc9a02bf/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c", size = 1786885, upload-time = "2025-04-02T02:16:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/f61f8492fa6569fa87927ad35a40c159408862f7e8e70deaaead349e2fba/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce", size = 1698059, upload-time = "2025-04-02T02:16:20.234Z" }, + { url = "https://files.pythonhosted.org/packages/ce/be/7097cf860a9ce8bbb0e8960704e12869e111abcd3fbd245153373079ccec/aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e", size = 1626527, upload-time = "2025-04-02T02:16:22.092Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1d/aaa841c340e8c143a8d53a1f644c2a2961c58cfa26e7b398d6bf75cf5d23/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b", size = 1644036, upload-time = "2025-04-02T02:16:23.707Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/59d870f76e9345e2b149f158074e78db457985c2b4da713038d9da3020a8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540", size = 1685270, upload-time = "2025-04-02T02:16:25.874Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b1/c6686948d4c79c3745595efc469a9f8a43cab3c7efc0b5991be65d9e8cb8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b", size = 1650852, upload-time = "2025-04-02T02:16:27.556Z" }, + { url = "https://files.pythonhosted.org/packages/fe/94/3e42a6916fd3441721941e0f1b8438e1ce2a4c49af0e28e0d3c950c9b3c9/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e", size = 1704481, upload-time = "2025-04-02T02:16:29.573Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6d/6ab5854ff59b27075c7a8c610597d2b6c38945f9a1284ee8758bc3720ff6/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c", size = 1735370, upload-time = "2025-04-02T02:16:31.191Z" }, + { url = "https://files.pythonhosted.org/packages/73/2a/08a68eec3c99a6659067d271d7553e4d490a0828d588e1daa3970dc2b771/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71", size = 1697619, upload-time = "2025-04-02T02:16:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/fea8dbbfb0cd68fbb56f0ae913270a79422d9a41da442a624febf72d2aaf/aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2", size = 411710, upload-time = "2025-04-02T02:16:34.525Z" }, + { url = "https://files.pythonhosted.org/packages/33/fb/41cde15fbe51365024550bf77b95a4fc84ef41365705c946da0421f0e1e0/aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682", size = 438012, upload-time = "2025-04-02T02:16:36.103Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/7c712b2d9fb4d5e5fd6d12f9ab76e52baddfee71e3c8203ca7a7559d7f51/aiohttp-3.11.16-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3814760a1a700f3cfd2f977249f1032301d0a12c92aba74605cfa6ce9f78489", size = 698005, upload-time = "2025-04-02T02:16:37.923Z" }, + { url = "https://files.pythonhosted.org/packages/51/3e/61057814f7247666d43ac538abcd6335b022869ade2602dab9bf33f607d2/aiohttp-3.11.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b751a6306f330801665ae69270a8a3993654a85569b3469662efaad6cf5cc50", size = 461106, upload-time = "2025-04-02T02:16:39.961Z" }, + { url = "https://files.pythonhosted.org/packages/4f/85/6b79fb0ea6e913d596d5b949edc2402b20803f51b1a59e1bbc5bb7ba7569/aiohttp-3.11.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad497f38a0d6c329cb621774788583ee12321863cd4bd9feee1effd60f2ad133", size = 453394, upload-time = "2025-04-02T02:16:41.562Z" }, + { url = "https://files.pythonhosted.org/packages/4b/04/e1bb3fcfbd2c26753932c759593a32299aff8625eaa0bf8ff7d9c0c34a36/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca37057625693d097543bd88076ceebeb248291df9d6ca8481349efc0b05dcd0", size = 1666643, upload-time = "2025-04-02T02:16:43.62Z" }, + { url = "https://files.pythonhosted.org/packages/0e/27/97bc0fdd1f439b8f060beb3ba8fb47b908dc170280090801158381ad7942/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5abcbba9f4b463a45c8ca8b7720891200658f6f46894f79517e6cd11f3405ca", size = 1721948, upload-time = "2025-04-02T02:16:45.617Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4f/bc4c5119e75c05ef15c5670ef1563bbe25d4ed4893b76c57b0184d815e8b/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f420bfe862fb357a6d76f2065447ef6f484bc489292ac91e29bc65d2d7a2c84d", size = 1774454, upload-time = "2025-04-02T02:16:48.562Z" }, + { url = "https://files.pythonhosted.org/packages/73/5b/54b42b2150bb26fdf795464aa55ceb1a49c85f84e98e6896d211eabc6670/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ede86453a6cf2d6ce40ef0ca15481677a66950e73b0a788917916f7e35a0bb", size = 1677785, upload-time = "2025-04-02T02:16:50.367Z" }, + { url = "https://files.pythonhosted.org/packages/10/ee/a0fe68916d3f82eae199b8535624cf07a9c0a0958c7a76e56dd21140487a/aiohttp-3.11.16-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fdec0213244c39973674ca2a7f5435bf74369e7d4e104d6c7473c81c9bcc8c4", size = 1608456, upload-time = "2025-04-02T02:16:52.158Z" }, + { url = "https://files.pythonhosted.org/packages/8b/48/83afd779242b7cf7e1ceed2ff624a86d3221e17798061cf9a79e0b246077/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72b1b03fb4655c1960403c131740755ec19c5898c82abd3961c364c2afd59fe7", size = 1622424, upload-time = "2025-04-02T02:16:54.386Z" }, + { url = "https://files.pythonhosted.org/packages/6f/27/452f1d5fca1f516f9f731539b7f5faa9e9d3bf8a3a6c3cd7c4b031f20cbd/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:780df0d837276276226a1ff803f8d0fa5f8996c479aeef52eb040179f3156cbd", size = 1660943, upload-time = "2025-04-02T02:16:56.887Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e1/5c7d63143b8d00c83b958b9e78e7048c4a69903c760c1e329bf02bac57a1/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ecdb8173e6c7aa09eee342ac62e193e6904923bd232e76b4157ac0bfa670609f", size = 1622797, upload-time = "2025-04-02T02:16:58.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9e/2ac29cca2746ee8e449e73cd2fcb3d454467393ec03a269d50e49af743f1/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6db7458ab89c7d80bc1f4e930cc9df6edee2200127cfa6f6e080cf619eddfbd", size = 1687162, upload-time = "2025-04-02T02:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6b/eaa6768e02edebaf37d77f4ffb74dd55f5cbcbb6a0dbf798ccec7b0ac23b/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2540ddc83cc724b13d1838026f6a5ad178510953302a49e6d647f6e1de82bc34", size = 1718518, upload-time = "2025-04-02T02:17:03.388Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/dda87cbad29472a51fa058d6d8257dfce168289adaeb358b86bd93af3b20/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b4e6db8dc4879015b9955778cfb9881897339c8fab7b3676f8433f849425913", size = 1675254, upload-time = "2025-04-02T02:17:05.579Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/d2fb08c614df401d92c12fcbc60e6e879608d5e8909ef75c5ad8d4ad8aa7/aiohttp-3.11.16-cp313-cp313-win32.whl", hash = "sha256:493910ceb2764f792db4dc6e8e4b375dae1b08f72e18e8f10f18b34ca17d0979", size = 410698, upload-time = "2025-04-02T02:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ed/853e36d5a33c24544cfa46585895547de152dfef0b5c79fa675f6e4b7b87/aiohttp-3.11.16-cp313-cp313-win_amd64.whl", hash = "sha256:42864e70a248f5f6a49fdaf417d9bc62d6e4d8ee9695b24c5916cb4bb666c802", size = 436395, upload-time = "2025-04-02T02:17:09.566Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohappyeyeballs", marker = "python_full_version >= '3.13.2'" }, + { name = "aiosignal", marker = "python_full_version >= '3.13.2'" }, + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "frozenlist", marker = "python_full_version >= '3.13.2'" }, + { name = "multidict", marker = "python_full_version >= '3.13.2'" }, + { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiohttp-asyncmdnsresolver" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiodns", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiodns", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "zeroconf", version = "0.146.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "zeroconf", version = "0.148.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/83/09fb97705e7308f94197a09b486669696ea20f28074c14b5811a38bdedc3/aiohttp_asyncmdnsresolver-0.1.1.tar.gz", hash = "sha256:8c65d4b08b42c8a260717a2766bd5967a1d437cee852a9b21f3928b5171a7c81", size = 36129, upload-time = "2025-02-14T14:46:44.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/d1/4f61508a43de82bb5c60cede3bb89cc57c5e8af7978d93ca03ad60b99368/aiohttp_asyncmdnsresolver-0.1.1-py3-none-any.whl", hash = "sha256:d04ded993e9f0e07c07a1bc687cde447d9d32e05bcf55ecbf94f63b33dcab93e", size = 13582, upload-time = "2025-02-14T14:46:41.985Z" }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/9e/6cdce7c3f346d8fd487adf68761728ad8cd5fbc296a7b07b92518350d31f/aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d", size = 35966, upload-time = "2018-03-06T15:45:42.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/e7/e436a0c0eb5127d8b491a9b83ecd2391c6ff7dcd5548dfaec2080a2340fd/aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e", size = 27564, upload-time = "2018-03-06T15:45:42.034Z" }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/d89e846a5444b3d5eb8985a6ddb0daef3774928e1bfbce8e84ec97b0ffa7/aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403", size = 38626, upload-time = "2025-03-31T14:16:20.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3b/40a68de458904bcc143622015fff2352b6461cd92fd66d3527bf1c6f5716/aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d", size = 25231, upload-time = "2025-03-31T14:16:18.478Z" }, +] + +[[package]] +name = "aiohttp-fast-zlib" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/af/fb60f9f5f7c619478346c456262c491493e1c73f3e1681ce73cdd204ef9f/aiohttp_fast_zlib-0.2.0.tar.gz", hash = "sha256:e2e6c27a7ffc825cdd50d6f80e302ebbc025b43c876c00f01dc2ae759905dce8", size = 8671, upload-time = "2024-11-14T15:45:15.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/54/35b33e95e01878b3252e686d050502d04dd4cc3192b4ee13cd300ab2a1aa/aiohttp_fast_zlib-0.2.0-py3-none-any.whl", hash = "sha256:ff50de72e95da3d1b7e6dd6fd64a3aedf743f488ad9202a8fde3baccf0fa1161", size = 8422, upload-time = "2024-11-14T15:45:14.089Z" }, +] + +[[package]] +name = "aiohttp-fast-zlib" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/73/c93543264f745202a6fe78ad8ddb7c13a9d3e3ea47cde26501d683bd46a4/aiohttp_fast_zlib-0.2.3.tar.gz", hash = "sha256:d7e34621f2ac47155d9ad5d78f15ffb066a4ee849cb3d55df0077395ab4b3eff", size = 8591, upload-time = "2025-02-22T17:52:51.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/55/9aebf9f5dac1a34bb0a4f300d2ec4692f86df44e458f3061a659dec2b98f/aiohttp_fast_zlib-0.2.3-py3-none-any.whl", hash = "sha256:41a93670f88042faff3ebbd039fd2fc37a0c956193c20eb758be45b1655a7e04", size = 8421, upload-time = "2025-02-22T17:52:49.971Z" }, +] + +[[package]] +name = "aiohttp-fast-zlib" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/a6/982f3a013b42e914a2420631afcaecb729c49525cc6cc58e15d27ee4cb4b/aiohttp_fast_zlib-0.3.0.tar.gz", hash = "sha256:963a09de571b67fa0ef9cb44c5a32ede5cb1a51bc79fc21181b1cddd56b58b28", size = 8770, upload-time = "2025-06-07T12:41:49.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/11/ea9ecbcd6cf68c5de690fd39b66341405ab091aa0c3598277e687aa65901/aiohttp_fast_zlib-0.3.0-py3-none-any.whl", hash = "sha256:d4cb20760a3e1137c93cb42c13871cbc9cd1fdc069352f2712cd650d6c0e537e", size = 8615, upload-time = "2025-06-07T12:41:47.454Z" }, +] + +[[package]] +name = "aiooui" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b7/ad0f86010bbabc4e556e98dd2921a923677188223cc524432695966f14fa/aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8", size = 369276, upload-time = "2025-01-19T00:12:44.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/fa/b1310457adbea7adb84d2c144159f3b41341c40c80df3c10ce6b266874b3/aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5", size = 367404, upload-time = "2025-01-19T00:12:42.57Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiozoneinfo" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "tzdata", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/05/fe5c1f5f72ca7fbb88b05eb9d47b90bfd898d494a1099e1ec1c3d0e5d44b/aiozoneinfo-0.2.1.tar.gz", hash = "sha256:457e2c665a2c7e093119efb87cc5e0da29e6f59aac504a544bec822c5be1cb6b", size = 8472, upload-time = "2024-06-24T12:30:11.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/a1/7f94ff464f01a65d30ebdb00b815b2cf9613b3c4314828d2aad576b0ff21/aiozoneinfo-0.2.1-py3-none-any.whl", hash = "sha256:04579f855f030cd0edb1758659c513142ef1aaf7fcc97b59eb2262ed0c453cce", size = 8011, upload-time = "2024-06-24T12:30:10.017Z" }, +] + +[[package]] +name = "aiozoneinfo" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "tzdata", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/00/e437a179ab78ed24780ded10bbb5d7e10832c07f62eab1d44ee2f335c95c/aiozoneinfo-0.2.3.tar.gz", hash = "sha256:987ce2a7d5141f3f4c2e9d50606310d0bf60d688ad9f087aa7267433ba85fff3", size = 8381, upload-time = "2025-02-04T19:32:06.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/a4/99e13bb4006999de2a4d63cee7497c3eb7f616b0aefc660c4c316179af3a/aiozoneinfo-0.2.3-py3-none-any.whl", hash = "sha256:5423f0354c9eed982e3f1c35edeeef1458d4cc6a10f106616891a089a8455661", size = 8009, upload-time = "2025-02-04T19:32:04.74Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "annotatedyaml" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "propcache", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/b6/e24fb814108d0a708cc8b26d67e61d5fee0735373dcaa8cd61cb140caf02/annotatedyaml-0.4.5.tar.gz", hash = "sha256:e251929cd7e741fa2e9ece13e24e29bb8f1b5c6ca3a9ef7292a66a3ae8b9390f", size = 15321, upload-time = "2025-03-22T17:50:37.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/1c/dc1159b82db421cd71ce09f877781aa8c3cc623c8d094b3532b5519ff149/annotatedyaml-0.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf113cfe1c7a85f0e61ea39a6d2f3fdcf12fe528e7d563f8eff4a89afdfaa7a1", size = 62194, upload-time = "2025-03-22T17:54:04.903Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/9bb9b9dabb8468627ae42384b666687b3b7541cf7e080d50d129bd93c48b/annotatedyaml-0.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f64ebe3a81d7ddc6bc261feca2092905043e493da369bd93ad6aab58399a0a", size = 61480, upload-time = "2025-03-22T17:54:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7e/35038f9eeea279c31cbb7181800db1f6e00f42e83b2dc30bc7bf9d686cb4/annotatedyaml-0.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480670331a3f906ddc760f21b302984ace4c674cfa3e6c48fdf76841dd0cdc1e", size = 73749, upload-time = "2025-03-22T17:54:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/c7/09/5154fef033fe9595f603f2fa55b1a55057a80e105bfb17ea9557ca68aa44/annotatedyaml-0.4.5-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:981b1dc193163d17757a8b8016b048e6d315de93055671a989f327276e1acd30", size = 79159, upload-time = "2025-03-22T17:54:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/02/f9/3c524ac4ba2a977640e221154deb6276a3b7463fa2525b5a0b7edd2d9ccc/annotatedyaml-0.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acea1969910a3a956fdb818734bfb0e5f7a377c18d1080846372c281d930dd9", size = 76137, upload-time = "2025-03-22T17:54:10.733Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4b/c066e26ffe6d472c4abf6cfdaf2e1760e2959a94a873559622407d953cf0/annotatedyaml-0.4.5-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:649a256ae447e97f075943ab6cfc15d582490f994ffc5523225ebdeaffd24164", size = 70327, upload-time = "2025-03-22T17:54:11.881Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/b521a82c88917b393aba710330a33b5083e2ce938917e21a312db8c7be3c/annotatedyaml-0.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a91b433c5250d3a42bbf5a72e38e2cd04f1fd48c82eae7f6dec3ecb3b4cc121e", size = 74790, upload-time = "2025-03-22T17:54:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/24/ec/1c17c3ea482acf68ad4490c6b6b70c43097ebd3b96ae9ea652dd38282968/annotatedyaml-0.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5a0ecba3df7c5fd4f2256669b4d375a08e7e48db1092f44e98fe35505121f1ea", size = 71456, upload-time = "2025-03-22T17:54:14.529Z" }, + { url = "https://files.pythonhosted.org/packages/5c/40/b7e6e9fe71456eec5502e24cf943d7dd33c116dc6858319ccc9135be1fd5/annotatedyaml-0.4.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2b0c706df48c8b96250b1f18728f815f3c7bdb6ad86310f3bc433cd21ca063ce", size = 82390, upload-time = "2025-03-22T17:54:15.754Z" }, + { url = "https://files.pythonhosted.org/packages/74/f8/d548281bbd2d3aa5fa232007f02ea165c145e72666a81b277ac2af698430/annotatedyaml-0.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ce311f389a6f149f0d7e76ba789ab5c543ed23c83fcd16e6f05e75934039f75", size = 78542, upload-time = "2025-03-22T17:54:16.86Z" }, + { url = "https://files.pythonhosted.org/packages/6e/81/8dda9d9700ac886ce4b5eec7203ebdf7c690ff178f81fe64eb9921ad1f3a/annotatedyaml-0.4.5-cp312-cp312-win32.whl", hash = "sha256:9ce177a6a1c751ac08beaa8b9e449d4b3ef759ab23ad88847970d55b625f58d9", size = 58103, upload-time = "2025-03-22T17:54:18.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/76/beba5536e610b5a06bf209379650bdb2767821ca757fc1b331f3ac043e00/annotatedyaml-0.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:6ca77b171137f8a2939c3fc4eae70d26fcefa4fa7e7a839d84f0bb1f4b979b4a", size = 63133, upload-time = "2025-03-22T17:54:19.225Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/262c3ebf8266595975f810998c6a82633eddc373764a927d919d33f3d3ce/annotatedyaml-0.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:971293ef07be457554ee97bcd6f7b0cb13df1c8d8ab1a2554880d78d9dc5d27a", size = 60968, upload-time = "2025-03-22T17:54:21.021Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/fd26ed4aa50c8a6670ae0909f8075262d50fa959eeff2185074f00cdc8aa/annotatedyaml-0.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8100a47d37b766f850bf8659fc6f973b14633f5d4a1957195af0a0e36449ffbe", size = 60414, upload-time = "2025-03-22T17:54:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/f5/96/0c52b99fb8cf39b585fca4a4656b829c1b0eec38943eef40c97044ed114b/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51a053d426ce1d1d7a783cea5185f5f5b3a4c3c2f269cd9cd2dfb07bd6671ee0", size = 72011, upload-time = "2025-03-22T17:54:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a6/7a77d92db7df4f491f5a90218c1d327bf32d37bfa18c99d3a9588d219d0f/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:2ca45e75b3091680553f21dca3f776075fb029f1a8499de61801cb0712f29de5", size = 77028, upload-time = "2025-03-22T17:54:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a0/bd6dc6eab687ab98a182cdf5fadb8a9456b6dab25cb1260857f324abcda0/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7354a88931bc73e05d4e1b24dd6c26b8618ea6412553b4c8084a7481932482bc", size = 74145, upload-time = "2025-03-22T17:54:25.988Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e1/ad12626d5096835d583455a02165f1d0cabdfd1796f5b07854f86fc61083/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75c3a91402dcfcf45967dcbbcd3ee151222c4881202be87f00c17cf0d627caae", size = 68149, upload-time = "2025-03-22T17:54:27.414Z" }, + { url = "https://files.pythonhosted.org/packages/25/48/a871c4c3c6e45b002a6f04a17b758e8db0120f79b43a494b298dff43ebfa/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:3d76ca28122fd063f27f298aa76f074f4bb8dd84501cf74cfec51931f0ed7ae0", size = 74388, upload-time = "2025-03-22T17:50:36.089Z" }, + { url = "https://files.pythonhosted.org/packages/03/b2/7ff9c2c479883a7f583ba5f0c380d937caf065eb994cbf671a656c6847b7/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea47e128d2a8f549fad47b4a579f9d0a0e11733130419cb5071eb242caf5e66e", size = 73542, upload-time = "2025-03-22T17:54:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/a9cb90c65717226cf7eb3f5f0808befb9c80e05641c8857e305a02bc6393/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b0b21600607faea68a6a8e99fab7671119a672c454b153aec3fc3410347650ee", size = 69904, upload-time = "2025-03-22T17:54:29.694Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/a8d04e2cf8d743c5364af8a41dd2110a4fee70489142114f4f99a87124f7/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:233864f23f89a43457759a526a01cccc9f60409b08070b806b5122ee5cc4cb9c", size = 80000, upload-time = "2025-03-22T17:54:30.826Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/24c949543c2378390856912ccf66d2b82b06ab68ec43ff8da48dd2e072e3/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:35e0be8088e81b60be70da401da23db5420795e1e3ba7451d232a02dd9a81f30", size = 76820, upload-time = "2025-03-22T17:54:31.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ca/8c85cf1f87234cf99a44ac2c9859e7446015932bcc205d06a95b0197739a/annotatedyaml-0.4.5-cp313-cp313-win32.whl", hash = "sha256:967fddfa8af4864f09190bde7905f05ab5bdd5f32fcca672e86033a39b0afbe8", size = 57338, upload-time = "2025-03-22T17:54:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/78/57/2cb75df5189ee009278895afa77941ba701d4fc72f5b6ce44b6f97295159/annotatedyaml-0.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:f53f9f8e4ae92081653337be56265cf7085a5bc216f5e15c4531b36de5cba365", size = 62040, upload-time = "2025-03-22T17:54:34.617Z" }, +] + +[[package]] +name = "annotatedyaml" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/4b/973067092ee348e331d125acd60c45245f11663373c219650814b43d0025/annotatedyaml-1.0.2.tar.gz", hash = "sha256:f9a49952994ef1952ca17d27bb6478342eb1189d2c28e4c0ddbbb32065471fb0", size = 15366, upload-time = "2025-10-04T14:36:26.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/0f/4482333d679e7174b74655d17b3969ab3754ae4d581752bac1002fe316c0/annotatedyaml-1.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:359a964daf3fccbb4818e6f08478d2e6712a2417a261cbd6472826ce5e8f1503", size = 58944, upload-time = "2025-10-04T14:41:49.516Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/ab5f9c67dd13b54e0100e8a4cdfd371c45ecfea1ba776a971d7b728087fe/annotatedyaml-1.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6d2dcf741bdedf893d04f958f3f1ad0b5b12b1fe27746f9918a24e2f347eac1", size = 60030, upload-time = "2025-10-04T14:41:50.859Z" }, + { url = "https://files.pythonhosted.org/packages/47/3f/785a22acee2fc16049ac00a9f708f11b1354e40578ae4e5076b989dc5f82/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:139533a395301f219bfd4ba2265b7a8c55cb4931aac7f730a8ff204a465e76d3", size = 70701, upload-time = "2025-10-04T14:41:52.091Z" }, + { url = "https://files.pythonhosted.org/packages/e8/93/712a6170903b6dd2a30aa59f76e39569f260fde38e9277d3a40ddbdf53f4/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:623f571e0d3819a3cbadce592a2c691274ccd46b09ad770f9271201d7476ea88", size = 65011, upload-time = "2025-10-04T14:41:53.35Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/5f3e9b72d871d67f89d70166d0e2affdbcf0cf87cd20276c84b6db968a52/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75927ec682f188efe25309e259c115e3976b702900ce1be93a971b328c87a10a", size = 71370, upload-time = "2025-10-04T14:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/a4/32/143d643d9f5d21f1b66666713d24adf68677790fb61700bc727078bdef2c/annotatedyaml-1.0.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:106ac5eaa022df4dfa42e307932aa2a197a19151de3bb41e98840cfc7f1745e1", size = 69434, upload-time = "2025-10-04T14:36:24.962Z" }, + { url = "https://files.pythonhosted.org/packages/83/b0/1ce75b81e42e033914f94159f633b923e57c507690eb0bba966475cab9a1/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:139d2626fe8faccba9cc79b4d8dca25a4d59e4a274508612842d78945bddeebe", size = 71333, upload-time = "2025-10-04T14:41:55.836Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/89451246b115dfc5fcfb3b3ca966f9fcdfc647b10a13eb517fa11f2e3ffd/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6f2fb86c18064f0dcfb01e3d1096f0575cdff509a24b748c2994f97eb0b70156", size = 65905, upload-time = "2025-10-04T14:41:57.121Z" }, + { url = "https://files.pythonhosted.org/packages/70/57/7008f39f1af0e0b36668cd9affe8a68846797ee1119ec36daac428ade742/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56d7235147b58d155b4ee93f2a92920b4c0be6a6852dc3fd810c67f6e56f8c15", size = 72181, upload-time = "2025-10-04T14:41:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/18/48/abfed2c0d5ff9d09aff2bb85d5035b56066925826972e0f703f59c2c0cb5/annotatedyaml-1.0.2-cp313-cp313-win32.whl", hash = "sha256:e53c74051a82c4cbd68db1371918a6399650f165579a2bf1f7e0a2ed58300564", size = 56097, upload-time = "2025-10-04T14:41:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/24d0e04d148340950aa26e9e2bea1d4047d6bd3588d0db86d2107d7ca2a8/annotatedyaml-1.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:13ca5d3a325103fd0a0c5a27af05f22118935f3e731e2df26f620ee85b56e85b", size = 60283, upload-time = "2025-10-04T14:41:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/1b/91/0acf5b74926c6964812d9ed752af77531ab4daa06fba1cb668d9006e9e1f/annotatedyaml-1.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c42f385c3f04f425d5948c16afbb94a876da867be276dbf2c2e7436b9a80792d", size = 58962, upload-time = "2025-10-04T14:42:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/71/f6/5dac1ce125984db4cb99d883f234e6a8c0e49358a9136047a490bc2ba51a/annotatedyaml-1.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b5d9d24ba907fd2e905eac69c88e651310c480980a17aa57faf0599ff21f586f", size = 60252, upload-time = "2025-10-04T14:42:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/81dea3e4272927518abb9c96ab299b8c4346c40267740bfb8d6b0cdb317f/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2572b7c3c630dae1dd163d6c6ba847493a7f987437941b32d0ad8354615f358a", size = 71219, upload-time = "2025-10-04T14:42:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/5aa3d7767cb92e8ba34cba582e5b088f42746e6d075f7d387fcdc4e5dd62/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b28fe13eb0014a0dd06c9a292466feed0cd298ab10525ef9a37089df01c7d333", size = 64459, upload-time = "2025-10-04T14:42:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/a0/eb/b29b84eec6d3a1fc3278ff2959388f347e7853a3f82fc5275c591a523835/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:987f73a13f121c775bcdb082214c17f114447fee7dad37db2f86b035893ad58d", size = 71175, upload-time = "2025-10-04T14:42:05.22Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7c/4f4bf854f4b62cade7485a9572773d4440ee535e905f166b441b2d3f19a7/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5cb4ee79c08da2b8f4f24b1775732ca6c497682f3c9b3fd65dee4ea084fc925c", size = 71824, upload-time = "2025-10-04T14:42:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/68/ac/1f903eeccde636723fcf664b372a6ab253b7f13c3de446ff5bce6852d696/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:da065a8c29556219fce1aa81b406e84f73bc2181067658e57428a8b2e662fc1b", size = 65343, upload-time = "2025-10-04T14:42:07.727Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/43e83b50a42ad5c51abf1a335cfc249e182f66542d7c7306ee07397b1956/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ba9937418c1b189b267540b47fa0dc24c148292739d06a6ca31c2ca8482f16", size = 72328, upload-time = "2025-10-04T14:42:08.656Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6a/f5d9c29633c499973f10330af31a8b135a564a4a2e056c32a6ff2c901559/annotatedyaml-1.0.2-cp314-cp314-win32.whl", hash = "sha256:003e16e91b40176dd8fe77d56c6c936106b408b62953e88ce3506e8ba10bf4e1", size = 57286, upload-time = "2025-10-04T14:42:09.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/d8c7464676094658f1caaee6762536ab43867d7153f7c637207c63fc4c97/annotatedyaml-1.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:17e64a7dde47a678db8aa4e934c3ed8da9a52ab1bc6946d12be86f323e6bd8c7", size = 61363, upload-time = "2025-10-04T14:42:10.968Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5d/7e384f4115a7bc113162f7b6eb5d561031e303f840f304b68e3f1b0541a1/annotatedyaml-1.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8698bbbd1d38f8c9ba95a107d7597f5af3f2ba295d1d14227f85b62377998ffc", size = 104776, upload-time = "2025-10-04T14:42:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/77bebdd30118c1e85f11d5a83a3bb5955409bba74d81cfb0f7b551273513/annotatedyaml-1.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cbc661dbc7c5f7ddf69fbf879da6a96745b8cd39ae1338dab3a0aa8eb208367", size = 107716, upload-time = "2025-10-04T14:42:14.151Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/bfc798abb154e398d5210304ba3beff9ad9c7b6ec4574ffb705493b8e2d5/annotatedyaml-1.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db1c3ca021bbd390354037ede5c255657afb2a7544b7cfa0e091b62b888aa462", size = 130361, upload-time = "2025-10-04T14:42:15.494Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, +] + +[[package]] +name = "astral" +version = "2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/c3/76dfe55a68c48a1a6f3d2eeab2793ebffa9db8adfba82774a7e0f5f43980/astral-2.2.tar.gz", hash = "sha256:e41d9967d5c48be421346552f0f4dedad43ff39a83574f5ff2ad32b6627b6fbe", size = 578223, upload-time = "2020-05-20T14:23:17.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/60/7cc241b9c3710ebadddcb323e77dd422c693183aec92449a1cf1fb59e1ba/astral-2.2-py2.py3-none-any.whl", hash = "sha256:b9ef70faf32e81a8ba174d21e8f29dc0b53b409ef035f27e0749ddc13cb5982a", size = 30775, upload-time = "2020-05-20T14:23:14.866Z" }, +] + +[[package]] +name = "async-interrupt" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/5a2d74465037b33ccdaf830e3d9ac008bccdbe4b0657983b90dc89191626/async_interrupt-1.2.0.tar.gz", hash = "sha256:d147559e2478501ad45ea43f52df23b246456715a7cb96e1aebdb4b71aed43d5", size = 8584, upload-time = "2024-08-21T13:23:54.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/f6/5638f86da774d30dae619a8d0d48df24cb17981b43948a8a3ee241b8b695/async_interrupt-1.2.0-py3-none-any.whl", hash = "sha256:a0126e882b9991d1c77839ab53e0e1b9f41f1b3d151a7032243f15011df5e4dc", size = 8898, upload-time = "2024-08-21T13:23:52.816Z" }, +] + +[[package]] +name = "async-interrupt" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/56/79/732a581e3ceb09f938d33ad8ab3419856181d95bb621aa2441a10f281e10/async_interrupt-1.2.2.tar.gz", hash = "sha256:be4331a029b8625777905376a6dc1370984c8c810f30b79703f3ee039d262bf7", size = 8484, upload-time = "2025-02-22T17:15:04.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/77/060b972fa7819fa9eea9a70acf8c7c0c58341a1e300ee5ccb063e757a4a7/async_interrupt-1.2.2-py3-none-any.whl", hash = "sha256:0a8deb884acfb5fe55188a693ae8a4381bbbd2cb6e670dac83869489513eec2c", size = 8907, upload-time = "2025-02-22T17:15:01.971Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "atomicwrites-homeassistant" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/5a/10ff0fd9aa04f78a0b31bb617c8d29796a12bea33f1e48aa54687d635e44/atomicwrites-homeassistant-1.4.1.tar.gz", hash = "sha256:256a672106f16745445228d966240b77b55f46a096d20305901a57aa5d1f4c2f", size = 12223, upload-time = "2022-07-08T20:56:46.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/1b/872dd3b11939edb4c0a27d2569a9b7e77d3b88995a45a331f376e13528c0/atomicwrites_homeassistant-1.4.1-py2.py3-none-any.whl", hash = "sha256:01457de800961db7d5b575f3c92e7fb56e435d88512c366afb0873f4f092bb0d", size = 7128, upload-time = "2022-07-08T20:56:44.186Z" }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678, upload-time = "2024-08-06T14:37:38.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001, upload-time = "2024-08-06T14:37:36.958Z" }, +] + +[[package]] +name = "attrs" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562, upload-time = "2025-01-25T11:30:12.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152, upload-time = "2025-01-25T11:30:10.164Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "audioop-lts" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/3b/69ff8a885e4c1c42014c2765275c4bd91fe7bc9847e9d8543dbcbb09f820/audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387", size = 30204, upload-time = "2024-08-04T21:14:43.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/91/a219253cc6e92db2ebeaf5cf8197f71d995df6f6b16091d1f3ce62cb169d/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a", size = 46252, upload-time = "2024-08-04T21:13:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f6/3cb21e0accd9e112d27cee3b1477cd04dafe88675c54ad8b0d56226c1e0b/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e", size = 27183, upload-time = "2024-08-04T21:13:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7e/f94c8a6a8b2571694375b4cf94d3e5e0f529e8e6ba280fad4d8c70621f27/audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6", size = 26726, upload-time = "2024-08-04T21:14:00.846Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f8/a0e8e7a033b03fae2b16bc5aa48100b461c4f3a8a38af56d5ad579924a3a/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe", size = 80718, upload-time = "2024-08-04T21:14:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ea/a98ebd4ed631c93b8b8f2368862cd8084d75c77a697248c24437c36a6f7e/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a", size = 88326, upload-time = "2024-08-04T21:14:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/33/79/e97a9f9daac0982aa92db1199339bd393594d9a4196ad95ae088635a105f/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300", size = 80539, upload-time = "2024-08-04T21:14:04.679Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d3/1051d80e6f2d6f4773f90c07e73743a1e19fcd31af58ff4e8ef0375d3a80/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059", size = 78577, upload-time = "2024-08-04T21:14:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/54f4c58bae8dc8c64a75071c7e98e105ddaca35449376fcb0180f6e3c9df/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e", size = 82074, upload-time = "2024-08-04T21:14:09.99Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/2e78daa7cebbea57e72c0e1927413be4db675548a537cfba6a19040d52fa/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48", size = 84210, upload-time = "2024-08-04T21:14:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/3ff8a74df2ec2fa6d2ae06ac86e4a27d6412dbb7d0e0d41024222744c7e0/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281", size = 85664, upload-time = "2024-08-04T21:14:12.394Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/21cc4e5878f6edbc8e54be4c108d7cb9cb6202313cfe98e4ece6064580dd/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959", size = 93255, upload-time = "2024-08-04T21:14:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/7f7418c362a899ac3b0bf13b1fde2d4ffccfdeb6a859abd26f2d142a1d58/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47", size = 87760, upload-time = "2024-08-04T21:14:14.74Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/577a8be87dc7dd2ba568895045cee7d32e81d85a7e44a29000fe02c4d9d4/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77", size = 84992, upload-time = "2024-08-04T21:14:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/ef/9a/4699b0c4fcf89936d2bfb5425f55f1a8b86dff4237cfcc104946c9cd9858/audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3", size = 26059, upload-time = "2024-08-04T21:14:20.438Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1c/1f88e9c5dd4785a547ce5fd1eb83fff832c00cc0e15c04c1119b02582d06/audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4", size = 30412, upload-time = "2024-08-04T21:14:21.342Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/c123fd29d89a6402ad261516f848437472ccc602abb59bba522af45e281b/audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0", size = 23578, upload-time = "2024-08-04T21:14:22.193Z" }, + { url = "https://files.pythonhosted.org/packages/7a/99/bb664a99561fd4266687e5cb8965e6ec31ba4ff7002c3fce3dc5ef2709db/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455", size = 46827, upload-time = "2024-08-04T21:14:23.034Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/f664171e867e0768ab982715e744430cf323f1282eb2e11ebfb6ee4c4551/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f", size = 27479, upload-time = "2024-08-04T21:14:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0d/2a79231ff54eb20e83b47e7610462ad6a2bea4e113fae5aa91c6547e7764/audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf", size = 27056, upload-time = "2024-08-04T21:14:28.061Z" }, + { url = "https://files.pythonhosted.org/packages/86/46/342471398283bb0634f5a6df947806a423ba74b2e29e250c7ec0e3720e4f/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab", size = 87802, upload-time = "2024-08-04T21:14:29.586Z" }, + { url = "https://files.pythonhosted.org/packages/56/44/7a85b08d4ed55517634ff19ddfbd0af05bf8bfd39a204e4445cd0e6f0cc9/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6", size = 95016, upload-time = "2024-08-04T21:14:30.481Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2a/45edbca97ea9ee9e6bbbdb8d25613a36e16a4d1e14ae01557392f15cc8d3/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543", size = 87394, upload-time = "2024-08-04T21:14:31.883Z" }, + { url = "https://files.pythonhosted.org/packages/14/ae/832bcbbef2c510629593bf46739374174606e25ac7d106b08d396b74c964/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074", size = 84874, upload-time = "2024-08-04T21:14:32.751Z" }, + { url = "https://files.pythonhosted.org/packages/26/1c/8023c3490798ed2f90dfe58ec3b26d7520a243ae9c0fc751ed3c9d8dbb69/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78", size = 88698, upload-time = "2024-08-04T21:14:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/5379d953d4918278b1f04a5a64b2c112bd7aae8f81021009da0dcb77173c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb", size = 90401, upload-time = "2024-08-04T21:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/99/6e/3c45d316705ab1aec2e69543a5b5e458d0d112a93d08994347fafef03d50/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2", size = 91864, upload-time = "2024-08-04T21:14:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/08/58/6a371d8fed4f34debdb532c0b00942a84ebf3e7ad368e5edc26931d0e251/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a", size = 98796, upload-time = "2024-08-04T21:14:37.185Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/d637aa35497e0034ff846fd3330d1db26bc6fd9dd79c406e1341188b06a2/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a", size = 94116, upload-time = "2024-08-04T21:14:38.145Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/7afc2abf46bbcf525a6ebc0305d85ab08dc2d1e2da72c48dbb35eee5b62c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63", size = 91520, upload-time = "2024-08-04T21:14:39.128Z" }, + { url = "https://files.pythonhosted.org/packages/65/6d/42d40da100be1afb661fd77c2b1c0dfab08af1540df57533621aea3db52a/audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509", size = 26482, upload-time = "2024-08-04T21:14:40.269Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/f08494dca79f65212f5b273aecc5a2f96691bf3307cac29acfcf84300c01/audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7", size = 30780, upload-time = "2024-08-04T21:14:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918, upload-time = "2024-08-04T21:14:42.803Z" }, +] + +[[package]] +name = "awesomeversion" +version = "24.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/e9/1baaf8619a3d66b467ba105976897e67b36dbad93b619753768357dbd475/awesomeversion-24.6.0.tar.gz", hash = "sha256:aee7ccbaed6f8d84e0f0364080c7734a0166d77ea6ccfcc4900b38917f1efc71", size = 11997, upload-time = "2024-06-24T11:09:27.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/a5/258ffce7048e8be24c6f402bcbf5d1b3933d5d63421d000a55e74248481b/awesomeversion-24.6.0-py3-none-any.whl", hash = "sha256:6768415b8954b379a25cebf21ed4f682cab10aebf3f82a6640aaaa15ec6821f2", size = 14716, upload-time = "2024-06-24T11:09:26.133Z" }, +] + +[[package]] +name = "awesomeversion" +version = "25.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/3a/c97ef69b8209aa9d7209b143345fe49c1e20126f62a775038ab6dcd78fd5/awesomeversion-25.8.0.tar.gz", hash = "sha256:e6cd08c90292a11f30b8de401863dcde7bc66a671d8173f9066ebd15d9310453", size = 70873, upload-time = "2025-08-03T08:54:07.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/b3/c6be343010721bfdd3058b708eb4868fa1a207534a3b6c80de74d35fb568/awesomeversion-25.8.0-py3-none-any.whl", hash = "sha256:1c314683abfcc3e26c62af9e609b585bbcbf2ec19568df2f60ff1034fb1dae28", size = 15919, upload-time = "2025-08-03T08:54:06.265Z" }, +] + +[[package]] +name = "bcrypt" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/7e/d95e7d96d4828e965891af92e43b52a4cd3395dc1c1ef4ee62748d0471d0/bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", size = 24294, upload-time = "2024-07-22T18:09:10.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/81/4e8f5bc0cd947e91fb720e1737371922854da47a94bc9630454e7b2845f8/bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", size = 471568, upload-time = "2024-07-22T18:08:55.603Z" }, + { url = "https://files.pythonhosted.org/packages/05/d2/1be1e16aedec04bcf8d0156e01b987d16a2063d38e64c3f28030a3427d61/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", size = 277372, upload-time = "2024-07-22T18:08:51.446Z" }, + { url = "https://files.pythonhosted.org/packages/e3/96/7a654027638ad9b7589effb6db77eb63eba64319dfeaf9c0f4ca953e5f76/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", size = 273488, upload-time = "2024-07-22T18:09:02.005Z" }, + { url = "https://files.pythonhosted.org/packages/46/54/dc7b58abeb4a3d95bab653405935e27ba32f21b812d8ff38f271fb6f7f55/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", size = 277759, upload-time = "2024-07-22T18:08:50.017Z" }, + { url = "https://files.pythonhosted.org/packages/ac/be/da233c5f11fce3f8adec05e8e532b299b64833cc962f49331cdd0e614fa9/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", size = 273796, upload-time = "2024-07-22T18:09:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b8/8b4add88d55a263cf1c6b8cf66c735280954a04223fcd2880120cc767ac3/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", size = 311082, upload-time = "2024-07-22T18:08:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/7b/76/2aa660679abbdc7f8ee961552e4bb6415a81b303e55e9374533f22770203/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", size = 305912, upload-time = "2024-07-22T18:08:40.049Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/2af7c45034aba6002d4f2b728c1a385676b4eab7d764410e34fd768009f2/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", size = 325185, upload-time = "2024-07-22T18:08:41.833Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5d/6843443ce4ab3af40bddb6c7c085ed4a8418b3396f7a17e60e6d9888416c/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", size = 335188, upload-time = "2024-07-22T18:08:29.25Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4c/ff8ca83d816052fba36def1d24e97d9a85739b9bbf428c0d0ecd296a07c8/bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", size = 156481, upload-time = "2024-07-22T18:09:00.303Z" }, + { url = "https://files.pythonhosted.org/packages/65/f1/e09626c88a56cda488810fb29d5035f1662873777ed337880856b9d204ae/bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", size = 151336, upload-time = "2024-07-22T18:08:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/96/86/8c6a84daed4dd878fbab094400c9174c43d9b838ace077a2f8ee8bc3ae12/bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", size = 472414, upload-time = "2024-07-22T18:08:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/e394515f4e23c17662e5aeb4d1859b11dc651be01a3bd03c2e919a155901/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", size = 277599, upload-time = "2024-07-22T18:08:53.974Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3b/ad784eac415937c53da48983756105d267b91e56aa53ba8a1b2014b8d930/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", size = 273491, upload-time = "2024-07-22T18:08:45.231Z" }, + { url = "https://files.pythonhosted.org/packages/cc/14/b9ff8e0218bee95e517b70e91130effb4511e8827ac1ab00b4e30943a3f6/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", size = 277934, upload-time = "2024-07-22T18:09:09.189Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/31938bb697600a04864246acde4918c4190a938f891fd11883eaaf41327a/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", size = 273804, upload-time = "2024-07-22T18:09:04.618Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c3/dae866739989e3f04ae304e1201932571708cb292a28b2f1b93283e2dcd8/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", size = 311275, upload-time = "2024-07-22T18:08:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/019bc2c63c6125ddf0483ee7d914a405860327767d437913942b476e9c9b/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", size = 306355, upload-time = "2024-07-22T18:09:06.053Z" }, + { url = "https://files.pythonhosted.org/packages/75/fe/9e137727f122bbe29771d56afbf4e0dbc85968caa8957806f86404a5bfe1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", size = 325381, upload-time = "2024-07-22T18:08:33.904Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d4/586b9c18a327561ea4cd336ff4586cca1a7aa0f5ee04e23a8a8bb9ca64f1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", size = 335685, upload-time = "2024-07-22T18:08:56.897Z" }, + { url = "https://files.pythonhosted.org/packages/24/55/1a7127faf4576138bb278b91e9c75307490178979d69c8e6e273f74b974f/bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", size = 155857, upload-time = "2024-07-22T18:08:30.827Z" }, + { url = "https://files.pythonhosted.org/packages/1c/2a/c74052e54162ec639266d91539cca7cbf3d1d3b8b36afbfeaee0ea6a1702/bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", size = 151717, upload-time = "2024-07-22T18:08:52.781Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, +] + +[[package]] +name = "bleak" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corebluetooth", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-libdispatch", marker = "sys_platform == 'darwin'" }, + { name = "winrt-runtime", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth-advertisement", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth-genericattributeprofile", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-enumeration", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-radios", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-foundation", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-foundation-collections", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-storage-streams", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/8a/5acbd4da6a5a301fab56ff6d6e9e6b6945e6e4a2d1d213898c21b1d3a19b/bleak-2.1.1.tar.gz", hash = "sha256:4600cc5852f2392ce886547e127623f188e689489c5946d422172adf80635cf9", size = 120634, upload-time = "2025-12-31T20:43:28.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fe/22aec895f040c1e457d6e6fcc79286fbb17d54602600ab2a58837bec7be1/bleak-2.1.1-py3-none-any.whl", hash = "sha256:61ac1925073b580c896a92a8c404088c5e5ec9dc3c5bd6fc17554a15779d83de", size = 141258, upload-time = "2025-12-31T20:43:27.302Z" }, +] + +[[package]] +name = "bleak-retry-connector" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bleak" }, + { name = "bluetooth-adapters", marker = "sys_platform == 'linux'" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/e1/28c82e31f4f1c555c029598225b8d895bb3ccdc6c99b0974a1322f9725a9/bleak_retry_connector-4.5.0.tar.gz", hash = "sha256:5db81f8510c63cbea7b85d94bfa2b0fd9a24f0704474a49727a634488623fa17", size = 18648, upload-time = "2026-01-07T18:34:24.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/52/24ef8647f2e0dab293ec68fc58c4f0ad4e00652b631d83835cf32c06d101/bleak_retry_connector-4.5.0-py3-none-any.whl", hash = "sha256:ea63420e8f20117ef04202ea13d5215bffb736720cf865ce9a5aa556ea677afe", size = 18638, upload-time = "2026-01-07T18:34:23.119Z" }, +] + +[[package]] +name = "bluetooth-adapters" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiooui" }, + { name = "bleak" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, + { name = "uart-devices" }, + { name = "usb-devices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/b37a52f5243cf8bd30cc7e25c1128750d129db15a7b2c7ef107ddb7429f9/bluetooth_adapters-2.1.1.tar.gz", hash = "sha256:f289e0f08814f74252a28862f488283680584744430d7eac45820f9c20ba041a", size = 17234, upload-time = "2025-09-12T17:18:48.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/11/8f344d5379df2d31eea73052128136702630f28b5fb55a8d30250c8112e2/bluetooth_adapters-2.1.1-py3-none-any.whl", hash = "sha256:1f93026e530dcb2f4515a92955fa6f85934f928b009a181ee57edc8b4affd25c", size = 20276, upload-time = "2025-09-12T17:18:47.763Z" }, +] + +[[package]] +name = "bluetooth-auto-recovery" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bluetooth-adapters" }, + { name = "btsocket" }, + { name = "pyric" }, + { name = "usb-devices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/8b/1d6f338ced9b47965382c8f1325bf3d22be65e6f838b4e465227c52d333c/bluetooth_auto_recovery-1.5.3.tar.gz", hash = "sha256:0b36aa6be84474fff81c1ce328f016a6553272ac47050b1fa60f03e36a8db46d", size = 12798, upload-time = "2025-09-13T17:17:09.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/ab/518f14a3c3e43c34c638485cd29bfa80bd35da5a151a434f7ac3c86e1e83/bluetooth_auto_recovery-1.5.3-py3-none-any.whl", hash = "sha256:5d66b859a54ef20fdf1bd3cf6762f153e86651babe716836770da9d9c47b01c4", size = 11750, upload-time = "2025-09-13T17:17:07.681Z" }, +] + +[[package]] +name = "bluetooth-data-tools" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/90/46dfa84798ca4e5c2f66d9a756bb207ed21d89a32b8ef8d3ea89e079455f/bluetooth_data_tools-1.28.4.tar.gz", hash = "sha256:0617a879c30e0410c3506e263ee9e9bd51b06d64db13b4ad0bfd765f794b756f", size = 16488, upload-time = "2025-10-28T15:23:05.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/ed/9bb3a560ff073b5ea6f1fe9ef66d4af0fb8071cd3f569ba58769217114bb/bluetooth_data_tools-1.28.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e3184f43c52ed1e39f9ad412c586c84b4e0841f052608e6ed7ef81daf656fb64", size = 385017, upload-time = "2025-10-28T15:35:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8237718607f1daeec1f4aefb3dfdd7b7b56bc1422fdc4e5f9ef991e3a9b2/bluetooth_data_tools-1.28.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1c5e524df9afae40142c3a3dcf128983df99e73158a2bc98f1709024ff185a22", size = 386609, upload-time = "2025-10-28T15:35:59.73Z" }, + { url = "https://files.pythonhosted.org/packages/35/8e/e6136f790c68261610160c0b8dfadd874d38bff8e3da0feb4bf1428b89fd/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bf7eb8b41995466af3401db3387726afda42487b291b94ab90e7d26aadb72ac", size = 414363, upload-time = "2025-10-28T15:36:00.975Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c9/af729a472e5e9274480b34c8218fda915b3d7add9247d766dee79143eebf/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b3a1e9838f147d6e80b5d9cf7e33c9e736f1f1bda9db00b4ea5ed45fd57d2e8", size = 132276, upload-time = "2025-10-28T15:36:02.622Z" }, + { url = "https://files.pythonhosted.org/packages/70/eb/111c66cc73fd4ec29071641ba6f8b68db033f3d6b9611aa332565c0e3286/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8688f54fd344f17f0c04bca6c2b4351c9fcb211d16becada60f5656305c04238", size = 414494, upload-time = "2025-10-28T15:36:03.709Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/0cb024304121380d374c62cf119647b77c88be3bea435291a71d98e956d4/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8f4233a9d8983ede1d4b319783266b5ae89dbd0f8ac48dcc9b0c2a1d6a60a0ca", size = 414311, upload-time = "2025-10-28T15:36:04.921Z" }, + { url = "https://files.pythonhosted.org/packages/53/00/e11af70293608c36507f14bf893f20b072d753ab1c929dc8103209cf9555/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:df2948eae3bd32242322d7f1a7f0d74e2d2f79e5e3254c7e06ec2ddbcacabe7b", size = 135039, upload-time = "2025-10-28T15:36:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/a4/24/47344c86c8abef13a7b39240c3ee5789e0c5fde16ea65c97f3a9a669c121/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e98f7bcd491711f5be161a0400721c9ecb782308f0eeb030f3bd450450f53d0", size = 416584, upload-time = "2025-10-28T15:36:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/b8/26/45358ddee23e4eed23c3e39959cb070351bcdce6698818334e029af7ab8f/bluetooth_data_tools-1.28.4-cp312-cp312-win32.whl", hash = "sha256:324fc45aad6e9a3115a5612959460eb82156579f5925009cb482427a0931207b", size = 287103, upload-time = "2025-10-28T15:36:09.101Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bc/171fabeff862a94f9926eebd5f9722cdfcd2b7134e2162c471d115cd31cf/bluetooth_data_tools-1.28.4-cp312-cp312-win_amd64.whl", hash = "sha256:c9192bcca07a926599a8221e6354a3ef628a8ecb3d904e437ea216a3aafcbcc7", size = 287106, upload-time = "2025-10-28T15:36:10.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/cd/fa868c3bed326976813c04bd89e833cb0032a6a18ffc03f843947caa29d3/bluetooth_data_tools-1.28.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ade5a22f394cee6b428474f5c23f8ce086ebc618b30fa478fc53703b5dc1bf09", size = 383602, upload-time = "2025-10-28T15:36:11.963Z" }, + { url = "https://files.pythonhosted.org/packages/d3/37/ef120dcce334ba8e3d97c06c9d46ab1db3b7474fad1fb867097b7c0a9355/bluetooth_data_tools-1.28.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea8569f42699e94e18a1be32e45c737f2795c7509f09fa27dd5d342a7855473c", size = 385073, upload-time = "2025-10-28T15:36:13.522Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/1ce2f4d2ce6a04a6a1be490cd2b975777fb76f5230818cefe24b7ed7ba9d/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dfaecb4269bc4830a7bd6f823e8a0a4c368d9135ee6805e6db5eecf1211a2e4", size = 412028, upload-time = "2025-10-28T15:36:14.709Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/f525cc4d4da3555f820a6ce79a3877424ba73f69f4d44a4389b19f7aaf15/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ff3d43804f3510bd11a267268c567b7fe5653b10243be48527ac01d8e15b3faa", size = 130572, upload-time = "2025-10-28T15:36:16.184Z" }, + { url = "https://files.pythonhosted.org/packages/6d/01/2c4b89de730e71c94f3552948aa8adf0a0b5a4dc21e642805bc8e014f41d/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e99be62bdcd2b94778eb230c6d73f4da4ad1493ccc33c09efc8432c5a242c071", size = 412805, upload-time = "2025-10-28T15:36:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/6ca04f0225b51ef27e79f369e9b9fff4bf104025a4e51d6fb2d943c38645/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:f85fbc0c540c3e64b5fc925f6b60d8c96d521548c7bfa3b1e8998ea4e5a59054", size = 140133, upload-time = "2025-10-28T15:23:03.49Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3e/675f9037c7b23df43229d95e8627fe10759f8c3c4a1ef6919b8d1683d4df/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c9dd29f39bddbcfa1dcfca13dcfd2a1111d5a0fbba708a8c98feb98bca10b7a", size = 411910, upload-time = "2025-10-28T15:36:19.207Z" }, + { url = "https://files.pythonhosted.org/packages/a1/12/4f2086f879c0595e065e62dd1bfbe8d371336308654e466ca10b6cf61d86/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8bbcd287a1d5b249639fc1ba99c7ab8f0d7257d43104cf349fab2c747b84b3cd", size = 132814, upload-time = "2025-10-28T15:36:20.789Z" }, + { url = "https://files.pythonhosted.org/packages/e2/72/56a3b3a15cd6c601c3c22cf8c58db788ea59e669db1af123a4113983302b/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7decde5838ccccf71ec626c3f0421a6265054cb1e5ced121bb6448434a0bb72f", size = 414926, upload-time = "2025-10-28T15:36:21.897Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7f/280e0fb57c8569ee04057231e17bc5b47fb037470d62af72b9e7d908fdd2/bluetooth_data_tools-1.28.4-cp313-cp313-win32.whl", hash = "sha256:7d4d65ee4cb3c0616d411f2352b9da8c97f789a42fe9c14a68b6d4b458d62d9a", size = 287105, upload-time = "2025-10-28T15:36:23.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/bcccc7fcaabd74a717663dbe4f4909c4f37edef6c95bfcde7b2548b04ec1/bluetooth_data_tools-1.28.4-cp313-cp313-win_amd64.whl", hash = "sha256:5f3bb83e8755d0ce2e3d62e70a35b73c569ddc63d7200658740e311042c60777", size = 287107, upload-time = "2025-10-28T15:36:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba322c36376532f3133c7d56bc80dd2859df9c78aa52de19ed7627b9fb/bluetooth_data_tools-1.28.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:81c6c2b7c844d30a0fd1527e38e47cdb0f350c0297fb11516bfa255b37241fbf", size = 383677, upload-time = "2025-10-28T15:36:33.905Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2e/74e7b4857ba10a524cd00177fbd78764c50810fb523020b7d5cbf0fdbac8/bluetooth_data_tools-1.28.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:99896987f48d762694cdea7a8a7091031cdf40dc65e8e934a7422746264865ba", size = 385890, upload-time = "2025-10-28T15:36:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/35bc257ed1935e55ac7bfb56172a290f094f8b982f65f68aadb0f03ceab5/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebac9d60786bd7c403f472fcda871cb74d0aef0d4e713715af2e5e095d15a625", size = 412966, upload-time = "2025-10-28T15:36:36.398Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/2ed3dff30e85029e631a211d93e11aab7dc4a899d9c96a15eca18541e66e/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:06a2750e49fed2310ddd7b51388b891cbd4457ee7392f3a17c387591cbb74ace", size = 129887, upload-time = "2025-10-28T15:36:38.429Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/824f3b34b0fab4e57efd457ea8b9bdf41d279a44eb19cfde5ede159d90b3/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5dccfe237237463c3d74fa425aaf8a9d78b26a5177e6777b10039699313a335", size = 412909, upload-time = "2025-10-28T15:36:39.552Z" }, + { url = "https://files.pythonhosted.org/packages/eb/88/f2217b88c32b470e5f9dc9fbce38f24b9548c0776be7c5e0db1249c42ae9/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4a071d7af2614af9a00f65063adaacda94f4357cc2dfedda7057c005f437dacd", size = 413005, upload-time = "2025-10-28T15:36:41.572Z" }, + { url = "https://files.pythonhosted.org/packages/6d/da/cde7557972e50cbb8a92291cc34e5de07f0e2bbc28a388151e738e9efe84/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:bd84c4f2d24103ff43044ccd3cf8c0e05ee285bd6f9eddc9772b2069cfb6c271", size = 131426, upload-time = "2025-10-28T15:36:42.645Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/925fd28e2695ba810b1f7f02f2d5ab8635a11d6e415ac4039446145f9e48/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e3895dbbdad2a39de5a7b36a4ddb5e2f8ad38029628e3eddfde31a5c56d81b5", size = 414955, upload-time = "2025-10-28T15:36:43.775Z" }, + { url = "https://files.pythonhosted.org/packages/03/b1/cbf3a2c8404862605e487200d45aefb130c0c0ce3df219230155eeb95199/bluetooth_data_tools-1.28.4-cp314-cp314-win32.whl", hash = "sha256:1d9b22827144329e3ca1348b8473fe6b48127707a81539848232847c4cb08e1d", size = 286157, upload-time = "2025-10-28T15:36:45.171Z" }, + { url = "https://files.pythonhosted.org/packages/c7/68/eb168b986eebc0c98fb0a6a521719a33d218bafc46c48c5279322d15e9b2/bluetooth_data_tools-1.28.4-cp314-cp314-win_amd64.whl", hash = "sha256:04c91b6f2dfaa419652356488fa50dfb0f54cb20b1f90f9e5e1d6911430d9688", size = 286151, upload-time = "2025-10-28T15:36:46.414Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/f2ce46cf82b32d6a62171753a2d6550d633af5b27f0ad2c2ff5fef1980a4/bluetooth_data_tools-1.28.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a44c48bf163606a2915d12ffb3ac1b022548e566c062907f98266e8a19c6173c", size = 488264, upload-time = "2025-10-28T15:36:47.582Z" }, + { url = "https://files.pythonhosted.org/packages/ba/32/c3bbee5b7c66190f0729e71fefe44adb49e7bb94407b110d972d817561a2/bluetooth_data_tools-1.28.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b76a6c8c6d610844c8712cecf207c16373cad3361fb29e6dbcdcb12f2700bcb9", size = 492846, upload-time = "2025-10-28T15:36:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/71/5c/751028e7fab907c0c2fc7749f088d19bf2b938e5cdd7d0e68ddbcacb7b79/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61b827616075ecee12c374b04b14d81575403849435bf915c9a3812138f046b7", size = 548041, upload-time = "2025-10-28T15:36:50.066Z" }, + { url = "https://files.pythonhosted.org/packages/77/02/4d8f4a9cb2a2beaaedda71fb3017f6bb5eb3de08656adfb9a8a773ec7912/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:525646baaf5f741ea071aa4babd8313e4e9bae75b46757c4b0f6aeadfa71b52a", size = 517778, upload-time = "2025-10-28T15:36:51.628Z" }, + { url = "https://files.pythonhosted.org/packages/89/9b/90d65fed47b531b0f0f4c8be012d35c97950c97fb7b74501bfe938c7f7ca/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c06b66ef406c68a95052a87640fa34d402d31120a8b0b62f99080169621697a", size = 546643, upload-time = "2025-10-28T15:36:52.971Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6b/c15363ccfc208a34cd6d627610350c72633e2a6764d37d04a1340fb13844/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:152232c157f2f6d8265c0141e56423bbedd9e84044fb815e69d786a73fb195c7", size = 548872, upload-time = "2025-10-28T15:36:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/85/2a/b649eeea14e6330da34f42dc1407424cd929af3ae1298b5651459d0c4bb8/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:243163028565955e73f19c0c462b619fd0f56e31875c30f5f3af2a48b43adb67", size = 524783, upload-time = "2025-10-28T15:36:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6e/96c762f8a49f65348748d72c515c5a79c9179c685d3e02694c380bdafa72/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0a1608bca00e24b6ca3b98ed7d797a03988a44285d74286e045446c8161a62ea", size = 551318, upload-time = "2025-10-28T15:36:57.062Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7d/796cbb679d19425ff381ebbe7a5238217b3f3e5c65b9a46e7be57ba105fc/bluetooth_data_tools-1.28.4-cp314-cp314t-win32.whl", hash = "sha256:25918d7ece36f29ebde21aaf70f3c1e1c63501206dd1c7713bbd8911d43d0dce", size = 286158, upload-time = "2025-10-28T15:36:58.717Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/639329ba05947018ba928162042dfb162a31b85757e27591bb6aa96c1f42/bluetooth_data_tools-1.28.4-cp314-cp314t-win_amd64.whl", hash = "sha256:276528d7ea2419ccab14ddf044ee7f65a5b6bc35c49264625560ad0c184dc67a", size = 286163, upload-time = "2025-10-28T15:36:59.861Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/ad/06f48f2d0e9ec91d136602c7009f5f68c84be3655cc6e7e2b59aff82ead4/boto3-1.42.26.tar.gz", hash = "sha256:0fbcf1922e62d180f3644bc1139425821b38d93c1e6ec27409325d2ae86131aa", size = 112877, upload-time = "2026-01-12T20:36:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/0c/094a63b0ab893995b1f2e7ddb5425e11f97403feb90cea0eb770c8905487/boto3-1.42.26-py3-none-any.whl", hash = "sha256:f116cfbe7408e0a9153da363f134d2f1b5008f17ee86af104f0ce59a62be1833", size = 140576, upload-time = "2026-01-12T20:36:38.244Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/c9/6ce745d4233aeb3abdb18205739b394f7955087f7603cb324a797adbf8d2/botocore-1.42.26.tar.gz", hash = "sha256:1c8855e3e811f015d930ccfe8751d4be295aae0562133d14b6f0b247cd6fd8d3", size = 14882582, upload-time = "2026-01-12T20:36:29.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/43/5993eab2114c0de7bbc21985b745aafe3b912f98fc63726c2a54680bb69d/botocore-1.42.26-py3-none-any.whl", hash = "sha256:71171c2d09ac07739f4efce398b15a4a8bc8769c17fb3bc99625e43ed11ad8b7", size = 14554661, upload-time = "2026-01-12T20:36:26.891Z" }, +] + +[[package]] +name = "btsocket" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/b1/0ae262ecf936f5d2472ff7387087ca674e3b88d8c76b3e0e55fbc0c6e956/btsocket-0.3.0.tar.gz", hash = "sha256:7ea495de0ff883f0d9f8eea59c72ca7fed492994df668fe476b84d814a147a0d", size = 19563, upload-time = "2024-06-10T07:05:27.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/2b/9bf3481131a24cb29350d69469448349362f6102bed9ae4a0a5bb228d731/btsocket-0.3.0-py2.py3-none-any.whl", hash = "sha256:949821c1b580a88e73804ad610f5173d6ae258e7b4e389da4f94d614344f1a9c", size = 14807, upload-time = "2024-06-10T07:05:26.381Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "chevron" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/1f/ca74b65b19798895d63a6e92874162f44233467c9e7c1ed8afd19016ebe9/chevron-0.14.0.tar.gz", hash = "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf", size = 11440, upload-time = "2021-01-02T22:47:59.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/93/342cc62a70ab727e093ed98e02a725d85b746345f05d2b5e5034649f4ec8/chevron-0.14.0-py3-none-any.whl", hash = "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443", size = 11595, upload-time = "2021-01-02T22:47:57.847Z" }, +] + +[[package]] +name = "ciso8601" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/bc/cf42c1b0042f91c90a6b00244f63b6fb137af15e43e29f07bb72cf955be8/ciso8601-2.3.1.tar.gz", hash = "sha256:3212c7ffe5d8080270548b5f2692ffd2039683b6628a8d2ad456122cc5793c4c", size = 31225, upload-time = "2023-10-30T19:54:34.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/3d/f6496a260ba6e58135fb3dd1108799f6dd9cadf634372e020bfbf0d27fea/ciso8601-2.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f39bb5936debf21c52e5d52b89f26857c303da80c43a72883946096a6ef5e561", size = 24211, upload-time = "2023-10-30T19:53:42.011Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a9/24ffa848a5878a50009d6177826c36b60e2e8807e4d54ee94817e790897b/ciso8601-2.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:21cf83ca945bb26ecd95364ae2c9ed0276378e5fe35ce1b64d4c6d5b33038ea3", size = 15591, upload-time = "2023-10-30T19:53:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/ea/74/77fd6e67a2a3489a1ac449570142e2f4137289be25027e235d4688470d56/ciso8601-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:013410263cba46748d2de29e9894341ae41223356cde7970478c32bd0984d10c", size = 15643, upload-time = "2023-10-30T19:53:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bd/fef5524974e3ba376e16df35e4197152edffdf7ac9d5d99bc173a9fcf256/ciso8601-2.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b26935687ef1837b56997d8c61f1d789e698be58b261410e629eda9c89812141", size = 39805, upload-time = "2023-10-30T19:53:46.036Z" }, + { url = "https://files.pythonhosted.org/packages/50/1c/d1cb5b2d2173abfc4d5a068981acddff763cce318e896ec87c140412c72d/ciso8601-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0d980a2a88030d4d8b2434623c250866a75b4979d289eba69bec445c51ace99f", size = 48335, upload-time = "2023-10-30T19:53:47.275Z" }, + { url = "https://files.pythonhosted.org/packages/76/ef/5ef06ccb6f6c023573634119f93df237687e3f4263e2f307b51a7208d103/ciso8601-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:87721de54e008fb1c4c3978553b05a9c417aa25b76ddf5702d6f7e8d9b109288", size = 49449, upload-time = "2023-10-30T19:53:48.646Z" }, + { url = "https://files.pythonhosted.org/packages/e6/45/f981bbd51f1c9fa3d730c3fbcb74b1f8928463e6ea8283257f6b164847e6/ciso8601-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f107a4c051e7c0416824279264d94f4ed3da0fbd82bd96ec3c3293426826de4", size = 17084, upload-time = "2023-10-30T19:53:49.745Z" }, +] + +[[package]] +name = "ciso8601" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/09/e9/d83711081c997540aee59ad2f49d81f01d33e8551d766b0ebde346f605af/ciso8601-2.3.2.tar.gz", hash = "sha256:ec1616969aa46c51310b196022e5d3926f8d3fa52b80ec17f6b4133623bd5434", size = 28214, upload-time = "2024-12-09T12:26:40.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/fc/e852e664bb90bf1112e17778512d6cbc5fa5f49b7c22969e4ee131f13d06/ciso8601-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75870a1e496a17e9e8d2ac90125600e1bafe51679d2836b2f6cb66908fef7ad6", size = 15755, upload-time = "2024-12-09T12:26:07.259Z" }, + { url = "https://files.pythonhosted.org/packages/22/da/c82e665c627836be4d7d0a8ed38518f9833124a6fd85735881cac72427b8/ciso8601-2.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:c117c415c43aa3db68ee16a2446cb85c5e88459650421d773f6f6444ce5e5819", size = 24291, upload-time = "2024-12-09T12:26:08.23Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6a/822b178b6c473533e5023aab6447b05d1683f95c3210eda5680f9262c93c/ciso8601-2.3.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:ce5f76297b6138dc5c085d4c5a0a631afded99f250233fe583dc365f67fe8a8d", size = 15713, upload-time = "2024-12-09T12:26:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/de/c3/63b89c7ec2a4f9bcbdeb3401485992d13eeb4da943accef58f0820c62552/ciso8601-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e3205e4cfd63100f454ea67100c7c6123af32da0022bdc6e81058e95476a8ad", size = 40253, upload-time = "2024-12-09T12:26:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/96/01/b12f356afaa6dfc339c4b964f01c7b78f7d844dfe087cbbc9c68a5f048c0/ciso8601-2.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5308a14ac72898f91332ccfded2f18a6c558ccd184ccff84c4fb36c7e4c2a0e6", size = 40087, upload-time = "2024-12-09T12:26:11.066Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/de5f920ebf5cdb2ef28237bdb48ac9ea980d794e16f1fbedffc430064208/ciso8601-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e825cb5ecd232775a94ef3c456ab19752ee8e66eaeb20562ea45472eaa8614ec", size = 40908, upload-time = "2024-12-09T12:26:12.092Z" }, + { url = "https://files.pythonhosted.org/packages/b2/3c/cd79c9305480cc9bf8dce286bd7ec2035a3d140b3f3ae0b1232087a65240/ciso8601-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a8f96f91bdeabee7ebca2c6e48185bea45e195f406ff748c87a3c9ecefb25cc", size = 40881, upload-time = "2024-12-09T12:26:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/f1b64a6dac1ff824ad6eee9c2b540fbe411f288b40218a06644fa2e4f075/ciso8601-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:3fe497819e50a245253a3b2d62ec4c68f8cf337d79dc18e2f3b0a74d24dc5e93", size = 17278, upload-time = "2024-12-09T12:26:14.027Z" }, +] + +[[package]] +name = "ciso8601" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/8a/075724aea06c98626109bfd670c27c248c87b9ba33e637f069bf46e8c4c3/ciso8601-2.3.3.tar.gz", hash = "sha256:db5d78d9fb0de8686fbad1c1c2d168ed52efb6e8bf8774ae26226e5034a46dae", size = 31909, upload-time = "2025-08-20T16:31:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/aa/b723a6981cfc42bbe992da23179f5dd1556e9054067985108ec6cbe34dd3/ciso8601-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e7ef14610446211c4102bf6c67f32619ab341e56db15bad6884385b43c12b064", size = 16111, upload-time = "2025-08-20T16:30:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e9/e547ec4dd75f28d8d217488130fa07767bc42fd643d61a18870487133c0e/ciso8601-2.3.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:523901aec6b0ccdf255c863ef161f476197f177c5cd33f2fbb35955c5f97fdb4", size = 24193, upload-time = "2025-08-20T16:30:38.067Z" }, + { url = "https://files.pythonhosted.org/packages/14/c8/801b78e30667cb31b4524e9dc26cbc2c03c012f9aa3f5ae21676461dc622/ciso8601-2.3.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:45f8254d1fb0a41e20f98e93075db7b56504adddf65e4c8b397671feba4861ca", size = 15917, upload-time = "2025-08-20T16:30:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/6b/dfc56a2a4e572a2a3f8c88a66dea6a9186a8e10da7c36cc84abc31bf795c/ciso8601-2.3.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:202ca99077577683e6a84d394ff2677ec19d9f406fbf35734f68be85d2bcd3f1", size = 41324, upload-time = "2025-08-20T16:30:40.321Z" }, + { url = "https://files.pythonhosted.org/packages/7c/57/cf66171cb5807fe345b03ce9e32fd91b3a8b6e5bd95710618a9a1b0f3fab/ciso8601-2.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7cec4e31c363e87221f2561e7083ce055a82de041e822e7c3775f8ce6250a7e", size = 41804, upload-time = "2025-08-20T16:30:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/15e8871d7ae2ff0f756128e246348bdede58c08edba13cd886450ceeb304/ciso8601-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:389fef3ccc3065fa21cb6ef7d03aee63ab980591b5d87b9f0bbe349f52b16bdc", size = 41209, upload-time = "2025-08-20T16:30:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/54/7563e20a158a4bdf3e8d13c63e02b71f9b73c662edc83cb4d5ab67171a7d/ciso8601-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4499cfbe4da092dea95ab81aefc78b98e2d7464518e6e80107cf2b9b1f65fa2", size = 41368, upload-time = "2025-08-20T16:30:43.397Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/6182006dd86365bb21d1f658f70c41e266ce0f97eaf353f9d7069c51851f/ciso8601-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:1df1ca3791c6f2d543f091d88e728a60a31681ff900d9eb02f1403cf31e9c177", size = 17566, upload-time = "2025-08-20T16:30:44.706Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/88154fe8247e4dcfdbaed8c6b8ccf32b1dd4389c6c95b1986bf31649eb00/ciso8601-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8afa073802c926c3244e1e5fcc5818afd3acb90fb7826a90f91ddbda0636ea70", size = 16109, upload-time = "2025-08-20T16:30:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/be/46/8d46372b3802c7201c20c8b316569f27253aaafba0cdd2cd033985e8b77e/ciso8601-2.3.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8a04e518b4adf8e35e030feaecdb4a835d39b9bb44d207e926aea8ce3447ad7c", size = 24189, upload-time = "2025-08-20T16:30:46.958Z" }, + { url = "https://files.pythonhosted.org/packages/13/80/1890e097cb76e41995de82f29c0289ca590d7135e0be3707e5b78f54350d/ciso8601-2.3.3-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:f79ad8372463ba4265981016d1648bc05f4922bc8044c4243fcbaef7a12ee9f7", size = 15925, upload-time = "2025-08-20T16:30:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e9/690a2a6beefd9d982c20adde3f09ff54a23291a699b0df7cf0c59027d9cf/ciso8601-2.3.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d5894a33f119b5ac1082df187dc58c74fe13c9c092e19ba36495c2b7cee3540b", size = 41352, upload-time = "2025-08-20T16:30:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/9a498ceb0ebd23f538e6685721c9fc4666701372c651874ed22ec46b1423/ciso8601-2.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09deebf3e326ec59d80019b4ad35175c90b99cde789c644b1496811fe3340587", size = 41866, upload-time = "2025-08-20T16:30:50.262Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0a/ee0981502aa1c9f28f7e89cf6cee08bdff2c6ed9d4289b00cceb8a1c500e/ciso8601-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3aa43ed59b2117baccc5bb760e5e53dad77cacba671d757c1e82e0a367b1f42a", size = 41271, upload-time = "2025-08-20T16:30:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/fb/65/24a888240324188d8350bc24fb58a6d759c0ca43adfa77210f3d60370b56/ciso8601-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:289515aa3a3b86a9c3450bf482f634138b98788332d136751507bfdfe46e6031", size = 41411, upload-time = "2025-08-20T16:30:52.439Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1f/febc9de191acb461e02e616e5366bc2b7757277a11b4bf215d4fb79516a8/ciso8601-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:e7288068a5bffbcc50cbe9cdaf3971f541fcd209c194fa6a59ad06066a3dcff0", size = 17573, upload-time = "2025-08-20T16:30:53.759Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/54ad0ae2257870076b4990545a8f16221470fecea0aa7a4e1f39506db8c5/ciso8601-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82db4047d74d8b1d129e7a8da578518729912c3bd19cb71541b147e41f426381", size = 16115, upload-time = "2025-08-20T16:30:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/9fe767d44520691e2b706769466852fbdeb44a82dc294c2766bce1049d22/ciso8601-2.3.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a553f3fc03a2ed5ca6f5716de0b314fa166461df01b45d8b36043ccac3a5e79f", size = 24214, upload-time = "2025-08-20T16:30:56.359Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ac/984fd3948f372c46c436a2b48da43f4fb7bc6f156a6f4bc858adaab79d42/ciso8601-2.3.3-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:ff59c26083b7bef6df4f0d96e4b649b484806d3d7bcc2de14ad43147c3aafb04", size = 15929, upload-time = "2025-08-20T16:30:58.352Z" }, + { url = "https://files.pythonhosted.org/packages/de/3a/5572917d4e0bec2c1ef0eda8652f9dc8d1850d29d3eef9e5e82ffe5d6791/ciso8601-2.3.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99a1fa5a730790431d0bfcd1f3a6387f60cddc6853d8dcc5c2e140cd4d67a928", size = 41578, upload-time = "2025-08-20T16:30:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cf/07321ce5cf099b98de0c02cd4bab4818610da69743003e94c8fb6e8a59cb/ciso8601-2.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c35265c1b0bd2ac30ed29b49818dd38b0d1dfda43086af605d8b91722727dec0", size = 42085, upload-time = "2025-08-20T16:31:00.338Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c7/3c521d6779ee433d9596eb3fcded79549bbe371843f25e62006c04f74dc9/ciso8601-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa9df2f84ab25454f14df92b2dd4f9aae03dbfa581565a716b3e89b8e2110c03", size = 41313, upload-time = "2025-08-20T16:31:01.313Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/efd40db0d6b512be1cbe4e7e750882c2e88f580e17f35b3e9cc9c23004b5/ciso8601-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32e06a35eb251cfc4bbe01a858c598da0a160e4ad7f42ff52477157ceaf48061", size = 41443, upload-time = "2025-08-20T16:31:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/515f9404faa39af8df5e2b899cafbca5dbe7cd2ffe5cc124ef393ffdaf1c/ciso8601-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:7657ba9730dc1340d73b9e61eca14f341c41dd308128c808b8b084d2b85bc03e", size = 17977, upload-time = "2025-08-20T16:31:03.429Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cronsim" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/d8/cfb8d51a51f6076ffa09902c02978c7db9764cca78f4ee832e691d20f44b/cronsim-2.6.tar.gz", hash = "sha256:5aab98716ef90ab5ac6be294b2c3965dbf76dc869f048846a0af74ebb506c10d", size = 20315, upload-time = "2024-11-02T14:34:02.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/dd/9c40c4e0f4d3cb6cf52eb335e9cc1fa140c1f3a87146fb6987f465b069da/cronsim-2.6-py3-none-any.whl", hash = "sha256:5e153ff8ed64da7ee8d5caac470dbeda8024ab052c3010b1be149772b4801835", size = 13500, upload-time = "2024-12-04T12:53:57.443Z" }, +] + +[[package]] +name = "cronsim" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/1a/02f105147f7f2e06ed4f734ff5a6439590bb275a53dd91fc73df6312298a/cronsim-2.7-py3-none-any.whl", hash = "sha256:1e1431fa08c51dc7f72e67e571c7c7a09af26420169b607badd4ca9677ffad1e", size = 14213, upload-time = "2025-10-21T16:38:20.431Z" }, +] + +[[package]] +name = "cryptography" +version = "43.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version < '3.13' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ba/0664727028b37e249e73879348cc46d45c5c1a2a2e81e8166462953c5755/cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", size = 686927, upload-time = "2024-09-03T20:04:20.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/28/b92c98a04ba762f8cdeb54eba5c4c84e63cac037a7c5e70117d337b15ad6/cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", size = 6223222, upload-time = "2024-09-03T20:04:14.466Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/1193774705783ba364121aa2a60132fa31a668b8ababd5edfa1662354ccd/cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", size = 3794751, upload-time = "2024-09-03T20:04:16.725Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4b/39bb3c4c8cfb3e94e736b8d8859ce5c81536e91a1033b1d26770c4249000/cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", size = 3981827, upload-time = "2024-09-03T20:03:55.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/dc/1471d4d56608e1013237af334b8a4c35d53895694fbb73882d1c4fd3f55e/cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", size = 3780034, upload-time = "2024-09-03T20:03:58.972Z" }, + { url = "https://files.pythonhosted.org/packages/ad/43/7a9920135b0d5437cc2f8f529fa757431eb6a7736ddfadfdee1cc5890800/cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", size = 3993407, upload-time = "2024-09-03T20:03:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/cc/42/9ab8467af6c0b76f3d9b8f01d1cf25b9c9f3f2151f4acfab888d21c55a72/cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", size = 3886457, upload-time = "2024-09-03T20:03:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/a4/65/430509e31700286ec02868a2457d2111d03ccefc20349d24e58d171ae0a7/cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", size = 4081499, upload-time = "2024-09-03T20:03:32.522Z" }, + { url = "https://files.pythonhosted.org/packages/bb/18/a04b6467e6e09df8c73b91dcee8878f4a438a43a3603dc3cd6f8003b92d8/cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", size = 2616504, upload-time = "2024-09-03T20:04:09.459Z" }, + { url = "https://files.pythonhosted.org/packages/cc/73/0eacbdc437202edcbdc07f3576ed8fb8b0ab79d27bf2c5d822d758a72faa/cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", size = 3067456, upload-time = "2024-09-03T20:03:40.775Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b6/bc54b371f02cffd35ff8dc6baba88304d7cf8e83632566b4b42e00383e03/cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", size = 6225263, upload-time = "2024-09-03T20:03:43.181Z" }, + { url = "https://files.pythonhosted.org/packages/00/0e/8217e348a1fa417ec4c78cd3cdf24154f5e76fd7597343a35bd403650dfd/cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", size = 3794368, upload-time = "2024-09-03T20:03:18.051Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ed/38b6be7254d8f7251fde8054af597ee8afa14f911da67a9410a45f602fc3/cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", size = 3981750, upload-time = "2024-09-03T20:04:18.775Z" }, + { url = "https://files.pythonhosted.org/packages/64/f3/b7946c3887cf7436f002f4cbb1e6aec77b8d299b86be48eeadfefb937c4b/cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", size = 3778925, upload-time = "2024-09-03T20:03:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7e/ebda4dd4ae098a0990753efbb4b50954f1d03003846b943ea85070782da7/cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", size = 3993152, upload-time = "2024-09-03T20:03:30.108Z" }, + { url = "https://files.pythonhosted.org/packages/43/f6/feebbd78a3e341e3913846a3bb2c29d0b09b1b3af1573c6baabc2533e147/cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", size = 3886392, upload-time = "2024-09-03T20:03:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4c/ab0b9407d5247576290b4fd8abd06b7f51bd414f04eef0f2800675512d61/cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", size = 4082606, upload-time = "2024-09-03T20:03:27.836Z" }, + { url = "https://files.pythonhosted.org/packages/05/36/e532a671998d6fcfdb9122da16434347a58a6bae9465e527e450e0bc60a5/cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", size = 2617948, upload-time = "2024-09-03T20:03:25.446Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c6/c09cee6968add5ff868525c3815e5dccc0e3c6e89eec58dc9135d3c40e88/cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", size = 3070445, upload-time = "2024-09-03T20:03:21.179Z" }, +] + +[[package]] +name = "cryptography" +version = "44.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819, upload-time = "2025-02-11T15:50:58.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022, upload-time = "2025-02-11T15:49:32.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865, upload-time = "2025-02-11T15:49:36.659Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562, upload-time = "2025-02-11T15:49:39.541Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923, upload-time = "2025-02-11T15:49:42.461Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194, upload-time = "2025-02-11T15:49:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790, upload-time = "2025-02-11T15:49:48.215Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343, upload-time = "2025-02-11T15:49:50.313Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127, upload-time = "2025-02-11T15:49:52.051Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666, upload-time = "2025-02-11T15:49:56.56Z" }, + { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811, upload-time = "2025-02-11T15:49:59.248Z" }, + { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882, upload-time = "2025-02-11T15:50:01.478Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989, upload-time = "2025-02-11T15:50:03.312Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714, upload-time = "2025-02-11T15:50:05.555Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269, upload-time = "2025-02-11T15:50:08.54Z" }, + { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461, upload-time = "2025-02-11T15:50:11.419Z" }, + { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314, upload-time = "2025-02-11T15:50:14.181Z" }, + { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675, upload-time = "2025-02-11T15:50:16.3Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429, upload-time = "2025-02-11T15:50:19.302Z" }, + { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039, upload-time = "2025-02-11T15:50:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713, upload-time = "2025-02-11T15:50:24.261Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193, upload-time = "2025-02-11T15:50:26.18Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566, upload-time = "2025-02-11T15:50:28.221Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371, upload-time = "2025-02-11T15:50:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303, upload-time = "2025-02-11T15:50:32.258Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.13.2' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/98/7a8df8c19a335c8028414738490fc3955c0cecbfdd37fcc1b9c3d04bd561/cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663", size = 7261255, upload-time = "2025-10-01T00:27:22.947Z" }, + { url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" }, + { url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" }, + { url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" }, + { url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" }, + { url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2b/531e37408573e1da33adfb4c58875013ee8ac7d548d1548967d94a0ae5c4/cryptography-46.0.2-cp311-abi3-win32.whl", hash = "sha256:8b9bf67b11ef9e28f4d78ff88b04ed0929fcd0e4f70bb0f704cfc32a5c6311ee", size = 3056077, upload-time = "2025-10-01T00:27:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cd/2f83cafd47ed2dc5a3a9c783ff5d764e9e70d3a160e0df9a9dcd639414ce/cryptography-46.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:758cfc7f4c38c5c5274b55a57ef1910107436f4ae842478c4989abbd24bd5acb", size = 3512585, upload-time = "2025-10-01T00:27:50.521Z" }, + { url = "https://files.pythonhosted.org/packages/00/36/676f94e10bfaa5c5b86c469ff46d3e0663c5dc89542f7afbadac241a3ee4/cryptography-46.0.2-cp311-abi3-win_arm64.whl", hash = "sha256:218abd64a2e72f8472c2102febb596793347a3e65fafbb4ad50519969da44470", size = 2927474, upload-time = "2025-10-01T00:27:52.91Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/47fc6223a341f26d103cb6da2216805e08a37d3b52bee7f3b2aee8066f95/cryptography-46.0.2-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:bda55e8dbe8533937956c996beaa20266a8eca3570402e52ae52ed60de1faca8", size = 7198626, upload-time = "2025-10-01T00:27:54.8Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/d66a8591207c28bbe4ac7afa25c4656dc19dc0db29a219f9809205639ede/cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a", size = 4287584, upload-time = "2025-10-01T00:27:57.018Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/fac3ab6302b928e0398c269eddab5978e6c1c50b2b77bb5365ffa8633b37/cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b", size = 4433796, upload-time = "2025-10-01T00:27:58.631Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/24392e5d3c58e2d83f98fe5a2322ae343360ec5b5b93fe18bc52e47298f5/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20", size = 4292126, upload-time = "2025-10-01T00:28:00.643Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/3d9f9359b84c16c49a5a336ee8be8d322072a09fac17e737f3bb11f1ce64/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73", size = 3993056, upload-time = "2025-10-01T00:28:02.8Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a3/4c44fce0d49a4703cc94bfbe705adebf7ab36efe978053742957bc7ec324/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee", size = 4967604, upload-time = "2025-10-01T00:28:04.783Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/49d73218747c8cac16bb8318a5513fde3129e06a018af3bc4dc722aa4a98/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2", size = 4465367, upload-time = "2025-10-01T00:28:06.864Z" }, + { url = "https://files.pythonhosted.org/packages/1b/64/9afa7d2ee742f55ca6285a54386ed2778556a4ed8871571cb1c1bfd8db9e/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f", size = 4291678, upload-time = "2025-10-01T00:28:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/50/48/1696d5ea9623a7b72ace87608f6899ca3c331709ac7ebf80740abb8ac673/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e", size = 4931366, upload-time = "2025-10-01T00:28:10.74Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/9dfc778401a334db3b24435ee0733dd005aefb74afe036e2d154547cb917/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36", size = 4464738, upload-time = "2025-10-01T00:28:12.491Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b1/abcde62072b8f3fd414e191a6238ce55a0050e9738090dc6cded24c12036/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a", size = 4419305, upload-time = "2025-10-01T00:28:14.145Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1f/3d2228492f9391395ca34c677e8f2571fb5370fe13dc48c1014f8c509864/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c", size = 4681201, upload-time = "2025-10-01T00:28:15.951Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/b687745804a93a55054f391528fcfc76c3d6bfd082ce9fb62c12f0d29fc1/cryptography-46.0.2-cp314-cp314t-win32.whl", hash = "sha256:e8633996579961f9b5a3008683344c2558d38420029d3c0bc7ff77c17949a4e1", size = 3022492, upload-time = "2025-10-01T00:28:17.643Z" }, + { url = "https://files.pythonhosted.org/packages/60/a5/8d498ef2996e583de0bef1dcc5e70186376f00883ae27bf2133f490adf21/cryptography-46.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:48c01988ecbb32979bb98731f5c2b2f79042a6c58cc9a319c8c2f9987c7f68f9", size = 3496215, upload-time = "2025-10-01T00:28:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/ee67aaef459a2706bc302b15889a1a8126ebe66877bab1487ae6ad00f33d/cryptography-46.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8e2ad4d1a5899b7caa3a450e33ee2734be7cc0689010964703a7c4bcc8dd4fd0", size = 2919255, upload-time = "2025-10-01T00:28:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" }, + { url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" }, + { url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" }, + { url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" }, + { url = "https://files.pythonhosted.org/packages/15/52/ea7e2b1910f547baed566c866fbb86de2402e501a89ecb4871ea7f169a81/cryptography-46.0.2-cp38-abi3-win32.whl", hash = "sha256:0b507c8e033307e37af61cb9f7159b416173bdf5b41d11c4df2e499a1d8e007c", size = 3036711, upload-time = "2025-10-01T00:28:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/9e/171f40f9c70a873e73c2efcdbe91e1d4b1777a03398fa1c4af3c56a2477a/cryptography-46.0.2-cp38-abi3-win_amd64.whl", hash = "sha256:f9b2dc7668418fb6f221e4bf701f716e05e8eadb4f1988a2487b11aedf8abe62", size = 3500007, upload-time = "2025-10-01T00:28:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" }, +] + +[[package]] +name = "dbus-fast" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/a4/e54607cf8b0a696beba591f1a543cff5b6a9e4b4f842fd55f7ba741d678d/dbus_fast-3.1.2.tar.gz", hash = "sha256:6c9e1b45e4b5e7df0c021bf1bf3f27649374e47c3de1afdba6d00a7d7bba4b3a", size = 73191, upload-time = "2025-11-17T03:41:10.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/87/1aa99b4ab3e051962071a0e443cfa725e80802c950484a501f01014ab0df/dbus_fast-3.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91362a0f2151926a882c652ee2ae7c41495a82228b045e7461e1ce687ab4b173", size = 789953, upload-time = "2025-11-17T03:49:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/fc/99/59bc4854b2a2355352373d08d07b89d5318c181a1356de307b07b8ae5d99/dbus_fast-3.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439c300cf0f1b9b4b81c1a55ac1ed65c2b90f203570c4d0243d2fc3eac8fc7cc", size = 839666, upload-time = "2025-11-17T03:49:33.81Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/69ae337cd516bce574fc1dd592ce6c3fdd46acae0168c9c20b26ce284396/dbus_fast-3.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9290039b2454357735a35cf81b98c208c19c1b4a244532bbb52135c5dc0b7f8c", size = 796378, upload-time = "2025-11-17T03:49:35.125Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/a6178c07c769ecdba3d46f8c458adfab25db9a4e5d16a8b567b2c61c3e03/dbus_fast-3.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9d275923c4ec24b63b1edf4871f05fc673fc08e1a838a9ddd02938b9c28fa44", size = 847893, upload-time = "2025-11-17T03:49:36.79Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/81e1acbe08e195f38a3ef863702dba88ade8fe56594041ff7e5a6ef9d137/dbus_fast-3.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdaa7c1cf132b72a8c66fd36c612b112063296d2d518463064ff44dc670d452a", size = 787581, upload-time = "2025-11-17T03:49:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/da668ad9947db726747e29aa1978642919a5cfbe633a473f76446ea3915d/dbus_fast-3.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:973afa96fcb97c680d50a66163ad2aa7327177e136a29fbeae280c660584536a", size = 836242, upload-time = "2025-11-17T03:49:40.854Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/ecb51606a7f5f41e6eeb2e347757b6ffbd83b9e51c85bb7787a737016ee4/dbus_fast-3.1.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:cea152a01991cb8b77eeb2403b156e5a8ba4300b729636aa732fc891c22e44d4", size = 812530, upload-time = "2025-11-17T03:41:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/fd/47/42eac517b9fe3949f3cb66dcc1d2b5fa4380bf606bef2374f3c77d7f4cb2/dbus_fast-3.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:618b819b19724477b77f5bf3f300d92fa51d0974bd25499e10c3417eadc4a732", size = 793323, upload-time = "2025-11-17T03:49:42.765Z" }, + { url = "https://files.pythonhosted.org/packages/92/ef/0956c728d7a4963b6503f201f4aa3c3b366ec21490b526bf155b8fa0ce52/dbus_fast-3.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66279b8491ba9d593c4793b423abbf1dce14dbb3f3e6d9967bb62be8c39244b4", size = 845052, upload-time = "2025-11-17T03:49:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/e0/87/2d322dbf5357393a426ae2873d60016008566b0a60a2c7f6e4f1783add50/dbus_fast-3.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8116564196c7e83cfc81be186378da7f093d36fbfef0669e1fe1f20ac891c50a", size = 801269, upload-time = "2025-11-17T03:49:47.418Z" }, + { url = "https://files.pythonhosted.org/packages/72/27/1bc75fcd4ea5fb298a28b9d204b65b0d00c7c89ba13a3e47185d48b8db8b/dbus_fast-3.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c55db7b62878bc039736d2687b1bd5eb4a5596b97a4b230c9d919daa961a1d9c", size = 842854, upload-time = "2025-11-17T03:49:48.986Z" }, + { url = "https://files.pythonhosted.org/packages/40/c4/f042243ff28cf083c189f7eff5cc04b86c150a33b3cdef7ef21b520d93a5/dbus_fast-3.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8064b36900098c31a3fe8dab7ef3931c853cbcf9f163ccb437a7379c61e6acc3", size = 808090, upload-time = "2025-11-17T03:49:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/1e1443cb8a35e11e24009882463c461ee5103c20c5fc7f04a9e3443462ce/dbus_fast-3.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:038d3e8803f62b1d789ce0c602cc8c317c47c21e67bb2dd544b9c0fc97b4b2e2", size = 850179, upload-time = "2025-11-17T03:49:51.956Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/9ebef541d192c22529af38d151a818ca18f6c3699dfc3644ff0e66e44599/dbus_fast-3.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71c99fb09c3a5637a0729230ac5f888b61abf754e10f23c629be476da830887c", size = 1529979, upload-time = "2025-11-17T03:49:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4f/0e0b0359d41ffaa652b066759321d8f2608246090c827061c43b72dcefae/dbus_fast-3.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a78eb3f19ff81fb7a8b16075160ebd1edc6135c59c929da0832511f315b5ede", size = 1606236, upload-time = "2025-11-17T03:49:56.667Z" }, + { url = "https://files.pythonhosted.org/packages/8e/18/1ad08968773f1d1822a1e147dbc98c9af220b582d2c8b95705c7e8f8019f/dbus_fast-3.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:366550946b281a5b4bb8d70815667d24565141e3c23dc7d40267a315b16def2c", size = 1545417, upload-time = "2025-11-17T03:49:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/89/94/b7ff6279e642b014cd4aef4d914b9fca3917c2c9c35df49db062023cbdfc/dbus_fast-3.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1d7cc1315586e4c50875c9a2d56b9ad2e056ec75e2f27c43cd80392f72d0f6e3", size = 1623709, upload-time = "2025-11-17T03:49:59.571Z" }, +] + +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "envs" +version = "1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/7f/2098df91ff1499860935b4276ea0c27d3234170b03f803a8b9c97e42f0e9/envs-1.4.tar.gz", hash = "sha256:9d8435c6985d1cdd68299e04c58e2bdb8ae6cf66b2596a8079e6f9a93f2a0398", size = 9230, upload-time = "2021-12-09T22:16:52.616Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/bc/f8c625a084b6074c2295f7eab967f868d424bb8ca30c7a656024b26fe04e/envs-1.4-py3-none-any.whl", hash = "sha256:4a1fcf85e4d4443e77c348ff7cdd3bfc4c0178b181d447057de342e4172e5ed1", size = 10988, upload-time = "2021-12-09T22:16:51.127Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "fnv-hash-fast" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "fnvhash", version = "0.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/35/a0f2baec714caa5e865dc284382bac7f959c643a03d7f2c2f8c38573b2b2/fnv_hash_fast-1.0.2.tar.gz", hash = "sha256:d4c528bfb0daa751afb17419a244b913b094b9f0634f9bd19aeffcdc60192589", size = 5808, upload-time = "2024-08-23T13:00:08.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/76/08c20543421f368f5d0215381b745268f5ba51916a286896fed62ed583e2/fnv_hash_fast-1.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d306b606c1686f7902f2da3193535e3523934ddf10cc540427d5a1d96a9818c4", size = 65081, upload-time = "2024-08-23T13:05:34.981Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/4cadf8a40268bed1c777f9d8e6cfe18a38056d46c2cc844ce34560bb9796/fnv_hash_fast-1.0.2-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d1dfd66728c70b6b3184729a8e2b98cf8d3548b65bc09ab49fff156d86095e62", size = 149973, upload-time = "2024-08-23T13:05:35.891Z" }, + { url = "https://files.pythonhosted.org/packages/40/9b/0b27c3116dcbde287961c413d9cddc9bfaa62783f5c43cf6a8a6743bd1da/fnv_hash_fast-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdeaed747d4af60c0ae4cd336ee349db0bba2e1bd46d7d94c8c6a1a7cf3ecbf4", size = 154213, upload-time = "2024-08-23T13:05:37.305Z" }, + { url = "https://files.pythonhosted.org/packages/14/ee/6b6079fd618a4ae9e1762ff525d204d7689aa8e7e378d24ca5df7e02a4c9/fnv_hash_fast-1.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e9f303ce7c394119cb205fe54124f956b3feefd388700f2268b209d78fa9a88c", size = 1237178, upload-time = "2024-08-23T13:05:39.099Z" }, + { url = "https://files.pythonhosted.org/packages/78/36/60431380699e98371a985bdbfc181a4adb32317d5ada1c5f10c84087afaa/fnv_hash_fast-1.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55b9ccbfb87aafc76ef133c70e76a5061a48432f6ba846263ef122a774bce09c", size = 1147938, upload-time = "2024-08-23T13:05:40.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d5/d2ef0509776ba09378f90ea87657b8c33112c2c24429e8596439472eb6ea/fnv_hash_fast-1.0.2-cp312-cp312-win32.whl", hash = "sha256:57507e52829dd463f2f755ca22fc9dc4a8d9a9c5d8cf1b0d5ec4eeddf90c9c48", size = 64267, upload-time = "2024-08-23T13:05:42.121Z" }, + { url = "https://files.pythonhosted.org/packages/1f/46/eb073c248b42b8137d47dff385b30c0ebfb3542536fc4581a944372a0c82/fnv_hash_fast-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ef4118d57d27a13271feb47b0ffef95a5122aaa2c4e15b4979cc8bf1bc81c14b", size = 66293, upload-time = "2024-08-23T13:05:43.392Z" }, + { url = "https://files.pythonhosted.org/packages/54/af/9bb74b23610d3f10b64ac41c6ac9a59402619ef13a4d880975d1dc07dfd3/fnv_hash_fast-1.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea6d4fb666684d7e15f2eb1aa92235b25286ea3081cdfb469ffcc7ee34c74b67", size = 64342, upload-time = "2024-08-23T13:05:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/a6/47/327e75a72d6f0de00491b8867953878a4c72c058e0f9ee37c0c64111a297/fnv_hash_fast-1.0.2-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f91ec27fbe3fc43440a250d3b8dac3f0ebd8cea91ffa432bea40ef611b089eeb", size = 145037, upload-time = "2024-08-23T13:05:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5f/90f485875dee1a7fd2252aacef6c10a7dcf991fcfee44f4e65ba13ab477e/fnv_hash_fast-1.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fb40cc6685a81595434c6cf1ef79d92d4d899e8bc823d9ad6a30287d612de0d", size = 149134, upload-time = "2024-08-23T13:05:46.624Z" }, + { url = "https://files.pythonhosted.org/packages/05/d3/79bc1b00223aada2bf906c99bb619d730bdbff293433f08ef3ba153ef198/fnv_hash_fast-1.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7b7f491a7c3cd679bda8fcd693234812a488bcb3dae0952ba312366b6f69796d", size = 1232050, upload-time = "2024-08-23T13:05:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0d/b523ee18b8dea412f0b1d711917b030a32edc1e183b36d7419d83030b7f5/fnv_hash_fast-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a318cb86ea4a91c95eb42bd49e9144fbdc83e0bb91a1e6f672f197f15a450d01", size = 1143237, upload-time = "2024-08-23T13:05:49.209Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/6cc3d9da7ca14341be9a9effe32b2bf105f6d08b3971b007c51f18b22d9a/fnv_hash_fast-1.0.2-cp313-cp313-win32.whl", hash = "sha256:0ac9b5da8fbb9f670a7ce877dfa9bccc942f6499e25801d63427e0f55e1aa902", size = 63690, upload-time = "2024-08-23T13:05:50.556Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/1d7f2fd7c2163e661774c07f8cceb1db434f2c5440ab644379d6de76753b/fnv_hash_fast-1.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5935a91ae5cc9edd2bd7a9028b0e5b1e371e5a383034357541b559a2e235e57", size = 65514, upload-time = "2024-08-23T13:05:51.41Z" }, +] + +[[package]] +name = "fnv-hash-fast" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "fnvhash", version = "0.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/36/fa1cab334dc1228235d76a22bdeab67b6895e08eb1870821c500e86b240b/fnv_hash_fast-1.4.0.tar.gz", hash = "sha256:12a2a437263f08815bd2d5759c12e881408718bb82cfffceb0341575f2c43f0a", size = 5661, upload-time = "2025-03-05T01:09:25.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/dfb9f6a3bb99df495c5d0a7ac801ff930cb7800306afb74e856b6517e1f0/fnv_hash_fast-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10e55bf0987b2e74af39211223e6d4e7c17380ad91e5bd9d799eb5ec78b3f19d", size = 19151, upload-time = "2025-03-05T01:15:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1e/450ce8cc0c668c529544605b8d3fa8789ad32450b086d68f39e4312f4bd3/fnv_hash_fast-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b49f0f8df92efc3f7d8894b1c5217379cb25d826716690b42692b93661b3eea0", size = 19214, upload-time = "2025-03-05T01:15:33.145Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7e/1ddd3a6449fa6c10de63f1409cd34f680e7a0c320449594c655bc0a0d702/fnv_hash_fast-1.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e489052acf4f279ffbd3ad7326b7306df9e91388295b207077e939fc10fdb9", size = 22469, upload-time = "2025-03-05T01:15:34.407Z" }, + { url = "https://files.pythonhosted.org/packages/89/f2/17edeb023de13afc9382ffa26458ceee78e9be36af88365a6f7f7ad77cc9/fnv_hash_fast-1.4.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4472a130fa7eb740c995f39c7c882e5b587a23489dcce5c8c96423f5adc443fb", size = 23998, upload-time = "2025-03-05T01:15:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/b3/62/a8ef494256b4822b0f5a012fbad9d3e65d1e738a696bc9eb06e9d657a9da/fnv_hash_fast-1.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acb872d1026293814ac5ca31adb80fb898e8b4823fc19dbb38cff12f1adad18", size = 22903, upload-time = "2025-03-05T01:15:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/a9/47/258d6ce1e2540152859ecf3894de68463cffc2e6dfa446cf3b95534a89b3/fnv_hash_fast-1.4.0-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:64f1a35e860442a24c36503a5bf5442bb548fe2f4bc8c06ae7c873dd387d7e96", size = 20304, upload-time = "2025-03-05T01:15:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/30f1e8cd83ad2502a0928712c2925abcbc58935e0a92176b2b2cb8bce29b/fnv_hash_fast-1.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e609e8412520328c454418fd355ea6e558a66684fef9a91c55a1f90524977134", size = 23084, upload-time = "2025-03-05T01:15:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/85/46/0077cf8fae9d17077b9f5d0fa7b236b1ed9a68199f1de785f08f7255173f/fnv_hash_fast-1.4.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:66f37b3fb877fd0719eaddcb924375c2a5a3d1dde443857720d41f69fe0a791c", size = 20806, upload-time = "2025-03-05T01:15:44.294Z" }, + { url = "https://files.pythonhosted.org/packages/63/8a/247182a9e4b9d1843f406d33ec590f1c9e6340702d40edfb1a50cfa5f208/fnv_hash_fast-1.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c53060f9d50164040649489f19c11b1b89bd04a061320e1bd7d1b6af714c236b", size = 24829, upload-time = "2025-03-05T01:15:46.201Z" }, + { url = "https://files.pythonhosted.org/packages/35/ba/0986c9c16a8ff70e57e09d5750cbda0a355772c0c84c5818be8ea63d48c2/fnv_hash_fast-1.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9fe7aff4247cdc66e099b2bb5470cc2bceb4e1ae10be3adb89a2f0e83c7ef919", size = 23597, upload-time = "2025-03-05T01:15:47.282Z" }, + { url = "https://files.pythonhosted.org/packages/b9/df/42b524e0289b3f9d19e0e6e1343f4a00fe1f09a252ef394efe5d49ed4d6c/fnv_hash_fast-1.4.0-cp312-cp312-win32.whl", hash = "sha256:1db95120a50ebddd9bca8eaf8d20883334f3aa6f743d4651a0368058110a8ceb", size = 19508, upload-time = "2025-03-05T01:15:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/b6/64/e176358d760d2eed7366557dba235ac1fcb282afed3c52961ff2f2fa033f/fnv_hash_fast-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:72a61703e02fcd2571631a07f09146352ec6f08320ce1604a24cba54bf310642", size = 21244, upload-time = "2025-03-05T01:15:49.588Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/178cf5b827f0d54caa3fbb28e6e7493ad5e0d75a165b57f8c2fe9fcd3519/fnv_hash_fast-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f1bbfe718df3aa10aafa2c40cf6bb9581a267c914a4005fc0f5e3ce21a01a12", size = 18552, upload-time = "2025-03-05T01:15:50.836Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5b/88866809455c974b5cf8c8c18c2ad293df796083703c0da8ec6c4e50135f/fnv_hash_fast-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25dd05a0e7f8381b2d881b122400862da4f855b80fa9d6f5a20cda0706fde9b7", size = 18585, upload-time = "2025-03-05T01:15:52.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/60/effbe5965ab6da6e6805ca18729f588bee486b8e4127b8bf52037908a3a3/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1038ab67c143f1119b2cad9fb3d909439e88f72f3b137015eb642ad91245734", size = 21761, upload-time = "2025-03-05T01:15:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/d80f730b9f535cec9a6db8c11e9e38413e0a398d9e7f2e0abde80b3587b3/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c5ae87deee0204f2aaabfc861bf322f0f1dea078c847e10e35bc67c7becfefd6", size = 23126, upload-time = "2025-03-05T01:15:55.387Z" }, + { url = "https://files.pythonhosted.org/packages/ce/70/0c795f5a92a58f6d105078c632a9a5cf1de4bcdd42d3fec20592ed7c59f4/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a07b27755378f08e399fc124f724f3af58f8f54167f6fd525068e51ba7e9e9f", size = 22152, upload-time = "2025-03-05T01:15:56.579Z" }, + { url = "https://files.pythonhosted.org/packages/9f/72/14a8bb3844037cc6afe0ba147020ba2d450f44d29a32a0d2863583822ee2/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37cb8e67d8d4df670a699d928a4ed74b7284724d73df736a8ff9f57178e6a720", size = 19755, upload-time = "2025-03-05T01:15:57.696Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f8/7cce1f63cd07c0c3d998ddd5f193b56247edd0ccb049c7549d05f559c889/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:5fa7945986ae71c68eef522335d33a671ed2c33952272ea4360d0c44331f90eb", size = 21792, upload-time = "2025-03-05T01:09:23.45Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/5e3d38f4ef3c7c862ba8cbda1d64b48b8ba314d937fe967815ee8e1d85bb/fnv_hash_fast-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e6dd8f20122d0fba69171858438eb74f1f12f4630178ab0db9d05bd0dfc68054", size = 22437, upload-time = "2025-03-05T01:15:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/89/85/bb90b080e30c4e082e3120874e218956a0b8a78c5913139b38c8056be4da/fnv_hash_fast-1.4.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c410adea6d70c8663a168b1f499bb3cb9ff743675921aa8b6fb99e07cd83eb45", size = 20259, upload-time = "2025-03-05T01:15:59.894Z" }, + { url = "https://files.pythonhosted.org/packages/83/00/788bb2e0f369c543a8bdbc5621a7070f31b13ee6992420e198636b42276a/fnv_hash_fast-1.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:579e5ba4265f4fc41dbe6fbe12d133d22aa7948455ef8a16fbe7bc5d1666d6d8", size = 24059, upload-time = "2025-03-05T01:16:01.121Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9d/ede35e832d6801ae8373abc8926c631cb6739e7d5e432366be44a9bf98d2/fnv_hash_fast-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c9568b00ab3e91af8a8409c2955b5d781f9a338c0c00428e788ff77f3baac3ec", size = 22927, upload-time = "2025-03-05T01:16:02.228Z" }, + { url = "https://files.pythonhosted.org/packages/df/5c/2aa4c0b1b1648d80567499062bfe0414384df2487e794885d5ff4dc235a4/fnv_hash_fast-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c04e54d919b5e0ef2cb6a2de0fbabb3d075ee2609324a678d2471c87542bbacb", size = 18938, upload-time = "2025-03-05T01:16:04.28Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7a/a666e222003f52941aa4b620f2658cc1f665a84657886e584a69613bfb58/fnv_hash_fast-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:5eec9b18aee7ed014ba0b8a2cf59d5c2f2d83af683c4ad87e9c03dd2f4f5d573", size = 20453, upload-time = "2025-03-05T01:16:08.226Z" }, +] + +[[package]] +name = "fnv-hash-fast" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "fnvhash", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/19/10f4e1b4bbfe7cf162d20bb4d54bd62935d652e2ea107ddb0b5a6c4e8b75/fnv_hash_fast-1.6.0.tar.gz", hash = "sha256:a09feefad2c827192dc4306826df3ffb7c6288f25ab7976d4588fdae9cbb7661", size = 5675, upload-time = "2025-10-04T19:35:00.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/8e/a250c545faccf828772830ae3db234ad8741ed57832d96b3aa9d6ebe8616/fnv_hash_fast-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8054c711283a4d598d516c17445d6c12304c93e48f7e97aba970549e0d5b413b", size = 13373, upload-time = "2025-10-04T19:45:09.032Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/fb0f0e516a46c2288f6dd4afea0ba1c7c3065ac0565fc12d9c662c7d2434/fnv_hash_fast-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:135bc62ec5d61a9a38222afce83d3d0c9d4d52fa8a2670f6acc86ee6818d2bfa", size = 13901, upload-time = "2025-10-04T19:45:10.25Z" }, + { url = "https://files.pythonhosted.org/packages/22/76/f4c7c8784b836da32fa84b1ef78455b3d26950c46da99cfeffa9dfdfbb95/fnv_hash_fast-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34cd7e776fd515dfcab4cf84bdb63d879653cc3f77f6f086822ed9c50645f75c", size = 15210, upload-time = "2025-10-04T19:45:11.198Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/a8c154b9b1fae5ab2ded2a389efef18c7332d93a1ab260f5cae02e34b3d8/fnv_hash_fast-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9189a9a1b820658c791728b4558b4aadad6b35429b1784bcaf57183788d3f489", size = 16299, upload-time = "2025-10-04T19:45:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/ee/14/674c4d4ac7bcc9145f351707d2d944c76c7bdc955700707468854e6cf00b/fnv_hash_fast-1.6.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893306251f69bf9591b51e75dfac2bac9703c4156b9c7e78dd2930e33b184f1f", size = 14208, upload-time = "2025-10-04T19:45:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/73/90/54752bc1cfcc281aef379c5926c00b127006e6a4197c701796dd382f81c6/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7107073d69dcf68b602bddf37a6ab2af6e9afcfc31239e215b75dcc5049667ae", size = 16197, upload-time = "2025-10-04T19:45:14.876Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4f/971d08ab32e41d7cc4255dc0d2e94be8b907def305b45fbe9c62f1c75db6/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f0b9cbb8538e981d4e0895f01af6510bae5dd6bcb1a8bf3db8cecd9877079e66", size = 14634, upload-time = "2025-10-04T19:45:15.822Z" }, + { url = "https://files.pythonhosted.org/packages/16/bf/ff169383b41176a15828aec88a39a8b1397041e6d3948273aec010688712/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11b93131512147038d54c92106a4035b1ae71488abe730952bdeb55a9d7dcb18", size = 15542, upload-time = "2025-10-04T19:45:17.064Z" }, + { url = "https://files.pythonhosted.org/packages/84/9d/3d98e9ab97db03666ddd9b98f5ae4b45cc3e5746e8987f7513b54ae49a50/fnv_hash_fast-1.6.0-cp312-cp312-win32.whl", hash = "sha256:df7666d14e01352a22351344fdbcb8916b873ea91d598b53e07736b0e64fb66c", size = 15140, upload-time = "2025-10-04T19:45:18.283Z" }, + { url = "https://files.pythonhosted.org/packages/b1/14/d629342b4b1ff8b2daaf8fdba74103b5786a69e071bd525b9c612b2fd3f5/fnv_hash_fast-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:60d7c0a89ee63076de139f0b619f5cc55378f3c4ed67488dde456dbf93479530", size = 15971, upload-time = "2025-10-04T19:45:19.219Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/c73abc05dd01434442dbd38a2e50166e9ba59f8db41cdf82649410c37d12/fnv_hash_fast-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34e4f2acc41aacd97877d396948b38efc7197a2dd91c15e818c049c4d48b0a0", size = 13350, upload-time = "2025-10-04T19:45:20.184Z" }, + { url = "https://files.pythonhosted.org/packages/75/f8/a79d5a29dcf3b0e41635056ee37fff9e2bc46e3625d44b163a4ac2b9160c/fnv_hash_fast-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1a1fe55163d38052ec90aaf16f190bb807342aa09f9680185b9772ce0407b62", size = 13864, upload-time = "2025-10-04T19:45:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/c1/41/fabca5bf0c5b36405517908974e93f1832780d692271295efaf8dba40afc/fnv_hash_fast-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d7c3a18e7aa483d18ff569554b07b5238403775f8e401245ab8b3c27bcb34cf", size = 15206, upload-time = "2025-10-04T19:45:22.242Z" }, + { url = "https://files.pythonhosted.org/packages/04/7f/1c5c4e451c0213b44235b39737cecf3e58f4195332b173f45e2c95a9b0d8/fnv_hash_fast-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8fdeee59431cc03afdb8a04c3c46b452dc2ded85973953b7077715e897a85b", size = 16301, upload-time = "2025-10-04T19:45:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7c/095bb6f7ed9bbb85d7451312388fd61dfcde194aad5a2e3902e8fd908a78/fnv_hash_fast-1.6.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6d8284c7ad0339def03252905f3456195ec9d77d8329225e5b09b226e3eb79ec", size = 14175, upload-time = "2025-10-04T19:45:24.397Z" }, + { url = "https://files.pythonhosted.org/packages/73/52/ced7073eaee3479b4e09fed73c1db5a51a9bc72f7546324126b76b4c2f9b/fnv_hash_fast-1.6.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:03642803cc4567dada952d7b1490d6eedd97cd960a83ebbb4a4b7c545629f33f", size = 14581, upload-time = "2025-10-04T19:34:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a3/877d7f9bce7efccb70607307b25abec35f1206f5dcb3b5a898ad67d61dbf/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3a540bae99086d3942a2976c16480916cb86d9f06a632023176fe4fa56d298b5", size = 16214, upload-time = "2025-10-04T19:45:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e5/f26eb6e262a8d2329aad6d618b102c238009a11e00d6c5914cb510d1d968/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9d6a447404ddfc0035a52de80747c36dce8ba0cc24c27610ca4be9c0ba46d783", size = 14649, upload-time = "2025-10-04T19:45:26.568Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/6791aa693e9400d00ef56be40586bc9de7b826756f0156a3f4e5b3b6d40b/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d016ee85cd9faccb2f958e5017eb60c8c6410b1700f85052f5dbf2b34084c7ef", size = 15554, upload-time = "2025-10-04T19:45:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/be/4e/65ce211d9cb8333fddba5b38a18014b2928b4b7a5678d8501cb764a89285/fnv_hash_fast-1.6.0-cp313-cp313-win32.whl", hash = "sha256:9a3751dc38c33b0be4fc4a5a5947ab6d9acbdb1017dfeff55ab3d1fa3ed6c03e", size = 15103, upload-time = "2025-10-04T19:45:28.391Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d8/8ff48a6beec92576d9fbae2b9b69db61503f062fb3bff4921495323a6847/fnv_hash_fast-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:1e8fb4c1cd62bc8d559dabeaf69fb25ba647232d980ffdb8e5f679d4aef8d03a", size = 15956, upload-time = "2025-10-04T19:45:29.418Z" }, + { url = "https://files.pythonhosted.org/packages/ef/17/9c724ac795f53578dd6be61d6a0466c4cd51550485b301764ddfc6ed5ad1/fnv_hash_fast-1.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:07bb79eaa44f91db2aab3b641194f68dc4ddd15701756f687c1a7a294bfa9c06", size = 13296, upload-time = "2025-10-04T19:45:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/0c/11/a2eb0a7fbfb5d5cb5d27df7f6d4e395ce2f328da16d32702909af00ffe82/fnv_hash_fast-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4176315430f9fcf5346a0339b0f55982e1715452345d70c2887755bfd5aa2b64", size = 13879, upload-time = "2025-10-04T19:45:32.063Z" }, + { url = "https://files.pythonhosted.org/packages/0e/85/3a297faae2416916f7a5cb858b08b500296bbc7d7136faf2cfbadde61e33/fnv_hash_fast-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31db9d944c91d286475870855b9203f4fb4794cb0674de5458e9d1231e07f37", size = 15222, upload-time = "2025-10-04T19:45:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/9c81426e4a22d15dc9c1a73536c6a7e2aeb8a71ac0b398d841ebd287e8e5/fnv_hash_fast-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fc1bbec5871060c6efa6a444a554496f372f1f4a7e83b99989be5ea6b97435f", size = 16379, upload-time = "2025-10-04T19:45:34.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/7f1454ebc9dee224d6ee5360111e3855802ce79f48f1808117998771ffaa/fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:91ed6df63ab2082b5b48a6b8f5d7eb7b51d39c2eeffd64821301bf6d9662ff11", size = 16252, upload-time = "2025-10-04T19:45:35.243Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/cedd70c2e09ba09f5834c7e50f8fed4a37bba38c0c2471849bb4dac91148/fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6d34541e15bbc3877da7541f059fb1eadf53031abe7fc4318b28421e02eff383", size = 15570, upload-time = "2025-10-04T19:45:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/9c68ad33e254af9809bbd504b9895a93cb67472fc39bcd656f02c2703637/fnv_hash_fast-1.6.0-cp314-cp314-win32.whl", hash = "sha256:74320b9033c13e851174edf959c167619907eb985176e795d17d7fbe29cf3a45", size = 15484, upload-time = "2025-10-04T19:45:37.392Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/8ead2c631323c8a755c8437641e832ba2eaf27bf2577535cf40d57b62def/fnv_hash_fast-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:540670ff837824939d2af90dd89cddbd02d238d778999a403cdb4a4de8c65a73", size = 16345, upload-time = "2025-10-04T19:45:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/da/7a/b5bd2b9a06269098af059e79e05ceff320a405b1c49b9f3d29708324179b/fnv_hash_fast-1.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:83aa2d791193e3b3f4132741c4dc09eed4f7df8000d76ad77fb9d24db8e59a88", size = 21338, upload-time = "2025-10-04T19:45:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/21/07/1688d543a7688529857cd43bcff3ac324c69fd2923a9b40a1adc120cef20/fnv_hash_fast-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b8d33f002bb336f9f0949a32d7da07cc9d340a9d07e4f16cc9ece982842eb4e0", size = 22455, upload-time = "2025-10-04T19:45:40.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/f7/588f43d8dd122fc884c3556f993a3e3db953afecc62fa812d439f69ec067/fnv_hash_fast-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0042af2a1cb7ffae412ec3cb6ae8c581a73610fd523f7e17ed58a5359505ffec", size = 25053, upload-time = "2025-10-04T19:45:41.74Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/6209457f59e0ff43b066ca8cbfeb800bc0af478e221e74beadaf0b58effa/fnv_hash_fast-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73308e11c0e5a2dba433fc5645672de4756a52b323de1dab20e45d4fe5e83994", size = 27875, upload-time = "2025-10-04T19:45:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/c4796f6b1ee6cb778620663d00eadb970a8271fb537ce75774d5acfeecdb/fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96282ecb75bec190af0111e82ddd38afc98e9cb867a1689e873ab6802af951b7", size = 27443, upload-time = "2025-10-04T19:45:44.274Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5b/846b8f977dda4f0e7f1ec4ffff6707b9e666dabb9eb203c4c2bfc4b0b6fe/fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cae16753c1d85ed358df13824bd8a474bfa9da34daddc1a90c72b25ff4177f51", size = 25765, upload-time = "2025-10-04T19:45:45.565Z" }, + { url = "https://files.pythonhosted.org/packages/2f/98/1371f0a765a3160a4c864de1b6d5ea696ba3ca822e3cea74357e15aca85d/fnv_hash_fast-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:e2efb5953475a5a0529ca9757d6782c5174a3b8a3fbdc4e1c1273ac1d293316b", size = 26343, upload-time = "2025-10-04T19:45:46.566Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/99586ce163eeead7373db6dc3aa01998c42211ad11bbd7f6d21824fc5c80/fnv_hash_fast-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6eb03cd17c134d412fed9f05dc6f9ff9a8aa3b8e69c0135603a521e77720c93", size = 28057, upload-time = "2025-10-04T19:45:48.033Z" }, +] + +[[package]] +name = "fnvhash" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/01/14ef74ea03ac12e8a80d43bbad5356ae809b125cd2072766e459bcc7d388/fnvhash-0.1.0.tar.gz", hash = "sha256:3e82d505054f9f3987b2b5b649f7e7b6f48349f6af8a1b8e4d66779699c85a8e", size = 1902, upload-time = "2015-11-28T12:21:00.722Z" } + +[[package]] +name = "fnvhash" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/43/30d2dd2b14621b2004f658ba5335e5a6f5a9c1338ed37678d7fd247b7a9c/fnvhash-0.2.1.tar.gz", hash = "sha256:0c7e885f44c8f06de07f442befebc590ee9ca0cc88846681f608496284ce9cd5", size = 19057, upload-time = "2025-05-05T16:59:10.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/92/7c8abc21a1de7159013c0b0bd2ecf06530959bb14fd5c3bf0045e788c6d9/fnvhash-0.2.1-py3-none-any.whl", hash = "sha256:00fab14bec841e4cb29b4fd2ed9358f8bf9f4600d9d8149cde27a191193a33e8", size = 18115, upload-time = "2025-05-05T16:59:09.269Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "ha-ffmpeg" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/3b/bd1284a9bc39cc119b0da551a81be6cf30dc3cfb369ce8c62fb648d7a2ea/ha_ffmpeg-3.2.2.tar.gz", hash = "sha256:80e4a77b3eda73df456ec9cc3295a898ed7cbb8cd2d59798f10e8c10a8e6c401", size = 7608, upload-time = "2024-11-08T13:32:14.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/66/7863e5a3713bb71c02f050f14a751b02e7a2d50eaf2109c96a1202e65d8b/ha_ffmpeg-3.2.2-py3-none-any.whl", hash = "sha256:4fd4a4f4cdaf3243d2737942f3f41f141e4437d2af1167655815dc03283b1652", size = 8749, upload-time = "2024-11-08T13:32:12.69Z" }, +] + +[[package]] +name = "habluetooth" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-interrupt", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "async-interrupt", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "bleak" }, + { name = "bleak-retry-connector" }, + { name = "bluetooth-adapters" }, + { name = "bluetooth-auto-recovery" }, + { name = "bluetooth-data-tools" }, + { name = "btsocket" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/89/0da109c6ed1704c991f9d466a3dbee99f8a921d006d6f44eac84fff95da7/habluetooth-5.8.0.tar.gz", hash = "sha256:7ecbe1ad6a4d3610f918dbe573bb9bee16064e7a4a61c95c37ef22b0c4533493", size = 49177, upload-time = "2025-12-02T19:30:53.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/e4/5f41bb8367db0f704bf27b7a346dda3e0e4965b1a2107c33af1078ca5271/habluetooth-5.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c5bd5ac723d2b38f5d56506408d56fa1de0f9a063410b6ce8061961849a1b63", size = 570041, upload-time = "2025-12-02T19:47:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/4f/20/22dea6dd139f6addf78d02bbd840ba565353aee29ca812a2488b14b6b4ff/habluetooth-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e920fe6dd4fbb601f22043c205d3baaae5ac9804206d16a424cd19e00493d88d", size = 563353, upload-time = "2025-12-02T19:47:28.371Z" }, + { url = "https://files.pythonhosted.org/packages/39/e0/e2ddf93dbe2cbd536c9d66c7189ce394ca40133ab84420748e94afda7be0/habluetooth-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1086a8b76364e8c008b3ddc54e420f0aa74187231edfa987d362912cae339350", size = 656230, upload-time = "2025-12-02T19:47:29.712Z" }, + { url = "https://files.pythonhosted.org/packages/0b/af/be98ead575eced913fa396fd985ed47786db20ae11f9e1a3cc954161be40/habluetooth-5.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1702528676be2ebe17a2bd357fe7dc09f2855b70fd83de64392f040b18f404c5", size = 643444, upload-time = "2025-12-02T19:47:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6a/cd096b7c405b867a1864b5326c4a250942c8fa734ae319ba0e0ba485aab4/habluetooth-5.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b875a4de30861d3a9359c154fa21532c235d337ca48d203ea0dc74df3e60885c", size = 700091, upload-time = "2025-12-02T19:47:32.281Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3c/aebb70fdad26c7f547773cd95d381610606e0bd36c45bfc7652f593f08ac/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c184a80d342f01087478ba643e5e35e04c57a364e55b7f3092742d98a18707b3", size = 664128, upload-time = "2025-12-02T19:47:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5b/22ecdabd000c0cd1e9984f0dec257549b96e011a978b973797e024cc9063/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e431d2c833c83e748490b3d2bb37a4625d36c89e7d3faac03286b916fca910e3", size = 647860, upload-time = "2025-12-02T19:47:35.671Z" }, + { url = "https://files.pythonhosted.org/packages/88/ac/7e87cbd3f5a34361d37b23c8ec8ed48bea7c81246f01db88e2b41ad7f752/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cbbe296186c7e66ef79dca12e4e69326f8748db405e7203451119c9d7c1e5a2e", size = 705598, upload-time = "2025-12-02T19:47:37.102Z" }, + { url = "https://files.pythonhosted.org/packages/a2/36/bd836cb9943eb0370c4fd96c5db44d9349e7320a767384bc5afb2a11a075/habluetooth-5.8.0-cp312-cp312-win32.whl", hash = "sha256:7ad8929500e12df7860ed5ef1b984ead31484779c33f61d641ca5adc7677f52b", size = 455051, upload-time = "2025-12-02T19:47:38.765Z" }, + { url = "https://files.pythonhosted.org/packages/cd/61/2c8fd5a43e31b87247863efe5e4e60232accf002f288ccba7ca5cf97f6b9/habluetooth-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e039e84b02b7d31e9f7ab7b15937dfaa47f39b31bfb73b04772cf3e378d23430", size = 525980, upload-time = "2025-12-02T19:47:40.324Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/6bb983149530899647ee0807e5fc46124b56f0b326c6a952df48ed5c2b21/habluetooth-5.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8a88e3f586427642355927d43ad80190964589ae99d0d579ac4a39395d60f9a7", size = 566214, upload-time = "2025-12-02T19:47:41.767Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/18ff974967494402f827dab3d69d9a7d51d7d62153d8872206c0d4c4b058/habluetooth-5.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bb028a5d46fdead2ad687182db10667da201f54cc0f8eeba54870a3c2d1797f", size = 560005, upload-time = "2025-12-02T19:47:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d1/77ca3c8b3213f1f8429902329cad1b2e2a637cd32df70748ff5e187aad3b/habluetooth-5.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c425044ee873d5730571601097bb55020a7ff293cf96972e8924cf1719aa5345", size = 656376, upload-time = "2025-12-02T19:47:45.138Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/b06804e511552f1e5553b5abfb48f7bec795ffbe6c3290d95abfe35e51b4/habluetooth-5.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dc8baf5eee8835cfe436b14a6b4dd087f8134a8fd5d6ec4072ea992ab5aaae41", size = 639827, upload-time = "2025-12-02T19:47:46.447Z" }, + { url = "https://files.pythonhosted.org/packages/ab/34/88fde875eb1df89f870aa11aa1b0ab8e1cfca104cd8e9af802dd831c4068/habluetooth-5.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d56f3c73f5c74bfc3817a56343f5ef38ef4e584b825cf1d0d45e148b8fc2ce20", size = 699639, upload-time = "2025-12-02T19:47:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/74c352a234cf9faca007e1d43af79493de8ce19416b9bdddb9d2894d801b/habluetooth-5.8.0-cp313-cp313-manylinux_2_41_x86_64.whl", hash = "sha256:b651fa1d34a4086bd4bab27e528a0ea11dc310e806e86bd877c1b77a8b58ff7c", size = 698468, upload-time = "2025-12-02T19:30:51.699Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fe/8a28d94411c0cc19409091692ec893a603d71e66ada35c2c5db6c716f265/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:468e04e601a457d0097f57a2b16f65debfe4ed8b219270d52ebfba5cf0051b5c", size = 663619, upload-time = "2025-12-02T19:47:49.452Z" }, + { url = "https://files.pythonhosted.org/packages/7a/be/fb3d81a1cad478120f6ea7d5085eff98e1336331fbf1a7ceca11ca0a6982/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:91d34f82e36a2292aac5465db2c5b268d31df25303870af6687ad824275648bd", size = 647900, upload-time = "2025-12-02T19:47:50.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a0/1a738c6675d41193e80defe425f630c5a4db6da4ef5ee4cbce60fb998a28/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b25442a1ab5ba73c7da0726491a8a96637a546433f45d39f195415219bf11bef", size = 705834, upload-time = "2025-12-02T19:47:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/ea/78/5bcd6c57f963a6b26ff725e32e8b0183a5a12e1e09f583e5bed481622555/habluetooth-5.8.0-cp313-cp313-win32.whl", hash = "sha256:ad59ce59ea06750aa7bb7905421db8654216a6b4a535d18f65b3aa9c23f892c0", size = 454143, upload-time = "2025-12-02T19:47:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/46/99/519e5030747e4c3fed7ef3789a5c04e507208c7ec9ed5ec8007549c4f173/habluetooth-5.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a12f07f7d0c770da9586e6c50a444553c2921766a51281e383a4ad23dfdbe2b", size = 524424, upload-time = "2025-12-02T19:47:55.289Z" }, + { url = "https://files.pythonhosted.org/packages/00/9c/a219114293d6d0b83d2c2b3cb505125de96edb73037b9f4f71c203e0b231/habluetooth-5.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12148fb6c41a4464c1336cd5bc127ed1c97cbfd0f2bedb2dbb16fecadd54e1c0", size = 568992, upload-time = "2025-12-02T19:47:56.921Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/5783e448e310f235550ef5b682d0518ea196e19f82484815943fee69640e/habluetooth-5.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7962af2e456676df1c09eb0e7a95b5c27696b76a82c2971a0dff25f017168453", size = 566035, upload-time = "2025-12-02T19:47:58.294Z" }, + { url = "https://files.pythonhosted.org/packages/97/60/80ea750a3af4d84455315eb038100e49419d8c5b8b14bb966a46ba8a8971/habluetooth-5.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84abc0819e462e6210620778ff9cf6ace8df04b6e9c04f41dd56abdaf9dade72", size = 666209, upload-time = "2025-12-02T19:47:59.656Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/0b45dfffbabfa5982e9721946e43f2fc7ff982c8d6598cb148c527797983/habluetooth-5.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a74652fe7ae5cb571138832cdf16945d17a9dc564e7c48e0c2173ca9796a7e90", size = 632911, upload-time = "2025-12-02T19:48:01.028Z" }, + { url = "https://files.pythonhosted.org/packages/25/0c/e3c9158eb988917f00fc93001a86bfccdf59c383ad95d90a5f897121bf98/habluetooth-5.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ddbe3e7c03d5df6d6064cd622d2235f1db2cb19282705d48393535ed1f3547b", size = 703092, upload-time = "2025-12-02T19:48:02.358Z" }, + { url = "https://files.pythonhosted.org/packages/1a/13/79858645697ebd3b87819d8b08a168d49532dccf38a0d8a66def0c6744b2/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c5f650be8b36e47016f418c73b36011754c742c0e4b8975b178a99fb40f94900", size = 674337, upload-time = "2025-12-02T19:48:03.643Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d3/55793994bd8500e459e5b5ecd1290b77c87d978df4a93a08abe031c353fd/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6c0ffe030bef47c268c658c2e0a1dcf8db3841727761f617953caee2831af6c", size = 637932, upload-time = "2025-12-02T19:48:05.126Z" }, + { url = "https://files.pythonhosted.org/packages/60/bf/118c76a8fda261ae42d2d15696c4277ec1a1c4da47c5ab030f7b50cce53b/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2248235680aa591bcba3fd626f859cb0fd89d84e03f1a596cf281d6802ba3d1b", size = 709054, upload-time = "2025-12-02T19:48:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b5/15a3807d972658bf22ee90f7bcc91ed8b470c2f9615f3c43eed836174bc9/habluetooth-5.8.0-cp314-cp314-win32.whl", hash = "sha256:56718953e05300e633f1f1f4588aa2b81ea327dc276228d21d0472038596352b", size = 463435, upload-time = "2025-12-02T19:48:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dd/5f761526da52753c14f1852137cd14cd9e2372266589eeda9d6ab7d09eaf/habluetooth-5.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:b4f48cf6485a39ae72eb3ce68b46ee5b991ab28fdb4c5daea97bfa2dfb17e432", size = 536193, upload-time = "2025-12-02T19:48:10.325Z" }, + { url = "https://files.pythonhosted.org/packages/46/2a/e8d31c5a53c1aa9c2af9bbe1760ee1976b9642828d31b9b8b5aa4a01245b/habluetooth-5.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aded66250091ca6f05740317b295197d9387ca88bcec138e01f7fbd145d3406c", size = 1124956, upload-time = "2025-12-02T19:48:11.74Z" }, + { url = "https://files.pythonhosted.org/packages/39/a0/41ebef6913efcfe1ebe22c62423a013d2978c0a890bb849f4702ab8fbee5/habluetooth-5.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c3a5dc4f2e01931fce07018ccc46584483c44fc24369098ec8af253def6421db", size = 1123851, upload-time = "2025-12-02T19:48:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cf/68ab568ac01c054d32ecbd4790fceafd8587d22dd3382c441a905d993e29/habluetooth-5.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289af0d5a67bef760203d3cbdfa9e9aa9cc1f7da29112dbc50474a16f3d66735", size = 1275887, upload-time = "2025-12-02T19:48:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/3e68a905858e9945027b49f872bd6d6c92a916fd26f1c9c16e0b1056609b/habluetooth-5.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d96008b6c09d3617016801aa4dba74d065e118d9a4cb84e913e16fa3f379e597", size = 1192706, upload-time = "2025-12-02T19:48:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/d7483c7e57f0c34acb6bfb83fd32bac2c31ffbfc111b7511dffd1610c752/habluetooth-5.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e569435c561e79267dd6b2e0718d5411c1face47a810bb1d8fe42a7a9a9d8ee4", size = 1335960, upload-time = "2025-12-02T19:48:17.911Z" }, + { url = "https://files.pythonhosted.org/packages/27/6d/03fda3288621b9ba3221f37b1e2c877b2e73fd0f82591f751a058e83b2ef/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:60b5eed41f69ce6ca6c98df7c5f3ba1185737f2008225cd19e43bef0dab55074", size = 1294245, upload-time = "2025-12-02T19:48:19.437Z" }, + { url = "https://files.pythonhosted.org/packages/d3/df/8d2df48c24c175f2dafa7026f8fb0f24793e69cc4e73f01f51ac5c5480c7/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:63b6d82223d64d19cd2feb23fafa7246b42574c628014ce21b010bb31fced5f1", size = 1215522, upload-time = "2025-12-02T19:48:21.197Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/5c0f86a9f35593102ca2a0a7fef6943e1400a0ea97fa9f296aec945b5992/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a106c1809516c86c7ff10a78ac68017dc23f73e5613735295eac628ca95a97b", size = 1350369, upload-time = "2025-12-02T19:48:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/ec8ae0810fafb3ff5ff00e312f0b99da91f22e547e2bc2b453f730b399ec/habluetooth-5.8.0-cp314-cp314t-win32.whl", hash = "sha256:ab75ee35c050d872377afd9ed48372538b218edc52a911aae32c5e58c5ae896b", size = 958959, upload-time = "2025-12-02T19:48:24.521Z" }, + { url = "https://files.pythonhosted.org/packages/fd/92/32d8279f955c2e5dd216a2a13ad4953bb1fd9a62512b280458dab07ad088/habluetooth-5.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:635ee560a003f884230d9600e536da4466bd632ae90572a454498b4795926271", size = 1129467, upload-time = "2025-12-02T19:48:25.979Z" }, +] + +[[package]] +name = "hass-nabucasa" +version = "0.86.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "acme", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version < '3.13'" }, + { name = "attrs", version = "24.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "ciso8601", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pycognito", marker = "python_full_version < '3.13'" }, + { name = "pyjwt", marker = "python_full_version < '3.13'" }, + { name = "snitun", version = "0.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "webrtc-models", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/4a/46bdeada82c7ea394c47c3d33a8b50a53b5c45330b8b26f54fd304161ec7/hass_nabucasa-0.86.0.tar.gz", hash = "sha256:d6951af95796a92a6e7e6d64f7ddac1ecbdda65f3d65374c8f3216650a579b89", size = 65176, upload-time = "2024-12-05T15:50:35.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/97/0691cf7e92fcc283b2ce50bb658de51b2a8fe8121b7129fec56c5200fa8d/hass_nabucasa-0.86.0-py3-none-any.whl", hash = "sha256:75153e438a451ea9653304f6011719974911325bd7dca5f0ba6297491901c8b0", size = 55818, upload-time = "2024-12-05T15:50:33.556Z" }, +] + +[[package]] +name = "hass-nabucasa" +version = "0.94.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "acme", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "async-timeout", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "attrs", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ciso8601", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pycognito", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "snitun", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/33/ac792655794278f3d429b821a5a6f4628eb9de75fdacc222da0a13cfc9a0/hass_nabucasa-0.94.0.tar.gz", hash = "sha256:2ae8ca877dbd7c128fd49f64383e69bd86a395ed175cf73d6f33478d07491947", size = 72357, upload-time = "2025-03-03T10:54:57.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/cd/fabe44c4513600e153082e15a0b98c69d728e8fa0ed5bac7658b330f8979/hass_nabucasa-0.94.0-py3-none-any.whl", hash = "sha256:5acbe999373b81e7f6cc8d2f5918fe9db0a9e18e52e6ddb19d7857818c039c46", size = 61929, upload-time = "2025-03-03T10:54:55.93Z" }, +] + +[[package]] +name = "hass-nabucasa" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "acme", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "async-timeout", marker = "python_full_version >= '3.13.2'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13.2'" }, + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "ciso8601", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "grpcio", marker = "python_full_version >= '3.13.2'" }, + { name = "josepy", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "litellm", marker = "python_full_version >= '3.13.2'" }, + { name = "pycognito", marker = "python_full_version >= '3.13.2'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13.2'" }, + { name = "sentence-stream", marker = "python_full_version >= '3.13.2'" }, + { name = "snitun", version = "0.45.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13.2'" }, + { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/63/132c981f9615db0681d2a08a6af3294b6568af833df7d5075368f41b9518/hass_nabucasa-1.7.0.tar.gz", hash = "sha256:a6d25a02a538e316625ea48c44e70def10687def7b05e08da91da72004dd3ea4", size = 106571, upload-time = "2025-12-03T12:24:40.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/9a/c3295290c3f616cd2271157971357e085a50d858a1bed6862ecf13a967b9/hass_nabucasa-1.7.0-py3-none-any.whl", hash = "sha256:2256974ce3f9b4f170ed19e902427557d08ea2ac2b88c5feafd6a572d3e552ed", size = 82666, upload-time = "2025-12-03T12:24:38.71Z" }, +] + +[[package]] +name = "hassil" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "unicode-rbnf", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/f4/bf2f642321114c4ca4586efb194274905388a09b1c95e52529eba2fd4d51/hassil-2.2.3.tar.gz", hash = "sha256:8516ebde2caf72362ea566cd677cb382138be3f5d36889fee21bb313bfd7d0d8", size = 46867, upload-time = "2025-02-04T17:36:22.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/ae/684cf7117bdd757bb7d92c20deb528db2d42a3d018fc788f1c415421d809/hassil-2.2.3-py3-none-any.whl", hash = "sha256:d22032c5268e6bdfc7fb60fa8f52f3a955d5ca982ccbfe535ed074c593e66bdf", size = 42097, upload-time = "2025-02-04T17:36:21.09Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "home-assistant-bluetooth" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "habluetooth", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/86/2e339e75b8e00c121e3de766718bc4363b3c41b3b0d6c9cb666479dddb3c/home_assistant_bluetooth-1.13.0.tar.gz", hash = "sha256:3fa8a0d05a844063501a37e0b98501337e7035623b345d5c285a778e9416fd93", size = 7760, upload-time = "2024-10-05T23:12:03.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c9/96c4491583c328773873887065960a538da90fc08aa2a7e81cfec738bb2a/home_assistant_bluetooth-1.13.0-py3-none-any.whl", hash = "sha256:caec3d6ced580d3bd015ab74a9ee7e91693650d0548637c5f294101167fc6e82", size = 7915, upload-time = "2024-10-05T23:12:01.65Z" }, +] + +[[package]] +name = "home-assistant-bluetooth" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "habluetooth", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/0e/c05ee603cab1adb847a305bc8f1034cbdbc0a5d15169fcf68c0d6d21e33f/home_assistant_bluetooth-1.13.1.tar.gz", hash = "sha256:0ae0e2a8491cc762ee9e694b8bc7665f1e2b4618926f63969a23a2e3a48ce55e", size = 7607, upload-time = "2025-02-04T16:11:15.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/9b/9904cec885cc32c45e8c22cd7e19d9c342e30074fdb7c58f3d5b33ea1adb/home_assistant_bluetooth-1.13.1-py3-none-any.whl", hash = "sha256:cdf13b5b45f7744165677831e309ee78fbaf0c2866c6b5931e14d1e4e7dae5d7", size = 7915, upload-time = "2025-02-04T16:11:13.163Z" }, +] + +[[package]] +name = "home-assistant-intents" +version = "2025.3.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/f1/9c13e5535bbcf4801f81d88f452581b113246e485d8ff9f9d64faffcf50f/home_assistant_intents-2025.3.28.tar.gz", hash = "sha256:3b93717525ae738f9163a2215bb0628321b86bd8418bfd64e1d5ce571b84fef4", size = 451905, upload-time = "2025-03-28T14:26:00.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/e5/627c5cb34ed05bbe3227834702327fab6cbed6c5d6f0c6f053a85cc2b10f/home_assistant_intents-2025.3.28-py3-none-any.whl", hash = "sha256:14f589a5a188f8b0c52f06ff8998c171fda25f8729de7a4011636295d90e7295", size = 470049, upload-time = "2025-03-28T14:25:59.107Z" }, +] + +[[package]] +name = "homeassistant" +version = "2024.12.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiodns", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohasupervisor", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp-cors", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp-fast-zlib", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiozoneinfo", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "astral", marker = "python_full_version < '3.13'" }, + { name = "async-interrupt", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version < '3.13'" }, + { name = "attrs", version = "24.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "awesomeversion", version = "24.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "bcrypt", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "certifi", marker = "python_full_version < '3.13'" }, + { name = "ciso8601", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "fnv-hash-fast", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "hass-nabucasa", version = "0.86.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "home-assistant-bluetooth", version = "1.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "httpx", version = "0.27.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "ifaddr", marker = "python_full_version < '3.13'" }, + { name = "jinja2", version = "3.1.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "lru-dict", marker = "python_full_version < '3.13'" }, + { name = "orjson", version = "3.10.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "pillow", version = "11.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "propcache", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "psutil-home-assistant", marker = "python_full_version < '3.13'" }, + { name = "pyjwt", marker = "python_full_version < '3.13'" }, + { name = "pyopenssl", version = "24.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "python-slugify", marker = "python_full_version < '3.13'" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "securetar", version = "2024.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "sqlalchemy", version = "2.0.36", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "ulid-transform", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "uv", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "voluptuous-openapi", version = "0.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "voluptuous-serialize", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "webrtc-models", marker = "python_full_version < '3.13'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/d8/973badc57e1e494c01e184b817bcea4c264c35a2cbefd18d5c845fbb5cc9/homeassistant-2024.12.5.tar.gz", hash = "sha256:3df79d8a02561cbbf606a1ce81579d0f2271c714a67477b60af3cd6025a2e9a0", size = 22691228, upload-time = "2024-12-20T11:05:11.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/c4/e2ffd3e6701f0e729d5ebc6b9e47061e4e7bd97c86872f57345d3493ea85/homeassistant-2024.12.5-py3-none-any.whl", hash = "sha256:a7b3e13ac8c2b93a79807c51e3715b36c8459c9cbddb8bf448d69d18a49b703d", size = 38984357, upload-time = "2024-12-20T11:05:03.426Z" }, +] + +[[package]] +name = "homeassistant" +version = "2025.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiodns", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohasupervisor", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp-asyncmdnsresolver", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp-cors", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp-fast-zlib", version = "0.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiozoneinfo", version = "0.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "annotatedyaml", version = "0.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "astral", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "async-interrupt", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "attrs", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "awesomeversion", version = "24.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "bcrypt", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "certifi", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ciso8601", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cronsim", version = "2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "fnv-hash-fast", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ha-ffmpeg", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "hass-nabucasa", version = "0.94.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "hassil", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "home-assistant-bluetooth", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "home-assistant-intents", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ifaddr", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "jinja2", version = "3.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "lru-dict", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "mutagen", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "numpy", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "orjson", version = "3.10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "packaging", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pillow", version = "11.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "propcache", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "psutil-home-assistant", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pymicro-vad", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyopenssl", version = "25.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyspeex-noise", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "python-slugify", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyturbojpeg", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "securetar", version = "2025.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "sqlalchemy", version = "2.0.39", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "standard-aifc", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "standard-telnetlib", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ulid-transform", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "uv", version = "0.6.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "voluptuous-openapi", version = "0.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "voluptuous-serialize", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "zeroconf", version = "0.146.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/09/9a651106e22f9ca121c6586106ab857e17da89db7258070695df231cbf59/homeassistant-2025.4.4.tar.gz", hash = "sha256:a8e84eb3a236271ffb19655e08127a77bef0dd10858ecbc5c4351b47ad1471fd", size = 25116020, upload-time = "2025-04-25T09:19:13.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/a9/2e778738a2c4e45d795a076796b341e8af6d3919e591b0fafa1ff2564702/homeassistant-2025.4.4-py3-none-any.whl", hash = "sha256:b7970fe3ce3a9d032a3a5dee3cbf71b227e7f23fad14a23855ee252542ef2631", size = 43206054, upload-time = "2025-04-25T09:19:07.669Z" }, +] + +[[package]] +name = "homeassistant" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiodns", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohasupervisor", version = "0.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp-asyncmdnsresolver", marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp-cors", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp-fast-zlib", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiozoneinfo", version = "0.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "annotatedyaml", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "astral", marker = "python_full_version >= '3.13.2'" }, + { name = "async-interrupt", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13.2'" }, + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13.2'" }, + { name = "awesomeversion", version = "25.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "bcrypt", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "certifi", marker = "python_full_version >= '3.13.2'" }, + { name = "ciso8601", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "cronsim", version = "2.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "fnv-hash-fast", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "hass-nabucasa", version = "1.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "home-assistant-bluetooth", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "ifaddr", marker = "python_full_version >= '3.13.2'" }, + { name = "jinja2", version = "3.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "lru-dict", marker = "python_full_version >= '3.13.2'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2'" }, + { name = "pillow", version = "12.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "psutil-home-assistant", marker = "python_full_version >= '3.13.2'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13.2'" }, + { name = "pyopenssl", version = "25.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "python-slugify", marker = "python_full_version >= '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "securetar", version = "2025.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "sqlalchemy", version = "2.0.41", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "standard-aifc", marker = "python_full_version >= '3.13.2'" }, + { name = "standard-telnetlib", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, + { name = "ulid-transform", version = "1.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "uv", version = "0.9.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous-openapi", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous-serialize", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13.2'" }, + { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "zeroconf", version = "0.148.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/96/b2ccc2497cda9085f53106a01164da3eed57f6065f05bdcf0a3c0167e1a2/homeassistant-2026.1.0.tar.gz", hash = "sha256:0faa7fa4c37b8ccf1eedc91794c69042f8f8559f2e7c205f1c279068262bdec6", size = 30167957, upload-time = "2026-01-07T18:15:25.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3f/57e3c7e3daf7cc8509eb139f4faca9198474f8c4f50871d8fe9479adf3da/homeassistant-2026.1.0-py3-none-any.whl", hash = "sha256:29148b15f12fdf398ed8290ad1c23e5241011b225d2f96febcc7390855f18061", size = 50115149, upload-time = "2026-01-07T18:15:19.236Z" }, +] + +[[package]] +name = "htmltools" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/1d/c568d17e9fb5ad5aa0ca3531c58d36fe69deb8eee4e53ff6425c1f99f210/htmltools-0.6.0.tar.gz", hash = "sha256:e8a3fb023d748935035db7ff17f620612ffc814a6a80b6ae388f7b7ab182adf7", size = 97152, upload-time = "2024-10-29T20:21:43.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/ba/aa99706246f1938ca905eb6eeb7db832ac2e157aa4b805acb5cd4cd1791a/htmltools-0.6.0-py3-none-any.whl", hash = "sha256:072a274ff5e2851e0acce13fc5bb2bbdbbad8268dc8b123f881c05012ce7dce0", size = 84954, upload-time = "2024-10-29T20:21:42.067Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.13'" }, + { name = "certifi", marker = "python_full_version < '3.13'" }, + { name = "httpcore", marker = "python_full_version < '3.13'" }, + { name = "idna", marker = "python_full_version < '3.13'" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189, upload-time = "2024-08-27T12:54:01.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.13'" }, + { name = "certifi", marker = "python_full_version >= '3.13'" }, + { name = "httpcore", marker = "python_full_version >= '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "python_full_version >= '3.13.2'" }, + { name = "fsspec", marker = "python_full_version >= '3.13.2'" }, + { name = "hf-xet", marker = "(python_full_version >= '3.13.2' and platform_machine == 'AMD64') or (python_full_version >= '3.13.2' and platform_machine == 'aarch64') or (python_full_version >= '3.13.2' and platform_machine == 'amd64') or (python_full_version >= '3.13.2' and platform_machine == 'arm64') or (python_full_version >= '3.13.2' and platform_machine == 'x86_64')" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "shellingham", marker = "python_full_version >= '3.13.2'" }, + { name = "tqdm", marker = "python_full_version >= '3.13.2'" }, + { name = "typer-slim", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/dd/1cc985c5dda36298b152f75e82a1c81f52243b78fb7e9cad637a29561ad1/huggingface_hub-1.3.1.tar.gz", hash = "sha256:e80e0cfb4a75557c51ab20d575bdea6bb6106c2f97b7c75d8490642f1efb6df5", size = 622356, upload-time = "2026-01-09T14:08:16.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/fb/cb8fe5f71d5622427f20bcab9e06a696a5aaf21bfe7bd0a8a0c63c88abf5/huggingface_hub-1.3.1-py3-none-any.whl", hash = "sha256:efbc7f3153cb84e2bb69b62ed90985e21ecc9343d15647a419fc0ee4b85f0ac3", size = 533351, upload-time = "2026-01-09T14:08:14.519Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "markupsafe", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245, upload-time = "2024-05-05T23:42:02.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271, upload-time = "2024-05-05T23:41:59.928Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "markupsafe", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "josepy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyopenssl", version = "24.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pyopenssl", version = "25.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/8a/cd416f56cd4492878e8d62701b4ad32407c5ce541f247abf31d6e5f3b79b/josepy-1.15.0.tar.gz", hash = "sha256:46c9b13d1a5104ffbfa5853e555805c915dcde71c2cd91ce5386e84211281223", size = 59310, upload-time = "2025-01-22T23:56:23.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/74/fc54f4b03cb66b0b351131fcf1797fe9d7c1e6ce9a38fd940d9bc2d9531b/josepy-1.15.0-py3-none-any.whl", hash = "sha256:878c08cedd0a892c98c6d1a90b3cb869736f9c751f68ec8901e7b05a0c040fed", size = 32774, upload-time = "2025-01-22T23:56:21.524Z" }, +] + +[[package]] +name = "josepy" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/ad/6f520aee9cc9618d33430380741e9ef859b2c560b1e7915e755c084f6bc0/josepy-2.2.0.tar.gz", hash = "sha256:74c033151337c854f83efe5305a291686cef723b4b970c43cfe7270cf4a677a9", size = 56500, upload-time = "2025-10-14T14:54:42.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/b2/b5caed897fbb1cc286c62c01feca977e08d99a17230ff3055b9a98eccf1d/josepy-2.2.0-py3-none-any.whl", hash = "sha256:63e9dd116d4078778c25ca88f880cc5d95f1cab0099bebe3a34c2e299f65d10b", size = 29211, upload-time = "2025-10-14T14:54:41.144Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.13.2'" }, + { name = "referencing", marker = "python_full_version >= '3.13.2'" }, + { name = "rpds-py", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + +[[package]] +name = "litellm" +version = "1.80.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "click", marker = "python_full_version >= '3.13.2'" }, + { name = "fastuuid", marker = "python_full_version >= '3.13.2'" }, + { name = "grpcio", marker = "python_full_version >= '3.13.2'" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "importlib-metadata", marker = "python_full_version >= '3.13.2'" }, + { name = "jinja2", version = "3.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "jsonschema", marker = "python_full_version >= '3.13.2'" }, + { name = "openai", marker = "python_full_version >= '3.13.2'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.13.2'" }, + { name = "tiktoken", marker = "python_full_version >= '3.13.2'" }, + { name = "tokenizers", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/41/9b28df3e4739df83ddb32dfb2bccb12ad271d986494c9fd60e4927a0a6c3/litellm-1.80.15.tar.gz", hash = "sha256:759d09f33c9c6028c58dcdf71781b17b833ee926525714e09a408602be27f54e", size = 13376508, upload-time = "2026-01-11T18:31:44.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3b/b1bd693721ccb3c9a37c8233d019a643ac57bef5a93f279e5a63839ee4db/litellm-1.80.15-py3-none-any.whl", hash = "sha256:f354e49456985a235b9ed99df1c19d686d30501f96e68882dcc5b29b1e7c59d9", size = 11670707, upload-time = "2026-01-11T18:31:41.67Z" }, +] + +[[package]] +name = "lru-dict" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/5c/385f080747eb3083af87d8e4c9068f3c4cab89035f6982134889940dafd8/lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d", size = 17174, upload-time = "2023-11-06T01:39:07.923Z" }, + { url = "https://files.pythonhosted.org/packages/3c/de/5ef2ed75ce55d7059d1b96177ba04fa7ee1f35564f97bdfcd28fccfbe9d2/lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549", size = 10742, upload-time = "2023-11-06T01:39:08.871Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/f69a6abb0062d2cf2ce0aaf0284b105b97d1da024ca6d3d0730e6151242e/lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f", size = 11079, upload-time = "2023-11-06T01:39:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/ea/59/cf891143abe58a455b8eaa9175f0e80f624a146a2bf9a1ca842ee0ef930a/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563", size = 32469, upload-time = "2023-11-06T01:39:11.091Z" }, + { url = "https://files.pythonhosted.org/packages/59/88/d5976e9f70107ce11e45d93c6f0c2d5eaa1fc30bb3c8f57525eda4510dff/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2", size = 33496, upload-time = "2023-11-06T01:39:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/94d6e910d54fc1fa05c0ee1cd608c39401866a18cf5e5aff238449b33c11/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6", size = 29914, upload-time = "2023-11-06T01:39:13.395Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b9/9db79780c8a3cfd66bba6847773061e5cf8a3746950273b9985d47bbfe53/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4", size = 32241, upload-time = "2023-11-06T01:39:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b6/08a623019daec22a40c4d6d2c40851dfa3d129a53b2f9469db8eb13666c1/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c", size = 37320, upload-time = "2023-11-06T01:39:15.875Z" }, + { url = "https://files.pythonhosted.org/packages/70/0b/d3717159c26155ff77679cee1b077d22e1008bf45f19921e193319cd8e46/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1", size = 35054, upload-time = "2023-11-06T01:39:17.063Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f2ae00de7c27984a19b88d2b09ac877031c525b01199d7841ec8fa657fd6/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4", size = 38613, upload-time = "2023-11-06T01:39:18.136Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0b/e30236aafe31b4247aa9ae61ba8aac6dde75c3ea0e47a8fb7eef53f6d5ce/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df", size = 37143, upload-time = "2023-11-06T01:39:19.571Z" }, + { url = "https://files.pythonhosted.org/packages/1c/28/b59bcebb8d76ba8147a784a8be7eab6a4ad3395b9236e73740ff675a5a52/lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc", size = 12653, upload-time = "2023-11-06T01:39:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/18/06d9710cb0a0d3634f8501e4bdcc07abe64a32e404d82895a6a36fab97f6/lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa", size = 13811, upload-time = "2023-11-06T01:39:21.599Z" }, +] + +[[package]] +name = "lzstring" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "future" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/0c/28347673b45e5f0975cdf1f6d69ede6ad049be873194c4e164d79aecd34c/lzstring-1.0.4.tar.gz", hash = "sha256:1afa61e598193fbcc211e0899f09a9679e33f9102bccc37fbfda0b7fef4d9ea2", size = 4256, upload-time = "2018-06-01T02:32:12.639Z" } + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-code-runner" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/d1/2a7753e05dcc711552721048a34341b63a6a2b09e70ac738c7a4c81ca414/markdown_code_runner-2.7.0.tar.gz", hash = "sha256:b36f9314839c0db3ee5e5d644e1b4471454af8f624eb15bd5a5d68dbc6430afe", size = 99832, upload-time = "2026-01-24T21:20:04.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/92/9d7fa0567cf3d38b551bb1a30c177285e0e42ae2b10c16946af7e8a38e73/markdown_code_runner-2.7.0-py3-none-any.whl", hash = "sha256:fad5d5fbad9c49687141ab9c39ed7e054f857b97aa79d11f031de157afd21c20", size = 14692, upload-time = "2026-01-24T21:20:03.382Z" }, +] + +[[package]] +name = "markdown-gfm-admonition" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/5b/1e7114e8fac6aadf2a25dab2e24a1eba54acd504d6a5747928a3eb09d7fd/markdown_gfm_admonition-0.3.0.tar.gz", hash = "sha256:20a37febc79222badb6dfbfcbf0a74b94c0a6259b0f27e9f783601fea59094b6", size = 5817, upload-time = "2025-11-28T09:01:38.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/86/ce4288122111c11bb2483a962357baa9793817d48ac1e99ce5ff47384efe/markdown_gfm_admonition-0.3.0-py3-none-any.whl", hash = "sha256:8e49bfea892f4c220b7f7b9479462360b69c68ae7cae7739e288b5382d0ae9b5", size = 6416, upload-time = "2025-11-28T09:01:37.543Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mashumaro" +version = "3.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/67/c4e235256baf6837106d2620c7123eb1e5786c704c7f7d7fa488ad6afc61/mashumaro-3.17.tar.gz", hash = "sha256:de1d8b1faffee58969c7f97e35963a92480a38d4c9858e92e0721efec12258ed", size = 189877, upload-time = "2025-10-03T21:09:27.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/2d/edc147aa1dce9e0e377687d453e62fdfcbccc08cd35d0b7413e3485c4b92/mashumaro-3.17-py3-none-any.whl", hash = "sha256:3964e2c804f62de9e4c58fb985de71dcd716f9507cc18374b1bd5c4f1a1b879b", size = 94198, upload-time = "2025-10-03T21:09:25.436Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mutagen" +version = "1.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186, upload-time = "2023-09-03T16:33:33.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "narwhals" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/6d/b57c64e5038a8cf071bce391bb11551657a74558877ac961e7fa905ece27/narwhals-2.15.0.tar.gz", hash = "sha256:a9585975b99d95084268445a1fdd881311fa26ef1caa18020d959d5b2ff9a965", size = 603479, upload-time = "2026-01-06T08:10:13.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl", hash = "sha256:cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6", size = 432856, upload-time = "2026-01-06T08:10:11.511Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090, upload-time = "2024-11-02T17:48:55.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658, upload-time = "2024-11-02T17:37:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258, upload-time = "2024-11-02T17:37:45.252Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249, upload-time = "2024-11-02T17:37:54.252Z" }, + { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704, upload-time = "2024-11-02T17:38:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089, upload-time = "2024-11-02T17:38:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185, upload-time = "2024-11-02T17:38:51.07Z" }, + { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751, upload-time = "2024-11-02T17:39:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705, upload-time = "2024-11-02T17:39:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077, upload-time = "2024-11-02T17:39:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858, upload-time = "2024-11-02T17:40:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263, upload-time = "2024-11-02T17:40:39.528Z" }, + { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771, upload-time = "2024-11-02T17:41:01.368Z" }, + { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805, upload-time = "2024-11-02T17:41:11.213Z" }, + { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380, upload-time = "2024-11-02T17:41:22.19Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451, upload-time = "2024-11-02T17:41:43.094Z" }, + { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822, upload-time = "2024-11-02T17:42:07.595Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822, upload-time = "2024-11-02T17:42:32.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598, upload-time = "2024-11-02T17:42:53.773Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021, upload-time = "2024-11-02T17:46:19.171Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405, upload-time = "2024-11-02T17:46:38.177Z" }, + { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062, upload-time = "2024-11-02T17:43:24.599Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839, upload-time = "2024-11-02T17:43:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031, upload-time = "2024-11-02T17:43:54.585Z" }, + { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977, upload-time = "2024-11-02T17:44:05.31Z" }, + { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951, upload-time = "2024-11-02T17:44:25.881Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655, upload-time = "2024-11-02T17:44:50.115Z" }, + { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902, upload-time = "2024-11-02T17:45:15.685Z" }, + { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180, upload-time = "2024-11-02T17:45:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907, upload-time = "2024-11-02T17:45:48.951Z" }, + { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098, upload-time = "2024-11-02T17:46:07.941Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d0/c12ddfd3a02274be06ffc71f3efc6d0e457b0409c4481596881e748cb264/numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f", size = 20233295, upload-time = "2025-01-19T00:02:09.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/e6/847d15770ab7a01e807bdfcd4ead5bdae57c0092b7dc83878171b6af97bb/numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467", size = 20912636, upload-time = "2025-01-18T23:23:58.337Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/f83580891577b13bd7e261416120e036d0d8fb508c8a43a73e38928b794b/numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a", size = 14098403, upload-time = "2025-01-18T23:25:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/2b/86/d019fb60a9d0f1d4cf04b014fe88a9135090adfadcc31c1fadbb071d7fa7/numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825", size = 5128938, upload-time = "2025-01-18T23:25:37.21Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1b/50985edb6f1ec495a1c36452e860476f5b7ecdc3fc59ea89ccad3c4926c5/numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37", size = 6661937, upload-time = "2025-01-18T23:26:05.86Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1b/17efd94cad1b9d605c3f8907fb06bcffc4ce4d1d14d46b95316cccccf2b9/numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748", size = 14049518, upload-time = "2025-01-18T23:26:33.364Z" }, + { url = "https://files.pythonhosted.org/packages/5b/73/65d2f0b698df1731e851e3295eb29a5ab8aa06f763f7e4188647a809578d/numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0", size = 16099146, upload-time = "2025-01-18T23:27:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/d5/69/308f55c0e19d4b5057b5df286c5433822e3c8039ede06d4051d96f1c2c4e/numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278", size = 15246336, upload-time = "2025-01-18T23:28:09.658Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d8/d8d333ad0d8518d077a21aeea7b7c826eff766a2b1ce1194dea95ca0bacf/numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba", size = 17863507, upload-time = "2025-01-18T23:28:56.146Z" }, + { url = "https://files.pythonhosted.org/packages/82/6e/0b84ad3103ffc16d6673e63b5acbe7901b2af96c2837174c6318c98e27ab/numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283", size = 6276491, upload-time = "2025-01-18T23:29:09.409Z" }, + { url = "https://files.pythonhosted.org/packages/fc/84/7f801a42a67b9772a883223a0a1e12069a14626c81a732bd70aac57aebc1/numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb", size = 12616372, upload-time = "2025-01-18T23:29:46.645Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/df5624001f4f5c3e0b78e9017bfab7fdc18a8d3b3d3161da3d64924dd659/numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc", size = 20899188, upload-time = "2025-01-18T23:31:15.292Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/d349c3b5ed66bd3cb0214be60c27e32b90a506946857b866838adbe84040/numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369", size = 14113972, upload-time = "2025-01-18T23:31:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/9d/50/949ec9cbb28c4b751edfa64503f0913cbfa8d795b4a251e7980f13a8a655/numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd", size = 5114294, upload-time = "2025-01-18T23:31:54.219Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f3/399c15629d5a0c68ef2aa7621d430b2be22034f01dd7f3c65a9c9666c445/numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be", size = 6648426, upload-time = "2025-01-18T23:32:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/2c/03/c72474c13772e30e1bc2e558cdffd9123c7872b731263d5648b5c49dd459/numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84", size = 14045990, upload-time = "2025-01-18T23:32:38.031Z" }, + { url = "https://files.pythonhosted.org/packages/83/9c/96a9ab62274ffafb023f8ee08c88d3d31ee74ca58869f859db6845494fa6/numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff", size = 16096614, upload-time = "2025-01-18T23:33:12.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/34/cd0a735534c29bec7093544b3a509febc9b0df77718a9b41ffb0809c9f46/numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0", size = 15242123, upload-time = "2025-01-18T23:33:46.412Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6d/541717a554a8f56fa75e91886d9b79ade2e595918690eb5d0d3dbd3accb9/numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de", size = 17859160, upload-time = "2025-01-18T23:34:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a5/fbf1f2b54adab31510728edd06a05c1b30839f37cf8c9747cb85831aaf1b/numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9", size = 6273337, upload-time = "2025-01-18T23:40:10.83Z" }, + { url = "https://files.pythonhosted.org/packages/56/e5/01106b9291ef1d680f82bc47d0c5b5e26dfed15b0754928e8f856c82c881/numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369", size = 12609010, upload-time = "2025-01-18T23:40:31.34Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/f23d9876de0f08dceb707c4dcf7f8dd7588266745029debb12a3cdd40be6/numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391", size = 20924451, upload-time = "2025-01-18T23:35:26.639Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ec/6ea85b2da9d5dfa1dbb4cb3c76587fc8ddcae580cb1262303ab21c0926c4/numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39", size = 14122390, upload-time = "2025-01-18T23:36:30.596Z" }, + { url = "https://files.pythonhosted.org/packages/68/05/bfbdf490414a7dbaf65b10c78bc243f312c4553234b6d91c94eb7c4b53c2/numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317", size = 5156590, upload-time = "2025-01-18T23:36:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/fe2e91b2642b9d6544518388a441bcd65c904cea38d9ff998e2e8ebf808e/numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49", size = 6671958, upload-time = "2025-01-18T23:37:05.361Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6f/6531a78e182f194d33ee17e59d67d03d0d5a1ce7f6be7343787828d1bd4a/numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2", size = 14019950, upload-time = "2025-01-18T23:37:38.605Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fb/13c58591d0b6294a08cc40fcc6b9552d239d773d520858ae27f39997f2ae/numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7", size = 16079759, upload-time = "2025-01-18T23:38:05.757Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/f2f8edd62abb4b289f65a7f6d1f3650273af00b91b7267a2431be7f1aec6/numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb", size = 15226139, upload-time = "2025-01-18T23:38:38.458Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/14a177f1a90b8ad8a592ca32124ac06af5eff32889874e53a308f850290f/numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648", size = 17856316, upload-time = "2025-01-18T23:39:11.454Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/242ae8d7b97f4e0e4ab8dd51231465fb23ed5e802680d629149722e3faf1/numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4", size = 6329134, upload-time = "2025-01-18T23:39:28.128Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/cd9e9b04012c015cb6320ab3bf43bc615e248dddfeb163728e800a5d96f0/numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576", size = 12696208, upload-time = "2025-01-18T23:39:51.85Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + +[[package]] +name = "openai" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.13.2'" }, + { name = "distro", marker = "python_full_version >= '3.13.2'" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "jiter", marker = "python_full_version >= '3.13.2'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2'" }, + { name = "sniffio", marker = "python_full_version >= '3.13.2'" }, + { name = "tqdm", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, +] + +[[package]] +name = "orjson" +version = "3.10.12" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/04/bb9f72987e7f62fb591d6c880c0caaa16238e4e530cbc3bdc84a7372d75f/orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff", size = 5438647, upload-time = "2024-11-23T19:42:56.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/2f/989adcafad49afb535da56b95d8f87d82e748548b2a86003ac129314079c/orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d", size = 248678, upload-time = "2024-11-23T19:41:33.346Z" }, + { url = "https://files.pythonhosted.org/packages/69/b9/8c075e21a50c387649db262b618ebb7e4d40f4197b949c146fc225dd23da/orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f", size = 136763, upload-time = "2024-11-23T19:41:35.539Z" }, + { url = "https://files.pythonhosted.org/packages/87/d3/78edf10b4ab14c19f6d918cf46a145818f4aca2b5a1773c894c5490d3a4c/orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70", size = 149137, upload-time = "2024-11-23T19:41:36.937Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/5db8852bdf990a0ddc997fa8f16b80895b8cc77c0fe3701569ed2b4b9e78/orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69", size = 140567, upload-time = "2024-11-23T19:41:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/9ce1e3e3db918512efadad489630c25841eb148513d21dab96f6b4157fa1/orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9", size = 156620, upload-time = "2024-11-23T19:41:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/05133d6bea24e292d2f7628b1e19986554f7d97b6412b3e51d812e38db2d/orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192", size = 131555, upload-time = "2024-11-23T19:41:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7a/b3fbffda8743135c7811e95dc2ab7cdbc5f04999b83c2957d046f1b3fac9/orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559", size = 139743, upload-time = "2024-11-23T19:41:42.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/13/95bbcc9a6584aa083da5ce5004ce3d59ea362a542a0b0938d884fd8790b6/orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc", size = 131733, upload-time = "2024-11-23T19:41:44.184Z" }, + { url = "https://files.pythonhosted.org/packages/e8/29/dddbb2ea6e7af426fcc3da65a370618a88141de75c6603313d70768d1df1/orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f", size = 415788, upload-time = "2024-11-23T19:41:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/53/df/4aea59324ac539975919b4705ee086aced38e351a6eb3eea0f5071dd5661/orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be", size = 142347, upload-time = "2024-11-23T19:41:48.128Z" }, + { url = "https://files.pythonhosted.org/packages/55/55/a52d83d7c49f8ff44e0daab10554490447d6c658771569e1c662aa7057fe/orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c", size = 130829, upload-time = "2024-11-23T19:41:49.702Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8b/b1beb1624dd4adf7d72e2d9b73c4b529e7851c0c754f17858ea13e368b33/orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708", size = 143659, upload-time = "2024-11-23T19:41:51.122Z" }, + { url = "https://files.pythonhosted.org/packages/13/91/634c9cd0bfc6a857fc8fab9bf1a1bd9f7f3345e0d6ca5c3d4569ceb6dcfa/orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb", size = 135221, upload-time = "2024-11-23T19:41:52.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/3f560735f46fa6f875a9d7c4c2171a58cfb19f56a633d5ad5037a924f35f/orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543", size = 248662, upload-time = "2024-11-23T19:41:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/a3/df/54817902350636cc9270db20486442ab0e4db33b38555300a1159b439d16/orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296", size = 126055, upload-time = "2024-11-23T19:41:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/55835914894e00332601a74540840f7665e81f20b3e2b9a97614af8565ed/orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e", size = 131507, upload-time = "2024-11-23T19:41:57.942Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/b91288361898e3158062a876b5013c519a5d13e692ac7686e3486c4133ab/orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f", size = 131686, upload-time = "2024-11-23T19:41:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/b2/15/08ce117d60a4d2d3fd24e6b21db463139a658e9f52d22c9c30af279b4187/orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e", size = 415710, upload-time = "2024-11-23T19:42:00.953Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/c09da5ed58f9c002cf83adff7a4cdf3e6cee742aa9723395f8dcdb397233/orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6", size = 142305, upload-time = "2024-11-23T19:42:02.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/d1/8612038d44f33fae231e9ba480d273bac2b0383ce9e77cb06bede1224ae3/orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e", size = 130815, upload-time = "2024-11-23T19:42:04.868Z" }, + { url = "https://files.pythonhosted.org/packages/67/2c/d5f87834be3591555cfaf9aecdf28f480a6f0b4afeaac53bad534bf9518f/orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc", size = 143664, upload-time = "2024-11-23T19:42:06.349Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/7d768fa3ca23c9b3e1e09117abeded1501119f1d8de0ab722938c91ab25d/orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825", size = 134944, upload-time = "2024-11-23T19:42:07.842Z" }, +] + +[[package]] +name = "orjson" +version = "3.10.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415, upload-time = "2025-03-24T17:00:23.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325, upload-time = "2025-03-24T16:59:19.784Z" }, + { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621, upload-time = "2025-03-24T16:59:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270, upload-time = "2025-03-24T16:59:22.514Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346, upload-time = "2025-03-24T16:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845, upload-time = "2025-03-24T16:59:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078, upload-time = "2025-03-24T16:59:27.288Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712, upload-time = "2025-03-24T16:59:28.613Z" }, + { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136, upload-time = "2025-03-24T16:59:29.987Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258, upload-time = "2025-03-24T16:59:31.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326, upload-time = "2025-03-24T16:59:32.709Z" }, + { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800, upload-time = "2025-03-24T16:59:34.134Z" }, + { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516, upload-time = "2025-03-24T16:59:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759, upload-time = "2025-03-24T16:59:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944, upload-time = "2025-03-24T16:59:38.814Z" }, + { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289, upload-time = "2025-03-24T16:59:40.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640, upload-time = "2025-03-24T16:59:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286, upload-time = "2025-03-24T16:59:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307, upload-time = "2025-03-24T16:59:44.143Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739, upload-time = "2025-03-24T16:59:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076, upload-time = "2025-03-24T16:59:47.776Z" }, + { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643, upload-time = "2025-03-24T16:59:49.258Z" }, + { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168, upload-time = "2025-03-24T16:59:51.027Z" }, + { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271, upload-time = "2025-03-24T16:59:52.449Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444, upload-time = "2025-03-24T16:59:53.825Z" }, + { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737, upload-time = "2025-03-24T16:59:55.599Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482, upload-time = "2025-03-24T16:59:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714, upload-time = "2025-03-24T16:59:58.666Z" }, + { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954, upload-time = "2025-03-24T17:00:00.101Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", size = 238259, upload-time = "2025-08-26T17:45:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", size = 127633, upload-time = "2025-08-26T17:45:16.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", size = 123061, upload-time = "2025-08-26T17:45:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", size = 127956, upload-time = "2025-08-26T17:45:19.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", size = 130790, upload-time = "2025-08-26T17:45:20.586Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", size = 132385, upload-time = "2025-08-26T17:45:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", size = 135305, upload-time = "2025-08-26T17:45:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", size = 132875, upload-time = "2025-08-26T17:45:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", size = 130940, upload-time = "2025-08-26T17:45:27.209Z" }, + { url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", size = 403852, upload-time = "2025-08-26T17:45:28.478Z" }, + { url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", size = 146293, upload-time = "2025-08-26T17:45:29.86Z" }, + { url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", size = 135470, upload-time = "2025-08-26T17:45:31.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", size = 136248, upload-time = "2025-08-26T17:45:32.567Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", size = 131437, upload-time = "2025-08-26T17:45:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", size = 125978, upload-time = "2025-08-26T17:45:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" }, + { url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" }, + { url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", size = 136216, upload-time = "2025-08-26T17:45:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", size = 131362, upload-time = "2025-08-26T17:45:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", size = 125989, upload-time = "2025-08-26T17:45:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", size = 238115, upload-time = "2025-08-26T17:46:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", size = 127493, upload-time = "2025-08-26T17:46:03.466Z" }, + { url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", size = 122998, upload-time = "2025-08-26T17:46:04.803Z" }, + { url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", size = 132915, upload-time = "2025-08-26T17:46:06.237Z" }, + { url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", size = 130907, upload-time = "2025-08-26T17:46:07.581Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", size = 403852, upload-time = "2025-08-26T17:46:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", size = 146309, upload-time = "2025-08-26T17:46:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", size = 135424, upload-time = "2025-08-26T17:46:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", size = 136266, upload-time = "2025-08-26T17:46:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", size = 131351, upload-time = "2025-08-26T17:46:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", size = 125985, upload-time = "2025-08-26T17:46:16.67Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pillow" +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780, upload-time = "2024-10-15T14:24:29.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642, upload-time = "2024-10-15T14:22:37.736Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999, upload-time = "2024-10-15T14:22:39.654Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794, upload-time = "2024-10-15T14:22:41.598Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762, upload-time = "2024-10-15T14:22:45.952Z" }, + { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468, upload-time = "2024-10-15T14:22:47.789Z" }, + { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824, upload-time = "2024-10-15T14:22:49.668Z" }, + { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436, upload-time = "2024-10-15T14:22:51.911Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714, upload-time = "2024-10-15T14:22:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631, upload-time = "2024-10-15T14:22:56.404Z" }, + { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533, upload-time = "2024-10-15T14:22:58.087Z" }, + { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890, upload-time = "2024-10-15T14:22:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300, upload-time = "2024-10-15T14:23:01.855Z" }, + { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742, upload-time = "2024-10-15T14:23:03.749Z" }, + { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349, upload-time = "2024-10-15T14:23:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714, upload-time = "2024-10-15T14:23:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514, upload-time = "2024-10-15T14:23:10.19Z" }, + { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055, upload-time = "2024-10-15T14:23:12.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751, upload-time = "2024-10-15T14:23:13.836Z" }, + { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378, upload-time = "2024-10-15T14:23:15.735Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588, upload-time = "2024-10-15T14:23:17.905Z" }, + { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509, upload-time = "2024-10-15T14:23:19.643Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791, upload-time = "2024-10-15T14:23:21.601Z" }, + { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854, upload-time = "2024-10-15T14:23:23.91Z" }, + { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369, upload-time = "2024-10-15T14:23:27.184Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703, upload-time = "2024-10-15T14:23:28.979Z" }, + { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550, upload-time = "2024-10-15T14:23:30.846Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038, upload-time = "2024-10-15T14:23:32.687Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197, upload-time = "2024-10-15T14:23:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169, upload-time = "2024-10-15T14:23:37.33Z" }, + { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828, upload-time = "2024-10-15T14:23:39.826Z" }, +] + +[[package]] +name = "pillow" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715, upload-time = "2025-01-02T08:13:58.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818, upload-time = "2025-01-02T08:11:22.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662, upload-time = "2025-01-02T08:11:25.19Z" }, + { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317, upload-time = "2025-01-02T08:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999, upload-time = "2025-01-02T08:11:33.499Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819, upload-time = "2025-01-02T08:11:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081, upload-time = "2025-01-02T08:11:39.598Z" }, + { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513, upload-time = "2025-01-02T08:11:43.083Z" }, + { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298, upload-time = "2025-01-02T08:11:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630, upload-time = "2025-01-02T08:11:49.401Z" }, + { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369, upload-time = "2025-01-02T08:11:52.02Z" }, + { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240, upload-time = "2025-01-02T08:11:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640, upload-time = "2025-01-02T08:11:58.329Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437, upload-time = "2025-01-02T08:12:01.797Z" }, + { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605, upload-time = "2025-01-02T08:12:05.224Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173, upload-time = "2025-01-02T08:12:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145, upload-time = "2025-01-02T08:12:11.411Z" }, + { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340, upload-time = "2025-01-02T08:12:15.29Z" }, + { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906, upload-time = "2025-01-02T08:12:17.485Z" }, + { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759, upload-time = "2025-01-02T08:12:20.382Z" }, + { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657, upload-time = "2025-01-02T08:12:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304, upload-time = "2025-01-02T08:12:28.069Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117, upload-time = "2025-01-02T08:12:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060, upload-time = "2025-01-02T08:12:32.362Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192, upload-time = "2025-01-02T08:12:34.361Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805, upload-time = "2025-01-02T08:12:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623, upload-time = "2025-01-02T08:12:41.912Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191, upload-time = "2025-01-02T08:12:45.186Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494, upload-time = "2025-01-02T08:12:47.098Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595, upload-time = "2025-01-02T08:12:50.47Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651, upload-time = "2025-01-02T08:12:53.356Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735, upload-time = "2024-12-01T18:29:16.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588, upload-time = "2024-12-01T18:28:03.327Z" }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825, upload-time = "2024-12-01T18:28:06.78Z" }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357, upload-time = "2024-12-01T18:28:08.575Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869, upload-time = "2024-12-01T18:28:10.396Z" }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884, upload-time = "2024-12-01T18:28:11.746Z" }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486, upload-time = "2024-12-01T18:28:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649, upload-time = "2024-12-01T18:28:14.297Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103, upload-time = "2024-12-01T18:28:15.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607, upload-time = "2024-12-01T18:28:18.015Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153, upload-time = "2024-12-01T18:28:19.937Z" }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151, upload-time = "2024-12-01T18:28:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812, upload-time = "2024-12-01T18:28:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829, upload-time = "2024-12-01T18:28:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704, upload-time = "2024-12-01T18:28:25.314Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050, upload-time = "2024-12-01T18:28:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117, upload-time = "2024-12-01T18:28:27.643Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002, upload-time = "2024-12-01T18:28:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639, upload-time = "2024-12-01T18:28:30.199Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049, upload-time = "2024-12-01T18:28:31.308Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819, upload-time = "2024-12-01T18:28:32.755Z" }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625, upload-time = "2024-12-01T18:28:34.083Z" }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934, upload-time = "2024-12-01T18:28:35.434Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361, upload-time = "2024-12-01T18:28:36.777Z" }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904, upload-time = "2024-12-01T18:28:38.041Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632, upload-time = "2024-12-01T18:28:39.401Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897, upload-time = "2024-12-01T18:28:40.996Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118, upload-time = "2024-12-01T18:28:42.38Z" }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851, upload-time = "2024-12-01T18:28:43.655Z" }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630, upload-time = "2024-12-01T18:28:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269, upload-time = "2024-12-01T18:28:47.602Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472, upload-time = "2024-12-01T18:28:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363, upload-time = "2024-12-01T18:28:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818, upload-time = "2024-12-01T18:29:14.716Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/92/76/f941e63d55c0293ff7829dd21e7cf1147e90a526756869a9070f287a68c9/propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5", size = 42722, upload-time = "2025-02-20T19:03:29.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/2c/921f15dc365796ec23975b322b0078eae72995c7b4d49eba554c6a308d70/propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e", size = 79867, upload-time = "2025-02-20T19:00:59.948Z" }, + { url = "https://files.pythonhosted.org/packages/11/a5/4a6cc1a559d1f2fb57ea22edc4245158cdffae92f7f92afcee2913f84417/propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af", size = 46109, upload-time = "2025-02-20T19:01:04.447Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/28bfd3af3a567ad7d667348e7f46a520bda958229c4d545ba138a044232f/propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5", size = 45635, upload-time = "2025-02-20T19:01:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/73/20/d75b42eaffe5075eac2f4e168f6393d21c664c91225288811d85451b2578/propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b", size = 242159, upload-time = "2025-02-20T19:01:10.047Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fb/4b537dd92f9fd4be68042ec51c9d23885ca5fafe51ec24c58d9401034e5f/propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667", size = 248163, upload-time = "2025-02-20T19:01:12.883Z" }, + { url = "https://files.pythonhosted.org/packages/e7/af/8a9db04ac596d531ca0ef7dde518feaadfcdabef7b17d6a5ec59ee3effc2/propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7", size = 248794, upload-time = "2025-02-20T19:01:15.291Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c4/ecfc988879c0fd9db03228725b662d76cf484b6b46f7e92fee94e4b52490/propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7", size = 243912, upload-time = "2025-02-20T19:01:16.95Z" }, + { url = "https://files.pythonhosted.org/packages/04/a2/298dd27184faa8b7d91cc43488b578db218b3cc85b54d912ed27b8c5597a/propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf", size = 229402, upload-time = "2025-02-20T19:01:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/be/0d/efe7fec316ca92dbf4bc4a9ba49ca889c43ca6d48ab1d6fa99fc94e5bb98/propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138", size = 226896, upload-time = "2025-02-20T19:01:23.57Z" }, + { url = "https://files.pythonhosted.org/packages/60/63/72404380ae1d9c96d96e165aa02c66c2aae6072d067fc4713da5cde96762/propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86", size = 221447, upload-time = "2025-02-20T19:01:26.142Z" }, + { url = "https://files.pythonhosted.org/packages/9d/18/b8392cab6e0964b67a30a8f4dadeaff64dc7022b5a34bb1d004ea99646f4/propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d", size = 222440, upload-time = "2025-02-20T19:01:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/6f/be/105d9ceda0f97eff8c06bac1673448b2db2a497444de3646464d3f5dc881/propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e", size = 234104, upload-time = "2025-02-20T19:01:31.256Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c9/f09a4ec394cfcce4053d8b2a04d622b5f22d21ba9bb70edd0cad061fa77b/propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64", size = 239086, upload-time = "2025-02-20T19:01:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/96f7f9ed6def82db67c972bdb7bd9f28b95d7d98f7e2abaf144c284bf609/propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c", size = 230991, upload-time = "2025-02-20T19:01:35.433Z" }, + { url = "https://files.pythonhosted.org/packages/5a/11/bee5439de1307d06fad176f7143fec906e499c33d7aff863ea8428b8e98b/propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d", size = 40337, upload-time = "2025-02-20T19:01:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/e4/17/e5789a54a0455a61cb9efc4ca6071829d992220c2998a27c59aeba749f6f/propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57", size = 44404, upload-time = "2025-02-20T19:01:38.946Z" }, + { url = "https://files.pythonhosted.org/packages/3a/0f/a79dd23a0efd6ee01ab0dc9750d8479b343bfd0c73560d59d271eb6a99d4/propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568", size = 77287, upload-time = "2025-02-20T19:01:40.897Z" }, + { url = "https://files.pythonhosted.org/packages/b8/51/76675703c90de38ac75adb8deceb3f3ad99b67ff02a0fa5d067757971ab8/propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9", size = 44923, upload-time = "2025-02-20T19:01:42.397Z" }, + { url = "https://files.pythonhosted.org/packages/01/9b/fd5ddbee66cf7686e73c516227c2fd9bf471dbfed0f48329d095ea1228d3/propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767", size = 44325, upload-time = "2025-02-20T19:01:43.976Z" }, + { url = "https://files.pythonhosted.org/packages/13/1c/6961f11eb215a683b34b903b82bde486c606516c1466bf1fa67f26906d51/propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8", size = 225116, upload-time = "2025-02-20T19:01:45.488Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ea/f8410c40abcb2e40dffe9adeed017898c930974650a63e5c79b886aa9f73/propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0", size = 229905, upload-time = "2025-02-20T19:01:49.454Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/a9bf90894001468bf8e6ea293bb00626cc9ef10f8eb7996e9ec29345c7ed/propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d", size = 233221, upload-time = "2025-02-20T19:01:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ce/fffdddd9725b690b01d345c1156b4c2cc6dca09ab5c23a6d07b8f37d6e2f/propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05", size = 227627, upload-time = "2025-02-20T19:01:53.695Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/45c89a5994a334735a3032b48e8e4a98c05d9536ddee0719913dc27da548/propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe", size = 214217, upload-time = "2025-02-20T19:01:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/bc60188c3290ff8f5f4a92b9ca2d93a62e449c8daf6fd11ad517ad136926/propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1", size = 212921, upload-time = "2025-02-20T19:01:57.893Z" }, + { url = "https://files.pythonhosted.org/packages/14/b3/39d60224048feef7a96edabb8217dc3f75415457e5ebbef6814f8b2a27b5/propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92", size = 208200, upload-time = "2025-02-20T19:02:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/9d/b3/0a6720b86791251273fff8a01bc8e628bc70903513bd456f86cde1e1ef84/propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787", size = 208400, upload-time = "2025-02-20T19:02:03.997Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4f/bb470f3e687790547e2e78105fb411f54e0cdde0d74106ccadd2521c6572/propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545", size = 218116, upload-time = "2025-02-20T19:02:06.042Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/277f7f9add469698ac9724c199bfe06f85b199542121a71f65a80423d62a/propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e", size = 222911, upload-time = "2025-02-20T19:02:08.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/e3/a7b9782aef5a2fc765b1d97da9ec7aed2f25a4e985703608e73232205e3f/propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626", size = 216563, upload-time = "2025-02-20T19:02:11.322Z" }, + { url = "https://files.pythonhosted.org/packages/ab/76/0583ca2c551aa08ffcff87b2c6849c8f01c1f6fb815a5226f0c5c202173e/propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374", size = 39763, upload-time = "2025-02-20T19:02:12.977Z" }, + { url = "https://files.pythonhosted.org/packages/80/ec/c6a84f9a36f608379b95f0e786c111d5465926f8c62f12be8cdadb02b15c/propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a", size = 43650, upload-time = "2025-02-20T19:02:15.041Z" }, + { url = "https://files.pythonhosted.org/packages/ee/95/7d32e3560f5bf83fc2f2a4c1b0c181d327d53d5f85ebd045ab89d4d97763/propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf", size = 82140, upload-time = "2025-02-20T19:02:16.562Z" }, + { url = "https://files.pythonhosted.org/packages/86/89/752388f12e6027a5e63f5d075f15291ded48e2d8311314fff039da5a9b11/propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0", size = 47296, upload-time = "2025-02-20T19:02:17.974Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4c/b55c98d586c69180d3048984a57a5ea238bdeeccf82dbfcd598e935e10bb/propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829", size = 46724, upload-time = "2025-02-20T19:02:19.588Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b6/67451a437aed90c4e951e320b5b3d7eb584ade1d5592f6e5e8f678030989/propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa", size = 291499, upload-time = "2025-02-20T19:02:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ff/e4179facd21515b24737e1e26e02615dfb5ed29416eed4cf5bc6ac5ce5fb/propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6", size = 293911, upload-time = "2025-02-20T19:02:24.248Z" }, + { url = "https://files.pythonhosted.org/packages/76/8d/94a8585992a064a23bd54f56c5e58c3b8bf0c0a06ae10e56f2353ae16c3d/propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db", size = 293301, upload-time = "2025-02-20T19:02:26.034Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b8/2c860c92b4134f68c7716c6f30a0d723973f881c32a6d7a24c4ddca05fdf/propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54", size = 281947, upload-time = "2025-02-20T19:02:27.838Z" }, + { url = "https://files.pythonhosted.org/packages/cd/72/b564be7411b525d11757b713c757c21cd4dc13b6569c3b2b8f6d3c96fd5e/propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121", size = 268072, upload-time = "2025-02-20T19:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/d94649e399e8d7fc051e5a4f2334efc567993525af083db145a70690a121/propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e", size = 275190, upload-time = "2025-02-20T19:02:32.255Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3c/446e125f5bbbc1922964dd67cb541c01cdb678d811297b79a4ff6accc843/propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e", size = 254145, upload-time = "2025-02-20T19:02:33.932Z" }, + { url = "https://files.pythonhosted.org/packages/f4/80/fd3f741483dc8e59f7ba7e05eaa0f4e11677d7db2077522b92ff80117a2a/propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a", size = 257163, upload-time = "2025-02-20T19:02:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/6292b5ce6ed0017e6a89024a827292122cc41b6259b30ada0c6732288513/propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac", size = 280249, upload-time = "2025-02-20T19:02:38.406Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f0/fd9b8247b449fe02a4f96538b979997e229af516d7462b006392badc59a1/propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e", size = 288741, upload-time = "2025-02-20T19:02:40.149Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/cf831fdc2617f86cfd7f414cfc487d018e722dac8acc098366ce9bba0941/propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf", size = 277061, upload-time = "2025-02-20T19:02:42.309Z" }, + { url = "https://files.pythonhosted.org/packages/42/78/9432542a35d944abeca9e02927a0de38cd7a298466d8ffa171536e2381c3/propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863", size = 42252, upload-time = "2025-02-20T19:02:44.447Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/960365f4f8978f48ebb56b1127adf33a49f2e69ecd46ac1f46d6cf78a79d/propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46", size = 46425, upload-time = "2025-02-20T19:02:48.071Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101, upload-time = "2025-02-20T19:03:27.202Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + +[[package]] +name = "psutil-home-assistant" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/4f/32a51f53d645044740d0513a6a029d782b35bdc51a55ea171ce85034f5b7/psutil-home-assistant-0.0.1.tar.gz", hash = "sha256:ebe4f3a98d76d93a3140da2823e9ef59ca50a59761fdc453b30b4407c4c1bdb8", size = 6045, upload-time = "2022-08-25T14:28:39.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/48/8a0acb683d1fee78b966b15e78143b673154abb921061515254fb573aacd/psutil_home_assistant-0.0.1-py3-none-any.whl", hash = "sha256:35a782e93e23db845fc4a57b05df9c52c2d5c24f5b233bd63b01bae4efae3c41", size = 6300, upload-time = "2022-08-25T14:28:38.083Z" }, +] + +[[package]] +name = "pycares" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/ad/9d1e96486d2eb5a2672c4d9a2dd372d015b8d7a332c6ac2722c4c8e6bbbf/pycares-4.11.0.tar.gz", hash = "sha256:c863d9003ca0ce7df26429007859afd2a621d3276ed9fef154a9123db9252557", size = 654473, upload-time = "2025-09-09T15:18:21.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/4e/4821b66feefaaa8ec03494c1a11614c430983572e54ff062b4589441e199/pycares-4.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b93d624560ba52287873bacff70b42c99943821ecbc810b959b0953560f53c36", size = 145906, upload-time = "2025-09-09T15:16:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/e8/81/93a505dcbb7533254b0ce1da519591dcda889d6a66dcdfa5737e3280e18a/pycares-4.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:775d99966e28c8abd9910ddef2de0f1e173afc5a11cea9f184613c747373ab80", size = 141972, upload-time = "2025-09-09T15:16:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d6/76994c8b21316e48ea6c3ce3298574c28f90c9c41428a3349a57104621c9/pycares-4.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:84fde689557361764f052850a2d68916050adbfd9321f6105aca1d8f1a9bd49b", size = 637832, upload-time = "2025-09-09T15:16:55.523Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/5ca7e316d0edb714d78974cb34f4883f63fe9f580644c2db99fb62b05f56/pycares-4.11.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:30ceed06f3bf5eff865a34d21562c25a7f3dad0ed336b9dd415330e03a6c50c4", size = 687751, upload-time = "2025-09-09T15:16:57.55Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/c5c578fdd335d7b1dcaea88fae3497390095b5b05a1ba34a29f62d037abb/pycares-4.11.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:97d971b3a88a803bb95ff8a40ea4d68da59319eb8b59e924e318e2560af8c16d", size = 678362, upload-time = "2025-09-09T15:16:58.859Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/9be4d838a9348dd2e72a90c34d186b918b66d499af5be79afa18a6ba2808/pycares-4.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2d5cac829da91ade70ce1af97dad448c6cd4778b48facbce1b015e16ced93642", size = 641069, upload-time = "2025-09-09T15:17:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/39/d6/8ea9b5dcef6b566cde034aa2b68743f7b0a19fa0fba9ea01a4f98b8a57fb/pycares-4.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee1ea367835eb441d246164c09d1f9703197af4425fc6865cefcde9e2ca81f85", size = 622357, upload-time = "2025-09-09T15:17:01.205Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/3401e89b5d2970e30e02f9beb29ad59e2a8f19ef2c68c978de2b764cacb0/pycares-4.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3139ec1f4450a4b253386035c5ecd2722582ae3320a456df5021ffe3f174260a", size = 670290, upload-time = "2025-09-09T15:17:02.413Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c4/ff6a166e1d1d1987339548a19d0b1d52ec3ead8b3a8a2247a0d96e56013c/pycares-4.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5d70324ca1d82c6c4b00aa678347f7560d1ef2ce1d181978903459a97751543a", size = 652958, upload-time = "2025-09-09T15:17:04.203Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/fc084b395921c9b862d31a83f809fe649c24314b51b527ad0ab0df33edd4/pycares-4.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2f8d9cfe0eb3a2997fde5df99b1aaea5a46dabfcfcac97b2d05f027c2cd5e28", size = 629239, upload-time = "2025-09-09T15:17:05.477Z" }, + { url = "https://files.pythonhosted.org/packages/b0/7f/2f26062bea95ab657f979217d50df563dc9fd9cc4c5dd21a6e7323e9efe7/pycares-4.11.0-cp312-cp312-win32.whl", hash = "sha256:1571a7055c03a95d5270c914034eac7f8bfa1b432fc1de53d871b821752191a4", size = 118918, upload-time = "2025-09-09T15:17:06.882Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/277473d20f3df4e00fa7e0ebb21955b2830b15247462aaf8f3fc8c4950be/pycares-4.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:7570e0b50db619b2ee370461c462617225dc3a3f63f975c6f117e2f0c94f82ca", size = 144560, upload-time = "2025-09-09T15:17:07.891Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f9/d65ad17ec921d8b7eb42161dec2024ee2f5c9f1c44cabf0dd1b7f4fac6c5/pycares-4.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:f199702740f3b766ed8c70efb885538be76cb48cd0cb596b948626f0b825e07a", size = 115695, upload-time = "2025-09-09T15:17:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a9/62fea7ad72ac1fed2ac9dd8e9a7379b7eb0288bf2b3ea5731642c3a6f7de/pycares-4.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c296ab94d1974f8d2f76c499755a9ce31ffd4986e8898ef19b90e32525f7d84", size = 145909, upload-time = "2025-09-09T15:17:10.491Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/0317d6d0d3bd7599c53b8f1db09ad04260647d2f6842018e322584791fd5/pycares-4.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0fcd3a8bac57a0987d9b09953ba0f8703eb9dca7c77f7051d8c2ed001185be8", size = 141974, upload-time = "2025-09-09T15:17:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/63/11/731b565ae1e81c43dac247a248ee204628186f6df97c9927bd06c62237f8/pycares-4.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bac55842047567ddae177fb8189b89a60633ac956d5d37260f7f71b517fd8b87", size = 637796, upload-time = "2025-09-09T15:17:12.815Z" }, + { url = "https://files.pythonhosted.org/packages/f5/30/a2631fe2ffaa85475cdbff7df1d9376bc0b2a6ae77ca55d53233c937a5da/pycares-4.11.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:4da2e805ed8c789b9444ef4053f6ef8040cd13b0c1ca6d3c4fe6f9369c458cb4", size = 687734, upload-time = "2025-09-09T15:17:14.015Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b7/b3a5f99d4ab776662e71d5a56e8f6ea10741230ff988d1f502a8d429236b/pycares-4.11.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:ea785d1f232b42b325578f0c8a2fa348192e182cc84a1e862896076a4a2ba2a7", size = 678320, upload-time = "2025-09-09T15:17:15.442Z" }, + { url = "https://files.pythonhosted.org/packages/ea/77/a00d962b90432993afbf3bd05da8fe42117e0d9037cd7fd428dc41094d7b/pycares-4.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aa160dc9e785212c49c12bb891e242c949758b99542946cc8e2098ef391f93b0", size = 641012, upload-time = "2025-09-09T15:17:16.728Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fb/9266979ba59d37deee1fd74452b2ae32a7395acafe1bee510ac023c6c9a5/pycares-4.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7830709c23bbc43fbaefbb3dde57bdd295dc86732504b9d2e65044df8fd5e9fb", size = 622363, upload-time = "2025-09-09T15:17:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/91/c2/16dbc3dc33781a3c79cbdd76dd1cda808d98ba078d9a63a725d6a1fad181/pycares-4.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ef1ab7abbd238bb2dbbe871c3ea39f5a7fc63547c015820c1e24d0d494a1689", size = 670294, upload-time = "2025-09-09T15:17:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/ff/75/f003905e55298a6dd5e0673a2dc11e31518a5141393b925dc05fcaba9fb4/pycares-4.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a4060d8556c908660512d42df1f4a874e4e91b81f79e3a9090afedc7690ea5ba", size = 652973, upload-time = "2025-09-09T15:17:20.388Z" }, + { url = "https://files.pythonhosted.org/packages/55/2a/eafb235c371979e11f8998d686cbaa91df6a84a34ffe4d997dfe57c45445/pycares-4.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a98fac4a3d4f780817016b6f00a8a2c2f41df5d25dfa8e5b1aa0d783645a6566", size = 629235, upload-time = "2025-09-09T15:17:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/05/99/60f19eb1c8eb898882dd8875ea51ad0aac3aff5780b27247969e637cc26a/pycares-4.11.0-cp313-cp313-win32.whl", hash = "sha256:faa8321bc2a366189dcf87b3823e030edf5ac97a6b9a7fc99f1926c4bf8ef28e", size = 118918, upload-time = "2025-09-09T15:17:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/2a/14/bc89ad7225cba73068688397de09d7cad657d67b93641c14e5e18b88e685/pycares-4.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:6f74b1d944a50fa12c5006fd10b45e1a45da0c5d15570919ce48be88e428264c", size = 144556, upload-time = "2025-09-09T15:17:24.341Z" }, + { url = "https://files.pythonhosted.org/packages/af/88/4309576bd74b5e6fc1f39b9bc5e4b578df2cadb16bdc026ac0cc15663763/pycares-4.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f7581793d8bb3014028b8397f6f80b99db8842da58f4409839c29b16397ad", size = 115692, upload-time = "2025-09-09T15:17:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/2a/70/a723bc79bdcac60361b40184b649282ac0ab433b90e9cc0975370c2ff9c9/pycares-4.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:df0a17f4e677d57bca3624752bbb515316522ad1ce0de07ed9d920e6c4ee5d35", size = 145910, upload-time = "2025-09-09T15:17:26.774Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/46311ef5a384b5f0bb206851135dde8f86b3def38fdbee9e3c03475d35ae/pycares-4.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b44e54cad31d3c3be5e8149ac36bc1c163ec86e0664293402f6f846fb22ad00", size = 142053, upload-time = "2025-09-09T15:17:27.956Z" }, + { url = "https://files.pythonhosted.org/packages/74/23/d236fc4f134d6311e4ad6445571e8285e84a3e155be36422ff20c0fbe471/pycares-4.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:80752133442dc7e6dd9410cec227c49f69283c038c316a8585cca05ec32c2766", size = 637878, upload-time = "2025-09-09T15:17:29.173Z" }, + { url = "https://files.pythonhosted.org/packages/f7/92/6edd41282b3f0e3d9defaba7b05c39730d51c37c165d9d3b319349c975aa/pycares-4.11.0-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:84b0b402dd333403fdce0e204aef1ef834d839c439c0c1aa143dc7d1237bb197", size = 687865, upload-time = "2025-09-09T15:17:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a9/4d7cf4d72600fd47d9518f9ce99703a3e8711fb08d2ef63d198056cdc9a9/pycares-4.11.0-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c0eec184df42fc82e43197e073f9cc8f93b25ad2f11f230c64c2dc1c80dbc078", size = 678396, upload-time = "2025-09-09T15:17:32.304Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4b/e546eeb1d8ff6559e2e3bef31a6ea0c6e57ec826191941f83a3ce900ca89/pycares-4.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ee751409322ff10709ee867d5aea1dc8431eec7f34835f0f67afd016178da134", size = 640786, upload-time = "2025-09-09T15:17:33.602Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f5/b4572d9ee9c26de1f8d1dc80730df756276b9243a6794fa3101bbe56613d/pycares-4.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1732db81e348bfce19c9bf9448ba660aea03042eeeea282824da1604a5bd4dcf", size = 621857, upload-time = "2025-09-09T15:17:34.74Z" }, + { url = "https://files.pythonhosted.org/packages/17/f2/639090376198bcaeff86562b25e1bce05a481cfb1e605f82ce62285230cd/pycares-4.11.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:702d21823996f139874aba5aa9bb786d69e93bde6e3915b99832eb4e335d31ae", size = 670130, upload-time = "2025-09-09T15:17:35.982Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c4/cf40773cd9c36a12cebbe1e9b6fb120f9160dc9bfe0398d81a20b6c69972/pycares-4.11.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:218619b912cef7c64a339ab0e231daea10c994a05699740714dff8c428b9694a", size = 653133, upload-time = "2025-09-09T15:17:37.179Z" }, + { url = "https://files.pythonhosted.org/packages/32/6b/06054d977b0a9643821043b59f523f3db5e7684c4b1b4f5821994d5fa780/pycares-4.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:719f7ddff024fdacde97b926b4b26d0cc25901d5ef68bb994a581c420069936d", size = 629344, upload-time = "2025-09-09T15:17:38.308Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/14bb0c2171a286d512e3f02d6168e608ffe5f6eceab78bf63e3073091ae3/pycares-4.11.0-cp314-cp314-win32.whl", hash = "sha256:d552fb2cb513ce910d1dc22dbba6420758a991a356f3cd1b7ec73a9e31f94d01", size = 121804, upload-time = "2025-09-09T15:17:39.388Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/6822f9ad6941027f70e1cf161d8631456531a87061588ed3b1dcad07d49d/pycares-4.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:23d50a0842e8dbdddf870a7218a7ab5053b68892706b3a391ecb3d657424d266", size = 148005, upload-time = "2025-09-09T15:17:40.44Z" }, + { url = "https://files.pythonhosted.org/packages/ea/24/24ff3a80aa8471fbb62785c821a8e90f397ca842e0489f83ebf7ee274397/pycares-4.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:836725754c32363d2c5d15b931b3ebd46b20185c02e850672cb6c5f0452c1e80", size = 119239, upload-time = "2025-09-09T15:17:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/2f3558d298ff8db31d5c83369001ab72af3b86a0374d9b0d40dc63314187/pycares-4.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c9d839b5700542b27c1a0d359cbfad6496341e7c819c7fea63db9588857065ed", size = 146408, upload-time = "2025-09-09T15:17:43.74Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c8/516901e46a1a73b3a75e87a35f3a3a4fe085f1214f37d954c9d7e782bd6d/pycares-4.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:31b85ad00422b38f426e5733a71dfb7ee7eb65a99ea328c508d4f552b1760dc8", size = 142371, upload-time = "2025-09-09T15:17:45.186Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/c3fba0aa575f331ebed91f87ba960ffbe0849211cdf103ab275bc0107ac6/pycares-4.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cdac992206756b024b371760c55719eb5cd9d6b2cb25a8d5a04ae1b0ff426232", size = 647504, upload-time = "2025-09-09T15:17:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e4/1cdc3ec9c92f8069ec18c58b016b2df7c44a088e2849f37ed457554961aa/pycares-4.11.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:ffb22cee640bc12ee0e654eba74ecfb59e2e0aebc5bccc3cc7ef92f487008af7", size = 697122, upload-time = "2025-09-09T15:17:47.772Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d5/bd8f370b97bb73e5bdd55dc2a78e18d6f49181cf77e88af0599d16f5c073/pycares-4.11.0-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:00538826d2eaf4a0e4becb0753b0ac8d652334603c445c9566c9eb273657eb4c", size = 687543, upload-time = "2025-09-09T15:17:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/33/38/49b77b9cf5dffc0b1fdd86656975c3bc1a58b79bdc883a9ef749b17a013c/pycares-4.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:29daa36548c04cdcd1a78ae187a4b7b003f0b357a2f4f1f98f9863373eedc759", size = 649565, upload-time = "2025-09-09T15:17:51.03Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/f6d57bfb99d00a6a7363f95c8d3a930fe82a868d9de24c64c8048d66f16a/pycares-4.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cf306f3951740d7bed36149a6d8d656a7d5432dd4bbc6af3bb6554361fc87401", size = 631242, upload-time = "2025-09-09T15:17:52.298Z" }, + { url = "https://files.pythonhosted.org/packages/33/a2/7b9121c71cfe06a8474e221593f83a78176fae3b79e5853d2dfd13ab01cc/pycares-4.11.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:386da2581db4ea2832629e275c061103b0be32f9391c5dfaea7f6040951950ad", size = 680304, upload-time = "2025-09-09T15:17:53.638Z" }, + { url = "https://files.pythonhosted.org/packages/5b/07/dfe76807f637d8b80e1a59dfc4a1bceabdd0205a45b2ebf78b415ae72af3/pycares-4.11.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:45d3254a694459fdb0640ef08724ca9d4b4f6ff6d7161c9b526d7d2e2111379e", size = 661039, upload-time = "2025-09-09T15:17:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9b/55d50c5acd46cbe95d0da27740a83e721d89c0ce7e42bff9891a9f29a855/pycares-4.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eddf5e520bb88b23b04ac1f28f5e9a7c77c718b8b4af3a4a7a2cc4a600f34502", size = 637560, upload-time = "2025-09-09T15:17:56.492Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/2b2e723d1b929dbe7f99e80a56abb29a4f86988c1f73195d960d706b1629/pycares-4.11.0-cp314-cp314t-win32.whl", hash = "sha256:8a75a406432ce39ce0ca41edff7486df6c970eb0fe5cfbe292f195a6b8654461", size = 122235, upload-time = "2025-09-09T15:17:57.576Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/bf3b3ed9345a38092e72cd9890a5df5c2349fc27846a714d823a41f0ee27/pycares-4.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3784b80d797bcc2ff2bf3d4b27f46d8516fe1707ff3b82c2580dc977537387f9", size = 148575, upload-time = "2025-09-09T15:17:58.699Z" }, + { url = "https://files.pythonhosted.org/packages/ce/20/c0c5cfcf89725fe533b27bc5f714dc4efa8e782bf697c36f9ddf04ba975d/pycares-4.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:afc6503adf8b35c21183b9387be64ca6810644ef54c9ef6c99d1d5635c01601b", size = 119690, upload-time = "2025-09-09T15:17:59.809Z" }, +] + +[[package]] +name = "pycognito" +version = "2024.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "envs" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/67/3975cf257fcc04903686ef87d39be386d894a0d8182f43d37e9cbfc9609f/pycognito-2024.5.1.tar.gz", hash = "sha256:e211c66698c2c3dc8680e95107c2b4a922f504c3f7c179c27b8ee1ab0fc23ae4", size = 31182, upload-time = "2024-05-16T10:02:28.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/7a/f38dd351f47596b22ddbde1b8906e7f43d14be391dcdbd0c2daba886f26c/pycognito-2024.5.1-py3-none-any.whl", hash = "sha256:c821895dc62b7aea410fdccae4f96d8be7cab374182339f50a03de0fcb93f9ea", size = 26607, upload-time = "2024-05-16T10:02:27.3Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "python_full_version >= '3.13.2'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/35/d319ed522433215526689bad428a94058b6dd12190ce7ddd78618ac14b28/pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd", size = 816358, upload-time = "2025-10-14T15:02:21.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/98/468cb649f208a6f1279448e6e5247b37ae79cf5e4041186f1e2ef3d16345/pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae", size = 460628, upload-time = "2025-10-14T15:02:19.623Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, +] + +[[package]] +name = "pymicro-vad" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/0f/a92acea368e2b37fbc706f6d049f04557497d981316a2f428b26f14666a9/pymicro_vad-1.0.1.tar.gz", hash = "sha256:60e0508b338b694c7ad71c633c0da6fcd2678a88abb8e948b80fa68934965111", size = 135575, upload-time = "2024-07-31T20:04:04.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/75/3804b5838f575daa913070667413c806e17cfb95ce4c2fae5ff595f3bc72/pymicro_vad-1.0.1-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:c4917f2862f8bfc862ce0cc46a63a5505f7d960d3987fe06b7b5e420446c098e", size = 274697, upload-time = "2024-07-31T20:04:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/ad10e2f9d40d40db185401cc486395138eacfe120de24272cb662e9b808f/pymicro_vad-1.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:499bf2799efde7f1135b215f52d5fab25718504deab919f5d3b0423a70d258ef", size = 137545, upload-time = "2024-07-31T20:04:35.774Z" }, + { url = "https://files.pythonhosted.org/packages/ff/18/8278176bc4e5f8d2f72bf1c6f6ff5c0fa46b665dd1b9d559810a8472ffef/pymicro_vad-1.0.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca40fd192807fa4e537e247747e70cc5d630ba13f035e2a60b4d00670c698f1", size = 147940, upload-time = "2024-07-31T20:04:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/c69ac5c5be879845946467701863cdfe6625be0ee63f11f843686fb9eebb/pymicro_vad-1.0.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c78d72d7f02d8d33caaf176ed13e097b08b81051920bb22a0be905f4bf1e17f", size = 153423, upload-time = "2024-07-31T20:04:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/9b/08/5c4fc50cd6156287028f1df96e2cc6b753308c6de46c51a8353853d47c05/pymicro_vad-1.0.1-cp312-cp312-manylinux_2_34_armv7l.whl", hash = "sha256:29c0ccb5b8bd77d564c90526828260e94ea132c08098198f82ac1c5d6df767d6", size = 133665, upload-time = "2024-07-31T21:56:39.644Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/25/d21d6cb3fd249c2c2aa96ee54279f40876a0c93e7161b3304bf21cbd0bfe/pyobjc_framework_corebluetooth-12.1.tar.gz", hash = "sha256:8060c1466d90bbb9100741a1091bb79975d9ba43911c9841599879fc45c2bbe0", size = 33157, upload-time = "2025-11-14T10:13:28.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/56/01fef62a479cdd6ff9ee40b6e062a205408ff386ce5ba56d7e14a71fcf73/pyobjc_framework_corebluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe72c9732ee6c5c793b9543f08c1f5bdd98cd95dfc9d96efd5708ec9d6eeb213", size = 13209, upload-time = "2025-11-14T09:44:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6c/831139ebf6a811aed36abfdfad846bc380dcdf4e6fb751a310ce719ddcfd/pyobjc_framework_corebluetooth-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a894f695e6c672f0260327103a31ad8b98f8d4fb9516a0383db79a82a7e58dc", size = 13229, upload-time = "2025-11-14T09:44:10.463Z" }, + { url = "https://files.pythonhosted.org/packages/09/3c/3a6fe259a9e0745aa4612dee86b61b4fd7041c44b62642814e146b654463/pyobjc_framework_corebluetooth-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1daf07a0047c3ed89fab84ad5f6769537306733b6a6e92e631581a0f419e3f32", size = 13409, upload-time = "2025-11-14T09:44:12.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/41/90640a4db62f0bf0611cf8a161129c798242116e2a6a44995668b017b106/pyobjc_framework_corebluetooth-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:15ba5207ca626dffe57ccb7c1beaf01f93930159564211cb97d744eaf0d812aa", size = 13222, upload-time = "2025-11-14T09:44:14.345Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/8ed2f0ca02b9abe204966142bd8c4501cf6da94234cc320c4c0562c467e8/pyobjc_framework_corebluetooth-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e5385195bd365a49ce70e2fb29953681eefbe68a7b15ecc2493981d2fb4a02b1", size = 13408, upload-time = "2025-11-14T09:44:16.558Z" }, +] + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/e8/75b6b9b3c88b37723c237e5a7600384ea2d84874548671139db02e76652b/pyobjc_framework_libdispatch-12.1.tar.gz", hash = "sha256:4035535b4fae1b5e976f3e0e38b6e3442ffea1b8aa178d0ca89faa9b8ecdea41", size = 38277, upload-time = "2025-11-14T10:16:46.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/6f/96e15c7b2f7b51fc53252216cd0bed0c3541bc0f0aeb32756fefd31bed7d/pyobjc_framework_libdispatch-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e9570d7a9a3136f54b0b834683bf3f206acd5df0e421c30f8fd4f8b9b556789", size = 15650, upload-time = "2025-11-14T09:52:59.284Z" }, + { url = "https://files.pythonhosted.org/packages/38/3a/d85a74606c89b6b293782adfb18711026ff79159db20fc543740f2ac0bc7/pyobjc_framework_libdispatch-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:58ffce5e6bcd7456b4311009480b195b9f22107b7682fb0835d4908af5a68ad0", size = 15668, upload-time = "2025-11-14T09:53:01.354Z" }, + { url = "https://files.pythonhosted.org/packages/cc/40/49b1c1702114ee972678597393320d7b33f477e9d24f2a62f93d77f23dfb/pyobjc_framework_libdispatch-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e9f49517e253716e40a0009412151f527005eec0b9a2311ac63ecac1bdf02332", size = 15938, upload-time = "2025-11-14T09:53:03.461Z" }, + { url = "https://files.pythonhosted.org/packages/59/d8/7d60a70fc1a546c6cb482fe0595cb4bd1368d75c48d49e76d0bc6c0a2d0f/pyobjc_framework_libdispatch-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0ebfd9e4446ab6528126bff25cfb09e4213ddf992b3208978911cfd3152e45f5", size = 15693, upload-time = "2025-11-14T09:53:05.531Z" }, + { url = "https://files.pythonhosted.org/packages/99/32/15e08a0c4bb536303e1568e2ba5cae1ce39a2e026a03aea46173af4c7a2d/pyobjc_framework_libdispatch-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:23fc9915cba328216b6a736c7a48438a16213f16dfb467f69506300b95938cc7", size = 15976, upload-time = "2025-11-14T09:53:07.936Z" }, +] + +[[package]] +name = "pyopenssl" +version = "24.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/70/ff56a63248562e77c0c8ee4aefc3224258f1856977e0c1472672b62dadb8/pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95", size = 184323, upload-time = "2024-07-20T17:26:31.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/dd/e0aa7ebef5168c75b772eda64978c597a9129b46be17779054652a7999e4/pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d", size = 58390, upload-time = "2024-07-20T17:26:29.057Z" }, +] + +[[package]] +name = "pyopenssl" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/26/e25b4a374b4639e0c235527bbe31c0524f26eda701d79456a7e1877f4cc5/pyopenssl-25.0.0.tar.gz", hash = "sha256:cd2cef799efa3936bb08e8ccb9433a575722b9dd986023f1cabc4ae64e9dac16", size = 179573, upload-time = "2025-01-12T17:22:48.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/d7/eb76863d2060dcbe7c7e6cccfd95ac02ea0b9acc37745a0d99ff6457aefb/pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90", size = 56453, upload-time = "2025-01-12T17:22:43.44Z" }, +] + +[[package]] +name = "pyopenssl" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, +] + +[[package]] +name = "pyrfc3339" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/7f/3c194647ecb80ada6937c38a162ab3edba85a8b6a58fa2919405f4de2509/pyrfc3339-2.1.0.tar.gz", hash = "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f", size = 12589, upload-time = "2025-08-23T16:40:31.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/90/0200184d2124484f918054751ef997ed6409cb05b7e8dcbf5a22da4c4748/pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3", size = 6758, upload-time = "2025-08-23T16:40:30.49Z" }, +] + +[[package]] +name = "pyric" +version = "0.1.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/64/a99f27d3b4347486c7bfc0aa516016c46dc4c0f380ffccbd742a61af1eda/PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9", size = 870401, upload-time = "2016-12-04T07:54:48.374Z" } + +[[package]] +name = "pyspeex-noise" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/1d/7d2ebb8f73c2b2e929b4ba5370b35dbc91f37268ea53f4b6acd9afa532cb/pyspeex_noise-1.0.2.tar.gz", hash = "sha256:56a888ca2ef7fdea2316aa7fad3636d2fcf5f4450f3a0db58caa7c10a614b254", size = 49882, upload-time = "2024-08-27T17:00:34.859Z" } + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pyturbojpeg" +version = "1.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/ba/37c075c7cc86b89a22db4ac46c2e4f444666f9a43975a512b7cf70ced2fd/PyTurboJPEG-1.7.5.tar.gz", hash = "sha256:5dd5f40dbf4159f41b6abaa123733910e8b1182df562b6ddb768991868b487d3", size = 12065, upload-time = "2024-07-28T08:34:03.778Z" } + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "rpds-py", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, + { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" }, + { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.13.2'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.13.2'" }, + { name = "idna", marker = "python_full_version < '3.13.2'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.13.2'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.13.2'" }, + { name = "idna", marker = "python_full_version >= '3.13.2'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "securetar" +version = "2024.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/c5/46fc614b4d23823d90fd34b9f41dd649ebf639fc9581c7828eb05f0bd2df/securetar-2024.11.0.tar.gz", hash = "sha256:2191d8c8234777bba287a9b3e8a16cd3ec78fb52d092d1ef1b57d14c81d6838d", size = 11100, upload-time = "2024-11-21T17:25:48.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/cc/82943abb46b97c1b476d3c4c2b86b9601862f31c79b468fafa2fc76a4f25/securetar-2024.11.0-py3-none-any.whl", hash = "sha256:e538dc403b1773f33a58d3ef5fa71ab14c51f060b784924b3745eb6b0b27bfaa", size = 9379, upload-time = "2024-11-21T17:25:46.826Z" }, +] + +[[package]] +name = "securetar" +version = "2025.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/5b/da5f56ad39cbb1ca49bd0d4cccde7e97ea7d01fa724fa953746fa2b32ee6/securetar-2025.2.1.tar.gz", hash = "sha256:59536a73fe5cecbc1f00b1838c8b1052464a024e2adcf6c9ce1d200d91990fb1", size = 16124, upload-time = "2025-02-25T14:17:51.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/e0/b93a18e9bb7f7d2573a9c6819d42d996851edde0b0406d017067d7d23a0a/securetar-2025.2.1-py3-none-any.whl", hash = "sha256:760ad9d93579d5923f3d0da86e0f185d0f844cf01795a8754539827bb6a1bab4", size = 11545, upload-time = "2025-02-25T14:17:50.832Z" }, +] + +[[package]] +name = "sentence-stream" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/69/f3d048692aac843f41102507f6257138392ec841c16718f0618d27051caf/sentence_stream-1.3.0.tar.gz", hash = "sha256:b06261d35729de97df9002a1cc708f9a888f662b80d5d6d008ee69c51f36041b", size = 10049, upload-time = "2026-01-08T16:25:06.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/b6/48339c109bab6f54ff608800773b9425464c6cbf7fd3f2ba01294d78be3d/sentence_stream-1.3.0-py3-none-any.whl", hash = "sha256:7448d131315b85eefdf238e5edd9caa62899acf609145d5e0e10c09812eb8a1d", size = 8707, upload-time = "2026-01-08T16:25:05.918Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "shiny" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "click", marker = "sys_platform != 'emscripten'" }, + { name = "htmltools" }, + { name = "linkify-it-py" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "narwhals" }, + { name = "orjson", version = "3.10.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "orjson", version = "3.10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "prompt-toolkit", marker = "sys_platform != 'emscripten'" }, + { name = "python-multipart", marker = "sys_platform != 'emscripten'" }, + { name = "questionary", marker = "sys_platform != 'emscripten'" }, + { name = "setuptools" }, + { name = "shinychat" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, + { name = "watchfiles", marker = "sys_platform != 'emscripten'" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/8c/570fb0c8aa3b3f2b30e2f401bbe4048e7c39fb5ffb70994520fdff32a8da/shiny-1.5.1.tar.gz", hash = "sha256:482fa54635a6dc7e914cdbaa9099fe445790af3053d849b4e9a81e0f61677d37", size = 4941243, upload-time = "2025-12-08T18:18:23.763Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/2d/c955cbc6e66aff92645a7af35fa017001dffecb849b764f609fdc68da088/shiny-1.5.1-py3-none-any.whl", hash = "sha256:5ed60168e94fce0b7fc65bc04b92398e2611a2410acbcb850f8ae3001b99de40", size = 3943136, upload-time = "2025-12-08T18:18:21.654Z" }, +] + +[[package]] +name = "shinychat" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "htmltools" }, + { name = "shiny" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/c6/6887a3618842e65794e5dcfb345825ccd43d58c56fefa9d31286d8e14d16/shinychat-0.2.8.tar.gz", hash = "sha256:d27f28ddf1d512a05ef2cc2f0cffbb75b5b6e3157d5e3690ce4e63d18bfa7492", size = 547318, upload-time = "2025-09-11T20:13:20.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/01/8658eaffa920f4c621f05acc42ea98248147706079287b3a7ab4e47e1ece/shinychat-0.2.8-py3-none-any.whl", hash = "sha256:6742a2354257280458269a8b5675f1254e5583be34b6eb1a626de44c0323286a", size = 561057, upload-time = "2025-09-11T20:13:18.683Z" }, +] + +[[package]] +name = "shinylive" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "chevron" }, + { name = "click" }, + { name = "lzstring" }, + { name = "setuptools" }, + { name = "shiny" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b4/a741e01b756d76bf5839d47c54562563353fd5c5849e0cbb8b5b8ea23d8b/shinylive-0.8.5.tar.gz", hash = "sha256:d485efa14a4053be0e6ec19a21a49e5d0b6bd8b99cd2f5cb81b0b04f57a1c249", size = 29519, upload-time = "2025-12-08T21:08:27.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/bb/8dc375e7fb90737fe64a3aa150441a667225672d5d4b735880e4a41f1686/shinylive-0.8.5-py3-none-any.whl", hash = "sha256:85ac83b5b86adf181f79937a50c997315d30cd944a565a31d1a99abc8d715dcd", size = 29432, upload-time = "2025-12-08T21:08:25.868Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "snitun" +version = "0.39.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "async-timeout", marker = "python_full_version < '3.13'" }, + { name = "attrs", version = "24.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/ff/2b7499dbfea2fa748620f8181aebdff26e24b6f78026516760e73d11a319/snitun-0.39.1.tar.gz", hash = "sha256:fadbe447eea786291d5c52e67eae0658f53a1f68c4b97425e17a9579df503d7e", size = 33073, upload-time = "2024-05-13T07:21:26.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/c3/1fd863e859a91d9087613195012d96e03c09063718b53deac610b4fd86b4/snitun-0.39.1-py3-none-any.whl", hash = "sha256:6ff55f6ba21d463877f9872de7d632fc18e400c8b42f8115c42d17e51075f674", size = 39089, upload-time = "2024-05-13T07:21:24.598Z" }, +] + +[[package]] +name = "snitun" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "async-timeout", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "attrs", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/5d/c39d5dee7119017efa571e7ce09fcb4f098734cb367adab59bed497ae0e9/snitun-0.40.0.tar.gz", hash = "sha256:f5a70b3aab07524f196d27baf7a8f8774b3b00c442e91392539dd11dbd033c9c", size = 33111, upload-time = "2024-12-18T12:43:16.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/07/9982bd349e7a1aef3f8077ccfcf7ee9b447bd70ccab8121ad786334a882a/snitun-0.40.0-py3-none-any.whl", hash = "sha256:dedb58d3042d13311142b55337ad6ce6ed339e43da9dca4c4c2c83df77c64ac0", size = 39122, upload-time = "2024-12-18T12:43:12.756Z" }, +] + +[[package]] +name = "snitun" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/e2/b5bbf04971d1c3e07a3e16a706ea3c1a4b711c6d8c9566e8012772d3351a/snitun-0.45.1.tar.gz", hash = "sha256:d76d48cf4190ea59e8f63892da9c18499bfc6ca796220a463c6f3b32099d661c", size = 43335, upload-time = "2025-09-25T05:24:07.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1b/83ff83003994bc8b56483c75a710de588896c167c7c42d66d059a2eb48dc/snitun-0.45.1-py3-none-any.whl", hash = "sha256:c1fa4536320ec3126926ade775c429e20664db1bc61d8fec0e181dc393d36ab4", size = 51236, upload-time = "2025-09-25T05:24:06.412Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.36" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/65/9cbc9c4c3287bed2499e05033e207473504dc4df999ce49385fb1f8b058a/sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", size = 9574485, upload-time = "2024-10-15T19:41:44.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/bf/005dc47f0e57556e14512d5542f3f183b94fde46e15ff1588ec58ca89555/SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", size = 2092378, upload-time = "2024-10-16T00:43:55.469Z" }, + { url = "https://files.pythonhosted.org/packages/94/65/f109d5720779a08e6e324ec89a744f5f92c48bd8005edc814bf72fbb24e5/SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", size = 2082778, upload-time = "2024-10-16T00:43:57.304Z" }, + { url = "https://files.pythonhosted.org/packages/60/f6/d9aa8c49c44f9b8c9b9dada1f12fa78df3d4c42aa2de437164b83ee1123c/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53", size = 3232191, upload-time = "2024-10-15T21:31:12.896Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ab/81d4514527c068670cb1d7ab62a81a185df53a7c379bd2a5636e83d09ede/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", size = 3243044, upload-time = "2024-10-15T20:16:28.954Z" }, + { url = "https://files.pythonhosted.org/packages/35/b4/f87c014ecf5167dc669199cafdb20a7358ff4b1d49ce3622cc48571f811c/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", size = 3178511, upload-time = "2024-10-15T21:31:16.792Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/badfc9293bc3ccba6ede05e5f2b44a760aa47d84da1fc5a326e963e3d4d9/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", size = 3205147, upload-time = "2024-10-15T20:16:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/c8/60/70e681de02a13c4b27979b7b78da3058c49bacc9858c89ba672e030f03f2/SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", size = 2062709, upload-time = "2024-10-15T20:16:29.946Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ed/f6cd9395e41bfe47dd253d74d2dfc3cab34980d4e20c8878cb1117306085/SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", size = 2088433, upload-time = "2024-10-15T20:16:33.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/5c/236398ae3678b3237726819b484f15f5c038a9549da01703a771f05a00d6/SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", size = 2087651, upload-time = "2024-10-16T00:43:59.168Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/55c47420c0d23fb67a35af8be4719199b81c59f3084c28d131a7767b0b0b/SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", size = 2078132, upload-time = "2024-10-16T00:44:01.279Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/1e843b36abff8c4a7aa2e37f9bea364f90d021754c2de94d792c2d91405b/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", size = 3164559, upload-time = "2024-10-15T21:31:18.961Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c5/07f18a897b997f6d6b234fab2bf31dccf66d5d16a79fe329aefc95cd7461/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", size = 3177897, upload-time = "2024-10-15T20:16:35.048Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cd/e16f3cbefd82b5c40b33732da634ec67a5f33b587744c7ab41699789d492/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", size = 3111289, upload-time = "2024-10-15T21:31:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/15/85/5b8a3b0bc29c9928aa62b5c91fcc8335f57c1de0a6343873b5f372e3672b/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", size = 3139491, upload-time = "2024-10-15T20:16:38.048Z" }, + { url = "https://files.pythonhosted.org/packages/a1/95/81babb6089938680dfe2cd3f88cd3fd39cccd1543b7cb603b21ad881bff1/SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", size = 2060439, upload-time = "2024-10-15T20:16:36.182Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/5f7428df55660d6879d0522adc73a3364970b5ef33ec17fa125c5dbcac1d/SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", size = 2084574, upload-time = "2024-10-15T20:16:38.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787, upload-time = "2024-10-15T20:04:30.265Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.39" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "greenlet", marker = "(python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'AMD64') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'WIN32') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'aarch64') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'amd64') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'ppc64le') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'win32') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'x86_64')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/8e/e77fcaa67f8b9f504b4764570191e291524575ddbfe78a90fc656d671fdc/sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22", size = 9644602, upload-time = "2025-03-11T18:27:09.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/86/b2cb432aeb00a1eda7ed33ce86d943c2452dc1642f3ec51bfe9eaae9604b/sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b", size = 2107210, upload-time = "2025-03-11T19:21:50.748Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b0/b2479edb3419ca763ba1b587161c292d181351a33642985506a530f9162b/sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c", size = 2097599, upload-time = "2025-03-11T19:21:52.273Z" }, + { url = "https://files.pythonhosted.org/packages/58/5e/c5b792a4abcc71e68d44cb531c4845ac539d558975cc61db1afbc8a73c96/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47", size = 3247012, upload-time = "2025-03-11T19:09:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a8/055fa8a7c5f85e6123b7e40ec2e9e87d63c566011d599b4a5ab75e033017/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd", size = 3257851, upload-time = "2025-03-11T19:32:43.917Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/aec16681e91a22ddf03dbaeb3c659bce96107c5f47d2a7c665eb7f24a014/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a", size = 3193155, upload-time = "2025-03-11T19:09:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/21/9d/cef697b137b9eb0b66ab8e9cf193a7c7c048da3b4bb667e5fcea4d90c7a2/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06", size = 3219770, upload-time = "2025-03-11T19:32:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/57/05/e109ca7dde837d8f2f1b235357e4e607f8af81ad8bc29c230fed8245687d/sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109", size = 2077567, upload-time = "2025-03-11T18:43:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/25ca068e38c29ed6be0fde2521888f19da923dbd58f5ff16af1b73ec9b58/sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338", size = 2103136, upload-time = "2025-03-11T18:43:15.316Z" }, + { url = "https://files.pythonhosted.org/packages/32/47/55778362642344324a900b6b2b1b26f7f02225b374eb93adc4a363a2d8ae/sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87", size = 2102484, upload-time = "2025-03-11T19:21:54.018Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e1/f5f26f67d095f408138f0fb2c37f827f3d458f2ae51881546045e7e55566/sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716", size = 2092955, upload-time = "2025-03-11T19:21:55.658Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c2/0db0022fc729a54fc7aef90a3457bf20144a681baef82f7357832b44c566/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4", size = 3179367, upload-time = "2025-03-11T19:09:31.059Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f33743d87d0b4e7a1f12e1631a4b9a29a8d0d7c0ff9b8c896d0bf897fb60/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d", size = 3192705, upload-time = "2025-03-11T19:32:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/c9/74/6814f31719109c973ddccc87bdfc2c2a9bc013bec64a375599dc5269a310/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e", size = 3125927, upload-time = "2025-03-11T19:09:32.678Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6b/18f476f4baaa9a0e2fbc6808d8f958a5268b637c8eccff497bf96908d528/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723", size = 3154055, upload-time = "2025-03-11T19:32:53.344Z" }, + { url = "https://files.pythonhosted.org/packages/b4/60/76714cecb528da46bc53a0dd36d1ccef2f74ef25448b630a0a760ad07bdb/sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7", size = 2075315, upload-time = "2025-03-11T18:43:16.946Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7c/76828886d913700548bac5851eefa5b2c0251ebc37921fe476b93ce81b50/sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0", size = 2099175, upload-time = "2025-03-11T18:43:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0f/d69904cb7d17e65c65713303a244ec91fd3c96677baf1d6331457fd47e16/sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f", size = 1898621, upload-time = "2025-03-11T19:20:33.027Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "greenlet", marker = "(python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +] + +[[package]] +name = "standard-aifc" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "standard-chunk", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, +] + +[[package]] +name = "standard-chunk" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, +] + +[[package]] +name = "standard-telnetlib" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/06/7bf7c0ec16574aeb1f6602d6a7bdb020084362fb4a9b177c5465b0aae0b6/standard_telnetlib-3.13.0.tar.gz", hash = "sha256:243333696bf1659a558eb999c23add82c41ffc2f2d04a56fae13b61b536fb173", size = 12636, upload-time = "2024-10-30T16:01:42.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/85/a1808451ac0b36c61dffe8aea21e45c64ba7da28f6cb0d269171298c6281/standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691", size = 9995, upload-time = "2024-10-30T16:01:29.289Z" }, +] + +[[package]] +name = "starlette" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/65/5a1fadcc40c5fdc7df421a7506b79633af8f5d5e3a95c3e72acacec644b9/starlette-0.51.0.tar.gz", hash = "sha256:4c4fda9b1bc67f84037d3d14a5112e523509c369d9d47b111b2f984b0cc5ba6c", size = 2647658, upload-time = "2026-01-10T20:23:15.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/c4/09985a03dba389d4fe16a9014147a7b02fa76ef3519bf5846462a485876d/starlette-0.51.0-py3-none-any.whl", hash = "sha256:fb460a3d6fd3c958d729fdd96aee297f89a51b0181f16401fe8fd4cb6129165d", size = 74133, upload-time = "2026-01-10T20:23:13.445Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "python_full_version >= '3.13.2'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.13.2' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uart-devices" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/08/a8fd6b3dd2cb92344fb4239d4e81ee121767430d7ce71f3f41282f7334e0/uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34", size = 5167, upload-time = "2025-02-22T16:47:05.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/64/edf33c2d7fba7d6bf057c9dc4235bfc699517ea4c996240a1a9c2bf51c29/uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123", size = 4827, upload-time = "2025-02-22T16:47:04.286Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + +[[package]] +name = "ulid-transform" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/64/5e/d48740b5aa3f1135c3c76f4bffaa7bddb209807d6ac11ad79666d451641b/ulid_transform-1.0.2.tar.gz", hash = "sha256:9b710f6adb93a7620910bce385c7e977a234ab321443ec3bc1e48ae931f1e5d4", size = 15743, upload-time = "2024-08-24T23:23:22.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/61/63d342c76937a4422ec0c581bf976717e243ae0f84d53592a00a78d18aaa/ulid_transform-1.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:eb88626b68fa34883722ade34df0fd3b51f55ab6730e8bda6532a087568bbb54", size = 41179, upload-time = "2024-08-24T23:28:52.585Z" }, + { url = "https://files.pythonhosted.org/packages/74/2e/b5e0afc1f9a19eefb293c55d8c6444c9132b8c026006e2b3f1e3848ce2a9/ulid_transform-1.0.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b39b0188c788dac1338e3e217fc83728189f1e4a91ff75afd5152ddb7a41fe9", size = 164907, upload-time = "2024-08-24T23:28:53.845Z" }, + { url = "https://files.pythonhosted.org/packages/b7/08/17391871f8585a0d3321a96f57ce27225d1c15d8497881935f180e668ab7/ulid_transform-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc58deae5c2a3868824f3ce8e103845a600b6420fab88c4b6f1cab8a45c657f4", size = 172564, upload-time = "2024-08-24T23:28:55.301Z" }, + { url = "https://files.pythonhosted.org/packages/05/5e/2e326e349ce3da7667817e12de333a5aaa18d904d8ee75122bb17dbdf61c/ulid_transform-1.0.2-cp312-cp312-manylinux_2_36_x86_64.whl", hash = "sha256:55812ff17d265a1318c16b2af3349bdd893f0c5ca4352ea429d82ea8a7ab36ac", size = 170072, upload-time = "2024-08-24T23:23:20.946Z" }, + { url = "https://files.pythonhosted.org/packages/1e/36/736752356192f19d0b65fc1da68b616ac185a6ee960b8c9f69df4d5d8622/ulid_transform-1.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a95449d9343ee6cfae49ace10b7ed094acbfcb19c4d56a5ba06874507e1e550f", size = 1243697, upload-time = "2024-08-24T23:28:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/80/78/768b512c3870d512692ec108b430debdcdaa5fa1cbb5d9411253f37747e8/ulid_transform-1.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e8b7ac6d9273bac1c1a242bbc6c3d9c3018bff8e00501293a8cf7493bff7190", size = 1155626, upload-time = "2024-08-24T23:28:57.898Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/ca24bafa285d37fef4edda165c9893cc23977ba7309cf1cbbf9347dd5de7/ulid_transform-1.0.2-cp312-cp312-win32.whl", hash = "sha256:72f719c10b3e9135a09c8d38182e4afacb2c8908ea566257c037b3a4a62f9b91", size = 38715, upload-time = "2024-08-24T23:28:59.036Z" }, + { url = "https://files.pythonhosted.org/packages/02/be/dcb545a55247636bbbf642d0a61c3db3106aab9fccd59f799630e3203478/ulid_transform-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:943aaa6c6888f93e4718bcd1cc852db5ee0b2694a1107dcf411bfa2b5e3eb3bd", size = 41041, upload-time = "2024-08-24T23:29:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/3dba6de60c854b9baa56aaff114f1a8f34327ea6bdceb7a76647dbf4b408/ulid_transform-1.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:76563b84d0852a2861cdd22fb082ae1f7a88f038c078be4ad716610eeea421a6", size = 40524, upload-time = "2024-08-24T23:29:01.873Z" }, + { url = "https://files.pythonhosted.org/packages/e8/48/323ed2ae0c80a875793115ae97fab331b9785b96d56064096011f31758e3/ulid_transform-1.0.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96c3c2423972cae3f26ef564c79507da49558553fb13e605dcf9395fab2924e4", size = 158855, upload-time = "2024-08-24T23:29:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/5b06e4bce8b0b6b1d4334ad83e6224684eac1744fcff0e9e4d215165af33/ulid_transform-1.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3887088b3a3bc4a98bcd65274a0324c50dfd6d56305434ddcc25024a1ed98b", size = 166633, upload-time = "2024-08-24T23:29:04.435Z" }, + { url = "https://files.pythonhosted.org/packages/56/a0/78b2f37777ef89c335a86f6803dd68d60fd0f700aa2c30990a4cb064aa4e/ulid_transform-1.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bd972e8c1695ad3e24b5d9eaa5ad352ff9237f58186d182f4ef48a1f7d352b1d", size = 1237875, upload-time = "2024-08-24T23:29:05.876Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/398dd416d45b0f7d497486c67d080bc113d1b0aefdeab85d16a12d2f852f/ulid_transform-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71c71a436f5e2c0a1580ad5269b1e7ad8193c1cbb69463342261e46f0f722f4d", size = 1150251, upload-time = "2024-08-24T23:29:07.016Z" }, + { url = "https://files.pythonhosted.org/packages/e0/92/47c5c4f09da84e6f73bf4b984a757a53086a8216b0c0ee4ccdb5a3f3c0e9/ulid_transform-1.0.2-cp313-cp313-win32.whl", hash = "sha256:2231ca1d83f5964a7cdf353f9d7cbc16a2e51eb8c9d5a9c743fe3aa0d17c6b3e", size = 38270, upload-time = "2024-08-24T23:29:08.192Z" }, + { url = "https://files.pythonhosted.org/packages/58/88/bdb72f143c76aabe2f5169a441f8d28973ed4e6fb598c4b3acdd77b379a0/ulid_transform-1.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:0d5bc5f3392b78ba9225dbb919b05fed7d62cff77f8674cc1389c01d3ae9a947", size = 40309, upload-time = "2024-08-24T23:29:09.395Z" }, +] + +[[package]] +name = "ulid-transform" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/f2/16c8e6f3d82debedeb1b09bec889ad4a1ca8a71d2d269c156dd80d049c2e/ulid_transform-1.4.0.tar.gz", hash = "sha256:5914a3c4277b0d25ebb67f47bfee2167ac858d970249ea275221fb3e5d91c9a0", size = 16023, upload-time = "2025-03-07T10:44:02.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/1d/c43d3e1bda52a321f6cde3526b3634602958dc8ccf1f20fd6616767fd1a1/ulid_transform-1.4.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:9b1429ca7403696b290e4e97ffadbf8ed0b7470a97ad7e273372c3deae5bfb2f", size = 51566, upload-time = "2025-03-07T10:44:00.79Z" }, +] + +[[package]] +name = "ulid-transform" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/25/44/2ef5e7218ad021fda3fedcb6c1347dd3bf972e9cbdea94644aaa7e4884bb/ulid_transform-1.5.2.tar.gz", hash = "sha256:9a5caf279ec21789ddc2f36b9008ce33a3197d7d66fdd7628fbebec9ba778829", size = 14247, upload-time = "2025-10-04T20:57:22.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/3a/7b9e8cfa2739fd31bdfc0e0e9065a237c2b4a1164331eb0a859e46b3dee8/ulid_transform-1.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2fbadf7fc2b72b1ab0ae176d42492e2b8f69817db32e653fe8d2817ca5b1a714", size = 41637, upload-time = "2025-10-04T21:03:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/f9c05b0c2b341db1adbffa2507665557cd1cce403150174322537e4d5532/ulid_transform-1.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5766373871bdf874e3468c1e1e9c291e628223fbc085ca10a961f26f720fe9d", size = 42186, upload-time = "2025-10-04T21:03:50.736Z" }, + { url = "https://files.pythonhosted.org/packages/25/c3/6903a4d068b2e93727ee5c590aa8025da6a7927b10718e624129ad5daf22/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5074c362d7bcf53b4a5acaeb0416542ba3838ebabe54d60c3b6ac920aa612d6c", size = 49292, upload-time = "2025-10-04T21:03:52.615Z" }, + { url = "https://files.pythonhosted.org/packages/7f/aa/36cf33b3a514f295ec379908597872ae5c3d334bec942777d6afe6449c60/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e986b5079764350a586006b6bfda04f9ae87170f03efe377a3a6ec3e0ac086e6", size = 46576, upload-time = "2025-10-04T21:03:53.574Z" }, + { url = "https://files.pythonhosted.org/packages/85/7f/f39c7b8c7987c82f01384c615a75bcf6bc3b37fab8a64029d40c71752fa5/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba5f9ed304f956e83c29642704a0f814c05c23c68cc028a27bd234acc2f04782", size = 49702, upload-time = "2025-10-04T21:03:54.611Z" }, + { url = "https://files.pythonhosted.org/packages/1e/45/68cb9b8a86bdabc3ba06a00451a6c8b720213cc7eb56adc1b3c741bbe421/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2303395992e6a3d95f589173a78952a8690bf3b8bd97d248eee580ad9c1898eb", size = 1028866, upload-time = "2025-10-04T21:03:55.708Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b1/17a9fbddf4371ec04f958ec157e10f4b2361829b9f9c7263b88a2cf8906a/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:07cd1b250293d731e3fcf1ad3a4eb260e78f03c51676bf96d4e126c77eca48c4", size = 894658, upload-time = "2025-10-04T21:03:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/43/421cbc0f751d09b09b563f69ea4b99a95c8a269417b6ff3266e27a5a8f3b/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:547a824a7db7436bc68afd39ccdec0ce5f7e0b235897cc7f01ab148257db9ab9", size = 1080539, upload-time = "2025-10-04T21:03:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/1f/39/39e85470bd409d05502b552946d1a852704bee92e2f38f43d576e551f300/ulid_transform-1.5.2-cp312-cp312-win32.whl", hash = "sha256:8b0650b56ee6bde9de7c7f247584c2be931834c8a49b306ed905d2c8a6653eaa", size = 39356, upload-time = "2025-10-04T21:04:00.308Z" }, + { url = "https://files.pythonhosted.org/packages/43/55/c8dedcde6ec1c75ab8c0b9e937f2d5032391ac5f95496fd80cb97dc018f4/ulid_transform-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:301aa542de87a792cc4e82dc7678cc9e3be61e60c16bbdf24f12bc4883e9e8e8", size = 41929, upload-time = "2025-10-04T21:04:01.235Z" }, + { url = "https://files.pythonhosted.org/packages/32/e5/f0d51b4b67e91dae04416623d81017863ea576dfa3ead2a8639761564423/ulid_transform-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:74ba0045e2ab94be1fa6a7901f9958cef6d35cda58546cdfbadc7129ebcdc88b", size = 41138, upload-time = "2025-10-04T21:04:02.198Z" }, + { url = "https://files.pythonhosted.org/packages/43/07/a80882cbc9557996996aae583c2d98bd90e54573390ae1332fed1a7e0124/ulid_transform-1.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6354467dab6aa922cdd7e4a8a2da31222d07609616df167e656dac7244d0f658", size = 41495, upload-time = "2025-10-04T21:04:03.173Z" }, + { url = "https://files.pythonhosted.org/packages/23/81/768b26d31ba4e7002abfd507e2ed42731e8bc4de5c38d10b5530ec19d52f/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4faec817e9e5a031d4887c69e1254c428683c62e6f26ae9fd2f0a330a7c4c85", size = 48781, upload-time = "2025-10-04T21:04:04.112Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/13f10f9cf258f4faf8340f9e7dbb9c001340c27b3b68a4baa9688af2b428/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40897a0189cef7cce7c0b26bcff9daafa9df2ce68249e7e6095090a6372ed6ac", size = 45918, upload-time = "2025-10-04T21:04:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/38/9d/aed53563a544556a2c547e33b950a861d006feeda1ded38e79ade6212672/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:397a2d6c2030a3c3d572dfa35c27c647911219daba93056a80091f76888f597e", size = 49232, upload-time = "2025-10-04T21:04:06.123Z" }, + { url = "https://files.pythonhosted.org/packages/c5/76/47c2a1940c732a3842163668a7ebe11796fc02c1fa170c4abe9868a78193/ulid_transform-1.5.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:6794612dbc085abdac5ee3c4e3ec141ab8eb0e7b31f94f56111cacbe36137339", size = 49269, upload-time = "2025-10-04T20:57:20.283Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/d15aa9cfe960ce9fe8e1a14a4b7222c928862ae14128e7480bcee142b546/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497e2dbca8b53c0d072da2c662ac8779faf698ccd52827932a8dcbc5d15960d4", size = 1028355, upload-time = "2025-10-04T21:04:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/37/d259be5021e95d443d8659e1bc2dd90b2248d6ef5ce8e4c97c434fe688ae/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a51bab261fc5a50e1eb81f022129ffc98e64b917a69b29ca8411a00464c09d47", size = 894259, upload-time = "2025-10-04T21:04:08.663Z" }, + { url = "https://files.pythonhosted.org/packages/15/1d/568e1abf4089d4f7b8bcef09abe99fd4b2a56c9ca1ea81fc94e8d7a3cdff/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52f728769c822a52c05310598d949541d1e929dd1a481b2e73c971beea089a2a", size = 1080159, upload-time = "2025-10-04T21:04:10.321Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/ecbd9ab6a1b4c9c6fc57a974ad0ccd960d9adaed4c04794dc18063a80c57/ulid_transform-1.5.2-cp313-cp313-win32.whl", hash = "sha256:0b90b0b7ed937f8cf245f8c71adbd73cf93fcc4915cac9150255595a016b53d0", size = 38989, upload-time = "2025-10-04T21:04:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3f/180a40f60252cd195d00f1a5328684a0c14cce2201014db1a72ce4a92c0c/ulid_transform-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:3c1354706ac87ecf3b941c836c04b96f83e5d50ba7b2c3e2f746da838405bc09", size = 41200, upload-time = "2025-10-04T21:04:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c5/8eb0aa7bcd5cfb772ae8535b63d8d5fe7503c3d0adda931e7ee7e5e9af39/ulid_transform-1.5.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bae1b0f6041bd8c7d49019c14fb94e42ccab2b59083e7b0cb9f4d13483d7435a", size = 41085, upload-time = "2025-10-04T21:04:13.468Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ff/45cfb8ceaa67eea28c7a1a90242c1ade01b2a1b714feec7af47c086c6945/ulid_transform-1.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9c4a61fd13ced6b0b96f5983ef4e57ad8adefed4361b6d0f55a2bbfbb18b17d8", size = 41575, upload-time = "2025-10-04T21:04:14.379Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/ab80688863e7228732736ec39764910891b0978ae0d1953395ce2d505cdc/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8065ddfd43827b1299a64da4437161a4f3fa1f03c05d838a3a8c82bc1d014518", size = 48948, upload-time = "2025-10-04T21:04:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dd/9bd352aac0fddf167a70dcab386cc4d8d099676531a89afa5019c6f1dbe7/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b60965067d13942b0eaaaf986da5eff4ba8f1261b379ca1dac78afe47d940f1a", size = 45789, upload-time = "2025-10-04T21:04:16.861Z" }, + { url = "https://files.pythonhosted.org/packages/cb/90/0b4b4e0ac6061ea90cbdc526e17a75aad0fefafadbe43c56bfd7a77b8a85/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e97a11b56b9e5537ef4521a97fc095b49d849c0ac0ec8d30a2974bd69e5948d", size = 49351, upload-time = "2025-10-04T21:04:18.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7b/57da5afcd538306c44c093ef314bef4b04768f04b3c509144ed201b7d374/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4915062eee740eefa937459ef468f7f1e35bd2ad5bffdf4245051d656df2c4", size = 1028528, upload-time = "2025-10-04T21:04:19.278Z" }, + { url = "https://files.pythonhosted.org/packages/a1/96/5d3c3464bb64b4fd6b6605787b3a4fef982ba207ba8a8ecc432543fe954e/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7d4cf4bb26fe102dfd1bd10c5b18712fe7640433839c8d9dd20e2d8ccefa972d", size = 894080, upload-time = "2025-10-04T21:04:20.548Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/05dc2a2b2f981617a3ba1cdd8277b86504c4293feefc3a3ba342bac7cbec/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad4953675e6dec400de633087f61cbb38d0ad978d57b60cc3539f7b821d9559", size = 1080292, upload-time = "2025-10-04T21:04:22.092Z" }, + { url = "https://files.pythonhosted.org/packages/f3/78/06032df3d6cc211a4d3edad92ed9433fa84e654c42936c1e268d53feab31/ulid_transform-1.5.2-cp314-cp314-win32.whl", hash = "sha256:d6793d4c477b30d95ed84123cc73d515ba4dac58cd01e7584637421b377349d3", size = 39903, upload-time = "2025-10-04T21:04:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ba/c0b0f757e9d2b5c565624227336fcc353c5e3160667d451ac361d342b11d/ulid_transform-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:dbbe98fd8b46431e3a15268e0dceeb80291ebfa7741d1ee692006928c0900d0c", size = 41987, upload-time = "2025-10-04T21:04:24.576Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/4f74de4fe96bb85fcc1e7fd572527aafd0999d48de3f6d1a41c66cdebc41/ulid_transform-1.5.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:08286ccc6bac0107e1bd5415a28e730d88089293ba5ce51dc5883175eccc31e2", size = 68442, upload-time = "2025-10-04T21:04:25.66Z" }, + { url = "https://files.pythonhosted.org/packages/03/58/854e4bd4539be70e5714002198e0565f897cf5b65203d9c230b362cc41df/ulid_transform-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed0b533c936cb120312cd98ca1c8ec1f8af66bac6bc08426c030b48291d5505e", size = 69113, upload-time = "2025-10-04T21:04:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/6e/45/73d6aa7c63e1e472bbe8e54a0892cdec78dc8438798e9ea518f1c371f640/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58617bae6fc21507f5151328faf7b77c6ba6a615b42efd18f494564354a3ce68", size = 85341, upload-time = "2025-10-04T21:04:28.046Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fb/263774b2249d683addd0de5746d4c4debbb33966277d4d33390150944ba3/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e24e68971a64a04af2d0b3df98bfe0087c85d35a1b02fa0bbf43a3a0a99dccf6", size = 78616, upload-time = "2025-10-04T21:04:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/88/cf/2eda3645a002a9fd141c19fd7416de87adaa12b25f8916b42b78644dbddd/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb5da66ec5e7d97f695dd16637d5a8816bb9661df43ff1f2de0d46071d96a7a8", size = 85582, upload-time = "2025-10-04T21:04:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/e08e975e4ed61fb2ae7014591fcc7e5f1a62966c7ed53fc1c95c3da78923/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:397646cf156aa46456cd8504075d117d2983ebf2cff01955c3add6280d0fb3c8", size = 1066091, upload-time = "2025-10-04T21:04:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e7/43474bf4ec56eadf2509939585894ef094dc364143596f62639bebd6d42e/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:dc5ac2ffa704a21df2a36cea47ac1022fb6f8ab03abe31a5f7b81312f972e2c2", size = 926160, upload-time = "2025-10-04T21:04:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/ca/48/36df80548d96ffdaa3ae124854bbd5a0a0b07da22a8d22b301e5ec17de6e/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6f29b8004fba0da7061b5eecf6101c6283377b6cd04f3626089cc67d9342c8fd", size = 1116228, upload-time = "2025-10-04T21:04:35.325Z" }, + { url = "https://files.pythonhosted.org/packages/9f/85/9cd506a4e0fe06946c3fd75b89c7d1e9a175a5ac11dfd8e4cc56658ff389/ulid_transform-1.5.2-cp314-cp314t-win32.whl", hash = "sha256:c09f58aff7a4974f560dd5fb19dd5144e8964371fcb1971bffa817c9abcb2232", size = 67476, upload-time = "2025-10-04T21:04:36.907Z" }, + { url = "https://files.pythonhosted.org/packages/41/a9/161ab974510c78a28155de33091a0063ed84b110e6d4fb866d5110112ce9/ulid_transform-1.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:445e14170301229a486e8815c2f9cec4a10b3e3cd4c9aa509689443d05e4f020", size = 72206, upload-time = "2025-10-04T21:04:38.286Z" }, +] + +[[package]] +name = "unicode-rbnf" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/1f/d952ba97832647e608700c36b22d1c4476016076c9ed1ce74ae814bea55a/unicode_rbnf-2.4.0.tar.gz", hash = "sha256:6d2f12a7581c69ea6218ee61fafcd2da46e1f9986bdcd0964c5151f7c2a938ac", size = 89069, upload-time = "2025-10-07T20:59:41.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/21/82f5d435808cba330668a8b69efb180e3ef9739d4998e8cd0381e8c9cb23/unicode_rbnf-2.4.0-py3-none-any.whl", hash = "sha256:0176b30ac9b7b84008d7dc0f23078055dc10d2671fdadfab5747943243e20e2d", size = 141691, upload-time = "2025-10-07T20:59:40.139Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "usb-devices" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/48/dbe6c4c559950ebebd413e8c40a8a60bfd47ddd79cb61b598a5987e03aad/usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d", size = 5421, upload-time = "2023-12-16T19:59:53.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c9/26171ae5b78d72dd006bbc51ca9baa2cbb889ae8e91608910207482108fd/usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf", size = 5349, upload-time = "2023-12-16T19:59:51.604Z" }, +] + +[[package]] +name = "uv" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/18/ad/66cc8e00c217e7fcf76598c880632b480aa38d4cad311596b78e99737498/uv-0.5.4.tar.gz", hash = "sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00", size = 2315678, upload-time = "2024-11-20T21:21:52.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/3e/6bf24d7bb0d11715ea783ecabcacdecdc8c51fca0144fcdad2090d65bae5/uv-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2", size = 13853445, upload-time = "2024-11-20T21:20:54.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/c3acbe2944cd694a5d61a7a461468fa886512c84014545bb8f3244092eaa/uv-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58", size = 13969300, upload-time = "2024-11-20T21:20:59.441Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c5/06e3b93045179b92d75cf94e6e224baec3226070f1cbc0e11d4898300b54/uv-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4", size = 12932325, upload-time = "2024-11-20T21:21:02.986Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/06ab86e9f0c270c495077ef2b588458172ed84f9c337de725c8b08872354/uv-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39", size = 13183356, upload-time = "2024-11-20T21:21:05.847Z" }, + { url = "https://files.pythonhosted.org/packages/c1/cb/bee01ef23e5020dc1f12d86ca8f82e95a723585db3ec64bfab4016e5616c/uv-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6", size = 13622310, upload-time = "2024-11-20T21:21:09.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/128fd874151919c71af51f528db28964e6d8e509fff12210ec9ba99b13fb/uv-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263", size = 14207832, upload-time = "2024-11-20T21:21:12.992Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2b/0fed8a49440494f6806dcb67021ca8f14d46f45a665235fc153791e19574/uv-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83", size = 14878796, upload-time = "2024-11-20T21:21:16.014Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/a6dc404d4d8884e26ad7bda004c101972fe7d81f86546a8628272812b897/uv-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25", size = 14687838, upload-time = "2024-11-20T21:21:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/74/9e/c2ebf66b90d48def06cda29626bb38068418ed135ca903beb293825ef66d/uv-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927", size = 18960541, upload-time = "2024-11-20T21:21:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/3d/67/28a8b4c23920ae1b1b0103ebae2fa176bd5677c4353b5e814a51bd183285/uv-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194", size = 14471756, upload-time = "2024-11-20T21:21:25.445Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1c/9698818f4c5493dfd5ab0899a90eee789cac214de2f171220bcdfaefc93a/uv-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01", size = 13389089, upload-time = "2024-11-20T21:21:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/30/31a9985d84ffb63fb9212fa2b565497e0ceb581be055e5cc760afbe26b11/uv-0.5.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c", size = 13612748, upload-time = "2024-11-20T21:21:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/26/8d/bae613187ba88d74f0268246ce140f23d399bab96d2cbc055d6e4adafd09/uv-0.5.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876", size = 13946421, upload-time = "2024-11-20T21:21:35.633Z" }, + { url = "https://files.pythonhosted.org/packages/0e/22/efd1eec81a566139bced68f4bd140c275edac3dac1bd6236cf8d756423db/uv-0.5.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119", size = 15752913, upload-time = "2024-11-20T21:21:39.413Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/0cc4ae143b9605c25e75772aea22876b5875db79982ba62bb6f8d3099fab/uv-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc", size = 14599503, upload-time = "2024-11-20T21:21:43.966Z" }, + { url = "https://files.pythonhosted.org/packages/51/9a/33d40a5068fd37c4f7b4fa82396e3ee90a691cd256f364ff398612c1d5d4/uv-0.5.4-py3-none-win32.whl", hash = "sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa", size = 13749570, upload-time = "2024-11-20T21:21:46.99Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c8/827e4da65cbdab2c1619767a68ab99a31de078e511b71ca9f24777df33f9/uv-0.5.4-py3-none-win_amd64.whl", hash = "sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb", size = 15573613, upload-time = "2024-11-20T21:21:50.328Z" }, +] + +[[package]] +name = "uv" +version = "0.6.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/46/32/ffa984c2ecbcf48d0ae813adf1aad79b3ecb5ffc743362088755d64ae3be/uv-0.6.10.tar.gz", hash = "sha256:cbbb03deb30af457cd93ad299ee5c3258ade3d900b4dee1af936c8a6d87d5bcb", size = 3109190, upload-time = "2025-03-26T01:08:35.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5a/5ef9324c333478608eaca8c97a374a869b861a9a614c1e6695045e06d90c/uv-0.6.10-py3-none-linux_armv6l.whl", hash = "sha256:06932d36f1afaf611522a6a7ec361dac48dc67a1147d24e9eadee9703b15faaf", size = 15825875, upload-time = "2025-03-26T01:07:49.96Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2f/001f6bb4342ba50cf921bd4a338ea40e5228ea6a817bd3101fbabaf010dd/uv-0.6.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e5c2ba1922c47a245d7393465fcee942df5a8bd8b80489a7b8860ba9d60102f9", size = 15967139, upload-time = "2025-03-26T01:07:53.521Z" }, + { url = "https://files.pythonhosted.org/packages/36/6b/f66dcd28508bceed7cff48efb9dfe62a50a40a0685c41fb5e6ecd45f33cd/uv-0.6.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd8a4bcfd33a0dcae3fc0936bff8602f74e5719cf839e3df233059a0b8c8330d", size = 14796758, upload-time = "2025-03-26T01:07:56.269Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a2/13eb03e8691b098f9ee63c4d3fa3a054c48bfa05a0a52aec3df33ab52376/uv-0.6.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4dd20c47898c15ebd4b5f48101062ea248e32513bfc61fc04bc822abfe39ce8a", size = 15252527, upload-time = "2025-03-26T01:07:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/58/90/053bde333fbf9030dff1354797bd74ce3624235bcf59d7558397749a88c1/uv-0.6.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:950c9cd7b75f67e25760d2f43ad4b0ee3f8c6724fe0a9cf9eff948b3044b6a6d", size = 15560957, upload-time = "2025-03-26T01:08:01.188Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/feb9ecc8ab8f9e1968d6783dd47e7ebd1dfcd0231c8b7b0efd7204625cec/uv-0.6.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acca1dca7be342b2b8e26e509aa07c3144cb009788140eee045da2aad6a0c6fe", size = 16249302, upload-time = "2025-03-26T01:08:03.734Z" }, + { url = "https://files.pythonhosted.org/packages/0f/14/9a2e40e25fba7b550cb57cce62a07ddf28350cb53e9e8bd2e70c0fbacdbb/uv-0.6.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13ac09945976dc0df0edde7e4ba3a46107036a114117c8ff84916e55216c2e32", size = 17196146, upload-time = "2025-03-26T01:08:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/5e/05/5c9cd846243aca204f96c2da13da0fb38b6143eb3827dedea0e1dc1bcf1c/uv-0.6.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:145e75b99d6b7bdce8e454a851cfcd5605ff0491d568244c66fa75ca6b071bd6", size = 16944298, upload-time = "2025-03-26T01:08:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/63233a3143535a6df34ee6dc8246ef09ee79d99b902a6cc1ee179c1898f9/uv-0.6.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666d9fe312c810bba77633dbd463dc85f5a6a0d07905726a014dc53d07c774d9", size = 21226376, upload-time = "2025-03-26T01:08:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d3/7e881e2a391203a7567cf03c72213701e63923591d2072c4e7fe694c919f/uv-0.6.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b98e8884093cbfb1a1cc3f855aa22f97ec8da1a87e0e761800e165d4f9224a45", size = 16621313, upload-time = "2025-03-26T01:08:14.435Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a9/124aa76690a04cf30344386358b772cdede17d84660ae1dce8643bf64939/uv-0.6.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e8a8a75cf34c0814c1eabdbe651741d44fb125a6dcbe159b2da02871bbfdec7e", size = 15461518, upload-time = "2025-03-26T01:08:17.002Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/19813f2ec2faac77da5548c35d6ae039d044b973ecbb0732e3f07662fd36/uv-0.6.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:5260f52386e217615553f2f42740ce2f64ba439ff0fd502dc5b06250eb8ae613", size = 15524130, upload-time = "2025-03-26T01:08:19.392Z" }, + { url = "https://files.pythonhosted.org/packages/09/3f/5637bbf27ac145a09ea8eba8e0c926f7a3fe8fc4b3b1c91131c4558f4ec2/uv-0.6.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:603aebbaf6be938120c73fd36e9fd85f5e1b671d3d4638b3086f478e2bb423d9", size = 15901256, upload-time = "2025-03-26T01:08:22.141Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/2c688a897efad60d6e0587027968c1fdb0a63f70a8bef33d0b8154cc0fcd/uv-0.6.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d1f1bc7d94a4a7fdd75142be71b6bf2d7e01282f322721da185d711f065d7b80", size = 16746279, upload-time = "2025-03-26T01:08:24.916Z" }, + { url = "https://files.pythonhosted.org/packages/75/d4/df57d3f40c93c21fceed94156da41183daabd15422506e0fe73236c458a1/uv-0.6.10-py3-none-win32.whl", hash = "sha256:df6560256b93441c70ea2c062975bce2307a32de280f103cedb8db4a0f542348", size = 15953827, upload-time = "2025-03-26T01:08:27.395Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/a71c95c85624544c56695ae2469745bbda834e77dfc1e29d76711409eda5/uv-0.6.10-py3-none-win_amd64.whl", hash = "sha256:d795721fdd32e0471c952b7cb02a030657b6e67625fe836f4df14a3ae4aa4921", size = 17425178, upload-time = "2025-03-26T01:08:29.898Z" }, + { url = "https://files.pythonhosted.org/packages/ce/07/e6ffe467e1e365f7dd7863c4d505b1941af8cf69c494d0dbda08ba907043/uv-0.6.10-py3-none-win_arm64.whl", hash = "sha256:5188dc7041f4166bf64182d76c32c873f750259b6e4621a1400c26ebeea8c8dd", size = 16169219, upload-time = "2025-03-26T01:08:32.812Z" }, +] + +[[package]] +name = "uv" +version = "0.9.17" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/52/1a/cb0c37ae8513b253bcbc13d42392feb7d95ea696eb398b37535a28df9040/uv-0.9.17.tar.gz", hash = "sha256:6d93ab9012673e82039cfa7f9f66f69b388bc3f910f9e8a2ebee211353f620aa", size = 3815957, upload-time = "2025-12-09T23:01:21.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/e2/b6e2d473bdc37f4d86307151b53c0776e9925de7376ce297e92eab2e8894/uv-0.9.17-py3-none-linux_armv6l.whl", hash = "sha256:c708e6560ae5bc3cda1ba93f0094148ce773b6764240ced433acf88879e57a67", size = 21254511, upload-time = "2025-12-09T23:00:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:233b3d90f104c59d602abf434898057876b87f64df67a37129877d6dab6e5e10", size = 20384366, upload-time = "2025-12-09T23:01:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4b8e5513d48a267bfa180ca7fefaf6f27b1267e191573b3dba059981143e88ef", size = 18924624, upload-time = "2025-12-09T23:01:10.291Z" }, + { url = "https://files.pythonhosted.org/packages/21/56/9daf8bbe4a9a36eb0b9257cf5e1e20f9433d0ce996778ccf1929cbe071a4/uv-0.9.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8f283488bbcf19754910cc1ae7349c567918d6367c596e5a75d4751e0080eee0", size = 20671687, upload-time = "2025-12-09T23:00:51.927Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c8/4050ff7dc692770092042fcef57223b8852662544f5981a7f6cac8fc488d/uv-0.9.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9cf8052ba669dc17bdba75dae655094d820f4044990ea95c01ec9688c182f1da", size = 20861866, upload-time = "2025-12-09T23:01:12.555Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/208e62b7db7a65cb3390a11604c59937e387d07ed9f8b63b54edb55e2292/uv-0.9.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06749461b11175a884be193120044e7f632a55e2624d9203398808907d346aad", size = 21858420, upload-time = "2025-12-09T23:01:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/86/2c/91288cd5a04db37dfc1e0dad26ead84787db5832d9836b4cc8e0fa7f3c53/uv-0.9.17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:35eb1a519688209160e48e1bb8032d36d285948a13b4dd21afe7ec36dc2a9787", size = 23471658, upload-time = "2025-12-09T23:00:49.503Z" }, + { url = "https://files.pythonhosted.org/packages/44/ba/493eba650ffad1df9e04fd8eabfc2d0aebc23e8f378acaaee9d95ca43518/uv-0.9.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bfb60a533e82690ab17dfe619ff7f294d053415645800d38d13062170230714", size = 23062950, upload-time = "2025-12-09T23:00:39.055Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/f7f679503c06843ba59451e3193f35fb7c782ff0afc697020d4718a7de46/uv-0.9.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd0f3e380ff148aff3d769e95a9743cb29c7f040d7ef2896cafe8063279a6bc1", size = 22080299, upload-time = "2025-12-09T23:00:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2c3d25fbd8f91b30d0fac69a13b8e2c2cd8e606d7e6e924c1423e4ff84e616", size = 22087554, upload-time = "2025-12-09T23:00:41.715Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:330e7085857e4205c5196a417aca81cfbfa936a97dd2a0871f6560a88424ebf2", size = 20823225, upload-time = "2025-12-09T23:00:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/e0f816cacd802a1cb25e71de9d60e57fa1f6c659eb5599cef708668618cc/uv-0.9.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:45880faa9f6cf91e3cda4e5f947da6a1004238fdc0ed4ebc18783a12ce197312", size = 22004893, upload-time = "2025-12-09T23:01:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/15/6b/700f6256ee191136eb06e40d16970a4fc687efdccf5e67c553a258063019/uv-0.9.17-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8e775a1b94c6f248e22f0ce2f86ed37c24e10ae31fb98b7e1b9f9a3189d25991", size = 20853850, upload-time = "2025-12-09T23:01:02.694Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6a/13f02e2ed6510223c40f74804586b09e5151d9319f93aab1e49d91db13bb/uv-0.9.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8650c894401ec96488a6fd84a5b4675e09be102f5525c902a12ba1c8ef8ff230", size = 21322623, upload-time = "2025-12-09T23:00:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/d0/18/2d19780cebfbec877ea645463410c17859f8070f79c1a34568b153d78e1d/uv-0.9.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:673066b72d8b6c86be0dae6d5f73926bcee8e4810f1690d7b8ce5429d919cde3", size = 22290123, upload-time = "2025-12-09T23:00:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/ab79bde3f7b6d2ac89f839ea40411a9cf3e67abede2278806305b6ba797e/uv-0.9.17-py3-none-win32.whl", hash = "sha256:7407d45afeae12399de048f7c8c2256546899c94bd7892dbddfae6766616f5a3", size = 20070709, upload-time = "2025-12-09T23:01:05.105Z" }, + { url = "https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl", hash = "sha256:22fcc26755abebdf366becc529b2872a831ce8bb14b36b6a80d443a1d7f84d3b", size = 22122852, upload-time = "2025-12-09T23:01:07.783Z" }, + { url = "https://files.pythonhosted.org/packages/37/ef/813cfedda3c8e49d8b59a41c14fcc652174facfd7a1caf9fee162b40ccbd/uv-0.9.17-py3-none-win_arm64.whl", hash = "sha256:6761076b27a763d0ede2f5e72455d2a46968ff334badf8312bb35988c5254831", size = 20435751, upload-time = "2025-12-09T23:01:19.732Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "voluptuous" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/91/af/a54ce0fb6f1d867e0b9f0efe5f082a691f51ccf705188fca67a3ecefd7f4/voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa", size = 51651, upload-time = "2024-07-02T19:10:00.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/a8/8f9cc6749331186e6a513bfe3745454f81d25f6e34c6024f88f80c71ed28/voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566", size = 31349, upload-time = "2024-07-02T19:09:58.125Z" }, +] + +[[package]] +name = "voluptuous" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/92/f4/0738e6849858deae22218be3bbb8207ba83a96e9d0ec7e8e8cd67b30e5ca/voluptuous-0.16.0.tar.gz", hash = "sha256:006535e22fed944aec17bef6e8725472476194743c87bd233e912eb463f8ff05", size = 54238, upload-time = "2025-12-18T23:18:46.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/00/0e0da784245c93cf346150ab67634177bf277f93b7a162bb56c928c39c04/voluptuous-0.16.0-py3-none-any.whl", hash = "sha256:ee342095263e1b5afbd4d418cb5adc92810eebfd07696bb033a261210df33db4", size = 31931, upload-time = "2025-12-18T23:18:44.694Z" }, +] + +[[package]] +name = "voluptuous-openapi" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/3a/0dda3268377c890561bdce1c445bd36a9c79fa77be7f456d9db45b664a7f/voluptuous_openapi-0.0.5.tar.gz", hash = "sha256:1619cd298da0024fa01338ac5a9ce3b3b7059205ce3c69230c24803b11308fb0", size = 10621, upload-time = "2024-07-30T06:18:46.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4e/b20c825f6cc78ed857f44afe32b7408c1d1c2c08cab4bcb5bb9b5e8fcb00/voluptuous_openapi-0.0.5-py3-none-any.whl", hash = "sha256:d51509503b3080b54a746ef357534f124ef7ae4f0ccecd3c3f261660b193c19a", size = 8636, upload-time = "2024-07-30T06:18:45.134Z" }, +] + +[[package]] +name = "voluptuous-openapi" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/6f/1075651d387c1570e4603080bdf0aa15aa254c21efb2688fdb18544cf4b9/voluptuous_openapi-0.0.6.tar.gz", hash = "sha256:4078c2acef23e04ceeab1ba58252590fcdc3ba6e3ed34521e8595374ab4de884", size = 13190, upload-time = "2025-01-07T07:19:07.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5c/331c21122901d4f5f4f6869683ab9859a08498074cee6075ff3eac3027a6/voluptuous_openapi-0.0.6-py3-none-any.whl", hash = "sha256:3561bbe5f46483f4cd9f631a0bd4a3ac3d7d74bab24f41bcd09b52501f712d5e", size = 9249, upload-time = "2025-01-07T07:19:05.948Z" }, +] + +[[package]] +name = "voluptuous-openapi" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/5f/e77f088f4eff2e0f572f651bb9c2259561ec32d0bff20f59dd107e392ad8/voluptuous_openapi-0.3.0.tar.gz", hash = "sha256:1ecd2386e63b59b791e0f64715e317c7018a2e5598333138bd1f28adfd348d5c", size = 17467, upload-time = "2025-12-31T00:40:24.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/2d/3a8728e638bac8fd5857bb7775e39a9a520ae90b1d6dffd8badfa9852ccc/voluptuous_openapi-0.3.0-py3-none-any.whl", hash = "sha256:db35598e9fc711cbaf7d7732de160de3edf89cc7411b6febe5a132f1f54ea931", size = 10896, upload-time = "2025-12-31T00:40:23.067Z" }, +] + +[[package]] +name = "voluptuous-serialize" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/09/c26b38ab35d9f61e9bf5c3e805215db1316dd73c77569b47ab36a40d19b1/voluptuous-serialize-2.6.0.tar.gz", hash = "sha256:79acdc58239582a393144402d827fa8efd6df0f5350cdc606d9242f6f9bca7c4", size = 7562, upload-time = "2023-02-15T21:09:08.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/86/355e1c65934760e2fb037219f1f360562567cf6731d281440c1d57d36856/voluptuous_serialize-2.6.0-py3-none-any.whl", hash = "sha256:85a5c8d4d829cb49186c1b5396a8a517413cc5938e1bb0e374350190cd139616", size = 6819, upload-time = "2023-02-15T21:09:06.512Z" }, +] + +[[package]] +name = "voluptuous-serialize" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/70/03a9b61324e1bb8b16682455b8b953bccd1001a28e43478c86f539e26285/voluptuous_serialize-2.7.0.tar.gz", hash = "sha256:d0da959f2fd93c8f1eb779c5d116231940493b51020c2c1026bab76eb56cd09e", size = 9202, upload-time = "2025-08-17T10:43:04.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/41/d536d9cf39821c35cc13aff403728e60e32b2fd711c240b6b9980af1c03f/voluptuous_serialize-2.7.0-py3-none-any.whl", hash = "sha256:ee3ebecace6136f38d0bf8c20ee97155db2486c6b2d0795563fafd04a519e76f", size = 7850, upload-time = "2025-08-17T10:43:03.498Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "webrtc-models" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mashumaro" }, + { name = "orjson", version = "3.10.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "orjson", version = "3.10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/e8/050ffe3b71ff44d3885eee2bed763ca937e2a30bc950d866f22ba657776b/webrtc_models-0.3.0.tar.gz", hash = "sha256:559c743e5cc3bcc8133be1b6fb5e8492a9ddb17151129c21cbb2e3f2a1166526", size = 9411, upload-time = "2024-11-18T17:43:45.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/e7/62f29980c9e8d75af93b642a0c37aa8e201fd5268ba3a7179c172549bac3/webrtc_models-0.3.0-py3-none-any.whl", hash = "sha256:8fddded3ffd7ca837de878033501927580799a2c1b7829f7ae8a0f43b49004ea", size = 7476, upload-time = "2024-11-18T17:43:44.165Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "winrt-runtime" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/dd/acdd527c1d890c8f852cc2af644aa6c160974e66631289420aa871b05e65/winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea", size = 21721, upload-time = "2025-06-06T14:40:27.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/54/3dd06f2341fab6abb06588a16b30e0b213b0125be7b79dafc3bdba3b334a/winrt_runtime-3.2.1-cp312-cp312-win32.whl", hash = "sha256:762b3d972a2f7037f7db3acbaf379dd6d8f6cda505f71f66c6b425d1a1eae2f1", size = 210090, upload-time = "2025-06-06T06:44:08.151Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a1/1d7248d5c62ccbea5f3e0da64ca4529ce99c639c3be2485b6ed709f5c740/winrt_runtime-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:06510db215d4f0dc45c00fbb1251c6544e91742a0ad928011db33b30677e1576", size = 241391, upload-time = "2025-06-06T06:44:09.442Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ae/6a205d8dafc79f7c242be7f940b1e0c1971fd64ab3079bda4b514aa3d714/winrt_runtime-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:14562c29a087ccad38e379e585fef333e5c94166c807bdde67b508a6261aa195", size = 415242, upload-time = "2025-06-06T06:44:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/79/d4/1a555d8bdcb8b920f8e896232c82901cc0cda6d3e4f92842199ae7dff70a/winrt_runtime-3.2.1-cp313-cp313-win32.whl", hash = "sha256:44e2733bc709b76c554aee6c7fe079443b8306b2e661e82eecfebe8b9d71e4d1", size = 210022, upload-time = "2025-06-06T06:44:11.767Z" }, + { url = "https://files.pythonhosted.org/packages/aa/24/2b6e536ca7745d788dfd17a2ec376fa03a8c7116dc638bb39b035635484f/winrt_runtime-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:3c1fdcaeedeb2920dc3b9039db64089a6093cad2be56a3e64acc938849245a6d", size = 241349, upload-time = "2025-06-06T06:44:12.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7f/6d72973279e2929b2a71ed94198ad4a5d63ee2936e91a11860bf7b431410/winrt_runtime-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:28f3dab083412625ff4d2b46e81246932e6bebddf67bea7f05e01712f54e6159", size = 415126, upload-time = "2025-06-06T06:44:13.702Z" }, + { url = "https://files.pythonhosted.org/packages/c8/87/88bd98419a9da77a68e030593fee41702925a7ad8a8aec366945258cbb31/winrt_runtime-3.2.1-cp314-cp314-win32.whl", hash = "sha256:9b6298375468ac2f6815d0c008a059fc16508c8f587e824c7936ed9216480dad", size = 210257, upload-time = "2025-09-20T07:06:41.054Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/e5c2a10d287edd9d3ee8dc24bf7d7f335636b92bf47119768b7dd2fd1669/winrt_runtime-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:e36e587ab5fd681ee472cd9a5995743f75107a1a84d749c64f7e490bc86bc814", size = 241873, upload-time = "2025-09-20T07:06:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/eb9e78397132175f70dd51dfa4f93e489c17d6b313ae9dce60369b8d84a7/winrt_runtime-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:35d6241a2ebd5598e4788e69768b8890ee1eee401a819865767a1fbdd3e9a650", size = 416222, upload-time = "2025-09-20T07:06:43.376Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/a0/1c8a0c469abba7112265c6cb52f0090d08a67c103639aee71fc690e614b8/winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505", size = 23732, upload-time = "2025-06-06T14:41:20.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/ff/c4a3de909a875b46fad5e9f4fd412bba48571405bfa802b878954abf128c/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win32.whl", hash = "sha256:18c833ec49e7076127463679e85efc59f61785ade0dc185c852586b21be1f31c", size = 105752, upload-time = "2025-06-06T07:00:10.684Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/bfee1f0c8d188c561c5b946ab21f6a0037e60dea110e80b1d6a1d529639f/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9b6702c462b216c91e32388023a74d0f87210cef6fd5d93b7191e9427ce2faca", size = 113356, upload-time = "2025-06-06T07:00:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/d9da9c29d36cabadef4e19c3e9ba6d2692f6a28224c81fcff757132ea0da/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:419fd1078c7749119f6b4bbf6be4e586e03a0ed544c03b83178f1d85f1b3d148", size = 104724, upload-time = "2025-06-06T07:00:12.406Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/797516c5c0f8d7f5b680862e0ed7c1087c58aec0bcf57a417fa90f7eb983/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win32.whl", hash = "sha256:12b0a16fb36ce0b42243ca81f22a6b53fbb344ed7ea07a6eeec294604f0505e4", size = 105757, upload-time = "2025-06-06T07:00:13.269Z" }, + { url = "https://files.pythonhosted.org/packages/05/6d/f60588846a065e69a2ec5e67c5f85eb45cb7edef2ee8974cd52fa8504de6/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6703dfbe444ee22426738830fb305c96a728ea9ccce905acfdf811d81045fdb3", size = 113363, upload-time = "2025-06-06T07:00:14.135Z" }, + { url = "https://files.pythonhosted.org/packages/2c/13/2d3c4762018b26a9f66879676ea15d7551cdbf339c8e8e0c56ea05ea31ef/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2cf8a0bfc9103e32dc7237af15f84be06c791f37711984abdca761f6318bbdb2", size = 104722, upload-time = "2025-06-06T07:00:14.999Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/91cfdf941a1ba791708ab3477fc4e46793c8fe9117fc3e0a8c5ac5d7a09c/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win32.whl", hash = "sha256:de36ded53ca3ba12fc6dd4deb14b779acc391447726543815df4800348aad63a", size = 109015, upload-time = "2025-09-20T07:09:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/7460655628d0f340a93524f5236bb9f8514eb0e1d334b38cba8a89f6c1a6/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3295d932cc93259d5ccb23a41e3a3af4c78ce5d6a6223b2b7638985f604fa34c", size = 115931, upload-time = "2025-09-20T07:09:51.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/70/e1248dea2ab881eb76b61ff1ad6cb9c07ac005faf99349e4af0b29bc3f1b/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1f61c178766a1bbce0669f44790c6161ff4669404c477b4aedaa576348f9e102", size = 109561, upload-time = "2025-09-20T07:09:52.733Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth-advertisement" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/fc/7ffe66ca4109b9e994b27c00f3d2d506e6e549e268791f755287ad9106d8/winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc", size = 16906, upload-time = "2025-06-06T14:41:21.448Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/15/ad05c28e049208c97011728e2debdb45439175f75efe357b6faa4c9ba099/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win32.whl", hash = "sha256:901933cc40de5eb7e5f4188897c899dd0b0f577cb2c13eab1a63c7dfe89b08c4", size = 90033, upload-time = "2025-06-06T07:00:23.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/48/074779081841f6eba4987930c4e7adcec38a5985b7dffd9fecc41f39a89c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6c66e7d4f4ca86d2c801d30efd2b9673247b59a2b4c365d9e11650303d68d89", size = 95824, upload-time = "2025-06-06T07:00:24.238Z" }, + { url = "https://files.pythonhosted.org/packages/aa/25/e01966033a02b2d0718710bb47ef4f6b9b5a619ca2c857e06eb5c8e3ed13/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:447d19defd8982d39944642eb7ebe89e4e20259ec9734116cf88879fb2c514ff", size = 89311, upload-time = "2025-06-06T07:00:25.029Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/8fc8e57605ea08dd0723c035ed0c2d0435dace2bc80a66d33aecfea49a56/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4122348ea525a914e85615647a0b54ae8b2f42f92cdbf89c5a12eea53ef6ed90", size = 90037, upload-time = "2025-06-06T07:00:25.818Z" }, + { url = "https://files.pythonhosted.org/packages/86/83/503cf815d84c5ba8c8bc61480f32e55579ebf76630163405f7df39aa297b/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:b66410c04b8dae634a7e4b615c3b7f8adda9c7d4d6902bcad5b253da1a684943", size = 95822, upload-time = "2025-06-06T07:00:26.666Z" }, + { url = "https://files.pythonhosted.org/packages/32/13/052be8b6642e6f509b30c194312b37bfee8b6b60ac3bd5ca2968c3ea5b80/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:07af19b1d252ddb9dd3eb2965118bc2b7cabff4dda6e499341b765e5038ca61d", size = 89326, upload-time = "2025-06-06T07:00:27.477Z" }, + { url = "https://files.pythonhosted.org/packages/27/3d/421d04a20037370baf13de929bc1dc5438b306a76fe17275ec5d893aae6c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win32.whl", hash = "sha256:2985565c265b3f9eab625361b0e40e88c94b03d89f5171f36146f2e88b3ee214", size = 92264, upload-time = "2025-09-20T07:09:53.563Z" }, + { url = "https://files.pythonhosted.org/packages/07/c7/43601ab82fe42bcff430b8466d84d92b31be06cc45c7fd64e9aac40f7851/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d102f3fac64fde32332e370969dfbc6f37b405d8cc055d9da30d14d07449a3c2", size = 97517, upload-time = "2025-09-20T07:09:54.411Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/e3303f6a25a2d98e424b06580fc85bbfd068f383424c67fa47cb1b357a46/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:ffeb5e946cd42c32c6999a62e240d6730c653cdfb7b49c7839afba375e20a62a", size = 94122, upload-time = "2025-09-20T07:09:55.187Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth-genericattributeprofile" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/21/aeeddc0eccdfbd25e543360b5cc093233e2eab3cdfb53ad3cabae1b5d04d/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71", size = 38896, upload-time = "2025-06-06T14:41:22.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/a1/75ac783a5faee9b455fef2f53b7fef97b21ed60d52401b44c690202141e4/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win32.whl", hash = "sha256:ef894d21e0a805f3e114940254636a8045335fa9de766c7022af5d127dfad557", size = 183326, upload-time = "2025-06-06T07:00:52.662Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d9/a9dcc15322d2f5c7dfd491bd7ab121e36437caf78ebfa92bc0dd0546e2ca/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:db05de95cd1b24a51abb69cb936a8b17e9214e015757d0b37e3a5e207ddceb3d", size = 187810, upload-time = "2025-06-06T07:00:53.594Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fc/47d00af076f558267097af3050910beda6bf8a21ceaa5830bbd26fcaf85e/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d4e131cf3d15fc5ad81c1bcde3509ac171298217381abed6bdf687f29871984", size = 184516, upload-time = "2025-06-06T07:00:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/93/30b45ce473d1a604908221a1fa035fe8d5e4bb9008e820ae671a21dab94c/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win32.whl", hash = "sha256:b1879c8dcf46bd2110b9ad4b0b185f4e2a5f95170d014539203a5fee2b2115f0", size = 183342, upload-time = "2025-06-06T07:00:56.16Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3b/eb9d99b82a36002d7885206d00ea34f4a23db69c16c94816434ded728fa3/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d8d89f01e9b6931fb48217847caac3227a0aeb38a5b7782af71c2e7b262ec30", size = 187844, upload-time = "2025-06-06T07:00:57.134Z" }, + { url = "https://files.pythonhosted.org/packages/84/9b/ebbbe9be9a3e640dcfc5f166eb48f2f9d8ce42553f83aa9f4c5dcd9eb5f5/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:4e71207bb89798016b1795bb15daf78afe45529f2939b3b9e78894cfe650b383", size = 184540, upload-time = "2025-06-06T07:00:58.081Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/cb447ca7730a1e05730272309b074da6a04af29a8c0f5121014db8a2fc02/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win32.whl", hash = "sha256:d5f83739ca370f0baf52b0400aebd6240ab80150081fbfba60fd6e7b2e7b4c5f", size = 185249, upload-time = "2025-09-20T07:09:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fa/f465d5d44dda166bf7ec64b7a950f57eca61f165bfe18345e9a5ea542def/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:13786a5853a933de140d456cd818696e1121c7c296ae7b7af262fc5d2cffb851", size = 193739, upload-time = "2025-09-20T07:09:59.893Z" }, + { url = "https://files.pythonhosted.org/packages/78/08/51c53ac3c704cd92da5ed7e7b9b57159052f6e46744e4f7e447ed708aa22/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:5140682da2860f6a55eb6faf9e980724dc457c2e4b4b35a10e1cebd8fc97d892", size = 194836, upload-time = "2025-09-20T07:10:00.87Z" }, +] + +[[package]] +name = "winrt-windows-devices-enumeration" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/dd/75835bfbd063dffa152109727dedbd80f6e92ea284855f7855d48cdf31c9/winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf", size = 23538, upload-time = "2025-06-06T14:41:26.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/3e/81642208ecd6c6c936f35a39a433c54e3f68e09d316546b8f953581ae334/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win32.whl", hash = "sha256:1db22b0292b93b0688d11ad932ad1f3629d4f471310281a2fbfe187530c2c1f3", size = 130249, upload-time = "2025-06-06T07:02:02.237Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/a9ede5f3f0d86abfc7590726cf711133d97419b49ced372fca532e4f0696/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a73bc88d7f510af454f2b392985501c96f39b89fd987140708ccaec1588ceebc", size = 141512, upload-time = "2025-06-06T07:02:03.424Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/4fad07c03124bdc3acd64f80f3bd3cc4417ea641e07bb16a9503afd3e554/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:2853d687803f0dd76ae1afe3648abc0453e09dff0e7eddbb84b792eddb0473ca", size = 135383, upload-time = "2025-06-06T07:02:04.312Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7d/ebd712ab8ccd599c593796fbcd606abe22b5a8e20db134aa87987d67ac0e/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win32.whl", hash = "sha256:14a71cdcc84f624c209cbb846ed6bd9767a9a9437b2bf26b48ac9a91599da6e9", size = 130276, upload-time = "2025-06-06T07:02:05.178Z" }, + { url = "https://files.pythonhosted.org/packages/70/de/f30daaaa0e6f4edb6bd7ddb3e058bd453c9ad90c032a4545c4d4639338aa/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6ca40d334734829e178ad46375275c4f7b5d6d2d4fc2e8879690452cbfb36015", size = 141536, upload-time = "2025-06-06T07:02:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/75/4b/9a6aafdc74a085c550641a325be463bf4b811f6f605766c9cd4f4b5c19d2/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2d14d187f43e4409c7814b7d1693c03a270e77489b710d92fcbbaeca5de260d4", size = 135362, upload-time = "2025-06-06T07:02:06.997Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/5785cd1ec54dc0f0e6f3e6a466d07a62b8014a6e2b782e80444ef87e83ab/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win32.whl", hash = "sha256:e087364273ed7c717cd0191fed4be9def6fdf229fe9b536a4b8d0228f7814106", size = 134252, upload-time = "2025-09-20T07:10:12.935Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f6/68d91068048410f49794c0b19c45759c63ca559607068cfe5affba2f211b/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:0da1ddb8285d97a6775c36265d7157acf1bbcb88bcc9a7ce9a4549906c822472", size = 145509, upload-time = "2025-09-20T07:10:13.797Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a4/898951d5bfc474aa9c7d133fe30870f0f2184f4ba3027eafb779d30eb7bc/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:09bf07e74e897e97a49a9275d0a647819254ddb74142806bbbcf4777ed240a22", size = 141334, upload-time = "2025-09-20T07:10:14.637Z" }, +] + +[[package]] +name = "winrt-windows-devices-radios" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/02/9704ea359ad8b0d6faa1011f98fb477e8fb6eac5201f39d19e73c2407e7b/winrt_windows_devices_radios-3.2.1.tar.gz", hash = "sha256:4dc9b9d1501846049eb79428d64ec698d6476c27a357999b78a8331072e18a0b", size = 5908, upload-time = "2025-06-06T14:41:44.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/e0/4731a3c412318b2c5e74a8803a32e2fb9afc2c98368c6b61a422eb359e7e/winrt_windows_devices_radios-3.2.1-cp312-cp312-win32.whl", hash = "sha256:c3e683ce682338a5a5ed465f735e223ba7a22f16d0bbea2d070962bc7657edbb", size = 38606, upload-time = "2025-06-06T07:08:01.477Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/91464854dfc9e0be9ce8dcbe2bd6a67c19b68ab91584fc5de0f4f13e78f8/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a116e552a3f38607b9be558fb2e7de9b4450d1f9080069944d74d80cdda1873e", size = 40172, upload-time = "2025-06-06T07:08:02.214Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0d/1bd62f606b6c4dfa936fccc4712be5506a40fc5d1b7177c3d3cbcaf30972/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4c28822f9251c9d547324f596b5c2581f050254ded05e5b786c650a3502744c1", size = 36989, upload-time = "2025-06-06T07:08:03.295Z" }, + { url = "https://files.pythonhosted.org/packages/d1/94/c22a14fd424632f3f3c0b25672218db9e8f4ae9e1355e0b148f2fe6015b5/winrt_windows_devices_radios-3.2.1-cp313-cp313-win32.whl", hash = "sha256:ae4a0065927fcd2d10215223f8a46be6fb89bad71cb4edd25dae3d01c137b3a8", size = 38613, upload-time = "2025-06-06T07:08:04.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/24cec0cc228642554b48d436a7617d7162fb952919c55fc26e2d99c310bd/winrt_windows_devices_radios-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:bf1a975f46a2aa271ffea1340be0c7e64985050d07433e701343dddc22a72290", size = 40180, upload-time = "2025-06-06T07:08:04.849Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/776453af26e78c0d0c0e1bfa89f86fd81322872f31a3e5dafb344dd47bf2/winrt_windows_devices_radios-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:10b298ed154c5824cea2de174afce1694ed2aabfb58826de814074027ffef96f", size = 36989, upload-time = "2025-06-06T07:08:05.576Z" }, + { url = "https://files.pythonhosted.org/packages/76/79/4627afae6b389ddd1e5f1d691663c6b14d6c8f98959082aed1217cc57ef9/winrt_windows_devices_radios-3.2.1-cp314-cp314-win32.whl", hash = "sha256:21452e1cae50e44cd1d5e78159e1b9986ac3389b66458ad89caa196ce5eca2d6", size = 39521, upload-time = "2025-09-20T07:11:17.992Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/c6aea91908ee7279ed51d12157bc8aeecb8850af2441073c3c91b261ad31/winrt_windows_devices_radios-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:6a8413e586fe597c6849607885cca7e0549da33ae5699165d11f7911534c6eaf", size = 41121, upload-time = "2025-09-20T07:11:18.747Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/652f14e3c501452ad8e0723518d9bbd729219b47f4a4dbe2966c2f82dca8/winrt_windows_devices_radios-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:39129fd9d09103adb003575f59881c1a5a70a43310547850150b46c6f4020312", size = 38114, upload-time = "2025-09-20T07:11:19.599Z" }, +] + +[[package]] +name = "winrt-windows-foundation" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/55/098ce7ea0679efcc1298b269c48768f010b6c68f90c588f654ec874c8a74/winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656", size = 30485, upload-time = "2025-06-06T14:41:53.344Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/f8/495e304ddedd5ff2f196efbde906265cb75ade4d79e2937837f72ef654a0/winrt_windows_foundation-3.2.1-cp312-cp312-win32.whl", hash = "sha256:867642ccf629611733db482c4288e17b7919f743a5873450efb6d69ae09fdc2b", size = 112169, upload-time = "2025-06-06T07:11:01.438Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5e/b5059e4ece095351c496c9499783130c302d25e353c18031d5231b1b3b3c/winrt_windows_foundation-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:45550c5b6c2125cde495c409633e6b1ea5aa1677724e3b95eb8140bfccbe30c9", size = 118668, upload-time = "2025-06-06T07:11:02.475Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/acbcb3ef07b1b67e2de4afab9176a5282cfd775afd073efe6828dfc65ace/winrt_windows_foundation-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:94f4661d71cb35ebc52be7af112f2eeabdfa02cb05e0243bf9d6bd2cafaa6f37", size = 109671, upload-time = "2025-06-06T07:11:03.538Z" }, + { url = "https://files.pythonhosted.org/packages/7b/71/5e87131e4aecc8546c76b9e190bfe4e1292d028bda3f9dd03b005d19c76c/winrt_windows_foundation-3.2.1-cp313-cp313-win32.whl", hash = "sha256:3998dc58ed50ecbdbabace1cdef3a12920b725e32a5806d648ad3f4829d5ba46", size = 112184, upload-time = "2025-06-06T07:11:04.459Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7f/8d5108461351d4f6017f550af8874e90c14007f9122fa2eab9f9e0e9b4e1/winrt_windows_foundation-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6e98617c1e46665c7a56ce3f5d28e252798416d1ebfee3201267a644a4e3c479", size = 118672, upload-time = "2025-06-06T07:11:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/44/f5/2edf70922a3d03500dab17121b90d368979bd30016f6dbca0d043f0c71f1/winrt_windows_foundation-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2a8c1204db5c352f6a563130a5a41d25b887aff7897bb677d4ff0b660315aad4", size = 109673, upload-time = "2025-06-06T07:11:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0a/d77346e39fe0c81f718cde49f83fe77c368c0e14c6418f72dfa1e7ef22d0/winrt_windows_foundation-3.2.1-cp314-cp314-win32.whl", hash = "sha256:35e973ab3c77c2a943e139302256c040e017fd6ff1a75911c102964603bba1da", size = 114590, upload-time = "2025-09-20T07:11:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/4d2b545bea0f34f68df6d4d4ca22950ff8a935497811dccdc0ca58737a05/winrt_windows_foundation-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22a7ebcec0d262e60119cff728f32962a02df60471ded8b2735a655eccc0ef5", size = 122148, upload-time = "2025-09-20T07:11:50.826Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ed/b9d3a11cac73444c0a3703200161cd7267dab5ab85fd00e1f965526e74a8/winrt_windows_foundation-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3be7fbae829b98a6a946db4fbaf356b11db1fbcbb5d4f37e7a73ac6b25de8b87", size = 114360, upload-time = "2025-09-20T07:11:51.626Z" }, +] + +[[package]] +name = "winrt-windows-foundation-collections" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/62/d21e3f1eeb8d47077887bbf0c3882c49277a84d8f98f7c12bda64d498a07/winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5", size = 16043, upload-time = "2025-06-06T14:41:53.983Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/0b/7802349391466d3f7e8f62f588f36a1a0b6560abfcdbdaa426fe21d322b4/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win32.whl", hash = "sha256:15704eef3125788f846f269cf54a3d89656fa09a1dc8428b70871f717d595ad6", size = 60060, upload-time = "2025-06-06T07:11:16.173Z" }, + { url = "https://files.pythonhosted.org/packages/37/94/5b888713e472746635a382e523513ab1b8200af55c5b56bc70e1e4369115/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:550dfb8c82fe74d9e0728a2a16a9175cc9e34ca2b8ef758d69b2a398894b698b", size = 69058, upload-time = "2025-06-06T07:11:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/829273622c9b37c67b97f187b92be318404f7d33db045e31d72b7d50f54c/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:810ad4bd11ab4a74fdbcd3ed33b597ef7c0b03af73fc9d7986c22bcf3bd24f84", size = 58793, upload-time = "2025-06-06T07:11:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cd/99ef050d80bea2922fa1ded93e5c250732634095d8bd3595dd808083e5ca/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4267a711b63476d36d39227883aeb3fb19ac92b88a9fc9973e66fbce1fd4aed9", size = 60063, upload-time = "2025-06-06T07:11:18.65Z" }, + { url = "https://files.pythonhosted.org/packages/94/93/4f75fd6a4c96f1e9bee198c5dc9a9b57e87a9c38117e1b5e423401886353/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:5e12a6e75036ee90484c33e204b85fb6785fcc9e7c8066ad65097301f48cdd10", size = 69057, upload-time = "2025-06-06T07:11:19.446Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/de47ccc390017ec5575e7e7fd9f659ee3747c52049cdb2969b1b538ce947/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:34b556255562f1b36d07fba933c2bcd9f0db167fa96727a6cbb4717b152ad7a2", size = 58792, upload-time = "2025-06-06T07:11:20.24Z" }, + { url = "https://files.pythonhosted.org/packages/e1/47/b3301d964422d4611c181348149a7c5956a2a76e6339de451a000d4ae8e7/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win32.whl", hash = "sha256:33188ed2d63e844c8adfbb82d1d3d461d64aaf78d225ce9c5930421b413c45ab", size = 62211, upload-time = "2025-09-20T07:11:52.411Z" }, + { url = "https://files.pythonhosted.org/packages/20/59/5f2c940ff606297129e93ebd6030c813e6a43a786de7fc33ccb268e0b06b/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d4cfece7e9c0ead2941e55a1da82f20d2b9c8003bb7a8853bb7f999b539f80a4", size = 70399, upload-time = "2025-09-20T07:11:53.254Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/2c8eb89062c71d4be73d618457ed68e7e2ba29a660ac26349d44fc121cbf/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3884146fea13727510458f6a14040b7632d5d90127028b9bfd503c6c655d0c01", size = 61392, upload-time = "2025-09-20T07:11:53.993Z" }, +] + +[[package]] +name = "winrt-windows-storage-streams" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/50/f4488b07281566e3850fcae1021f0285c9653992f60a915e15567047db63/winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f", size = 34335, upload-time = "2025-06-06T14:43:23.905Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/e7/7d3f2a4a442f264e05cab2bdf20ed1b95cb3f753bd1b0f277f2b49fb8335/winrt_windows_storage_streams-3.2.1-cp312-cp312-win32.whl", hash = "sha256:77c1f0e004b84347b5bd705e8f0fc63be8cd29a6093be13f1d0869d0d97b7d78", size = 127787, upload-time = "2025-06-06T14:02:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2f/cc36f475f8af293f40e2c2a5d6c2e75a189c2c2d4d01ecb3551578518c79/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4508ee135af53e4fc142876abbf4bc7c2a95edfc7d19f52b291a8499cacd6dc", size = 131849, upload-time = "2025-06-06T14:02:03.09Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/896fb734f7456910ec412f3f3adfdc3f0dc3134864a496d5b120592f3bfd/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:040cb94e6fb26b0d00a00e8b88b06fadf29dfe18cf24ed6cb3e69709c3613307", size = 128144, upload-time = "2025-06-06T14:02:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d2/24d9f59bdc05e741261d5bec3bcea9a848d57714126a263df840e2b515a8/winrt_windows_storage_streams-3.2.1-cp313-cp313-win32.whl", hash = "sha256:401bb44371720dc43bd1e78662615a2124372e7d5d9d65dfa8f77877bbcb8163", size = 127774, upload-time = "2025-06-06T14:02:04.752Z" }, + { url = "https://files.pythonhosted.org/packages/15/59/601724453b885265c7779d5f8025b043a68447cbc64ceb9149d674d5b724/winrt_windows_storage_streams-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:202c5875606398b8bfaa2a290831458bb55f2196a39c1d4e5fa88a03d65ef915", size = 131827, upload-time = "2025-06-06T14:02:05.601Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/a419675a6087c9ea496968c9b7805ef234afa585b7483e2269608a12b044/winrt_windows_storage_streams-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ca3c5ec0aab60895006bf61053a1aca6418bc7f9a27a34791ba3443b789d230d", size = 128180, upload-time = "2025-06-06T14:02:06.759Z" }, + { url = "https://files.pythonhosted.org/packages/55/70/2869ea2112c565caace73c9301afd1d7afcc49bdd37fac058f0178ba95d4/winrt_windows_storage_streams-3.2.1-cp314-cp314-win32.whl", hash = "sha256:5cd0dbad86fcc860366f6515fce97177b7eaa7069da261057be4813819ba37ee", size = 131701, upload-time = "2025-09-20T07:17:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/aae50b1d0e37b5a61055759aedd42c6c99d7c17ab8c3e568ab33c0288938/winrt_windows_storage_streams-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3c5bf41d725369b9986e6d64bad7079372b95c329897d684f955d7028c7f27a0", size = 135566, upload-time = "2025-09-20T07:17:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c3/6d3ce7a58e6c828e0795c9db8790d0593dd7fdf296e513c999150deb98d4/winrt_windows_storage_streams-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:293e09825559d0929bbe5de01e1e115f7a6283d8996ab55652e5af365f032987", size = 134393, upload-time = "2025-09-20T07:17:18.802Z" }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "idna", marker = "python_full_version < '3.13.2'" }, + { name = "multidict", marker = "python_full_version < '3.13.2'" }, + { name = "propcache", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "propcache", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062, upload-time = "2024-12-01T20:35:23.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644, upload-time = "2024-12-01T20:33:39.204Z" }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962, upload-time = "2024-12-01T20:33:40.808Z" }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795, upload-time = "2024-12-01T20:33:42.322Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368, upload-time = "2024-12-01T20:33:43.956Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314, upload-time = "2024-12-01T20:33:46.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987, upload-time = "2024-12-01T20:33:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914, upload-time = "2024-12-01T20:33:50.875Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765, upload-time = "2024-12-01T20:33:52.641Z" }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444, upload-time = "2024-12-01T20:33:54.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760, upload-time = "2024-12-01T20:33:56.286Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484, upload-time = "2024-12-01T20:33:58.375Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864, upload-time = "2024-12-01T20:34:00.22Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537, upload-time = "2024-12-01T20:34:03.54Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861, upload-time = "2024-12-01T20:34:05.73Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097, upload-time = "2024-12-01T20:34:07.664Z" }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399, upload-time = "2024-12-01T20:34:09.61Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789, upload-time = "2024-12-01T20:34:11.414Z" }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144, upload-time = "2024-12-01T20:34:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974, upload-time = "2024-12-01T20:34:15.234Z" }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587, upload-time = "2024-12-01T20:34:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386, upload-time = "2024-12-01T20:34:19.842Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421, upload-time = "2024-12-01T20:34:21.975Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384, upload-time = "2024-12-01T20:34:24.717Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689, upload-time = "2024-12-01T20:34:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453, upload-time = "2024-12-01T20:34:29.605Z" }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872, upload-time = "2024-12-01T20:34:31.454Z" }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497, upload-time = "2024-12-01T20:34:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981, upload-time = "2024-12-01T20:34:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229, upload-time = "2024-12-01T20:34:38.657Z" }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383, upload-time = "2024-12-01T20:34:40.501Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152, upload-time = "2024-12-01T20:34:42.814Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723, upload-time = "2024-12-01T20:34:44.699Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109, upload-time = "2024-12-01T20:35:20.834Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "idna", marker = "python_full_version >= '3.13.2'" }, + { name = "multidict", marker = "python_full_version >= '3.13.2'" }, + { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/ad/87b7b591551c74de67b08dcf172532fbb6df6f0e626dce9f220aae293052/zensical-0.0.15.tar.gz", hash = "sha256:b3200c91b30370671c50b8b4aa41c20e55ff2814b9003ee23c9b6f923a0c19be", size = 3816831, upload-time = "2025-12-24T11:15:49.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/07/ede00d39ff6cff7ab4971d15caa04a6710b126cbf0c8342add0337f4db89/zensical-0.0.15-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:13f205d24baeb4a77d096c3d385a8496850567b45c397cd54dde7c22dcbf0da8", size = 11934661, upload-time = "2025-12-24T11:15:09.019Z" }, + { url = "https://files.pythonhosted.org/packages/d6/32/24449c59f90a6a17dd0d9740ee44f245887d0c177c9c1dc452b9eb812024/zensical-0.0.15-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:99702504d38d8f7da4bf9a69513d2f65a0371dcb8f0e9f180c861cb285adb61f", size = 11820384, upload-time = "2025-12-24T11:15:12.265Z" }, + { url = "https://files.pythonhosted.org/packages/ac/db/64fed914788f2f5a8880dcc9a08ce25bcf1a7f2586a09e5d7d61003fd652/zensical-0.0.15-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:051ea368d268ffebf7ed7b6422f211a57680b2810fcde7f251816eb5c8d2f488", size = 12128961, upload-time = "2025-12-24T11:15:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/59/ca/04fd676880acad9571736e470e1f7bd8e6a2391f09952e69800f1b77fc75/zensical-0.0.15-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31a333afaf54d042bb51ac7dc623bd1b3bbe173c0ce4c7b72764cdef143ff4e2", size = 12098586, upload-time = "2025-12-24T11:15:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/8b/02/6a6ecad3b4cb07e11c8d160882cc937f8e3e96868f598c371892f1e594d7/zensical-0.0.15-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:072f80b7346ad4657a4c23008ae5cd3b3511e3bfac8d6bb277cbb6b3c11b6387", size = 12418552, upload-time = "2025-12-24T11:15:23.096Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b7/2a7205dfbeeb8612fbb1e22ad9f770804a07e440b276dcf23cbce3592da4/zensical-0.0.15-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec1b146adb3ee4146a3f800e0a4e6c8e681092d87ac51c60e35e99b68a724b31", size = 12193618, upload-time = "2025-12-24T11:15:26.359Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f4/5f38022d09e668622e49e8b5606bbfb5177d42ab1b4e91706ef0d38e2422/zensical-0.0.15-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:07a24e11a0e00d14f37d209cc37df446f91908109ca86ab3fe41c0b981e32668", size = 12308805, upload-time = "2025-12-24T11:15:29.602Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/6dd7657d6e1c1cd4b01ad531cea6a95eb660a2f960b03415d2184720a283/zensical-0.0.15-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:cc2bc9b4f89863d8b3443b0b9446ccaf5181085c3fc7a7e1bbc4708afe864f7d", size = 12367060, upload-time = "2025-12-24T11:15:32.621Z" }, + { url = "https://files.pythonhosted.org/packages/d3/83/781bdfbf459ec84b085085b142f433541de70d642d665227442a4deb9360/zensical-0.0.15-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:a3809be6d5f25bbd69dbe43715a60bf9d0186c51a1ec821e02f106e660e632b4", size = 12493255, upload-time = "2025-12-24T11:15:36.126Z" }, + { url = "https://files.pythonhosted.org/packages/54/10/0440e848658b97467c15c356f9038906fa90ac52b7f969ed1cce5deaf017/zensical-0.0.15-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4fd9382db77eff79eb347d4d0b6194c1c285e4151ad104bc9c8f65a27cb71d7f", size = 12430315, upload-time = "2025-12-24T11:15:39.697Z" }, + { url = "https://files.pythonhosted.org/packages/19/d9/50f3e833075e678047cd6e302f731b4b992762d366949124c2eeed6bd96c/zensical-0.0.15-cp310-abi3-win32.whl", hash = "sha256:1d29712dd4659e26b351417534e2ad6364f506514e168ac4c0ed42093b3a9469", size = 11559368, upload-time = "2025-12-24T11:15:42.893Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/cf06fc620dd94b2ffaa3e9df47b174603e0234096fd857b25dcda6c4a538/zensical-0.0.15-cp310-abi3-win_amd64.whl", hash = "sha256:0d0b303ec8a7aec2e733239f21d3602ce29e0eaabe4e4d60c9bcccb5c2d162dc", size = 11740088, upload-time = "2025-12-24T11:15:45.631Z" }, +] + +[[package]] +name = "zeroconf" +version = "0.146.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "ifaddr", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/ac/d4c67df0649c77f343e4976ef0a00e19a6e5c3342a6eaa6e64d7b853224f/zeroconf-0.146.0.tar.gz", hash = "sha256:a48010a1931acdba5b26e99326464788daeef96dcb7b9a44d1832352f76da49c", size = 161804, upload-time = "2025-03-05T01:47:18.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/db/4efc6ef84737933e4b88852d4f79f028aa593e2d10981538c3ff7fd71d99/zeroconf-0.146.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9fbd7f6bfd421d28561657463795a8a4487fa2adecb872b5f19f04edcb9c3b3", size = 1862910, upload-time = "2025-03-05T02:20:37.359Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/4c2d673f8cc3aa5f51568c9df08643a26c5c74c8cd2ee78e375c3dca01e2/zeroconf-0.146.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2972be542d6ae32ef715f899ad2bb3fdee877c42fd4a9bb148033b4ccaca6a08", size = 1716714, upload-time = "2025-03-05T02:20:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/68edc28d172799ef75d22aecad9478199c7e73223dcd1f15d316d084cca6/zeroconf-0.146.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:459542edd64b276296da5776cf86de5a7d1529f25038fc8a93a8a70b014f2671", size = 2154698, upload-time = "2025-03-05T02:20:40.249Z" }, + { url = "https://files.pythonhosted.org/packages/45/14/1918a965c33a86701d598cacee4e01a9aa5d5142998eb3f3b5b637c2de22/zeroconf-0.146.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8517c68576a3fece27882ff5743e69620ffc5a973b3d8ed21a28210b52da95d5", size = 2329442, upload-time = "2025-03-05T02:20:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ee/30da2f51619dc04a5c8a0d392ae0b56965e7918560b7c6ecc76955196909/zeroconf-0.146.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a63b901281619bab0106c993c80ed1db534e38d1c57d5553806f3430467d493", size = 2270277, upload-time = "2025-03-05T02:20:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/85/cb/ab152a4a52632f5919f2d223344df4c7fd1611ce661abeabe39956e28a91/zeroconf-0.146.0-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:85291bab82fe11df5c410152f410ab86ca3dae27327222530027ec7edf878dea", size = 2109845, upload-time = "2025-03-05T02:20:44.527Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b6/8487c734f6de0ea462ba77203c9630d2a06d38fa04340fa5d5fbeb406ea7/zeroconf-0.146.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:14443655df3b4fe7510eefe49f5b1385db57ac1fd7ab2aabb36e18cc614e0158", size = 2296624, upload-time = "2025-03-05T02:20:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/781833c5e0f7c9f0542c2795b9c56fa43d9755868da54ed9c2b2be7d1655/zeroconf-0.146.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7f794cd616ffb2c0588286bc33e3cb2df9f9bd277cd93b714050639b3d7564e5", size = 2158011, upload-time = "2025-03-05T02:20:47.581Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/52af0de3fa26e3df976cf0fdaf395d7c228c157734450a9612e20ddbd4be/zeroconf-0.146.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e3ab0015afb0189cb80f115ffd5985f9bfab29e73bb073d4a9cb0a21ff5c9dc9", size = 2502891, upload-time = "2025-03-05T02:20:49.227Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c2/83774e9a1a4cd40b426fe2eefaba2ff0b130105114d1a2c1814fd7e64060/zeroconf-0.146.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6534b6acf6db2dd1888053dbece9ce330d951c3bd98fbe27f28d6bc29311b64f", size = 2466408, upload-time = "2025-03-05T02:20:51.055Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e1/89b49a92c63f1a63a511cbfd0557afd49a82cc9649b1f73ba814768eb7cf/zeroconf-0.146.0-cp312-cp312-win32.whl", hash = "sha256:3361cd796898b244e638439e155576a8551b85c15ae1332eaefcd1b8ec447393", size = 1431606, upload-time = "2025-03-05T02:20:52.574Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f7/2e18ca60637aee55d13cb846c275b7f8dc23bd8aee5090858e4b16d1c55f/zeroconf-0.146.0-cp312-cp312-win_amd64.whl", hash = "sha256:eac992020f1b0a10fb9facd18e9e65baf4c0dd5a0a9c56f95c8868f5befa61e5", size = 1661168, upload-time = "2025-03-05T02:20:54.086Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/25e258fbd064e7b8f1497b9e345f29d6e44dd250a0fc5afd91aa04aafa57/zeroconf-0.146.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:faec552163f3007247ef9713fc817627d89844a782da8c1479b11ea3d3370684", size = 1840888, upload-time = "2025-03-05T02:20:56.799Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1c/1fd373e225e7282c244003683740ca58bc39270cb79fa13b13435c6dc88a/zeroconf-0.146.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0fc7b22e33b9d5d81b5aee58af6447fc1cf9b11e04dc2a04e1a5d00b3ae4d23a", size = 1697122, upload-time = "2025-03-05T02:20:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/2a42f1f88f69b11db2524469e5dc6752dc819e4fe9b985e293be74b38dee/zeroconf-0.146.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6e24daeeb926f8c7d426c46082224786e26aa5b4ce9fb573133e52ff8bae81", size = 2143632, upload-time = "2025-03-05T02:21:00.927Z" }, + { url = "https://files.pythonhosted.org/packages/81/8c/6caf3a48575c2bcf7ba2739b2cda6f117a305c03e87b53d08f19c859fae3/zeroconf-0.146.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9a798ea22c24a4148364f85b46ab33541072715bf8abccae2e3fd0c069f5808f", size = 2315076, upload-time = "2025-03-05T02:21:02.419Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b3/b86fa34f8d682b1bd3e144896b2c2cfb8a6c3308c1f773a7dcdb733d677b/zeroconf-0.146.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97a536e5b3d694e342bc908149342db2aae08a6f56466db63b6dffc26d2399ae", size = 2260655, upload-time = "2025-03-05T02:21:04.68Z" }, + { url = "https://files.pythonhosted.org/packages/06/2a/9b509a9d70c9f98b1b60f8d0002ac457df8f401325be6545ecb1f8071e8a/zeroconf-0.146.0-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97a09c01424e2356c41b39f7c2fb7a743623c2d413a082e861030e28090aebb", size = 2097673, upload-time = "2025-03-05T02:21:06.287Z" }, + { url = "https://files.pythonhosted.org/packages/90/70/2fb1c0470fb4a230d9cd63e245b5d4bd349a19454d363499eb4cdee3b54a/zeroconf-0.146.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:55c2a0087847d5c8bc00cc1e85cb1d048e8b70b09b4e949a2b763f33389819bb", size = 2307311, upload-time = "2025-03-05T01:47:15.987Z" }, + { url = "https://files.pythonhosted.org/packages/64/c3/351b7c1d07c9bf43d75bbbf18f9d07f8081373e85865b5faa78b661ae882/zeroconf-0.146.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b87d01dc8d7d10b929cc63330cf2e0f726f105a57e8d86df5d946b93a0e6280f", size = 2297954, upload-time = "2025-03-05T02:21:08.129Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b17dda38b9c5b916b1491acb6f32044d198c89ee74074dec0a277f0d8f49/zeroconf-0.146.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f844214eed7b66c1db3ea4ab2dddf0d84b91c340d83b2721656f70efb8588ae4", size = 2152669, upload-time = "2025-03-05T02:21:09.702Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ef97dcd5abdcfb6c4ea8a52d2cb08982541c43a9ec64dff1335ecc45a901/zeroconf-0.146.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cf9e85463a4fdeed8c5ea3b13e4a6c6de924d90b8b0982021e7331632f80192e", size = 2495779, upload-time = "2025-03-05T02:21:11.629Z" }, + { url = "https://files.pythonhosted.org/packages/90/f5/755bd701c69da699b6f0bd939972cd0978af6a7399174048a337de610f87/zeroconf-0.146.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fc1dd03301d370c21a8c5fbbe0a6a54a068a08384fa673d596c3f2424153aeca", size = 2459359, upload-time = "2025-03-05T02:21:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/0c/24/12f936d8ec82e3ca14291e3c6f45f2a2b3d1139f5ae19670fd4624a66f86/zeroconf-0.146.0-cp313-cp313-win32.whl", hash = "sha256:b4e70e77a67b3f39e91b5c02df82ab49a54bfc4edb1aa5779e404a711938c5af", size = 1427510, upload-time = "2025-03-05T02:21:16.184Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/9ccf706c4f3dad7b72956d5123e2b228d0411a6f977f4db410ff6b8963c0/zeroconf-0.146.0-cp313-cp313-win_amd64.whl", hash = "sha256:5274ba298d2edd5d02bb3937181a1e82deef773075b04374eac149bd40fccd96", size = 1655847, upload-time = "2025-03-05T02:21:18.359Z" }, +] + +[[package]] +name = "zeroconf" +version = "0.148.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "ifaddr", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/46/10db987799629d01930176ae523f70879b63577060d63e05ebf9214aba4b/zeroconf-0.148.0.tar.gz", hash = "sha256:03fcca123df3652e23d945112d683d2f605f313637611b7d4adf31056f681702", size = 164447, upload-time = "2025-10-05T00:21:19.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b3/6c08ccbda1e78c8f538d8add49fac2fe49ef85ee34b62877df4154715583/zeroconf-0.148.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aef8699ea47cd47c9219e3f110a35ad50c13c34c7c6db992f3c9f75feec6ef8f", size = 1735431, upload-time = "2025-10-05T01:08:09.375Z" }, + { url = "https://files.pythonhosted.org/packages/cb/37/6b91c4a4258863e485602e6b1eb098fe406142a653112e8719c49b69afc4/zeroconf-0.148.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9097e7010b9f9a64e5f2084493e9973d446bd85c7a7cbef5032b2b0a2ecc5a12", size = 1701594, upload-time = "2025-10-05T01:08:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/5eaaf66d39b3bccc17b52187eebb2dde93f761f4ee8b6c83b8fe764273f5/zeroconf-0.148.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdc566c387260fb7bf89f91d00460d0c9b9373dfddcf1fcc980ab3f7270154f9", size = 2134103, upload-time = "2025-10-05T01:08:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/19/a5/e4ebe7b5fbea512fe13efb466d855124126d2f531a18216c7cb509b8a4dd/zeroconf-0.148.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10cbd4134cacc22c3b3b169d7f782472a1dd36895e1421afa4f681caf181c07b", size = 1930109, upload-time = "2025-10-05T01:08:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/7f7c5cee5279afe2a6a8b9657de9a587ccb34168d7c99acc6d2b40b9d87e/zeroconf-0.148.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde01541e6a45c4d1b6e6d97b532ea241abc32c183745a74021b134d867388d8", size = 2230425, upload-time = "2025-10-05T01:08:16.296Z" }, + { url = "https://files.pythonhosted.org/packages/cd/41/0e1999db76e390fca9eef8257455955445a0386b94ce0ef6ce74896d7e2a/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8ceab8f10ab6fc0847a2de74377663793a974fdba77e7e6ba1ff47679f4bb845", size = 2161052, upload-time = "2025-10-05T01:08:17.976Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/6585fe6308b8f1ac0ac4d37ac69064ec2a36b81cf9080813cb666229694c/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0a8c36c37d8835420fc337be4aaa03c3a34272028919de575124c10d31a7e304", size = 2015005, upload-time = "2025-10-05T01:08:20.318Z" }, + { url = "https://files.pythonhosted.org/packages/74/ec/a9d0a577be157170f513e6ad6ebb3cd8dd9602c670d74911e9c5534e1c1d/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:848d57df1bb3b48279ba9b66e6c1f727570e2c8e7e0c4518c2daffaf23419d03", size = 2253785, upload-time = "2025-10-05T01:08:21.971Z" }, + { url = "https://files.pythonhosted.org/packages/ae/43/6679c16d4e6897c9aa502ee35c122bb605eee855612fad2ef6e0e13722c4/zeroconf-0.148.0-cp312-cp312-win32.whl", hash = "sha256:ba6eaa6b769924391c213dc391f36bd1c7e3ebe45fa3fa0cd97451b4f9ccef5c", size = 1295810, upload-time = "2025-10-05T01:08:23.575Z" }, + { url = "https://files.pythonhosted.org/packages/8e/42/a2d61df82086ddd32b9a5870ac683e8e5038cae38e2433c4fa03fe044235/zeroconf-0.148.0-cp312-cp312-win_amd64.whl", hash = "sha256:cec84ae7028db4a3addcc18628d12456cf39a9e973abee4a41e3b94d0db7df4c", size = 1533317, upload-time = "2025-10-05T01:08:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/46/09/394a24a633645063557c5144c9abb694699df76155dcab5e1e3078dd1323/zeroconf-0.148.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ad889929bdc3953530546a4a2486d8c07f5a18d4ef494a98446bf17414897a7", size = 1714465, upload-time = "2025-10-05T01:08:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/3d/db/f57c4bfcceb67fe474705cbadba3f8f7a88bdc95892e74ba6d85e24d28c3/zeroconf-0.148.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:29fb10be743650eb40863f1a1ee868df1869357a0c2ab75140ee3d7079540c1e", size = 1683877, upload-time = "2025-10-05T01:08:30.42Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/b3e2d39c40802a8cc9415357acdb76ff01bc29e25ffaa811771b6fffc428/zeroconf-0.148.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2995e74969c577461060539164c47e1ba674470585cb0f954ebeb77f032f3c2", size = 2122874, upload-time = "2025-10-05T01:08:32.11Z" }, + { url = "https://files.pythonhosted.org/packages/66/eb/0ac2bf51d58d47cfa854628036a7ad95544a1802bc890f3d69649dc35e46/zeroconf-0.148.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5be50346efdc20823f9d68d8757612767d11ceb8da7637d46080977b87912551", size = 1922164, upload-time = "2025-10-05T01:08:33.78Z" }, + { url = "https://files.pythonhosted.org/packages/59/ff/c7372507c7e25ad3499fe08d4678deb1ed41c57f78ff5df43bd2d4d98cfc/zeroconf-0.148.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc88fd01b5552ffb4d5bc551d027ac28a1852c03ceab754d02bd0d5f04c54e85", size = 2214119, upload-time = "2025-10-05T01:08:35.478Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/57f0889f47923b4fa4364b62b7b3ffc347f6bad09a25ce4e578b8991a86d/zeroconf-0.148.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:5af260c74187751c0df6a40f38d6fd17cb8658a734b0e1148a86084b71c1977c", size = 2137609, upload-time = "2025-10-05T00:21:15.953Z" }, + { url = "https://files.pythonhosted.org/packages/3b/33/9cb5558695c1377941dbb10a5591f88a787f9e1fba130642693d5c80663b/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6078c73a76d49ba969ca2bb7067e4d58ebd2b79a5f956e45c4c989b11d36e03", size = 2154314, upload-time = "2025-10-05T01:08:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/38/06/cf4e17a86922b4561d85d36f50f1adada1328723e882d95aa42baefa5479/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3e686bf741158f4253d5e0aa6a8f9d34b3140bf5826c0aca9b906273b9c77a5f", size = 2004973, upload-time = "2025-10-05T01:08:39.825Z" }, + { url = "https://files.pythonhosted.org/packages/a4/61/937a405783317639cd11e7bfab3879669896297b6ca2edfb0d2d9c8dbb30/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52d6ac06efe05a1e46089cfde066985782824f64b64c6982e8678e70b4b49453", size = 2237775, upload-time = "2025-10-05T01:08:41.535Z" }, + { url = "https://files.pythonhosted.org/packages/03/43/a1751c4b63e108a2318c2266e5afdd9d62292250aa8b1a8ed1674090885c/zeroconf-0.148.0-cp313-cp313-win32.whl", hash = "sha256:b9ba58e2bbb0cff020b54330916eaeb8ee8f4b0dde852e84f670f4ca3a0dd059", size = 1291073, upload-time = "2025-10-05T01:08:43.757Z" }, + { url = "https://files.pythonhosted.org/packages/5e/69/5f4f9eb14506e2afd2d423472e566d5455334d0c8740b933914d642bdbb5/zeroconf-0.148.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee3fcc2edcc04635cf673c400abac2f0c22c9786490fbfb971e0a860a872bf26", size = 1528568, upload-time = "2025-10-05T01:08:45.505Z" }, + { url = "https://files.pythonhosted.org/packages/a5/46/ac86e3a3ff355058cd0818b01a3a97ca3f2abc0a034f1edb8eea27cea65c/zeroconf-0.148.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:2158d8bfefcdb90237937df65b2235870ccef04644497e4e29d3ab5a4b3199b6", size = 1714870, upload-time = "2025-10-05T01:08:47.624Z" }, + { url = "https://files.pythonhosted.org/packages/de/02/c5e8cd8dfda0ca16c7309c8d12c09a3114e5b50054bce3c93da65db8b8e4/zeroconf-0.148.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:695f6663bf8df30fe1826a2c4d5acd8213d9cbd9111f59d375bf1ad635790e98", size = 1697756, upload-time = "2025-10-05T01:08:49.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/04/a66c1011d05d7bb8ae6a847d41ac818271a942390f3d8c83c776389ca094/zeroconf-0.148.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa65a24ec055be0a1cba2b986ac3e1c5d97a40abe164991aabc6a6416cc9df02", size = 2146784, upload-time = "2025-10-05T01:08:51.766Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/2239d87c3f60f886bd2dd299e9c63b811efd58b8b6fc659d8fd0900db3bc/zeroconf-0.148.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79890df4ff696a5cdc4a59152957be568bea1423ed13632fc09e2a196c6721d5", size = 1899394, upload-time = "2025-10-05T01:08:53.457Z" }, + { url = "https://files.pythonhosted.org/packages/fb/60/534a4b576a8f9f5edff648ac9a5417323bef3086a77397f2f2058125a3c8/zeroconf-0.148.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c0ca6e8e063eb5a385469bb8d8dec12381368031cb3a82c446225511863ede3", size = 2221319, upload-time = "2025-10-05T01:08:55.271Z" }, + { url = "https://files.pythonhosted.org/packages/b5/8c/1c8e9b7d604910830243ceb533d796dae98ed0c72902624a642487edfd61/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ece6f030cc7a771199760963c11ce4e77ed95011eedffb1ca5186247abfec24a", size = 2178586, upload-time = "2025-10-05T01:08:56.966Z" }, + { url = "https://files.pythonhosted.org/packages/16/55/178c4b95840dc687d45e413a74d2236a25395ab036f4813628271306ab9d/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c3f860ad0003a8999736fa2ae4c2051dd3c2e5df1bc1eaea2f872f5fcbd1f1c1", size = 1972371, upload-time = "2025-10-05T01:08:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/fb/86/b599421fe634d9f3a2799f69e6e7db9f13f77d326331fa2bb5982e936665/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ab8e687255cf54ebeae7ede6a8be0566aec752c570e16dbea84b3f9b149ba829", size = 2244286, upload-time = "2025-10-05T01:09:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cb/a30c42057be5da6bb4cbe1ab53bc3a7d9a29cd59caae097d3072a9375c14/zeroconf-0.148.0-cp314-cp314-win32.whl", hash = "sha256:6b1a6ddba3328d741798c895cecff21481863eb945c3e5d30a679461f4435684", size = 1321693, upload-time = "2025-10-05T01:09:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/2c/38/06873cdf769130af463ef5acadbaf4a50826a7274374bc3b9a4ec5d32678/zeroconf-0.148.0-cp314-cp314-win_amd64.whl", hash = "sha256:2588f1ca889f57cdc09b3da0e51175f1b6153ce0f060bf5eb2a8804c5953b135", size = 1563980, upload-time = "2025-10-05T01:09:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/36/fb/53d749793689279bc9657d818615176577233ad556d62f76f719e86ead1d/zeroconf-0.148.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:40fe100381365c983a89e4b219a7ececcc2a789ac179cd26d4a6bbe00ae3e8fe", size = 3418152, upload-time = "2025-10-05T01:09:06.71Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/5eb647f7277378cbfdb6943dc8e60c3b17cdd1556f5082ccfdd6813e1ce8/zeroconf-0.148.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b9c7bcae8af8e27593bad76ee0f0c21d43c6a2324cd1e34d06e6e08cb3fd922", size = 3389671, upload-time = "2025-10-05T01:09:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/3134aa54d30a9ae2e2473212eab586fe1779f845bf241e68729eca63d2ab/zeroconf-0.148.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8ba75dacd58558769afb5da24d83da4fdc2a5c43a52f619aaa107fa55d3fdc", size = 4123125, upload-time = "2025-10-05T01:09:11.064Z" }, + { url = "https://files.pythonhosted.org/packages/12/23/4a0284254ebce373ff1aee7240932a0599ecf47e3c711f93242a861aa382/zeroconf-0.148.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75f9a8212c541a4447c064433862fd4b23d75d47413912a28204d2f9c4929a59", size = 3651426, upload-time = "2025-10-05T01:09:13.725Z" }, + { url = "https://files.pythonhosted.org/packages/76/9a/7b79ef986b5467bb8f17b9a9e6eea887b0b56ecafc00515c81d118e681b4/zeroconf-0.148.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be64c0eb48efa1972c13f7f17a7ac0ed7932ebb9672e57f55b17536412146206", size = 4263151, upload-time = "2025-10-05T01:09:15.732Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0a/caa6d05548ca7cf28a0b8aa20a9dbb0f8176172f28799e53ea11f78692a3/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac1d4ee1d5bac71c27aea6d1dc1e1485423a1631a81be1ea65fb45ac280ade96", size = 4191717, upload-time = "2025-10-05T01:09:18.071Z" }, + { url = "https://files.pythonhosted.org/packages/46/f6/dbafa3b0f2d7a09315ed3ad588d36de79776ce49e00ec945c6195cad3f18/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8da9bdb39ead9d5971136046146cd5e11413cb979c011e19f717b098788b5c37", size = 3793490, upload-time = "2025-10-05T01:09:20.045Z" }, + { url = "https://files.pythonhosted.org/packages/c4/05/f8b88937659075116c122355bdd9ce52376cc46e2269d91d7d4f10c9a658/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6e3dd22732df47a126aefb5ca4b267e828b47098a945d4468d38c72843dd6df", size = 4311455, upload-time = "2025-10-05T01:09:22.042Z" }, + { url = "https://files.pythonhosted.org/packages/58/c0/359bdb3b435d9c573aec1f877f8a63d5e81145deb6c160de89647b237363/zeroconf-0.148.0-cp314-cp314t-win32.whl", hash = "sha256:cdc8083f0b5efa908ab6c8e41687bcb75fd3d23f49ee0f34cbc58422437a456f", size = 2755961, upload-time = "2025-10-05T01:09:24.041Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ab/7b487afd5d1fd053c5a018565be734ac6d5e554bce938c7cc126154adcfc/zeroconf-0.148.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f72c1f77a89638e87f243a63979f0fd921ce391f83e18e17ec88f9f453717701", size = 3309977, upload-time = "2025-10-05T01:09:26.039Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/webapp/README.md b/webapp/README.md new file mode 100644 index 00000000..e561b8f4 --- /dev/null +++ b/webapp/README.md @@ -0,0 +1,10 @@ +To run this app locally, install the requirements and run `shiny run`. + +``` +$ cd webapp +$ pip install -r requirements-dev.txt +$ shiny run +``` + +After the server starts, which should take only a moment, you can open +[http://127.0.0.1:8000](http://127.0.0.1:8000) to see the interface. diff --git a/webapp/__init__.py b/webapp/__init__.py new file mode 100644 index 00000000..33dc1b66 --- /dev/null +++ b/webapp/__init__.py @@ -0,0 +1 @@ +"""Shiny webapp for Adaptive Lighting.""" diff --git a/webapp/app.py b/webapp/app.py new file mode 100644 index 00000000..d46debe1 --- /dev/null +++ b/webapp/app.py @@ -0,0 +1,344 @@ +"""Simple web app to visualize brightness over time.""" + +import datetime as dt +from contextlib import suppress +from pathlib import Path +from typing import Any + +import matplotlib.pyplot as plt +import numpy as np +import shinyswatch +from astral import Observer +from homeassistant_util_color import color_temperature_to_rgb +from shiny import App, render, ui + + +def date_range(tzinfo: dt.tzinfo) -> list[dt.datetime]: + """Return a list of datetimes for the current day.""" + start_of_day = dt.datetime.now(tzinfo).replace( + hour=0, + minute=0, + second=0, + microsecond=0, + ) + # one second before the next day + end_of_day = start_of_day + dt.timedelta(days=1) - dt.timedelta(seconds=1) + hours_range = [start_of_day] + while hours_range[-1] < end_of_day: + hours_range.append(hours_range[-1] + dt.timedelta(minutes=1)) + return hours_range[:-1] + + +def copy_color_and_brightness_module() -> None: + """Copy the color_and_brightness module to the webapp folder.""" + with suppress(Exception): + webapp_folder = Path(__file__).parent.absolute() + module = ( + webapp_folder.parent + / "custom_components" + / "adaptive_lighting" + / "color_and_brightness.py" + ) + new_module = webapp_folder / module.name + with module.open() as f: + lines = [ + line.replace("homeassistant.util.color", "homeassistant_util_color") + for line in f.readlines() + ] + with new_module.open("r") as f: + existing_lines = f.readlines() + if existing_lines != lines: + with new_module.open("w") as f: + f.writelines(lines) + + +copy_color_and_brightness_module() + +from color_and_brightness import SunLightSettings # noqa: E402 + + +def plot_brightness(inputs: dict[str, Any], sleep_mode: bool): + """Plot the brightness over time for different modes.""" + # Define the time range for our simulation + sun_linear = SunLightSettings(**inputs, brightness_mode="linear") + sun_tanh = SunLightSettings(**inputs, brightness_mode="tanh") + sun = SunLightSettings(**inputs, brightness_mode="default") + # Calculate the brightness for each time in the time range for all modes + dt_range = date_range(sun.timezone) + time_range = [time_to_float(dt) for dt in dt_range] + brightness_linear_values = [ + sun_linear.brightness_pct(dt, sleep_mode) for dt in dt_range + ] + brightness_tanh_values = [ + sun_tanh.brightness_pct(dt, sleep_mode) for dt in dt_range + ] + brightness_default_values = [sun.brightness_pct(dt, sleep_mode) for dt in dt_range] + + # Plot the brightness over time for both modes + fig, ax = plt.subplots(figsize=(10, 6)) + ax.plot(time_range, brightness_linear_values, label="Linear Mode") + ax.plot(time_range, brightness_tanh_values, label="Tanh Mode") + ax.plot(time_range, brightness_default_values, label="Default Mode", c="C5") + sunrise_time = sun.sun.sunrise(dt.date.today()) + sunset_time = sun.sun.sunset(dt.date.today()) + ax.vlines( + time_to_float(sunrise_time), + 0, + 100, + color="C2", + label="Sunrise", + linestyles="dashed", + ) + ax.vlines( + time_to_float(sunset_time), + 0, + 100, + color="C3", + label="Sunset", + linestyles="dashed", + ) + ax.set_xlim(0, 24) + ax.set_xticks(np.arange(0, 25, 1)) + yticks = np.arange(0, 105, 5) + ytick_labels = [f"{label:.0f}%" for label in yticks] + ax.set_yticks(yticks, ytick_labels) + ax.set_xlabel("Time (hours)") + ax.set_ylabel("Brightness") + ax.set_title("Brightness over Time for Different Modes") + + # Add text box + textstr = "\n".join( + ( + f"Sunrise Time = {sunrise_time.time()}", + f"Sunset Time = {sunset_time.time()}", + f"Max Brightness = {sun.max_brightness:.0f}%", + f"Min Brightness = {sun.min_brightness:.0f}%", + f"Time Light = {sun.brightness_mode_time_light}", + f"Time Dark = {sun.brightness_mode_time_dark}", + ), + ) + + ax.legend() + + ax.text( + 0.4, + 0.55, + textstr, + transform=ax.transAxes, + fontsize=10, + verticalalignment="center", + bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}, + ) + ax.grid(visible=True) + + return fig + + +def plot_color_temp(inputs: dict[str, Any], sleep_mode: bool) -> plt.Figure: + """Plot the color temperature over time for different modes.""" + sun = SunLightSettings(**inputs, brightness_mode="default") + dt_range = date_range(tzinfo=sun.timezone) + time_range = [time_to_float(dt) for dt in dt_range] + settings = [sun.brightness_and_color(dt, sleep_mode) for dt in dt_range] + if sleep_mode and sun.sleep_rgb_or_color_temp == "color_temp": + colors = [ + color_temperature_to_rgb(setting["color_temp_kelvin"]) + for setting in settings + ] + else: + colors = [setting["rgb_color"] for setting in settings] + color_temp_values = np.array([(*col, 255) for col in colors]) / 255 + color_temp_values = color_temp_values.reshape(-1, 1, 4) + sun_position = [setting["sun_position"] for setting in settings] + fig, ax = plt.subplots(figsize=(10, 6)) + + # Display as a horizontal bar + ax.imshow( + np.rot90(color_temp_values)[:, ::1], + aspect="auto", + extent=[0, 24, -1, 1], + origin="upper", + ) + # Plot a curve on top of the imshow + ax.plot(time_range, sun_position, color="k", label="Sun Position") + + sunrise_time = sun.sun.sunrise(dt.date.today()) + sunset_time = sun.sun.sunset(dt.date.today()) + ax.vlines( + time_to_float(sunrise_time), + -1, + 1, + color="C2", + label="Sunrise", + linestyles="dashed", + ) + ax.vlines( + time_to_float(sunset_time), + -1, + 1, + color="C3", + label="Sunset", + linestyles="dashed", + ) + + ax.set_xlim(0, 24) + ax.set_xticks(np.arange(0, 25, 1)) + yticks = np.arange(-1, 1.1, 0.1) + ax.set_yticks(yticks, [f"{label*100:.0f}%" for label in yticks]) + ax.set_xlabel("Time (hours)") + ax.legend() + ax.set_ylabel("Sun position (%)") + ax.set_title("RGB Color Intensity over Time") + ax.grid(visible=False) + return fig + + +SEC_PER_HR = 60 * 60 +desc_top = """ +**Experience the Dynamics of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) in Real-Time.** + +Have you ever wondered how the intricate settings of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) impact your home ambiance? The Adaptive Lighting Simulator WebApp is here to demystify just that. + +(More text below the plots) +""" + +desc_bottom = """ +Harnessing the technology of the popular Adaptive Lighting integration for Home Assistant, this webapp provides a hands-on, visual platform to explore, tweak, and understand the myriad of parameters that dictate the behavior of your smart lights. Whether you're aiming for a subtle morning glow or a cozy evening warmth, observe firsthand how each tweak changes the ambiance. + +**Why Use the Simulator?** +- **Interactive Exploration**: No more guesswork. See in real-time how changes to settings influence the lighting dynamics. +- **Circadian Cycle Preview**: Understand how Adaptive Lighting adjusts throughout the day based on specific parameters, ensuring your lighting aligns with your circadian rhythms. +- **Tailored Testing**: Play with parameters and find the perfect combination that suits your personal or family's needs. +- **Educational Experience**: For both newbies and experts, delve deep into the intricacies of Adaptive Lighting's logic and potential. + +Dive into the simulator, experiment with different settings, and fine-tune the behavior of Adaptive Lighting to perfection. Whether you're setting it up for the first time or optimizing an existing setup, this tool ensures you get the most out of your smart lighting experience. +""" + +# Shiny UI +app_ui = ui.page_fluid( + ui.panel_title("🌞 Adaptive Lighting Simulator WebApp 🌛"), + ui.layout_sidebar( + ui.sidebar( + ui.input_switch("adapt_until_sleep", "adapt_until_sleep", value=False), + ui.input_switch("sleep_mode", "sleep_mode", value=False), + ui.input_slider("min_brightness", "min_brightness", 1, 100, 30, post="%"), + ui.input_slider("max_brightness", "max_brightness", 1, 100, 100, post="%"), + ui.input_numeric("min_color_temp", "min_color_temp", 2000), + ui.input_numeric("max_color_temp", "max_color_temp", 6666), + ui.input_slider( + "sleep_brightness", + "sleep_brightness", + 1, + 100, + 1, + post="%", + ), + ui.input_radio_buttons( + "sleep_rgb_or_color_temp", + "sleep_rgb_or_color_temp", + ["rgb_color", "color_temp"], + ), + ui.input_numeric("sleep_color_temp", "sleep_color_temp", 2000), + ui.input_text("sleep_rgb_color", "sleep_rgb_color", "255,0,0"), + ui.input_slider( + "brightness_mode_time_dark", + "brightness_mode_time_dark", + 1, + 5 * SEC_PER_HR, + 3 * SEC_PER_HR, + post=" sec", + ), + ui.input_slider( + "brightness_mode_time_light", + "brightness_mode_time_light", + 1, + 5 * SEC_PER_HR, + 0.5 * SEC_PER_HR, + post=" sec", + ), + ui.input_slider( + "sunrise_time", + "sunrise_time", + 0, + 24, + 6, + step=0.5, + post=" hr", + ), + ui.input_slider( + "sunset_time", + "sunset_time", + 0, + 24, + 18, + step=0.5, + post=" hr", + ), + ), + ui.markdown(desc_top), + ui.output_plot(id="brightness_plot"), + ui.output_plot(id="color_temp_plot"), + ui.markdown(desc_bottom), + ), + theme=shinyswatch.theme.sandstone, +) + + +def float_to_time(value: float) -> dt.time: + """Convert a float to a time object.""" + hours = int(value) + minutes = int((value - hours) * 60) + return dt.time(hours, minutes) + + +def time_to_float(time: dt.time | dt.datetime) -> float: + """Convert a time object to a float.""" + return time.hour + time.minute / 60 + + +def _kw(input): + return { + "name": "Adaptive Lighting Simulator", + "adapt_until_sleep": input.adapt_until_sleep(), + "max_brightness": input.max_brightness(), + "min_brightness": input.min_brightness(), + "min_color_temp": input.min_color_temp(), + "max_color_temp": input.max_color_temp(), + "sleep_brightness": input.sleep_brightness(), + "sleep_rgb_or_color_temp": input.sleep_rgb_or_color_temp(), + "sleep_color_temp": input.sleep_color_temp(), + "sleep_rgb_color": [int(x) for x in input.sleep_rgb_color().split(",")], + "sunrise_time": float_to_time(input.sunrise_time()), + "sunset_time": float_to_time(input.sunset_time()), + "brightness_mode_time_dark": dt.timedelta( + seconds=input.brightness_mode_time_dark(), + ), + "brightness_mode_time_light": dt.timedelta( + seconds=input.brightness_mode_time_light(), + ), + "sunrise_offset": dt.timedelta(0), + "sunset_offset": dt.timedelta(0), + "min_sunrise_time": None, + "max_sunrise_time": None, + "min_sunset_time": None, + "max_sunset_time": None, + "astral_observer": Observer(), + "timezone": dt.timezone.utc, + } + + +def server(input, output, session): # noqa: ARG001 + """Shiny server.""" + + @output + @render.plot + def brightness_plot(): + return plot_brightness(_kw(input), sleep_mode=input.sleep_mode()) + + @output + @render.plot + def color_temp_plot(): + return plot_color_temp(_kw(input), sleep_mode=input.sleep_mode()) + + +app = App(app_ui, server) diff --git a/webapp/color_and_brightness.py b/webapp/color_and_brightness.py new file mode 100644 index 00000000..85e351c5 --- /dev/null +++ b/webapp/color_and_brightness.py @@ -0,0 +1,532 @@ +"""Switch for the Adaptive Lighting integration.""" + +from __future__ import annotations + +import bisect +import colorsys +import datetime +import logging +import math +from dataclasses import dataclass +from datetime import UTC, timedelta +from enum import Enum +from functools import cached_property, partial +from typing import Any, Literal, cast + +import astral.sun +from homeassistant_util_color import ( + color_RGB_to_xy, + color_temperature_to_rgb, + color_xy_to_hs, +) + + +class SunEvent(str, Enum): + """A set of sun events that happen during a day.""" + + # Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET + # We re-define them here to not depend on homeassistant in this file. + SUNRISE = "sunrise" + SUNSET = "sunset" + NOON = "solar_noon" + MIDNIGHT = "solar_midnight" + + +_ORDER = (SunEvent.SUNRISE, SunEvent.NOON, SunEvent.SUNSET, SunEvent.MIDNIGHT) +_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} + +utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) +utcnow.__doc__ = "Get now in UTC time." + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class SunEvents: + """Track the state of the sun and associated light settings.""" + + name: str + astral_observer: astral.Observer + sunrise_time: datetime.time | None + min_sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None + sunset_time: datetime.time | None + min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None + sunrise_offset: datetime.timedelta = datetime.timedelta() + sunset_offset: datetime.timedelta = datetime.timedelta() + timezone: datetime.tzinfo = UTC + + def sunrise(self, dt: datetime.date) -> datetime.datetime: + """Return the (adjusted) sunrise time for the given datetime.""" + sunrise = ( + astral.sun.sunrise(self.astral_observer, dt) + if self.sunrise_time is None + else self._replace_time(dt, self.sunrise_time) + ) + self.sunrise_offset + if self.min_sunrise_time is not None: + min_sunrise = self._replace_time(dt, self.min_sunrise_time) + sunrise = max(min_sunrise, sunrise) + if self.max_sunrise_time is not None: + max_sunrise = self._replace_time(dt, self.max_sunrise_time) + sunrise = min(max_sunrise, sunrise) + return sunrise + + def sunset(self, dt: datetime.date) -> datetime.datetime: + """Return the (adjusted) sunset time for the given datetime.""" + sunset = ( + astral.sun.sunset(self.astral_observer, dt) + if self.sunset_time is None + else self._replace_time(dt, self.sunset_time) + ) + self.sunset_offset + if self.min_sunset_time is not None: + min_sunset = self._replace_time(dt, self.min_sunset_time) + sunset = max(min_sunset, sunset) + if self.max_sunset_time is not None: + max_sunset = self._replace_time(dt, self.max_sunset_time) + sunset = min(max_sunset, sunset) + return sunset + + def _replace_time( + self, + dt: datetime.date, + time: datetime.time, + ) -> datetime.datetime: + date_time = datetime.datetime.combine(dt, time) + dt_with_tz = date_time.replace(tzinfo=self.timezone) + return dt_with_tz.astimezone(UTC) + + def noon_and_midnight( + self, + dt: datetime.datetime, + sunset: datetime.datetime | None = None, + sunrise: datetime.datetime | None = None, + ) -> tuple[datetime.datetime, datetime.datetime]: + """Return the (adjusted) noon and midnight times for the given datetime.""" + if ( + self.sunrise_time is None + and self.sunset_time is None + and self.min_sunrise_time is None + and self.max_sunrise_time is None + and self.min_sunset_time is None + and self.max_sunset_time is None + ): + solar_noon = astral.sun.noon(self.astral_observer, dt) + solar_midnight = astral.sun.midnight(self.astral_observer, dt) + return solar_noon, solar_midnight + + if sunset is None: + sunset = self.sunset(dt) + if sunrise is None: + sunrise = self.sunrise(dt) + + middle = abs(sunset - sunrise) / 2 + if sunset > sunrise: + noon = sunrise + middle + midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1) + else: + midnight = sunset + middle + noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1) + return noon, midnight + + def sun_events(self, dt: datetime.datetime) -> list[tuple[SunEvent, float]]: + """Get the four sun event's timestamps at 'dt'.""" + sunrise = self.sunrise(dt) + sunset = self.sunset(dt) + solar_noon, solar_midnight = self.noon_and_midnight(dt, sunset, sunrise) + events: list[tuple[SunEvent, float]] = [ + (SunEvent.SUNRISE, sunrise.timestamp()), + (SunEvent.SUNSET, sunset.timestamp()), + (SunEvent.NOON, solar_noon.timestamp()), + (SunEvent.MIDNIGHT, solar_midnight.timestamp()), + ] + self._validate_sun_event_order(events) + return events + + def _validate_sun_event_order(self, events: list[tuple[SunEvent, float]]) -> None: + """Check if the sun events are in the expected order.""" + events = sorted(events, key=lambda x: x[1]) + events_names, _ = zip(*events, strict=True) + if events_names not in _ALLOWED_ORDERS: + msg = ( + f"{self.name}: The sun events {events_names} are not in the expected" + " order. The Adaptive Lighting integration will not work!" + " This might happen if your sunrise/sunset offset is too large or" + " your manually set sunrise/sunset time is past/before noon/midnight." + ) + _LOGGER.error(msg) + raise ValueError(msg) + + def prev_and_next_events( + self, + dt: datetime.datetime, + ) -> list[tuple[SunEvent, float]]: + """Get the previous and next sun event.""" + events = [ + event + for days in [-1, 0, 1] + for event in self.sun_events(dt + timedelta(days=days)) + ] + events = sorted(events, key=lambda x: x[1]) + i_now = bisect.bisect([ts for _, ts in events], dt.timestamp()) + return events[i_now - 1 : i_now + 1] + + def sun_position(self, dt: datetime.datetime) -> float: + """Calculate the position of the sun, between [-1, 1].""" + target_ts = dt.timestamp() + (_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) + h, x = ( + (prev_ts, next_ts) + if next_event in (SunEvent.SUNSET, SunEvent.SUNRISE) + else (next_ts, prev_ts) + ) + # k = -1 between sunset and sunrise (sun below horizon) + # k = 1 between sunrise and sunset (sun above horizon) + k = 1 if next_event in (SunEvent.SUNSET, SunEvent.NOON) else -1 + return k * (1 - ((target_ts - h) / (h - x)) ** 2) + + def closest_event( + self, + dt: datetime.datetime, + ) -> tuple[Literal[SunEvent.SUNRISE, SunEvent.SUNSET], float]: + """Get the closest sunset or sunrise event.""" + (prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) + if SunEvent.SUNRISE in (prev_event, next_event): + ts_event = prev_ts if prev_event == SunEvent.SUNRISE else next_ts + return SunEvent.SUNRISE, ts_event + if SunEvent.SUNSET in (prev_event, next_event): + ts_event = prev_ts if prev_event == SunEvent.SUNSET else next_ts + return SunEvent.SUNSET, ts_event + msg = "No sunrise or sunset event found." + raise ValueError(msg) + + +@dataclass(frozen=True) +class SunLightSettings: + """Track the state of the sun and associated light settings.""" + + name: str + astral_observer: astral.Observer + adapt_until_sleep: bool + max_brightness: int + max_color_temp: int + min_brightness: int + min_color_temp: int + sleep_brightness: int + sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"] + sleep_color_temp: int + sleep_rgb_color: tuple[int, int, int] + sunrise_time: datetime.time | None + min_sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None + sunset_time: datetime.time | None + min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None + brightness_mode_time_dark: datetime.timedelta + brightness_mode_time_light: datetime.timedelta + brightness_mode: Literal["default", "linear", "tanh"] = "default" + sunrise_offset: datetime.timedelta = datetime.timedelta() + sunset_offset: datetime.timedelta = datetime.timedelta() + timezone: datetime.tzinfo = UTC + + @cached_property + def sun(self) -> SunEvents: + """Return the SunEvents object.""" + return SunEvents( + name=self.name, + astral_observer=self.astral_observer, + sunrise_time=self.sunrise_time, + sunrise_offset=self.sunrise_offset, + min_sunrise_time=self.min_sunrise_time, + max_sunrise_time=self.max_sunrise_time, + sunset_time=self.sunset_time, + sunset_offset=self.sunset_offset, + min_sunset_time=self.min_sunset_time, + max_sunset_time=self.max_sunset_time, + timezone=self.timezone, + ) + + def _brightness_pct_default(self, dt: datetime.datetime) -> float: + """Calculate the brightness percentage using the default method.""" + sun_position = self.sun.sun_position(dt) + if sun_position > 0: + return self.max_brightness + delta_brightness = self.max_brightness - self.min_brightness + return (delta_brightness * (1 + sun_position)) + self.min_brightness + + def _brightness_pct_tanh(self, dt: datetime.datetime) -> float: + event, ts_event = self.sun.closest_event(dt) + dark = self.brightness_mode_time_dark.total_seconds() + light = self.brightness_mode_time_light.total_seconds() + if event == SunEvent.SUNRISE: + brightness = scaled_tanh( + dt.timestamp() - ts_event, + x1=-dark, + x2=+light, + y1=0.05, # be at 5% of range at x1 + y2=0.95, # be at 95% of range at x2 + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + elif event == SunEvent.SUNSET: + brightness = scaled_tanh( + dt.timestamp() - ts_event, + x1=-light, # shifted timestamp for the start of sunset + x2=+dark, # shifted timestamp for the end of sunset + y1=0.95, # be at 95% of range at the start of sunset + y2=0.05, # be at 5% of range at the end of sunset + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + else: + msg = "Unsupported sun event" + raise ValueError(msg) + return clamp(brightness, self.min_brightness, self.max_brightness) + + def _brightness_pct_linear(self, dt: datetime.datetime) -> float: + event, ts_event = self.sun.closest_event(dt) + # at ts_event - dt_start, brightness == start_brightness + # at ts_event + dt_end, brightness == end_brightness + dark = self.brightness_mode_time_dark.total_seconds() + light = self.brightness_mode_time_light.total_seconds() + if event == SunEvent.SUNRISE: + brightness = lerp( + dt.timestamp() - ts_event, + x1=-dark, + x2=+light, + y1=self.min_brightness, + y2=self.max_brightness, + ) + elif event == SunEvent.SUNSET: + brightness = lerp( + dt.timestamp() - ts_event, + x1=-light, + x2=+dark, + y1=self.max_brightness, + y2=self.min_brightness, + ) + else: + msg = "Unsupported sun event" + raise ValueError(msg) + return clamp(brightness, self.min_brightness, self.max_brightness) + + def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None: + """Calculate the brightness in %.""" + if is_sleep: + return self.sleep_brightness + assert self.brightness_mode in ("default", "linear", "tanh") + if self.brightness_mode == "default": + return self._brightness_pct_default(dt) + if self.brightness_mode == "linear": + return self._brightness_pct_linear(dt) + if self.brightness_mode == "tanh": + return self._brightness_pct_tanh(dt) + return None + + def color_temp_kelvin(self, sun_position: float) -> int: + """Calculate the color temperature in Kelvin.""" + if sun_position > 0: + delta = self.max_color_temp - self.min_color_temp + ct = (delta * sun_position) + self.min_color_temp + return 5 * round(ct / 5) # round to nearest 5 + if sun_position == 0 or not self.adapt_until_sleep: + return self.min_color_temp + if self.adapt_until_sleep and sun_position < 0: + delta = abs(self.min_color_temp - self.sleep_color_temp) + ct = (delta * abs(1 + sun_position)) + self.sleep_color_temp + return 5 * round(ct / 5) # round to nearest 5 + msg = "Should not happen" + raise ValueError(msg) + + def brightness_and_color( + self, + dt: datetime.datetime, + is_sleep: bool, + ) -> dict[str, Any]: + """Calculate the brightness and color.""" + sun_position = self.sun.sun_position(dt) + rgb_color: tuple[int, int, int] + # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) + force_rgb_color = False + brightness_pct = self.brightness_pct(dt, is_sleep) + if is_sleep: + color_temp_kelvin = self.sleep_color_temp + rgb_color = self.sleep_rgb_color + elif ( + self.sleep_rgb_or_color_temp == "rgb_color" + and self.adapt_until_sleep + and sun_position < 0 + ): + # Feature requested in + # https://github.com/basnijholt/adaptive-lighting/issues/624 + # This will result in a perceptible jump in color at sunset and sunrise + # because the `color_temperature_to_rgb` function is not 100% accurate. + min_color_rgb = color_temperature_to_rgb(self.min_color_temp) + rgb_color = lerp_color_hsv( + min_color_rgb, + self.sleep_rgb_color, + sun_position, + ) + color_temp_kelvin = self.color_temp_kelvin(sun_position) + force_rgb_color = True + else: + color_temp_kelvin = self.color_temp_kelvin(sun_position) + r, g, b = color_temperature_to_rgb(color_temp_kelvin) + rgb_color = (round(r), round(g), round(b)) + # backwards compatibility for versions < 1.3.1 - see #403 + color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) + xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) + hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) + return { + "brightness_pct": brightness_pct, + "color_temp_kelvin": color_temp_kelvin, + "color_temp_mired": color_temp_mired, + "rgb_color": rgb_color, + "xy_color": xy_color, + "hs_color": hs_color, + "sun_position": sun_position, + "force_rgb_color": force_rgb_color, + } + + def get_settings( + self, + is_sleep: bool, + transition: float | None, + ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: + """Get all light settings. + + Calculating all values takes <0.5ms. + """ + dt = utcnow() + timedelta(seconds=transition or 0) + return self.brightness_and_color(dt, is_sleep) + + +def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: + """Compute the values of 'a' and 'b' for a scaled and shifted tanh function. + + Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b' + for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points. + + The derivation is as follows: + + 1. Start with the equation of the tanh function: + y = 0.5 * (tanh(a * (x - b)) + 1) + + 2. Rearrange the equation to isolate tanh: + tanh(a * (x - b)) = 2*y - 1 + + 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b': + a * (x - b) = artanh(2*y - 1) + + 4. Plug in the points (x1, y1) and (x2, y2) to get two equations. + Using these, we can solve for 'a' and 'b' as: + a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1) + b = x1 - (artanh(2*y1 - 1) / a) + + Parameters + ---------- + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). + y2 + y-coordinate of the second point (should be between 0 and 1). + + Returns + ------- + a + Coefficient 'a' for the tanh function. + b + Coefficient 'b' for the tanh function. + + Notes + ----- + The values of y1 and y2 should lie between 0 and 1, inclusive. + + """ + a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) + b = x1 - (math.atanh(2 * y1 - 1) / a) + return a, b + + +def scaled_tanh( + x: float, + x1: float, + x2: float, + y1: float = 0.05, + y2: float = 0.95, + y_min: float = 0.0, + y_max: float = 100.0, +) -> float: + """Apply a scaled and shifted tanh function to a given input. + + This function represents a transformation of the tanh function that scales and shifts + the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2' + (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min' + and 'y_max', respectively. + + The equation of the function is as follows: + y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1) + + Parameters + ---------- + x + The input to the function. + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). Defaults to 0.05. + y2 + y-coordinate of the second point (should be between 0 and 1). Defaults to 0.95. + y_min + The minimum value of the output range. Defaults to 0. + y_max + The maximum value of the output range. Defaults to 100. + + Returns + ------- + float: The output of the function, which lies in the range [y_min, y_max]. + + """ + a, b = find_a_b(x1, x2, y1, y2) + return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) + + +def lerp_color_hsv( + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], + t: float, +) -> tuple[int, int, int]: + """Linearly interpolate between two RGB colors in HSV color space.""" + t = abs(t) + assert 0 <= t <= 1 + + # Convert RGB to HSV + hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) + hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) + + # Linear interpolation in HSV space + hsv = ( + hsv1[0] + t * (hsv2[0] - hsv1[0]), + hsv1[1] + t * (hsv2[1] - hsv1[1]), + hsv1[2] + t * (hsv2[2] - hsv1[2]), + ) + + # Convert back to RGB + rgb = tuple(round(x * 255) for x in colorsys.hsv_to_rgb(*hsv)) + assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" + return cast("tuple[int, int, int]", rgb) + + +def lerp(x: float, x1: float, x2: float, y1: float, y2: float) -> float: + """Linearly interpolate between two values.""" + return y1 + (x - x1) * (y2 - y1) / (x2 - x1) + + +def clamp(value: float, minimum: float, maximum: float) -> float: + """Clamp value between minimum and maximum.""" + return max(minimum, min(value, maximum)) diff --git a/webapp/homeassistant_util_color.py b/webapp/homeassistant_util_color.py new file mode 100644 index 00000000..c1541f4c --- /dev/null +++ b/webapp/homeassistant_util_color.py @@ -0,0 +1,774 @@ +"""Color util methods.""" + +# Slightly modified from homeassistant.util.color at +# https://github.com/home-assistant/core/blob/798fb3e31a6ba87358adc93a4c5b772b64451712/homeassistant/util/color.py#L14 +# to remove the dependency on homeassistant.util.color in sun.py +from __future__ import annotations + +import colorsys +import math +from dataclasses import dataclass +from typing import NamedTuple + + +class RGBColor(NamedTuple): + """RGB hex values.""" + + r: int + g: int + b: int + + +# Official CSS3 colors from w3.org: +# https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4 +# names do not have spaces in them so that we can compare against +# requests more easily (by removing spaces from the requests as well). +# This lets "dark seagreen" and "dark sea green" both match the same +# color "darkseagreen". +COLORS = { + "aliceblue": RGBColor(240, 248, 255), + "antiquewhite": RGBColor(250, 235, 215), + "aqua": RGBColor(0, 255, 255), + "aquamarine": RGBColor(127, 255, 212), + "azure": RGBColor(240, 255, 255), + "beige": RGBColor(245, 245, 220), + "bisque": RGBColor(255, 228, 196), + "black": RGBColor(0, 0, 0), + "blanchedalmond": RGBColor(255, 235, 205), + "blue": RGBColor(0, 0, 255), + "blueviolet": RGBColor(138, 43, 226), + "brown": RGBColor(165, 42, 42), + "burlywood": RGBColor(222, 184, 135), + "cadetblue": RGBColor(95, 158, 160), + "chartreuse": RGBColor(127, 255, 0), + "chocolate": RGBColor(210, 105, 30), + "coral": RGBColor(255, 127, 80), + "cornflowerblue": RGBColor(100, 149, 237), + "cornsilk": RGBColor(255, 248, 220), + "crimson": RGBColor(220, 20, 60), + "cyan": RGBColor(0, 255, 255), + "darkblue": RGBColor(0, 0, 139), + "darkcyan": RGBColor(0, 139, 139), + "darkgoldenrod": RGBColor(184, 134, 11), + "darkgray": RGBColor(169, 169, 169), + "darkgreen": RGBColor(0, 100, 0), + "darkgrey": RGBColor(169, 169, 169), + "darkkhaki": RGBColor(189, 183, 107), + "darkmagenta": RGBColor(139, 0, 139), + "darkolivegreen": RGBColor(85, 107, 47), + "darkorange": RGBColor(255, 140, 0), + "darkorchid": RGBColor(153, 50, 204), + "darkred": RGBColor(139, 0, 0), + "darksalmon": RGBColor(233, 150, 122), + "darkseagreen": RGBColor(143, 188, 143), + "darkslateblue": RGBColor(72, 61, 139), + "darkslategray": RGBColor(47, 79, 79), + "darkslategrey": RGBColor(47, 79, 79), + "darkturquoise": RGBColor(0, 206, 209), + "darkviolet": RGBColor(148, 0, 211), + "deeppink": RGBColor(255, 20, 147), + "deepskyblue": RGBColor(0, 191, 255), + "dimgray": RGBColor(105, 105, 105), + "dimgrey": RGBColor(105, 105, 105), + "dodgerblue": RGBColor(30, 144, 255), + "firebrick": RGBColor(178, 34, 34), + "floralwhite": RGBColor(255, 250, 240), + "forestgreen": RGBColor(34, 139, 34), + "fuchsia": RGBColor(255, 0, 255), + "gainsboro": RGBColor(220, 220, 220), + "ghostwhite": RGBColor(248, 248, 255), + "gold": RGBColor(255, 215, 0), + "goldenrod": RGBColor(218, 165, 32), + "gray": RGBColor(128, 128, 128), + "green": RGBColor(0, 128, 0), + "greenyellow": RGBColor(173, 255, 47), + "grey": RGBColor(128, 128, 128), + "honeydew": RGBColor(240, 255, 240), + "hotpink": RGBColor(255, 105, 180), + "indianred": RGBColor(205, 92, 92), + "indigo": RGBColor(75, 0, 130), + "ivory": RGBColor(255, 255, 240), + "khaki": RGBColor(240, 230, 140), + "lavender": RGBColor(230, 230, 250), + "lavenderblush": RGBColor(255, 240, 245), + "lawngreen": RGBColor(124, 252, 0), + "lemonchiffon": RGBColor(255, 250, 205), + "lightblue": RGBColor(173, 216, 230), + "lightcoral": RGBColor(240, 128, 128), + "lightcyan": RGBColor(224, 255, 255), + "lightgoldenrodyellow": RGBColor(250, 250, 210), + "lightgray": RGBColor(211, 211, 211), + "lightgreen": RGBColor(144, 238, 144), + "lightgrey": RGBColor(211, 211, 211), + "lightpink": RGBColor(255, 182, 193), + "lightsalmon": RGBColor(255, 160, 122), + "lightseagreen": RGBColor(32, 178, 170), + "lightskyblue": RGBColor(135, 206, 250), + "lightslategray": RGBColor(119, 136, 153), + "lightslategrey": RGBColor(119, 136, 153), + "lightsteelblue": RGBColor(176, 196, 222), + "lightyellow": RGBColor(255, 255, 224), + "lime": RGBColor(0, 255, 0), + "limegreen": RGBColor(50, 205, 50), + "linen": RGBColor(250, 240, 230), + "magenta": RGBColor(255, 0, 255), + "maroon": RGBColor(128, 0, 0), + "mediumaquamarine": RGBColor(102, 205, 170), + "mediumblue": RGBColor(0, 0, 205), + "mediumorchid": RGBColor(186, 85, 211), + "mediumpurple": RGBColor(147, 112, 219), + "mediumseagreen": RGBColor(60, 179, 113), + "mediumslateblue": RGBColor(123, 104, 238), + "mediumspringgreen": RGBColor(0, 250, 154), + "mediumturquoise": RGBColor(72, 209, 204), + "mediumvioletred": RGBColor(199, 21, 133), + "midnightblue": RGBColor(25, 25, 112), + "mintcream": RGBColor(245, 255, 250), + "mistyrose": RGBColor(255, 228, 225), + "moccasin": RGBColor(255, 228, 181), + "navajowhite": RGBColor(255, 222, 173), + "navy": RGBColor(0, 0, 128), + "navyblue": RGBColor(0, 0, 128), + "oldlace": RGBColor(253, 245, 230), + "olive": RGBColor(128, 128, 0), + "olivedrab": RGBColor(107, 142, 35), + "orange": RGBColor(255, 165, 0), + "orangered": RGBColor(255, 69, 0), + "orchid": RGBColor(218, 112, 214), + "palegoldenrod": RGBColor(238, 232, 170), + "palegreen": RGBColor(152, 251, 152), + "paleturquoise": RGBColor(175, 238, 238), + "palevioletred": RGBColor(219, 112, 147), + "papayawhip": RGBColor(255, 239, 213), + "peachpuff": RGBColor(255, 218, 185), + "peru": RGBColor(205, 133, 63), + "pink": RGBColor(255, 192, 203), + "plum": RGBColor(221, 160, 221), + "powderblue": RGBColor(176, 224, 230), + "purple": RGBColor(128, 0, 128), + "red": RGBColor(255, 0, 0), + "rosybrown": RGBColor(188, 143, 143), + "royalblue": RGBColor(65, 105, 225), + "saddlebrown": RGBColor(139, 69, 19), + "salmon": RGBColor(250, 128, 114), + "sandybrown": RGBColor(244, 164, 96), + "seagreen": RGBColor(46, 139, 87), + "seashell": RGBColor(255, 245, 238), + "sienna": RGBColor(160, 82, 45), + "silver": RGBColor(192, 192, 192), + "skyblue": RGBColor(135, 206, 235), + "slateblue": RGBColor(106, 90, 205), + "slategray": RGBColor(112, 128, 144), + "slategrey": RGBColor(112, 128, 144), + "snow": RGBColor(255, 250, 250), + "springgreen": RGBColor(0, 255, 127), + "steelblue": RGBColor(70, 130, 180), + "tan": RGBColor(210, 180, 140), + "teal": RGBColor(0, 128, 128), + "thistle": RGBColor(216, 191, 216), + "tomato": RGBColor(255, 99, 71), + "turquoise": RGBColor(64, 224, 208), + "violet": RGBColor(238, 130, 238), + "wheat": RGBColor(245, 222, 179), + "white": RGBColor(255, 255, 255), + "whitesmoke": RGBColor(245, 245, 245), + "yellow": RGBColor(255, 255, 0), + "yellowgreen": RGBColor(154, 205, 50), + # And... + "homeassistant": RGBColor(3, 169, 244), +} + + +@dataclass +class XYPoint: + """Represents a CIE 1931 XY coordinate pair.""" + + x: float + y: float + + +@dataclass +class GamutType: + """Represents the Gamut of a light.""" + + red: XYPoint + green: XYPoint + blue: XYPoint + + +def color_name_to_rgb(color_name: str) -> RGBColor: + """Convert color name to RGB hex value.""" + # COLORS map has no spaces in it, so make the color_name have no + # spaces in it as well for matching purposes + hex_value = COLORS.get(color_name.replace(" ", "").lower()) + if not hex_value: + msg = "Unknown color" + raise ValueError(msg) + + return hex_value + + +# pylint: disable=invalid-name + + +def color_RGB_to_xy( + iR: int, + iG: int, + iB: int, + Gamut: GamutType | None = None, +) -> tuple[float, float]: + """Convert from RGB color to XY color.""" + return color_RGB_to_xy_brightness(iR, iG, iB, Gamut)[:2] + + +# Taken from: +# https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md +# License: Code is given as is. Use at your own risk and discretion. +def color_RGB_to_xy_brightness( + iR: int, + iG: int, + iB: int, + Gamut: GamutType | None = None, +) -> tuple[float, float, int]: + """Convert from RGB color to XY color.""" + if iR + iG + iB == 0: + return 0.0, 0.0, 0 + + R = iR / 255 + B = iB / 255 + G = iG / 255 + + # Gamma correction + R = pow((R + 0.055) / (1.0 + 0.055), 2.4) if (R > 0.04045) else (R / 12.92) + G = pow((G + 0.055) / (1.0 + 0.055), 2.4) if (G > 0.04045) else (G / 12.92) + B = pow((B + 0.055) / (1.0 + 0.055), 2.4) if (B > 0.04045) else (B / 12.92) + + # Wide RGB D65 conversion formula + X = R * 0.664511 + G * 0.154324 + B * 0.162028 + Y = R * 0.283881 + G * 0.668433 + B * 0.047685 + Z = R * 0.000088 + G * 0.072310 + B * 0.986039 + + # Convert XYZ to xy + x = X / (X + Y + Z) + y = Y / (X + Y + Z) + + # Brightness + Y = 1 if Y > 1 else Y + brightness = round(Y * 255) + + # Check if the given xy value is within the color-reach of the lamp. + if Gamut: + in_reach = check_point_in_lamps_reach((x, y), Gamut) + if not in_reach: + xy_closest = get_closest_point_to_point((x, y), Gamut) + x = xy_closest[0] + y = xy_closest[1] + + return round(x, 3), round(y, 3), brightness + + +def color_xy_to_RGB( + vX: float, + vY: float, + Gamut: GamutType | None = None, +) -> tuple[int, int, int]: + """Convert from XY to a normalized RGB.""" + return color_xy_brightness_to_RGB(vX, vY, 255, Gamut) + + +# Converted to Python from Obj-C, original source from: +# https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md +def color_xy_brightness_to_RGB( + vX: float, + vY: float, + ibrightness: int, + Gamut: GamutType | None = None, +) -> tuple[int, int, int]: + """Convert from XYZ to RGB.""" + if Gamut and not check_point_in_lamps_reach((vX, vY), Gamut): + xy_closest = get_closest_point_to_point((vX, vY), Gamut) + vX = xy_closest[0] + vY = xy_closest[1] + + brightness = ibrightness / 255.0 + if brightness == 0.0: + return (0, 0, 0) + + Y = brightness + + if vY == 0.0: + vY += 0.00000000001 + + X = (Y / vY) * vX + Z = (Y / vY) * (1 - vX - vY) + + # Convert to RGB using Wide RGB D65 conversion. + r = X * 1.656492 - Y * 0.354851 - Z * 0.255038 + g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152 + b = X * 0.051713 - Y * 0.121364 + Z * 1.011530 + + # Apply reverse gamma correction. + r, g, b = ( + 12.92 * x if (x <= 0.0031308) else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055) + for x in (r, g, b) + ) + + # Bring all negative components to zero. + r, g, b = (max(0, x) for x in (r, g, b)) + + # If one component is greater than 1, weight components by that value. + max_component = max(r, g, b) + if max_component > 1: + r, g, b = (x / max_component for x in (r, g, b)) + + ir, ig, ib = (int(x * 255) for x in (r, g, b)) + + return (ir, ig, ib) + + +def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> tuple[int, int, int]: + """Convert a hsb into its rgb representation.""" + if fS == 0.0: + fV = int(fB * 255) + return fV, fV, fV + + r = g = b = 0 + h = fH / 60 + f = h - float(math.floor(h)) + p = fB * (1 - fS) + q = fB * (1 - fS * f) + t = fB * (1 - (fS * (1 - f))) + + if int(h) == 0: + r = int(fB * 255) + g = int(t * 255) + b = int(p * 255) + elif int(h) == 1: + r = int(q * 255) + g = int(fB * 255) + b = int(p * 255) + elif int(h) == 2: + r = int(p * 255) + g = int(fB * 255) + b = int(t * 255) + elif int(h) == 3: + r = int(p * 255) + g = int(q * 255) + b = int(fB * 255) + elif int(h) == 4: + r = int(t * 255) + g = int(p * 255) + b = int(fB * 255) + elif int(h) == 5: + r = int(fB * 255) + g = int(p * 255) + b = int(q * 255) + + return (r, g, b) + + +def color_RGB_to_hsv(iR: float, iG: float, iB: float) -> tuple[float, float, float]: + """Convert an rgb color to its hsv representation. + + Hue is scaled 0-360 + Sat is scaled 0-100 + Val is scaled 0-100 + """ + fHSV = colorsys.rgb_to_hsv(iR / 255.0, iG / 255.0, iB / 255.0) + return round(fHSV[0] * 360, 3), round(fHSV[1] * 100, 3), round(fHSV[2] * 100, 3) + + +def color_RGB_to_hs(iR: float, iG: float, iB: float) -> tuple[float, float]: + """Convert an rgb color to its hs representation.""" + return color_RGB_to_hsv(iR, iG, iB)[:2] + + +def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> tuple[int, int, int]: + """Convert an hsv color into its rgb representation. + + Hue is scaled 0-360 + Sat is scaled 0-100 + Val is scaled 0-100 + """ + fRGB = colorsys.hsv_to_rgb(iH / 360, iS / 100, iV / 100) + return (int(fRGB[0] * 255), int(fRGB[1] * 255), int(fRGB[2] * 255)) + + +def color_hs_to_RGB(iH: float, iS: float) -> tuple[int, int, int]: + """Convert an hsv color into its rgb representation.""" + return color_hsv_to_RGB(iH, iS, 100) + + +def color_xy_to_hs( + vX: float, + vY: float, + Gamut: GamutType | None = None, +) -> tuple[float, float]: + """Convert an xy color to its hs representation.""" + h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY, Gamut)) + return h, s + + +def color_hs_to_xy( + iH: float, + iS: float, + Gamut: GamutType | None = None, +) -> tuple[float, float]: + """Convert an hs color to its xy representation.""" + return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut) + + +def match_max_scale( + input_colors: tuple[int, ...], + output_colors: tuple[float, ...], +) -> tuple[int, ...]: + """Match the maximum value of the output to the input.""" + max_in = max(input_colors) + max_out = max(output_colors) + factor = 0.0 if max_out == 0 else max_in / max_out + return tuple(int(round(i * factor)) for i in output_colors) + + +def color_rgb_to_rgbw(r: int, g: int, b: int) -> tuple[int, int, int, int]: + """Convert an rgb color to an rgbw representation.""" + # Calculate the white channel as the minimum of input rgb channels. + # Subtract the white portion from the remaining rgb channels. + w = min(r, g, b) + rgbw = (r - w, g - w, b - w, w) + + # Match the output maximum value to the input. This ensures the full + # channel range is used. + return match_max_scale((r, g, b), rgbw) # type: ignore[return-value] + + +def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]: + """Convert an rgbw color to an rgb representation.""" + # Add the white channel to the rgb channels. + rgb = (r + w, g + w, b + w) + + # Match the output maximum value to the input. This ensures the + # output doesn't overflow. + return match_max_scale((r, g, b, w), rgb) # type: ignore[return-value] + + +def color_rgb_to_rgbww( + r: int, + g: int, + b: int, + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int, int, int, int]: + """Convert an rgb color to an rgbww representation.""" + # Find the color temperature when both white channels have equal brightness + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + mired_range = max_mireds - min_mireds + mired_midpoint = min_mireds + mired_range / 2 + color_temp_kelvin = color_temperature_mired_to_kelvin(mired_midpoint) + w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) + + # Find the ratio of the midpoint white in the input rgb channels + white_level = min( + r / w_r if w_r else 0, + g / w_g if w_g else 0, + b / w_b if w_b else 0, + ) + + # Subtract the white portion from the rgb channels. + rgb = (r - w_r * white_level, g - w_g * white_level, b - w_b * white_level) + rgbww = (*rgb, round(white_level * 255), round(white_level * 255)) + + # Match the output maximum value to the input. This ensures the full + # channel range is used. + return match_max_scale((r, g, b), rgbww) # type: ignore[return-value] + + +def color_rgbww_to_rgb( + r: int, + g: int, + b: int, + cw: int, + ww: int, + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int, int]: + """Convert an rgbww color to an rgb representation.""" + # Calculate color temperature of the white channels + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + mired_range = max_mireds - min_mireds + try: + ct_ratio = ww / (cw + ww) + except ZeroDivisionError: + ct_ratio = 0.5 + color_temp_mired = min_mireds + ct_ratio * mired_range + if color_temp_mired: + color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) + else: + color_temp_kelvin = 0 + w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) + white_level = max(cw, ww) / 255 + + # Add the white channels to the rgb channels. + rgb = (r + w_r * white_level, g + w_g * white_level, b + w_b * white_level) + + # Match the output maximum value to the input. This ensures the + # output doesn't overflow. + return match_max_scale((r, g, b, cw, ww), rgb) # type: ignore[return-value] + + +def color_rgb_to_hex(r: int, g: int, b: int) -> str: + """Return a RGB color from a hex color string.""" + return f"{round(r):02x}{round(g):02x}{round(b):02x}" + + +def rgb_hex_to_rgb_list(hex_string: str) -> list[int]: + """Return an RGB color value list from a hex color string.""" + return [ + int(hex_string[i : i + len(hex_string) // 3], 16) + for i in range(0, len(hex_string), len(hex_string) // 3) + ] + + +def color_temperature_to_hs(color_temperature_kelvin: float) -> tuple[float, float]: + """Return an hs color from a color temperature in Kelvin.""" + return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin)) + + +def color_temperature_to_rgb( + color_temperature_kelvin: float, +) -> tuple[float, float, float]: + """Return an RGB color from a color temperature in Kelvin. + + This is a rough approximation based on the formula provided by T. Helland + http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ + """ + # range check + if color_temperature_kelvin < 1000: + color_temperature_kelvin = 1000 + elif color_temperature_kelvin > 40000: + color_temperature_kelvin = 40000 + + tmp_internal = color_temperature_kelvin / 100.0 + + red = _get_red(tmp_internal) + + green = _get_green(tmp_internal) + + blue = _get_blue(tmp_internal) + + return red, green, blue + + +def color_temperature_to_rgbww( + temperature: int, + brightness: int, + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int, int, int, int]: + """Convert color temperature in kelvin to rgbcw. + + Returns a (r, g, b, cw, ww) tuple. + """ + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + temperature = color_temperature_kelvin_to_mired(temperature) + mired_range = max_mireds - min_mireds + cold = ((max_mireds - temperature) / mired_range) * brightness + warm = brightness - cold + return (0, 0, 0, round(cold), round(warm)) + + +def rgbww_to_color_temperature( + rgbww: tuple[int, int, int, int, int], + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int]: + """Convert rgbcw to color temperature in kelvin. + + Returns a tuple (color_temperature, brightness). + """ + _, _, _, cold, warm = rgbww + return _white_levels_to_color_temperature(cold, warm, min_kelvin, max_kelvin) + + +def _white_levels_to_color_temperature( + cold: int, + warm: int, + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int]: + """Convert whites to color temperature in kelvin. + + Returns a tuple (color_temperature, brightness). + """ + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + brightness = warm / 255 + cold / 255 + if brightness == 0: + # Return the warmest color if brightness is 0 + return (min_kelvin, 0) + return round( + color_temperature_mired_to_kelvin( + ((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds, + ), + ), min(255, round(brightness * 255)) + + +def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float: + """Clamp the given color component value between the given min and max values. + + The range defined by the minimum and maximum values is inclusive, i.e. given a + color_component of 0 and a minimum of 10, the returned value is 10. + """ + color_component_out = max(color_component, minimum) + return min(color_component_out, maximum) + + +def _get_red(temperature: float) -> float: + """Get the red component of the temperature in RGB space.""" + if temperature <= 66: + return 255 + tmp_red = 329.698727446 * math.pow(temperature - 60, -0.1332047592) + return _clamp(tmp_red) + + +def _get_green(temperature: float) -> float: + """Get the green component of the given color temp in RGB space.""" + if temperature <= 66: + green = 99.4708025861 * math.log(temperature) - 161.1195681661 + else: + green = 288.1221695283 * math.pow(temperature - 60, -0.0755148492) + return _clamp(green) + + +def _get_blue(temperature: float) -> float: + """Get the blue component of the given color temperature in RGB space.""" + if temperature >= 66: + return 255 + if temperature <= 19: + return 0 + blue = 138.5177312231 * math.log(temperature - 10) - 305.0447927307 + return _clamp(blue) + + +def color_temperature_mired_to_kelvin(mired_temperature: float) -> int: + """Convert absolute mired shift to degrees kelvin.""" + return math.floor(1000000 / mired_temperature) + + +def color_temperature_kelvin_to_mired(kelvin_temperature: float) -> int: + """Convert degrees kelvin to mired shift.""" + return math.floor(1000000 / kelvin_temperature) + + +# The following 5 functions are adapted from rgbxy provided by Benjamin Knight +# License: The MIT License (MIT), 2014. +# https://github.com/benknight/hue-python-rgb-converter +def cross_product(p1: XYPoint, p2: XYPoint) -> float: + """Calculate the cross product of two XYPoints.""" + return float(p1.x * p2.y - p1.y * p2.x) + + +def get_distance_between_two_points(one: XYPoint, two: XYPoint) -> float: + """Calculate the distance between two XYPoints.""" + dx = one.x - two.x + dy = one.y - two.y + return math.sqrt(dx * dx + dy * dy) + + +def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: + """Find the closest point from P to a line defined by A and B. + + This point will be reproducible by the lamp + as it is on the edge of the gamut. + """ + AP = XYPoint(P.x - A.x, P.y - A.y) + AB = XYPoint(B.x - A.x, B.y - A.y) + ab2 = AB.x * AB.x + AB.y * AB.y + ap_ab = AP.x * AB.x + AP.y * AB.y + t = ap_ab / ab2 + + if t < 0.0: + t = 0.0 + elif t > 1.0: + t = 1.0 + + return XYPoint(A.x + AB.x * t, A.y + AB.y * t) + + +def get_closest_point_to_point( + xy_tuple: tuple[float, float], + Gamut: GamutType, +) -> tuple[float, float]: + """Get the closest matching color within the gamut of the light. + + Should only be used if the supplied color is outside of the color gamut. + """ + xy_point = XYPoint(xy_tuple[0], xy_tuple[1]) + + # find the closest point on each line in the CIE 1931 'triangle'. + pAB = get_closest_point_to_line(Gamut.red, Gamut.green, xy_point) + pAC = get_closest_point_to_line(Gamut.blue, Gamut.red, xy_point) + pBC = get_closest_point_to_line(Gamut.green, Gamut.blue, xy_point) + + # Get the distances per point and see which point is closer to our Point. + dAB = get_distance_between_two_points(xy_point, pAB) + dAC = get_distance_between_two_points(xy_point, pAC) + dBC = get_distance_between_two_points(xy_point, pBC) + + lowest = dAB + closest_point = pAB + + if dAC < lowest: + lowest = dAC + closest_point = pAC + + if dBC < lowest: + lowest = dBC + closest_point = pBC + + # Change the xy value to a value which is within the reach of the lamp. + cx = closest_point.x + cy = closest_point.y + + return (cx, cy) + + +def check_point_in_lamps_reach(p: tuple[float, float], Gamut: GamutType) -> bool: + """Check if the provided XYPoint can be recreated by a Hue lamp.""" + v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y) + v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y) + + q = XYPoint(p[0] - Gamut.red.x, p[1] - Gamut.red.y) + s = cross_product(q, v2) / cross_product(v1, v2) + t = cross_product(v1, q) / cross_product(v1, v2) + + return (s >= 0.0) and (t >= 0.0) and (s + t <= 1.0) + + +def check_valid_gamut(Gamut: GamutType) -> bool: + """Check if the supplied gamut is valid.""" + # Check if the three points of the supplied gamut are not on the same line. + v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y) + v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y) + not_on_line = cross_product(v1, v2) > 0.0001 + + # Check if all six coordinates of the gamut lie between 0 and 1. + red_valid = ( + Gamut.red.x >= 0 and Gamut.red.x <= 1 and Gamut.red.y >= 0 and Gamut.red.y <= 1 + ) + green_valid = ( + Gamut.green.x >= 0 + and Gamut.green.x <= 1 + and Gamut.green.y >= 0 + and Gamut.green.y <= 1 + ) + blue_valid = ( + Gamut.blue.x >= 0 + and Gamut.blue.x <= 1 + and Gamut.blue.y >= 0 + and Gamut.blue.y <= 1 + ) + + return not_on_line and red_valid and green_valid and blue_valid diff --git a/webapp/requirements-dev.txt b/webapp/requirements-dev.txt new file mode 100644 index 00000000..9703163f --- /dev/null +++ b/webapp/requirements-dev.txt @@ -0,0 +1,4 @@ +astral==2.2 +matplotlib +shinyswatch +shiny diff --git a/webapp/requirements.txt b/webapp/requirements.txt new file mode 100644 index 00000000..fd8793e1 --- /dev/null +++ b/webapp/requirements.txt @@ -0,0 +1,6 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.txt.in --output-file requirements.txt +astral==2.2 + # via -r requirements.txt.in +pytz==2026.3.post1 + # via astral diff --git a/webapp/requirements.txt.in b/webapp/requirements.txt.in new file mode 100644 index 00000000..39c93a83 --- /dev/null +++ b/webapp/requirements.txt.in @@ -0,0 +1 @@ +astral==2.2 diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 00000000..dfa1b309 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,120 @@ +# Adaptive Lighting Documentation Site Configuration +# Built with Zensical - https://zensical.org + +[project] +site_name = "Adaptive Lighting" +site_description = "Automatically adjust brightness and color of lights based on the sun position" +site_author = "Bas Nijholt" +site_url = "https://adaptive-lighting.nijho.lt/" +copyright = "Copyright © 2020-2026 Bas Nijholt" + +repo_url = "https://github.com/basnijholt/adaptive-lighting" +repo_name = "GitHub" +edit_uri = "edit/main/docs" + +nav = [ + { "Home" = "index.md" }, + { "Getting Started" = "getting-started.md" }, + { "Configuration" = "configuration.md" }, + { "Services" = "services.md" }, + { "Automation Examples" = "automation-examples.md" }, + { "Troubleshooting" = "troubleshooting.md" }, + { "Advanced" = [ + { "Brightness Modes" = "advanced/brightness-modes.md" }, + { "Manual Control" = "advanced/manual-control.md" }, + { "Sleep Mode" = "advanced/sleep-mode.md" }, + ] }, + { "See Also" = "see-also.md" }, +] + +[project.theme] +custom_dir = "docs/overrides" +language = "en" +logo = "assets/logo.png" + +features = [ + "announce.dismiss", + "content.action.edit", + "content.action.view", + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.footnote.tooltips", + "content.tabs.link", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.path", + "navigation.top", + "navigation.tracking", + "search.highlight", +] + +# Three-way toggle: system preference -> light -> dark -> system preference +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "amber" +accent = "orange" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "amber" +accent = "orange" +toggle.icon = "lucide/moon-star" +toggle.name = "Switch to system preference" + +[project.theme.font] +text = "Inter" +code = "JetBrains Mono" + +[project.theme.icon] +repo = "lucide/github" + +[project.extra] +generator = false + +[project.extra.analytics] +provider = "custom" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/basnijholt/adaptive-lighting" + +[[project.extra.social]] +icon = "fontawesome/brands/discord" +link = "https://discord.gg/home-assistant" + +# Enable GitHub-style admonitions (> [!NOTE], > [!TIP], etc.) +[project.markdown_extensions.gfm_admonition] + +# Enable markdown inside HTML blocks (e.g.,
) +[project.markdown_extensions.md_in_html] + +# Enable attribute lists for adding CSS classes (e.g., {.md-button}) +[project.markdown_extensions.attr_list] + +# Enable syntax highlighting for fenced code blocks +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true + +[project.markdown_extensions.pymdownx.superfences] + +# Enable tabs (=== "Tab Name" syntax) +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true + +# Enable emoji/icon support +[project.markdown_extensions.pymdownx.emoji] +emoji_index = "zensical.extensions.emoji.twemoji" +emoji_generator = "zensical.extensions.emoji.to_svg"