[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: varetas3d\nthanks_dev: # Replace with a single thanks.dev username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/01-issue_report.yml",
    "content": "---\nname: Issue Report\ndescription: Create an issue report to help us improve\ntitle: \"[ISSUE] \"\nlabels: bug\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **Please fill out this form to report a bug with the KidsChore Integration.**\n\n  - type: input\n    id: home_assistant_version\n    attributes:\n      label: Home Assistant Version\n      description: \"What version of Home Assistant are you using?\"\n      placeholder: \"e.g., 2024.11.1\"\n    validations:\n      required: true\n\n  - type: input\n    id: integration_version\n    attributes:\n      label: KidsChore Integration Version\n      description: \"What version of the integration are you using?\"\n      placeholder: \"e.g., 0.4.8\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: installation_method\n    attributes:\n      label: Installation Method\n      description: \"How did you install the integration?\"\n      options:\n        - HACS\n        - Manual\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: prior_issue_check\n    attributes:\n      label: Did you check for existing issues?\n      description: \"You should check if there's a current or closed issue.\"\n      options:\n        - label: Yes, I have checked for existing issues\n          required: true\n        - label: No, I have not checked for existing issues\n\n  - type: checkboxes\n    id: debug_enabled\n    attributes:\n      label: Did you enable debug logging before and are ready to post logs?\n      options:\n        - label: Yes, I have enabled debug logging\n          required: true\n        - label: No, I have not enabled debug logging\n\n  - type: textarea\n    id: issue_description\n    attributes:\n      label: Describe the Issue\n      description: \"A clear and concise description of what the bug is.\"\n      placeholder: \"Provide a detailed description...\"\n    validations:\n      required: true\n\n  - type: markdown\n    attributes:\n      value: |\n        ### **Logs**\n\n        Please add the following to your `configuration.yaml` on your Home Assistant and restart:\n\n        ```yaml\n        logger:\n          default: warning\n          logs:\n            custom_components.kidschores: debug\n        ```\n\n        See [Home Assistant Logger Documentation](https://www.home-assistant.io/integrations/logger) for more information.\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs\n      description: \"Paste your logs here.\"\n      render: yaml\n\n  - type: textarea\n    id: additional_context\n    attributes:\n      label: Additional Context\n      description: \"Add any other context about the problem here.\"\n      placeholder: \"Any additional information...\"\n---\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/02-feature_reques.yml",
    "content": "---\nname: Feature Request\ndescription: Suggest an idea for this project\ntitle: \"[REQ] \"\nlabels: enhancement\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **Please describe the feature you would like to see added to the KidsChores Integration.**\n\n  - type: checkboxes\n    id: problem_exists\n    attributes:\n      label: Is your feature request related to a problem?\n      options:\n        - label: \"Yes\"\n\n  - type: textarea\n    id: problem\n    attributes:\n      label: Please describe the problem\n      description: A clear and concise description of what the problem is.\n      placeholder: Ex. I'm always frustrated when [...]\n    validations:\n      required: false\n      \n  - type: textarea\n    id: solution\n    attributes:\n      label: Describe the solution you'd like\n      description: A clear and concise description of what you want to happen.\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Describe alternatives you've considered\n      description: A clear and concise description of any alternative solutions or features you've considered.\n\n  - type: textarea\n    id: additional_context\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/workflows/hassfest.yaml",
    "content": "name: Validate with hassfest\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  validate:\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: \"actions/checkout@v3\"\n      - uses: home-assistant/actions/hassfest@master\n"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "content": "name: HACS Action\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n\njobs:\n  validate-hacs:\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: \"actions/checkout@v3\"\n      - name: HACS validation\n        uses: \"hacs/action@main\"\n        with:\n          category: integration\n"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__/\n*.py[cod]\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "> [!IMPORTANT]\n> **⚠️ ACTIVE DEVELOPMENT HAS MOVED TO CHOREOPS**\n>\n> `KidsChores` has officially evolved into a new, expanded integration called **[ChoreOps](https://github.com/ccpk1/choreops)**.\n> \n> Based on incredible feedback from this community, the backend has been completely re-architected to Home Assistant Platinum standards to support *everyone* in the household (not just kids!), along with an entirely new \"Over-The-Air\" dashboard system.\n> \n> **Please migrate to ChoreOps to access all new features and continued support.**\n> Upgrading is safe and easy: ChoreOps includes a built-in migration tool and runs safely in parallel with KidsChores. You can set it up, migrate your data, and ensure you are completely happy before removing your old configuration.\n> \n> 👉 **[Get started with ChoreOps here!](https://github.com/ccpk1/choreops)**\n\n---\n\n[![HACS Custom](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)\n![GitHub Release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/ad-ha/kidschores-ha?include_prereleases)\n![GitHub Downloads (all assets, latest release)](https://img.shields.io/github/downloads/ad-ha/kidschores-ha/latest/total)\n\n[![GitHub Actions](https://github.com/ad-ha/kidschores-ha/actions/workflows/validate.yaml/badge.svg)](https://github.com/ad-ha/kidschores-ha/actions/workflows/validate.yaml)\n[![Hassfest](https://github.com/ad-ha/kidschores-ha/actions/workflows/hassfest.yaml/badge.svg)](https://github.com/ad-ha/kidschores-ha/actions/workflows/hassfest.yaml)\n\n<h1>KidsChores</h1>\n\n<p align=\"center\">\n  <img src=\"https://github.com/user-attachments/assets/e95bdb54-2c4c-4a84-96b4-f47a46a1228a\" alt=\"KidsChores logo\" width=\"300\">\n</p>\n<p align=\"center\">\n  <a href=\"https://buymeacoffee.com/varetas3d\" target=\"_blank\">\n    <img src=\"https://cdn.buymeacoffee.com/buttons/default-orange.png\" alt=\"Buy Me A Coffee\" height=\"41\" width=\"174\">\n  </a>\n</p>\n<p>\n  <br>\n  <br>\n</p>\n\n# 🏆 KidsChores: The Ultimate Home Assistant Chore & Reward System\n\n**The easiest-to-use and most feature-rich chore management system for Home Assistant.**\nGet up and running in **10 minutes or less**, with **unmatched capabilities** to gamify the process and keep kids engaged!\n\n✅ **Track chores effortlessly** – Assign chores, set due dates, and track completions.\n\n✅ **Gamify the experience** – **Badges, Achievements, and Challenges** keep kids motivated.\n\n✅ **Bonuses & Penalties** – Reward extra effort and enforce accountability.\n\n✅ **Customizable Rewards** – Give coins, stars, points, or any currency system you choose.\n\n✅ **Built-in User Access Control** – Restricts actions based on roles (kids, parents, admins).\n\n✅ **Smart Notifications** – Notify kids and parents; parents can approve chores & rewards from their phone or watch.\n\n✅ **Calendar Integration & Custom Scheduling** – Automatically manage recurring chores and sync with Home Assistant’s calendar.\n\n✅ **Works Offline & Keeps Data Local** – Everything is processed locally for **privacy & security**.\n<br><br>\n**\"Designed for kids, but flexible for the whole family—assign chores to anyone, from toddlers to teens to adults!\"**\n\n📖 **[System Overviews, FAQ's, Tips & Tricks, and Usage Examples in the Wiki →](https://github.com/ad-ha/kidschores-ha/wiki)**\n\n---\n\n## ⚡ Quick Installation\n\n📌 **Via HACS (Recommended)**\n\n1. Ensure **HACS** is installed. ([HACS Setup Guide](https://hacs.xyz/docs/installation/manual))\n2. In Home Assistant, go to **HACS > Custom Repositories**.\n3. Add `https://github.com/ad-ha/kidschores-ha` as an **Integration**.\n4. Search for **KidsChores**, install, and **restart Home Assistant**.\n\n📖 **[Full Setup & Configuration Guide →](https://github.com/ad-ha/kidschores-ha/wiki/Installation-&-Setup)**\n\n---\n\n## 🌟 Key Features\n\n### 👧👦 Multi-User Management\n\n- **Profile Creation & Customization:**\n\n  - Create and manage individual profiles for multiple kids and parents.\n  - Track each child’s progress, achievements, and performance with ease.\n\n- **Effortless Management:**\n  - Handle multiple kids with a single integration while monitoring individual statistics and trends.\n  - **Built-in Access Control** (Restrict actions based on user roles to prevent unauthorized changes). **[Learn More →](https://github.com/ad-ha/kidschores-ha/wiki/Access-Control:-Overview-&-Best-Practices)**\n \n  ---\n\n### ⭐ **Customizable Points System**\n\n- Personalize the points system by choosing your own name and icon (e.g., Stars, Bucks, Coins) to better resonate with your family.\n\n  ---\n\n### 🧹 **Chore Management**\n\n- **Assign & Track Chores:**\n\n  - Easily define chores with descriptions, icons, due dates, and customizable recurring schedules.  **[Learn More →](https://github.com/ad-ha/kidschores-ha/wiki/Chore-Status-and-Recurrence-Handling)**\n  - Supports **individual chores** (assigned to a single kid) and **shared chores** (requiring participation from multiple kids).\n  - **Labels** can be used to **group chores** by type, location, or difficulty—or to **exclude specific chores** based on your family's needs.\n\n- **Smart Notifications & Workflow Approvals:**\n\n  - Parents and kids receive **dynamic notifications** for **chore claims, approvals, and overdue tasks**.\n  - Notifications are **actionable** on **phones, tablets, and smartwatches**, allowing parents to **approve or reject** tasks with a single tap.\n  - **Customizable reminders** help ensure chores stay on track and are completed on time.\n\n- **Dynamic Chore States & Actions:**\n  - Leverage dynamic buttons to claim, approve, or disapprove chores—completion with built-in authorization and contextual notifications.\n  - Monitor progress with sensors that update on a per-kid and global level.\n \n  ---\n\n### 🎁 **Reward System**\n\n- Rewards help **motivate kids** by offering incentives they want while reinforcing responsibility. Parents can **create a list of rewards**, assign a **point cost**, and let kids claim them when they have enough points.\n\n- **Customizable & Goal-Oriented:**\n\n  - Add rewards tailored to your kid’s interests (e.g., extra screen time, a special outing).\n  - Assign point values to **encourage saving** and **set goals**.\n\n- **Seamless Claim & Workflow Approval Process:**\n  - Kids can **claim rewards** when they meet the point requirement.\n  - Parents receive an **approval notification**; once approved, **points are automatically deducted**, and the parent is responsible for delivering the reward.\n \n  ---\n\n### 🏅 **Badge System**\n\n- Badges reward **milestone achievements** and encourage consistency by tracking progress over time.  **[Learn More →](https://github.com/ad-ha/kidschores-ha/wiki/Badges:-Overview-&-Examples)**\n\n- **Earned Through Chores & Points:**\n\n  - Kids can unlock badges by **completing chores** or **earning points** (e.g., 100 chores or 100 points).\n  - Badge progress is **tracked from the start**, so kids receive credit for past achievements.\n\n- **Multipliers & Tracking:**\n  - Badges can apply a **points multiplier** to boost future earnings (e.g., 1.5x points per chore).\n  - Tracks each kid’s **highest badge earned** and **full badge history**.\n \n  ---\n\n### ⚖️ **Bonuses & Penalties**\n\n- Bonuses and penalties allow parents to **reinforce positive behavior** and **correct missteps** by adjusting points dynamically.  **[Learn More →](https://github.com/ad-ha/kidschores-ha/wiki/Bonuses-&-Penalties:-Overview-&-Examples)**\n\n- **Bonuses: Reward Extra Effort**\n\n  - Award **extra points** for exceptional behavior, teamwork, or going above expectations.\n  - Can be applied manually or automatically through the system, with **custom labels and tracking**.\n\n- **Penalties: Encourage Accountability**\n  - Deduct points for missed chores or rule-breaking to **reinforce responsibility**.\n  - Easily track applied penalties and ensure fair, transparent adjustments.\n \n  ---\n\n### 🏆 **Challenges & Achievements**\n\n- Challenges and achievements **motivate kids with structured goals**, rewarding consistency beyond daily chore completions.  **[Learn More →](https://github.com/ad-ha/kidschores-ha/wiki/Challenges-&-Achievements:-Overview-&-Functionality)**\n\n- **Achievements: Personal Milestones**\n\n  - Earned by **completing a set number of chores** or **maintaining streaks** over time (e.g., 100 total chores, 30-day streak).\n  - Tracks individual progress and provides **long-term motivation**.\n\n- **Challenges: Time-Bound Goals**\n  - Require kids to **complete specific tasks within a set timeframe** (e.g., 50 chores in a month).\n  - Can be **individual or shared**, encouraging teamwork toward a common goal.\n\n  ---\n\n### 📅 **Calendar Integration**\n\n- KidsChores integrates with **Home Assistant’s calendar**, allowing chores and challenges to displayed alongside other household events.\n\n- **Sync Chores to Calendar:**\n\n  - View **due dates** for individual and shared chores directly in the Home Assistant calendar.\n  - Helps parents and kids **plan ahead** and stay organized.\n\n- **Track Challenges & Time-Sensitive Goals:**\n  - Challenges with set timeframes (e.g., \"Complete 50 chores in a month\") appear in the calendar for **easy progress tracking**.\n  - Provides a **visual timeline** of ongoing and upcoming challenges.\n\n  ---\n\n### 📊 **Detailed Statistics & Advanced Controls**\n\n- KidsChores provides **comprehensive tracking** through **real-time sensors and interactive buttons**, giving parents full insight into chore activity and progress.\n\n- **Comprehensive Sensors & Data Tracking:**\n\n  - Monitor **daily, weekly, and monthly stats** on chore completions, points earned, rewards redeemed, badges awarded, and penalties applied.\n  - Analyze **historical trends** to celebrate progress, adjust incentives, and identify areas for improvement.\n\n- **Interactive Controls & Automation:**\n\n  - Use dynamic buttons for **claiming chores, approving rewards, and applying bonuses or penalties** directly from the UI.\n  - Seamlessly integrate with Home Assistant automations for **custom alerts, reports, and dashboard insights**.\n\n- 📖 **[View the Full List of Sensors & Actions →](https://github.com/ad-ha/kidschores-ha/wiki/Sensors-&-Buttons)**\n\n  ---\n\n### 🛠 Customization & User-Friendly Interface\n\n- **🛠 Dynamic Buttons & Actions:**\n\n  - Manage chores and points directly from the Home Assistant UI with buttons for claiming, approving, redeeming, and adjusting points.\n\n- **🌐 Multilingual Support:**\n\n  - Currently available in English and Spanish to cater to a diverse user base.\n\n- **🔧 Easy Setup & Maintenance:**\n\n  - KidsChores offers a **fully interactive Options Flow** with a **user-friendly setup wizard** and **comprehensive configuration menus**, allowing you to manage everything **directly from the Home Assistant UI**—**no YAML or coding required**. With an intuitive frontend interface, you can effortlessly configure:\n    - **Points**\n    - **Kids & Parents**\n    - **Chores**\n    - **Rewards**\n    - **Badges**\n    - **Penalties & Bonuses**\n    - **Achievements & Challenges**\n\n- **Organize with Home Assistant Labels:**\n  - Use **labels** to categorize and manage chores, rewards, penalties, badges, and challenges—making it easier to filter, group, or exclude specific tasks based on your needs.\n \n---\n\n### ⚙️ Make KidsChores Your Own\n\n---\n\n- If that's still not enough for you—**this is Home Assistant!** With a little customization, you can make KidsChores work exactly how you want.  \n\n  📅 **Want to set schedules from your Google Calendar?**  \n  📲 **Want to claim chores using NFC tags?**  \n  ✅ **Want to automatically approve specific chores?**  \n  ⏳ **Want to automatically apply a penalty or a custom alert when a chore goes overdue?**  \n  \n  The **[Tips & Tricks](https://github.com/ad-ha/kidschores-ha/wiki/Tips-&-Tricks)** section of the Wiki is packed with ideas to help you **customize, automate, and extend** KidsChores to fit your family's needs.  \n\n---\n\n## 🔐 **Security & Privacy**\n\n🔹 **100% Local & Private** – Your data stays on your Home Assistant instance, ensuring complete privacy.\n\n🔹 **No External Data Sharing** – No cloud services, no third-party access—everything runs securely on your local network.\n\n🔹 **Built-in User Access Control** – Restrict actions based on roles to prevent unauthorized changes.\n\nWith **KidsChores**, your family’s information remains private, secure, and fully under your control.\n\n---\n\n## 🤝 Join the Community & Contribute\n\n🚀 **Get Help & Share Ideas**\n\n- 💬 **Join Community Discussions** → [Home Assistant Forum](https://community.home-assistant.io/t/kidschores-family-chore-management-integration)\n- 🛠️ **Report Issues & Request Features** → [GitHub Issues](https://github.com/ad-ha/kidschores-ha/issues)\n\n👨‍💻 **Want to contribute?**\n\n- Submit a **pull request**: [GitHub Contributions](https://github.com/ad-ha/kidschores-ha/pulls).\n- Help with **translations** and **documentation updates**.\n\n---\n\nKidsChores makes managing chores effortless, engaging, and rewarding for the whole family. With built-in gamification, smart automation, and flexible tracking, it turns daily routines into a fun and structured experience.\n\nWhether you want to **encourage responsibility**, **motivate with rewards**, or simply **streamline household tasks**, KidsChores has you covered.\n\n**Get started today and transform how your family manages chores, rewards, and accountability!**\n\n---\n\n## LICENSE\n\nThis project is licensed under the [GPL-3.0 license](LICENSE). See the LICENSE file for details.\n\n---\n\n## DISCLAIMER\n\nTHIS PROJECT IS NOT AFFILIATED WITH OR ENDORSED BY ANY OFFICIAL ENTITY. The information provided is for educational purposes only, and the developers assume no legal responsibility for the functionality or security of your devices.\n"
  },
  {
    "path": "custom_components/kidschores/__init__.py",
    "content": "# File: __init__.py\n\"\"\"Initialization file for the KidsChores integration.\n\nHandles setting up the integration, including loading configuration entries,\ninitializing data storage, and preparing the coordinator for data handling.\n\nKey Features:\n- Config entry setup and unload support.\n- Coordinator initialization for data synchronization.\n- Storage management for persistent data handling.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.typing import ConfigType\nfrom homeassistant.exceptions import ConfigEntryNotReady\n\nfrom .const import (\n    DOMAIN,\n    LOGGER,\n    NOTIFICATION_EVENT,\n    STORAGE_KEY,\n    PLATFORMS,\n)\nfrom .coordinator import KidsChoresDataCoordinator\nfrom .notification_action_handler import async_handle_notification_action\nfrom .storage_manager import KidsChoresStorageManager\nfrom .services import async_setup_services, async_unload_services\n\n\nasync def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:\n    \"\"\"Set up the integration from a config entry.\"\"\"\n    LOGGER.info(\"Starting setup for KidsChores entry: %s\", entry.entry_id)\n\n    # Initialize the storage manager to handle persistent data.\n    storage_manager = KidsChoresStorageManager(hass, STORAGE_KEY)\n    # Initialize new file.\n    await storage_manager.async_initialize()\n\n    # Create the data coordinator for managing updates and synchronization.\n    coordinator = KidsChoresDataCoordinator(hass, entry, storage_manager)\n\n    try:\n        # Perform the first refresh to load data.\n        await coordinator.async_config_entry_first_refresh()\n    except ConfigEntryNotReady as e:\n        LOGGER.error(\"Failed to refresh coordinator data: %s\", e)\n        raise ConfigEntryNotReady from e\n\n    # Store the coordinator and data manager in hass.data.\n    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {\n        \"coordinator\": coordinator,\n        \"storage_manager\": storage_manager,\n    }\n\n    # Set up services required by the integration.\n    async_setup_services(hass)\n\n    # Forward the setup to supported platforms (sensors, buttons, etc.).\n    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)\n\n    # Listen for notification actions from the companion app.\n    hass.bus.async_listen(\n        NOTIFICATION_EVENT,\n        lambda event: asyncio.run_coroutine_threadsafe(\n            async_handle_notification_action(hass, event), hass.loop\n        ),\n    )\n\n    LOGGER.info(\"KidsChores setup complete for entry: %s\", entry.entry_id)\n    return True\n\n\nasync def async_unload_entry(hass, entry):\n    \"\"\"Unload a config entry.\n\n    Args:\n        hass: Home Assistant instance.\n        entry: Config entry to unload.\n\n    \"\"\"\n    LOGGER.info(\"Unloading KidsChores entry: %s\", entry.entry_id)\n\n    # Unload platforms\n    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)\n\n    if unload_ok:\n        hass.data[DOMAIN].pop(entry.entry_id)\n\n        # Await service unloading\n        await async_unload_services(hass)\n\n    return unload_ok\n\n\nasync def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:\n    \"\"\"Handle removal of a config entry.\"\"\"\n    LOGGER.info(\"Removing KidsChores entry: %s\", entry.entry_id)\n\n    # Safely check if data exists before attempting to access it\n    if DOMAIN in hass.data and entry.entry_id in hass.data[DOMAIN]:\n        storage_manager: KidsChoresStorageManager = hass.data[DOMAIN][entry.entry_id][\n            \"storage_manager\"\n        ]\n        await storage_manager.async_delete_storage()\n\n    LOGGER.info(\"KidsChores entry data cleared: %s\", entry.entry_id)\n"
  },
  {
    "path": "custom_components/kidschores/button.py",
    "content": "# File: button.py\n\"\"\"Buttons for KidsChores integration.\n\nFeatures:\n1) Chore Buttons (Claim & Approve) with user-defined or default icons.\n2) Reward Buttons using user-defined or default icons.\n3) Penalty Buttons using user-defined or default icons.\n4) Bonus Buttons using user-defined or default icons.\n5) PointsAdjustButton: manually increments/decrements a kid's points (e.g., +1, -1, +2, -2, etc.).\n6) ApproveRewardButton: allows parents to approve rewards claimed by kids.\n\n\"\"\"\n\nfrom homeassistant.auth.models import User\nfrom homeassistant.components.button import ButtonEntity\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\nfrom homeassistant.exceptions import HomeAssistantError\n\nfrom .const import (\n    ATTR_LABELS,\n    BUTTON_BONUS_PREFIX,\n    BUTTON_DISAPPROVE_CHORE_PREFIX,\n    BUTTON_DISAPPROVE_REWARD_PREFIX,\n    BUTTON_PENALTY_PREFIX,\n    BUTTON_REWARD_PREFIX,\n    CONF_POINTS_LABEL,\n    DATA_PENDING_CHORE_APPROVALS,\n    DATA_PENDING_REWARD_APPROVALS,\n    DEFAULT_BONUS_ICON,\n    DEFAULT_CHORE_APPROVE_ICON,\n    DEFAULT_CHORE_CLAIM_ICON,\n    DEFAULT_DISAPPROVE_ICON,\n    DEFAULT_PENALTY_ICON,\n    DEFAULT_POINTS_ADJUST_MINUS_ICON,\n    DEFAULT_POINTS_ADJUST_MINUS_MULTIPLE_ICON,\n    DEFAULT_POINTS_ADJUST_PLUS_ICON,\n    DEFAULT_POINTS_ADJUST_PLUS_MULTIPLE_ICON,\n    DEFAULT_POINTS_LABEL,\n    DEFAULT_REWARD_ICON,\n    DOMAIN,\n    ERROR_NOT_AUTHORIZED_ACTION_FMT,\n    LOGGER,\n)\nfrom .coordinator import KidsChoresDataCoordinator\nfrom .kc_helpers import (\n    is_user_authorized_for_global_action,\n    is_user_authorized_for_kid,\n    get_friendly_label,\n)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n):\n    \"\"\"Set up dynamic buttons.\n\n    - Chores (Claim & Approve & Disapprove)\n    - Rewards (Redeem & Approve & Disapprove)\n    - Penalties\n    - Kid points adjustments (e.g., +1, -1, +10, -10, etc.)\n    - Approve Reward Workflow\n\n    \"\"\"\n    data = hass.data[DOMAIN][entry.entry_id]\n    coordinator: KidsChoresDataCoordinator = data[\"coordinator\"]\n\n    points_label = entry.options.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL)\n\n    entities = []\n\n    # Create buttons for chores (Claim, Approve & Disapprove)\n    for chore_id, chore_info in coordinator.chores_data.items():\n        chore_name = chore_info.get(\"name\", f\"Chore {chore_id}\")\n        assigned_kids_ids = chore_info.get(\"assigned_kids\", [])\n\n        # If user defined an icon, use it; else fallback to default for chore claim\n        chore_claim_icon = chore_info.get(\"icon\", DEFAULT_CHORE_CLAIM_ICON)\n        # For \"approve,\" use a distinct icon\n        chore_approve_icon = chore_info.get(\"icon\", DEFAULT_CHORE_APPROVE_ICON)\n\n        for kid_id in assigned_kids_ids:\n            kid_name = coordinator._get_kid_name_by_id(kid_id) or f\"Kid {kid_id}\"\n            # Claim Button\n            entities.append(\n                ClaimChoreButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    chore_id=chore_id,\n                    chore_name=chore_name,\n                    icon=chore_claim_icon,\n                )\n            )\n            # Approve Button\n            entities.append(\n                ApproveChoreButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    chore_id=chore_id,\n                    chore_name=chore_name,\n                    icon=chore_approve_icon,\n                )\n            )\n            # Disapprove Button\n            entities.append(\n                DisapproveChoreButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    chore_id=chore_id,\n                    chore_name=chore_name,\n                )\n            )\n\n    # Create reward buttons (Redeem, Approve & Disapprove)\n    for kid_id, kid_info in coordinator.kids_data.items():\n        kid_name = kid_info.get(\"name\", f\"Kid {kid_id}\")\n        for reward_id, reward_info in coordinator.rewards_data.items():\n            # If no user-defined icon, fallback to DEFAULT_REWARD_ICON\n            reward_icon = reward_info.get(\"icon\", DEFAULT_REWARD_ICON)\n            # Redeem Reward Button\n            entities.append(\n                RewardButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    reward_id=reward_id,\n                    reward_name=reward_info.get(\"name\", f\"Reward {reward_id}\"),\n                    icon=reward_icon,\n                )\n            )\n            # Approve Reward Button\n            entities.append(\n                ApproveRewardButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    reward_id=reward_id,\n                    reward_name=reward_info.get(\"name\", f\"Reward {reward_id}\"),\n                    icon=reward_info.get(\"icon\", DEFAULT_REWARD_ICON),\n                )\n            )\n            # Disapprove Reward Button\n            entities.append(\n                DisapproveRewardButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    reward_id=reward_id,\n                    reward_name=reward_info.get(\"name\", f\"Reward {reward_id}\"),\n                )\n            )\n\n    # Create penalty buttons\n    for kid_id, kid_info in coordinator.kids_data.items():\n        kid_name = kid_info.get(\"name\", f\"Kid {kid_id}\")\n        for penalty_id, penalty_info in coordinator.penalties_data.items():\n            # If no user-defined icon, fallback to DEFAULT_PENALTY_ICON\n            penalty_icon = penalty_info.get(\"icon\", DEFAULT_PENALTY_ICON)\n            entities.append(\n                PenaltyButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    penalty_id=penalty_id,\n                    penalty_name=penalty_info.get(\"name\", f\"Penalty {penalty_id}\"),\n                    icon=penalty_icon,\n                )\n            )\n\n    # Create bonus buttons\n    for kid_id, kid_info in coordinator.kids_data.items():\n        kid_name = kid_info.get(\"name\", f\"Kid {kid_id}\")\n        for bonus_id, bonus_info in coordinator.bonuses_data.items():\n            # If no user-defined icon, fallback to DEFAULT_BONUS_ICON\n            bonus_icon = bonus_info.get(\"icon\", DEFAULT_BONUS_ICON)\n            entities.append(\n                BonusButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    bonus_id=bonus_id,\n                    bonus_name=bonus_info.get(\"name\", f\"Bonus {bonus_id}\"),\n                    icon=bonus_icon,\n                )\n            )\n\n    # Create \"points adjustment\" buttons for each kid (±1, ±2, ±10, etc.)\n    POINT_DELTAS = [+1, -1, +2, -2, +10, -10]\n    for kid_id, kid_info in coordinator.kids_data.items():\n        kid_name = kid_info.get(\"name\", f\"Kid {kid_id}\")\n        for delta in POINT_DELTAS:\n            entities.append(\n                PointsAdjustButton(\n                    coordinator=coordinator,\n                    entry=entry,\n                    kid_id=kid_id,\n                    kid_name=kid_name,\n                    delta=delta,\n                    points_label=points_label,\n                )\n            )\n\n    async_add_entities(entities)\n\n\n# ------------------ Chore Buttons ------------------\nclass ClaimChoreButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button to claim a chore as done (set chore state=claimed).\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"claim_chore_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        chore_id: str,\n        chore_name: str,\n        icon: str,\n    ):\n        \"\"\"Initialize the claim chore button.\"\"\"\n\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._chore_id = chore_id\n        self._chore_name = chore_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{chore_id}_claim\"\n        self._attr_icon = icon\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"chore_name\": chore_name,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_chore_claim_{chore_name}\"\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_kid(\n                self.hass, user_id, self._kid_id\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"claim chores\")\n                )\n\n            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None\n            user_name = user_obj.name if user_obj else \"Unknown\"\n\n            self.coordinator.claim_chore(\n                kid_id=self._kid_id,\n                chore_id=self._chore_id,\n                user_name=user_name,\n            )\n            LOGGER.info(\n                \"Chore '%s' claimed by kid '%s' (user: %s)\",\n                self._chore_name,\n                self._kid_name,\n                user_name,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to claim chore '%s' for kid '%s': %s\",\n                self._chore_name,\n                self._kid_name,\n                e,\n            )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to claim chore '%s' for kid '%s': %s\",\n                self._chore_name,\n                self._kid_name,\n                e,\n            )\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include extra state attributes for the button.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        stored_labels = chore_info.get(\"chore_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n\n\nclass ApproveChoreButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button to approve a claimed chore for a kid (set chore state=approved or partial).\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"approve_chore_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        chore_id: str,\n        chore_name: str,\n        icon: str,\n    ):\n        \"\"\"Initialize the approve chore button.\"\"\"\n\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._chore_id = chore_id\n        self._chore_name = chore_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{chore_id}_approve\"\n        self._attr_icon = icon\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"chore_name\": chore_name,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_chore_approval_{chore_name}\"\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_global_action(\n                self.hass, user_id, \"approve_chore\"\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"approve chores\")\n                )\n\n            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None\n            parent_name = user_obj.name if user_obj else \"ParentOrAdmin\"\n\n            self.coordinator.approve_chore(\n                parent_name=parent_name,\n                kid_id=self._kid_id,\n                chore_id=self._chore_id,\n            )\n            LOGGER.info(\n                \"Chore '%s' approved for kid '%s'\",\n                self._chore_name,\n                self._kid_name,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to approve chore '%s' for kid '%s': %s\",\n                self._chore_name,\n                self._kid_name,\n                e,\n            )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to approve chore '%s' for kid '%s': %s\",\n                self._chore_name,\n                self._kid_name,\n                e,\n            )\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include extra state attributes for the button.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        stored_labels = chore_info.get(\"chore_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n\n\nclass DisapproveChoreButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button to disapprove a chore.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"disapprove_chore_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        chore_id: str,\n        chore_name: str,\n        icon: str = DEFAULT_DISAPPROVE_ICON,\n    ):\n        \"\"\"Initialize the disapprove chore button.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._chore_id = chore_id\n        self._chore_name = chore_name\n        self._attr_unique_id = (\n            f\"{entry.entry_id}_{BUTTON_DISAPPROVE_CHORE_PREFIX}{kid_id}_{chore_id}\"\n        )\n        self._attr_icon = icon\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"chore_name\": chore_name,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_chore_disapproval_{chore_name}\"\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            # Check if there's a pending approval for this kid and chore.\n            pending_approvals = self.coordinator._data.get(\n                DATA_PENDING_CHORE_APPROVALS, []\n            )\n            if not any(\n                approval[\"kid_id\"] == self._kid_id\n                and approval[\"chore_id\"] == self._chore_id\n                for approval in pending_approvals\n            ):\n                raise HomeAssistantError(\n                    f\"No pending approval found for chore '{self._chore_name}' for kid '{self._kid_name}'.\"\n                )\n\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_global_action(\n                self.hass, user_id, \"disapprove_chore\"\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"disapprove chores\")\n                )\n\n            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None\n            parent_name = user_obj.name if user_obj else \"ParentOrAdmin\"\n\n            self.coordinator.disapprove_chore(\n                parent_name=parent_name,\n                kid_id=self._kid_id,\n                chore_id=self._chore_id,\n            )\n            LOGGER.info(\n                \"Chore '%s' disapproved for kid '%s' by parent '%s'\",\n                self._chore_name,\n                self._kid_name,\n                parent_name,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to disapprove chore '%s' for kid '%s': %s\",\n                self._chore_name,\n                self._kid_name,\n                e,\n            )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to disapprove chore '%s' for kid '%s': %s\",\n                self._chore_name,\n                self._kid_name,\n                e,\n            )\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include extra state attributes for the button.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        stored_labels = chore_info.get(\"chore_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n\n\n# ------------------ Reward Buttons ------------------\nclass RewardButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button to redeem a reward for a kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"claim_reward_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        reward_id: str,\n        reward_name: str,\n        icon: str,\n    ):\n        \"\"\"Initialize the reward button.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._reward_id = reward_id\n        self._reward_name = reward_name\n        self._attr_unique_id = (\n            f\"{entry.entry_id}_{BUTTON_REWARD_PREFIX}{kid_id}_{reward_id}\"\n        )\n        self._attr_icon = icon\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"reward_name\": reward_name,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_reward_claim_{reward_name}\"\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_kid(\n                self.hass, user_id, self._kid_id\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"redeem rewards\")\n                )\n\n            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None\n            parent_name = user_obj.name if user_obj else \"Unknown\"\n\n            self.coordinator.redeem_reward(\n                parent_name=parent_name,\n                kid_id=self._kid_id,\n                reward_id=self._reward_id,\n            )\n            LOGGER.info(\n                \"Reward '%s' redeemed for kid '%s' by parent '%s'\",\n                self._reward_name,\n                self._kid_name,\n                parent_name,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to redeem reward '%s' for kid '%s': %s\",\n                self._reward_name,\n                self._kid_name,\n                e,\n            )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to redeem reward '%s' for kid '%s': %s\",\n                self._reward_name,\n                self._kid_name,\n                e,\n            )\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include extra state attributes for the button.\"\"\"\n        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})\n        stored_labels = reward_info.get(\"reward_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n\n\nclass ApproveRewardButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button for parents to approve a reward claimed by a kid.\n\n    Prevents unauthorized or premature reward approvals.\n    \"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"approve_reward_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        reward_id: str,\n        reward_name: str,\n        icon: str,\n    ):\n        \"\"\"Initialize the approve reward button.\"\"\"\n\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._reward_id = reward_id\n        self._reward_name = reward_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{reward_id}_approve_reward\"\n        self._attr_icon = icon\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"reward_name\": reward_name,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_reward_approval_{reward_name}\"\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_global_action(\n                self.hass, user_id, \"approve_reward\"\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"approve rewards\")\n                )\n\n            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None\n            parent_name = user_obj.name if user_obj else \"ParentOrAdmin\"\n\n            # Approve the reward\n            self.coordinator.approve_reward(\n                parent_name=parent_name,\n                kid_id=self._kid_id,\n                reward_id=self._reward_id,\n            )\n\n            LOGGER.info(\n                \"Reward '%s' approved for kid '%s' by parent '%s'\",\n                self._reward_name,\n                self._kid_name,\n                parent_name,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to approve reward '%s' for kid '%s': %s\",\n                self._reward_name,\n                self._kid_name,\n                e,\n            )\n            # Send a persistent notification for the error\n            if user_id:\n                self.hass.components.persistent_notification.create(\n                    f\"Failed to approve reward '{self._reward_name}' for {self._kid_name}: {e}\",\n                    title=\"Reward Approval Failed\",\n                    notification_id=f\"approve_reward_error_{self._reward_id}\",\n                )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to approve reward '%s' for kid '%s': %s\",\n                self._reward_name,\n                self._kid_name,\n                e,\n            )\n            # Send a persistent notification for the unexpected error\n            if user_id:\n                self.hass.components.persistent_notification.create(\n                    f\"An unexpected error occurred while approving reward '{self._reward_name}' for {self._kid_name}\",\n                    title=\"Reward Approval Error\",\n                    notification_id=f\"approve_reward_unexpected_error_{self._reward_id}\",\n                )\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include extra state attributes for the button.\"\"\"\n        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})\n        stored_labels = reward_info.get(\"reward_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n\n\nclass DisapproveRewardButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button to disapprove a reward.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"disapprove_reward_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        reward_id: str,\n        reward_name: str,\n        icon: str = DEFAULT_DISAPPROVE_ICON,\n    ):\n        \"\"\"Initialize the disapprove reward button.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._reward_id = reward_id\n        self._reward_name = reward_name\n        self._attr_unique_id = (\n            f\"{entry.entry_id}_{BUTTON_DISAPPROVE_REWARD_PREFIX}{kid_id}_{reward_id}\"\n        )\n        self._attr_icon = icon\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"reward_name\": reward_name,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_reward_disapproval_{reward_name}\"\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            # Check if there's a pending approval for this kid and reward.\n            pending_approvals = self.coordinator._data.get(\n                DATA_PENDING_REWARD_APPROVALS, []\n            )\n            if not any(\n                approval[\"kid_id\"] == self._kid_id\n                and approval[\"reward_id\"] == self._reward_id\n                for approval in pending_approvals\n            ):\n                raise HomeAssistantError(\n                    f\"No pending approval found for reward '{self._reward_name}' for kid '{self._kid_name}'.\"\n                )\n\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_global_action(\n                self.hass, user_id, \"disapprove_reward\"\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"disapprove rewards\")\n                )\n\n            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None\n            parent_name = user_obj.name if user_obj else \"ParentOrAdmin\"\n\n            self.coordinator.disapprove_reward(\n                parent_name=parent_name,\n                kid_id=self._kid_id,\n                reward_id=self._reward_id,\n            )\n            LOGGER.info(\n                \"Reward '%s' disapproved for kid '%s' by parent '%s'\",\n                self._reward_name,\n                self._kid_name,\n                parent_name,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to disapprove reward '%s' for kid '%s': %s\",\n                self._reward_name,\n                self._kid_name,\n                e,\n            )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to disapprove reward '%s' for kid '%s': %s\",\n                self._reward_name,\n                self._kid_name,\n                e,\n            )\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include extra state attributes for the button.\"\"\"\n        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})\n        stored_labels = reward_info.get(\"reward_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n\n\n# ------------------ Penalty Button ------------------\nclass PenaltyButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button to apply a penalty for a kid.\n\n    Uses user-defined or default penalty icon.\n    \"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"penalty_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        penalty_id: str,\n        penalty_name: str,\n        icon: str,\n    ):\n        \"\"\"Initialize the penalty button.\"\"\"\n\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._penalty_id = penalty_id\n        self._penalty_name = penalty_name\n        self._attr_unique_id = (\n            f\"{entry.entry_id}_{BUTTON_PENALTY_PREFIX}{kid_id}_{penalty_id}\"\n        )\n        self._attr_icon = icon\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"penalty_name\": penalty_name,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_penalty_{penalty_name}\"\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_global_action(\n                self.hass, user_id, \"apply_penalty\"\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"apply penalties\")\n                )\n\n            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None\n            parent_name = user_obj.name if user_obj else \"Unknown\"\n\n            self.coordinator.apply_penalty(\n                parent_name=parent_name,\n                kid_id=self._kid_id,\n                penalty_id=self._penalty_id,\n            )\n            LOGGER.info(\n                \"Penalty '%s' applied to kid '%s' by '%s'\",\n                self._penalty_name,\n                self._kid_name,\n                parent_name,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to apply penalty '%s' for kid '%s': %s\",\n                self._penalty_name,\n                self._kid_name,\n                e,\n            )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to apply penalty '%s' for kid '%s': %s\",\n                self._penalty_name,\n                self._kid_name,\n                e,\n            )\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include extra state attributes for the button.\"\"\"\n        penalty_info = self.coordinator.penalties_data.get(self._penalty_id, {})\n        stored_labels = penalty_info.get(\"penalty_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n\n\n# ------------------ Points Adjust Button ------------------\nclass PointsAdjustButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button that increments or decrements a kid's points by 'delta'.\n\n    For example: +1, -1, +10, -10, etc.\n    Uses icons from const.py for plus/minus, or fallback if desired.\n    \"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"manual_adjustment_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        delta: int,\n        points_label: str,\n    ):\n        \"\"\"Initialize the points adjust buttons.\"\"\"\n\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._delta = delta\n        self._points_label = str(points_label)\n\n        sign_label = f\"+{delta}\" if delta >= 0 else f\"-{delta}\"\n        sign_text = f\"plus_{delta}\" if delta >= 0 else f\"minus_{delta}\"\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_adjust_points_{delta}\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"sign_label\": sign_label,\n            \"points_label\": points_label,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_{sign_text}_points\"\n\n        # Decide the icon based on whether delta is positive or negative\n        if delta >= 2:\n            self._attr_icon = DEFAULT_POINTS_ADJUST_PLUS_MULTIPLE_ICON\n        elif delta > 0:\n            self._attr_icon = DEFAULT_POINTS_ADJUST_PLUS_ICON\n        elif delta <= -2:\n            self._attr_icon = DEFAULT_POINTS_ADJUST_MINUS_MULTIPLE_ICON\n        elif delta < 0:\n            self._attr_icon = DEFAULT_POINTS_ADJUST_MINUS_ICON\n        else:\n            self._attr_icon = DEFAULT_POINTS_ADJUST_PLUS_ICON\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_global_action(\n                self.hass, user_id, \"adjust_points\"\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"adjust points\")\n                )\n\n            current_points = self.coordinator.kids_data[self._kid_id][\"points\"]\n            new_points = current_points + self._delta\n            self.coordinator.update_kid_points(\n                kid_id=self._kid_id,\n                new_points=new_points,\n            )\n            LOGGER.info(\n                \"Adjusted points for kid '%s' by %d => total %d\",\n                self._kid_name,\n                self._delta,\n                new_points,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to adjust points for kid '%s' by %d: %s\",\n                self._kid_name,\n                self._delta,\n                e,\n            )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to adjust points for kid '%s' by %d: %s\",\n                self._kid_name,\n                self._delta,\n                e,\n            )\n\n\nclass BonusButton(CoordinatorEntity, ButtonEntity):\n    \"\"\"Button to apply a bonus for a kid.\n\n    Uses user-defined or default bonus icon.\n    \"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"bonus_button\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        bonus_id: str,\n        bonus_name: str,\n        icon: str,\n    ):\n        \"\"\"Initialize the bonus button.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._bonus_id = bonus_id\n        self._bonus_name = bonus_name\n        self._attr_unique_id = (\n            f\"{entry.entry_id}_{BUTTON_BONUS_PREFIX}{kid_id}_{bonus_id}\"\n        )\n        self._attr_icon = icon\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"bonus_name\": bonus_name,\n        }\n        self.entity_id = f\"button.kc_{kid_name}_bonus_{bonus_name}\"\n\n    async def async_press(self):\n        \"\"\"Handle the button press event.\"\"\"\n        try:\n            user_id = self._context.user_id if self._context else None\n            if user_id and not await is_user_authorized_for_global_action(\n                self.hass, user_id, \"apply_bonus\"\n            ):\n                raise HomeAssistantError(\n                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format(\"apply bonus\")\n                )\n\n            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None\n            parent_name = user_obj.name if user_obj else \"Unknown\"\n\n            self.coordinator.apply_bonus(\n                parent_name=parent_name,\n                kid_id=self._kid_id,\n                bonus_id=self._bonus_id,\n            )\n            LOGGER.info(\n                \"Bonus '%s' applied to kid '%s' by '%s'\",\n                self._bonus_name,\n                self._kid_name,\n                parent_name,\n            )\n            await self.coordinator.async_request_refresh()\n\n        except HomeAssistantError as e:\n            LOGGER.error(\n                \"Authorization failed to apply bonus '%s' for kid '%s': %s\",\n                self._bonus_name,\n                self._kid_name,\n                e,\n            )\n        except Exception as e:\n            LOGGER.error(\n                \"Failed to apply bonus '%s' for kid '%s': %s\",\n                self._bonus_name,\n                self._kid_name,\n                e,\n            )\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include extra state attributes for the button.\"\"\"\n        bonus_info = self.coordinator.bonuses_data.get(self._bonus_id, {})\n        stored_labels = bonus_info.get(\"bonus_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n"
  },
  {
    "path": "custom_components/kidschores/calendar.py",
    "content": "# File: calendar.py\n\nimport datetime\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.components.calendar import CalendarEntity, CalendarEvent\nfrom homeassistant.util import dt as dt_util\n\nfrom .const import (\n    DOMAIN,\n    FREQUENCY_BIWEEKLY,\n    FREQUENCY_CUSTOM,\n    FREQUENCY_DAILY,\n    FREQUENCY_MONTHLY,\n    FREQUENCY_NONE,\n    FREQUENCY_WEEKLY,\n    LOGGER,\n    WEEKDAY_OPTIONS,\n    ATTR_KID_NAME,\n)\n\n# Map weekday integers (0=Monday, …) to e.g. \"mon\",\"tue\",\"wed\" in WEEKDAY_OPTIONS.\nWEEKDAY_MAP = {i: key for i, key in enumerate(WEEKDAY_OPTIONS.keys())}\n\n# For chores without a due_date, we generate up to 3 months\nFOREVER_DURATION = datetime.timedelta(days=90)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant, entry: ConfigEntry, async_add_entities\n):\n    \"\"\"Set up the KidsChores calendar platform.\"\"\"\n    try:\n        coordinator = hass.data[DOMAIN][entry.entry_id][\"coordinator\"]\n    except KeyError:\n        LOGGER.error(\"Coordinator not found in hass.data for entry %s\", entry.entry_id)\n        return\n\n    entities = []\n    for kid_id, kid_info in coordinator.kids_data.items():\n        kid_name = kid_info.get(\"name\", f\"Kid {kid_id}\")\n        entities.append(KidsChoresCalendarEntity(coordinator, kid_id, kid_name, entry))\n    async_add_entities(entities)\n\n\nclass KidsChoresCalendarEntity(CalendarEntity):\n    \"\"\"Calendar entity representing a kid's combined chores + challenges.\"\"\"\n\n    def __init__(self, coordinator, kid_id: str, kid_name: str, config_entry):\n        super().__init__()\n        self.coordinator = coordinator\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._config_entry = config_entry\n        self._attr_name = f\"KidsChores Calendar: {kid_name}\"\n        self._attr_unique_id = f\"{config_entry.entry_id}_{kid_id}_calendar\"\n        self.entity_id = f\"calendar.kc_{kid_name}\"\n\n    async def async_get_events(\n        self, hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime\n    ) -> list[CalendarEvent]:\n        \"\"\"\n        Return CalendarEvent objects for:\n         - chores assigned to this kid\n         - challenges assigned to this kid\n        overlapping [start, end].\n        \"\"\"\n        local_tz = dt_util.get_time_zone(self.hass.config.time_zone)\n        if start.tzinfo is None:\n            start = start.replace(tzinfo=local_tz)\n        if end.tzinfo is None:\n            end = end.replace(tzinfo=local_tz)\n\n        events: list[CalendarEvent] = []\n\n        # 1) Generate chore events\n        for chore in self.coordinator.chores_data.values():\n            if self._kid_id in chore.get(\"assigned_kids\", []):\n                events.extend(self._generate_events_for_chore(chore, start, end))\n\n        # 2) Generate challenge events\n        for challenge in self.coordinator.challenges_data.values():\n            if self._kid_id in challenge.get(\"assigned_kids\", []):\n                evs = self._generate_events_for_challenge(challenge, start, end)\n                events.extend(evs)\n\n        return events\n\n    def _generate_events_for_chore(\n        self,\n        chore: dict,\n        window_start: datetime.datetime,\n        window_end: datetime.datetime,\n    ) -> list[CalendarEvent]:\n        \"\"\"Same recurring-chores logic from earlier solutions.\"\"\"\n        events: list[CalendarEvent] = []\n\n        summary = chore.get(\"name\", \"Unnamed Chore\")\n        description = chore.get(\"description\", \"\")\n        recurring = chore.get(\"recurring_frequency\", FREQUENCY_NONE)\n        applicable_days = chore.get(\"applicable_days\", [])\n\n        # Parse chore due_date if any\n        due_date_str = chore.get(\"due_date\")\n        due_dt: datetime.datetime | None = None\n        if due_date_str:\n            dt_parsed = dt_util.parse_datetime(due_date_str)\n            if dt_parsed:\n                due_dt = dt_util.as_local(dt_parsed)\n\n        def is_midnight(dt_obj: datetime.datetime) -> bool:\n            return (dt_obj.hour, dt_obj.minute, dt_obj.second) == (0, 0, 0)\n\n        def overlaps(ev: CalendarEvent) -> bool:\n            \"\"\"Check if event overlaps [window_start, window_end].\"\"\"\n            sdt = ev.start\n            edt = ev.end\n            if isinstance(sdt, datetime.date) and not isinstance(\n                sdt, datetime.datetime\n            ):\n                tz = dt_util.get_time_zone(self.hass.config.time_zone)\n                sdt = datetime.datetime.combine(sdt, datetime.time.min, tzinfo=tz)\n            if isinstance(edt, datetime.date) and not isinstance(\n                edt, datetime.datetime\n            ):\n                tz = dt_util.get_time_zone(self.hass.config.time_zone)\n                edt = datetime.datetime.combine(edt, datetime.time.min, tzinfo=tz)\n            if not sdt or not edt:\n                return False\n            return (edt > window_start) and (sdt < window_end)\n\n        # --- Non-recurring chores ---\n        if recurring == FREQUENCY_NONE:\n            if due_dt:\n                # single event if in window\n                if window_start <= due_dt <= window_end:\n                    if is_midnight(due_dt):\n                        e = CalendarEvent(\n                            summary=summary,\n                            start=due_dt.date(),\n                            end=due_dt.date() + datetime.timedelta(days=1),\n                            description=description,\n                        )\n                    else:\n                        e = CalendarEvent(\n                            summary=summary,\n                            start=due_dt,\n                            end=due_dt + datetime.timedelta(hours=1),\n                            description=description,\n                        )\n                    if overlaps(e):\n                        events.append(e)\n            else:\n                # No due_date => possibly show on applicable_days for next 3 months\n                if applicable_days:\n                    gen_start = window_start\n                    gen_end = min(\n                        window_end,\n                        dt_util.as_local(datetime.datetime.now() + FOREVER_DURATION),\n                    )\n                    current = gen_start\n                    while current <= gen_end:\n                        if WEEKDAY_MAP[current.weekday()] in applicable_days:\n                            e = CalendarEvent(\n                                summary=summary,\n                                start=current.date(),\n                                end=current.date() + datetime.timedelta(days=1),\n                                description=description,\n                            )\n                            if overlaps(e):\n                                events.append(e)\n                        current += datetime.timedelta(days=1)\n\n            return events\n\n        # --- Recurring chores with a due_date ---\n        if due_dt:\n            cutoff = min(due_dt, window_end)\n            if cutoff < window_start:\n                return events\n\n            if recurring == FREQUENCY_DAILY:\n                if window_start <= due_dt <= window_end:\n                    if is_midnight(due_dt):\n                        e = CalendarEvent(\n                            summary=summary,\n                            start=due_dt.date(),\n                            end=due_dt.date() + datetime.timedelta(days=1),\n                            description=description,\n                        )\n                    else:\n                        e = CalendarEvent(\n                            summary=summary,\n                            start=due_dt,\n                            end=due_dt + datetime.timedelta(hours=1),\n                            description=description,\n                        )\n                    if overlaps(e):\n                        events.append(e)\n\n            elif recurring == FREQUENCY_WEEKLY:\n                start_event = due_dt - datetime.timedelta(weeks=1)\n                end_event = due_dt\n                if start_event < window_end and end_event > window_start:\n                    e = CalendarEvent(\n                        summary=summary,\n                        start=start_event.date(),\n                        end=(end_event.date() + datetime.timedelta(days=1)),\n                        description=description,\n                    )\n                    if overlaps(e):\n                        events.append(e)\n\n            elif recurring == FREQUENCY_BIWEEKLY:\n                start_event = due_dt - datetime.timedelta(weeks=2)\n                end_event = due_dt\n                if start_event < window_end and end_event > window_start:\n                    e = CalendarEvent(\n                        summary=summary,\n                        start=start_event.date(),\n                        end=(end_event.date() + datetime.timedelta(days=1)),\n                        description=description,\n                    )\n                    if overlaps(e):\n                        events.append(e)\n\n            elif recurring == FREQUENCY_MONTHLY:\n                first_day = due_dt.replace(day=1)\n                if first_day < window_end and due_dt > window_start:\n                    e = CalendarEvent(\n                        summary=summary,\n                        start=first_day.date(),\n                        end=(due_dt.date() + datetime.timedelta(days=1)),\n                        description=description,\n                    )\n                    if overlaps(e):\n                        events.append(e)\n\n            elif recurring == FREQUENCY_CUSTOM:\n                interval = chore.get(\"custom_interval\", 1)\n                unit = chore.get(\"custom_interval_unit\", \"days\")\n                if unit == \"days\":\n                    start_event = due_dt - datetime.timedelta(days=interval)\n                elif unit == \"weeks\":\n                    start_event = due_dt - datetime.timedelta(weeks=interval)\n                elif unit == \"months\":\n                    start_event = due_dt - datetime.timedelta(days=30 * interval)\n                else:\n                    start_event = due_dt\n\n                if start_event < window_end and due_dt > window_start:\n                    e = CalendarEvent(\n                        summary=summary,\n                        start=start_event.date(),\n                        end=(due_dt.date() + datetime.timedelta(days=1)),\n                        description=description,\n                    )\n                    if overlaps(e):\n                        events.append(e)\n\n            return events\n\n        # --- Recurring chores without a due_date => next 3 months\n        gen_start = window_start\n        future_limit = dt_util.as_local(datetime.datetime.now() + FOREVER_DURATION)\n        cutoff = min(window_end, future_limit)\n\n        if recurring == FREQUENCY_DAILY:\n            current = gen_start\n            while current <= cutoff:\n                if (\n                    applicable_days\n                    and WEEKDAY_MAP[current.weekday()] not in applicable_days\n                ):\n                    current += datetime.timedelta(days=1)\n                    continue\n                e = CalendarEvent(\n                    summary=summary,\n                    start=current.date(),\n                    end=current.date() + datetime.timedelta(days=1),\n                    description=description,\n                )\n                if overlaps(e):\n                    events.append(e)\n                current += datetime.timedelta(days=1)\n            return events\n\n        if recurring in (FREQUENCY_WEEKLY, FREQUENCY_BIWEEKLY):\n            week_delta = 7 if recurring == FREQUENCY_WEEKLY else 14\n            current = gen_start\n            # align to Monday\n            while current.weekday() != 0:\n                current += datetime.timedelta(days=1)\n            while current <= cutoff:\n                # multi-day block from Monday..Sunday (or 2 weeks for biweekly)\n                block_days = 6 if recurring == FREQUENCY_WEEKLY else 13\n                start_block = current\n                end_block = current + datetime.timedelta(days=block_days)\n                e = CalendarEvent(\n                    summary=summary,\n                    start=start_block.date(),\n                    end=end_block.date() + datetime.timedelta(days=1),\n                    description=description,\n                )\n                if overlaps(e):\n                    events.append(e)\n                current += datetime.timedelta(days=week_delta)\n            return events\n\n        if recurring == FREQUENCY_MONTHLY:\n            cur = gen_start\n            while cur <= cutoff:\n                first_day = cur.replace(day=1)\n                next_month = first_day + datetime.timedelta(days=32)\n                next_month = next_month.replace(day=1)\n                last_day = next_month - datetime.timedelta(days=1)\n\n                e = CalendarEvent(\n                    summary=summary,\n                    start=first_day.date(),\n                    end=last_day.date() + datetime.timedelta(days=1),\n                    description=description,\n                )\n                if overlaps(e):\n                    events.append(e)\n                cur = next_month\n            return events\n\n        if recurring == FREQUENCY_CUSTOM:\n            interval = chore.get(\"custom_interval\", 1)\n            unit = chore.get(\"custom_interval_unit\", \"days\")\n            if unit == \"days\":\n                step = datetime.timedelta(days=interval)\n            elif unit == \"weeks\":\n                step = datetime.timedelta(weeks=interval)\n            elif unit == \"months\":\n                step = datetime.timedelta(days=30 * interval)\n            else:\n                step = datetime.timedelta(days=interval)\n\n            current = gen_start\n            while current <= cutoff:\n                # Check applicable days\n                if (\n                    applicable_days\n                    and WEEKDAY_MAP[current.weekday()] not in applicable_days\n                ):\n                    current += step\n                    continue\n                e = CalendarEvent(\n                    summary=summary,\n                    start=current.date(),\n                    end=current.date() + step,\n                    description=description,\n                )\n                if overlaps(e):\n                    events.append(e)\n                current += step\n            return events\n\n        return events\n\n    def _generate_events_for_challenge(\n        self,\n        challenge: dict,\n        window_start: datetime.datetime,\n        window_end: datetime.datetime,\n    ) -> list[CalendarEvent]:\n        \"\"\"\n        Produce a single multi-day event for each challenge that has valid start_date/end_date.\n        Only if it overlaps the requested [window_start, window_end].\n        \"\"\"\n        events: list[CalendarEvent] = []\n\n        challenge_name = challenge.get(\"name\", \"Unnamed Challenge\")\n        description = challenge.get(\"description\", \"\")\n        start_str = challenge.get(\"start_date\")\n        end_str = challenge.get(\"end_date\")\n        if not start_str or not end_str:\n            return events  # no valid date range => skip\n\n        start_dt = dt_util.parse_datetime(start_str)\n        end_dt = dt_util.parse_datetime(end_str)\n        if not start_dt or not end_dt:\n            return events  # parsing failed => skip\n\n        # Convert to local\n        local_start = dt_util.as_local(start_dt)\n        local_end = dt_util.as_local(end_dt)\n\n        # If the challenge times are midnight-based, we can treat them as all-day.\n        # But let's keep it simpler => always treat as an all-day block from date(start) to date(end)+1\n        # so the user sees a big multi-day block.\n        if local_start > window_end or local_end < window_start:\n            return events  # out of range\n\n        # Build an all-day event from local_start.date() to local_end.date() + 1 day\n        ev = CalendarEvent(\n            summary=f\"Challenge: {challenge_name}\",\n            start=local_start.date(),\n            end=local_end.date() + datetime.timedelta(days=1),\n            description=description,\n        )\n\n        # Overlap check (similar logic):\n        def overlaps(e: CalendarEvent) -> bool:\n            sdt = e.start\n            edt = e.end\n            # convert if needed\n            tz = dt_util.get_time_zone(self.hass.config.time_zone)\n            if isinstance(sdt, datetime.date) and not isinstance(\n                sdt, datetime.datetime\n            ):\n                sdt = datetime.datetime.combine(sdt, datetime.time.min, tzinfo=tz)\n            if isinstance(edt, datetime.date) and not isinstance(\n                edt, datetime.datetime\n            ):\n                edt = datetime.datetime.combine(edt, datetime.time.min, tzinfo=tz)\n            return bool(sdt and edt and (edt > window_start) and (sdt < window_end))\n\n        if overlaps(ev):\n            events.append(ev)\n\n        return events\n\n    @property\n    def event(self) -> CalendarEvent | None:\n        \"\"\"\n        Return a single \"current\" event (chore or challenge) if one is active now (±1h).\n        Otherwise None.\n        \"\"\"\n        now = dt_util.as_local(datetime.datetime.utcnow())\n        window_start = now - datetime.timedelta(hours=1)\n        window_end = now + datetime.timedelta(hours=1)\n        all_events = self._generate_all_events(window_start, window_end)\n        for e in all_events:\n            # Convert date->datetime for comparison\n            tz = dt_util.get_time_zone(self.hass.config.time_zone)\n            sdt = e.start\n            edt = e.end\n            if isinstance(sdt, datetime.date) and not isinstance(\n                sdt, datetime.datetime\n            ):\n                sdt = datetime.datetime.combine(sdt, datetime.time.min, tzinfo=tz)\n            if isinstance(edt, datetime.date) and not isinstance(\n                edt, datetime.datetime\n            ):\n                edt = datetime.datetime.combine(edt, datetime.time.min, tzinfo=tz)\n            if sdt and edt and sdt <= now < edt:\n                return e\n        return None\n\n    def _generate_all_events(\n        self, window_start: datetime.datetime, window_end: datetime.datetime\n    ) -> list[CalendarEvent]:\n        \"\"\"Generate chores + challenges for this kid in the given window.\"\"\"\n        events = []\n        # chores\n        for chore in self.coordinator.chores_data.values():\n            if self._kid_id in chore.get(\"assigned_kids\", []):\n                events.extend(\n                    self._generate_events_for_chore(chore, window_start, window_end)\n                )\n        # challenges\n        for challenge in self.coordinator.challenges_data.values():\n            if self._kid_id in challenge.get(\"assigned_kids\", []):\n                events.extend(\n                    self._generate_events_for_challenge(\n                        challenge, window_start, window_end\n                    )\n                )\n        return events\n\n    @property\n    def extra_state_attributes(self):\n        return {ATTR_KID_NAME: self._kid_name}\n"
  },
  {
    "path": "custom_components/kidschores/config_flow.py",
    "content": "# File: config_flow.py\n\"\"\"Multi-step config flow for the KidsChores integration, storing entities by internal_id.\n\nEnsures that all add/edit/delete operations reference entities via internal_id for consistency.\n\"\"\"\n\nimport datetime\nimport uuid\nimport voluptuous as vol\n\nfrom homeassistant import config_entries\nfrom homeassistant.core import callback\nfrom homeassistant.helpers import config_validation as cv\nfrom homeassistant.util import dt as dt_util\nfrom typing import Any, Optional\n\nfrom .const import (\n    ACHIEVEMENT_TYPE_STREAK,\n    CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW,\n    CONF_APPLICABLE_DAYS,\n    CONF_ACHIEVEMENTS,\n    CONF_BADGES,\n    CONF_CHALLENGES,\n    CONF_CHORES,\n    CONF_KIDS,\n    CONF_NOTIFY_ON_APPROVAL,\n    CONF_NOTIFY_ON_CLAIM,\n    CONF_NOTIFY_ON_DISAPPROVAL,\n    CONF_PARENTS,\n    CONF_PENALTIES,\n    CONF_POINTS_ICON,\n    CONF_POINTS_LABEL,\n    CONF_REWARDS,\n    CONF_BONUSES,\n    DEFAULT_APPLICABLE_DAYS,\n    DEFAULT_NOTIFY_ON_APPROVAL,\n    DEFAULT_NOTIFY_ON_CLAIM,\n    DEFAULT_NOTIFY_ON_DISAPPROVAL,\n    DEFAULT_POINTS_ICON,\n    DEFAULT_POINTS_LABEL,\n    FREQUENCY_CUSTOM,\n    DOMAIN,\n    LOGGER,\n)\nfrom .flow_helpers import (\n    build_points_schema,\n    build_kid_schema,\n    build_parent_schema,\n    build_chore_schema,\n    build_badge_schema,\n    build_reward_schema,\n    build_penalty_schema,\n    build_achievement_schema,\n    build_challenge_schema,\n    ensure_utc_datetime,\n    build_bonus_schema,\n)\n\n\nclass KidsChoresConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):\n    \"\"\"Config Flow for KidsChores with internal_id-based entity management.\"\"\"\n\n    VERSION = 1\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the config flow.\"\"\"\n        self._data: dict[str, Any] = {}\n        self._kids_temp: dict[str, dict[str, Any]] = {}\n        self._parents_temp: dict[str, dict[str, Any]] = {}\n        self._chores_temp: dict[str, dict[str, Any]] = {}\n        self._badges_temp: dict[str, dict[str, Any]] = {}\n        self._rewards_temp: dict[str, dict[str, Any]] = {}\n        self._achievements_temp: dict[str, dict[str, Any]] = {}\n        self._challenges_temp: dict[str, dict[str, Any]] = {}\n        self._penalties_temp: dict[str, dict[str, Any]] = {}\n        self._bonuses_temp: dict[str, dict[str, Any]] = {}\n\n        self._kid_count: int = 0\n        self._parents_count: int = 0\n        self._chore_count: int = 0\n        self._badge_count: int = 0\n        self._reward_count: int = 0\n        self._achievement_count: int = 0\n        self._challenge_count: int = 0\n        self._penalty_count: int = 0\n        self._bonus_count: int = 0\n\n        self._kid_index: int = 0\n        self._parents_index: int = 0\n        self._chore_index: int = 0\n        self._badge_index: int = 0\n        self._reward_index: int = 0\n        self._achievement_index: int = 0\n        self._challenge_index: int = 0\n        self._penalty_index: int = 0\n        self._bonus_index: int = 0\n\n    async def async_step_user(self, user_input: Optional[dict[str, Any]] = None):\n        \"\"\"Start the config flow with an intro step.\"\"\"\n\n        # Check if there's an existing KidsChores entry\n        if any(self._async_current_entries()):\n            return self.async_abort(reason=\"single_instance_allowed\")\n\n        # Continue your normal flow\n        return await self.async_step_intro()\n\n    async def async_step_intro(self, user_input=None):\n        \"\"\"Intro / welcome step. Press Next to continue.\"\"\"\n        if user_input is not None:\n            return await self.async_step_points_label()\n\n        return self.async_show_form(step_id=\"intro\", data_schema=vol.Schema({}))\n\n    async def async_step_points_label(self, user_input=None):\n        \"\"\"Let the user define a custom label for points.\"\"\"\n        errors = {}\n\n        if user_input is not None:\n            points_label = user_input.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL)\n            points_icon = user_input.get(CONF_POINTS_ICON, DEFAULT_POINTS_ICON)\n\n            self._data[CONF_POINTS_LABEL] = points_label\n            self._data[CONF_POINTS_ICON] = points_icon\n\n            return await self.async_step_kid_count()\n\n        points_schema = build_points_schema(\n            default_label=DEFAULT_POINTS_LABEL, default_icon=DEFAULT_POINTS_ICON\n        )\n\n        return self.async_show_form(\n            step_id=\"points_label\", data_schema=points_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # KIDS\n    # --------------------------------------------------------------------------\n    async def async_step_kid_count(self, user_input=None):\n        \"\"\"Ask how many kids to define initially.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._kid_count = int(user_input[\"kid_count\"])\n                if self._kid_count < 0:\n                    raise ValueError\n                if self._kid_count == 0:\n                    return await self.async_step_chore_count()\n                self._kid_index = 0\n                return await self.async_step_kids()\n            except ValueError:\n                errors[\"base\"] = \"invalid_kid_count\"\n\n        schema = vol.Schema({vol.Required(\"kid_count\", default=1): vol.Coerce(int)})\n        return self.async_show_form(\n            step_id=\"kid_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_kids(self, user_input=None):\n        \"\"\"Collect each kid's info using internal_id as the primary key.\n\n        Store in self._kids_temp as a dict keyed by internal_id.\n        \"\"\"\n        errors = {}\n        if user_input is not None:\n            kid_name = user_input[\"kid_name\"].strip()\n            ha_user_id = user_input.get(\"ha_user\") or \"\"\n            enable_mobile_notifications = user_input.get(\n                \"enable_mobile_notifications\", True\n            )\n            notify_service = user_input.get(\"mobile_notify_service\") or \"\"\n            enable_persist = user_input.get(\"enable_persistent_notifications\", True)\n\n            if not kid_name:\n                errors[\"kid_name\"] = \"invalid_kid_name\"\n            elif any(\n                kid_data[\"name\"] == kid_name for kid_data in self._kids_temp.values()\n            ):\n                errors[\"kid_name\"] = \"duplicate_kid\"\n            else:\n                internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n                self._kids_temp[internal_id] = {\n                    \"name\": kid_name,\n                    \"ha_user_id\": ha_user_id,\n                    \"enable_notifications\": enable_mobile_notifications,\n                    \"mobile_notify_service\": notify_service,\n                    \"use_persistent_notifications\": enable_persist,\n                    \"internal_id\": internal_id,\n                }\n                LOGGER.debug(\"Added kid: %s with ID: %s\", kid_name, internal_id)\n\n            self._kid_index += 1\n            if self._kid_index >= self._kid_count:\n                return await self.async_step_parent_count()\n            return await self.async_step_kids()\n\n        # Retrieve HA users for linking\n        users = await self.hass.auth.async_get_users()\n        kid_schema = build_kid_schema(\n            self.hass,\n            users=users,\n            default_kid_name=\"\",\n            default_ha_user_id=None,\n            default_enable_mobile_notifications=False,\n            default_mobile_notify_service=None,\n            default_enable_persistent_notifications=False,\n        )\n        return self.async_show_form(\n            step_id=\"kids\", data_schema=kid_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # PARENTS\n    # --------------------------------------------------------------------------\n    async def async_step_parent_count(self, user_input=None):\n        \"\"\"Ask how many parents to define initially.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._parents_count = int(user_input[\"parent_count\"])\n                if self._parents_count < 0:\n                    raise ValueError\n                if self._parents_count == 0:\n                    return await self.async_step_chore_count()\n                self._parents_index = 0\n                return await self.async_step_parents()\n            except ValueError:\n                errors[\"base\"] = \"invalid_parent_count\"\n\n        schema = vol.Schema({vol.Required(\"parent_count\", default=1): vol.Coerce(int)})\n        return self.async_show_form(\n            step_id=\"parent_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_parents(self, user_input=None):\n        \"\"\"Collect each parent's info using internal_id as the primary key.\n\n        Store in self._parents_temp as a dict keyed by internal_id.\n        \"\"\"\n        errors = {}\n        if user_input is not None:\n            parent_name = user_input[\"parent_name\"].strip()\n            ha_user_id = user_input.get(\"ha_user_id\") or \"\"\n            associated_kids = user_input.get(\"associated_kids\", [])\n            enable_mobile_notifications = user_input.get(\n                \"enable_mobile_notifications\", True\n            )\n            notify_service = user_input.get(\"mobile_notify_service\") or \"\"\n            enable_persist = user_input.get(\"enable_persistent_notifications\", True)\n\n            if not parent_name:\n                errors[\"parent_name\"] = \"invalid_parent_name\"\n            elif any(\n                parent_data[\"name\"] == parent_name\n                for parent_data in self._parents_temp.values()\n            ):\n                errors[\"parent_name\"] = \"duplicate_parent\"\n            else:\n                internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n                self._parents_temp[internal_id] = {\n                    \"name\": parent_name,\n                    \"ha_user_id\": ha_user_id,\n                    \"associated_kids\": associated_kids,\n                    \"enable_notifications\": enable_mobile_notifications,\n                    \"mobile_notify_service\": notify_service,\n                    \"use_persistent_notifications\": enable_persist,\n                    \"internal_id\": internal_id,\n                }\n                LOGGER.debug(\"Added parent: %s with ID: %s\", parent_name, internal_id)\n\n            self._parents_index += 1\n            if self._parents_index >= self._parents_count:\n                return await self.async_step_chore_count()\n            return await self.async_step_parents()\n\n        # Retrieve kids for association from _kids_temp\n        kids_dict = {\n            kid_data[\"name\"]: kid_id for kid_id, kid_data in self._kids_temp.items()\n        }\n\n        users = await self.hass.auth.async_get_users()\n\n        parent_schema = build_parent_schema(\n            self.hass,\n            users=users,\n            kids_dict=kids_dict,\n            default_parent_name=\"\",\n            default_ha_user_id=None,\n            default_associated_kids=[],\n            default_enable_mobile_notifications=False,\n            default_mobile_notify_service=None,\n            default_enable_persistent_notifications=False,\n            internal_id=None,\n        )\n        return self.async_show_form(\n            step_id=\"parents\", data_schema=parent_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # CHORES\n    # --------------------------------------------------------------------------\n    async def async_step_chore_count(self, user_input=None):\n        \"\"\"Ask how many chores to define.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._chore_count = int(user_input[\"chore_count\"])\n                if self._chore_count < 0:\n                    raise ValueError\n                if self._chore_count == 0:\n                    return await self.async_step_badge_count()\n                self._chore_index = 0\n                return await self.async_step_chores()\n            except ValueError:\n                errors[\"base\"] = \"invalid_chore_count\"\n\n        schema = vol.Schema({vol.Required(\"chore_count\", default=1): vol.Coerce(int)})\n        return self.async_show_form(\n            step_id=\"chore_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_chores(self, user_input=None):\n        \"\"\"Collect chore details using internal_id as the primary key.\n\n        Store in self._chores_temp as a dict keyed by internal_id.\n        \"\"\"\n        errors = {}\n\n        if user_input is not None:\n            chore_name = user_input[\"chore_name\"].strip()\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if user_input.get(\"due_date\"):\n                raw_due = user_input[\"due_date\"]\n                try:\n                    due_date_str = ensure_utc_datetime(self.hass, raw_due)\n                    due_dt = dt_util.parse_datetime(due_date_str)\n                    if due_dt and due_dt < dt_util.utcnow():\n                        errors[\"due_date\"] = \"due_date_in_past\"\n                except ValueError:\n                    errors[\"due_date\"] = \"invalid_due_date\"\n                    due_date_str = None\n            else:\n                due_date_str = None\n\n            if not chore_name:\n                errors[\"chore_name\"] = \"invalid_chore_name\"\n            elif any(\n                chore_data[\"name\"] == chore_name\n                for chore_data in self._chores_temp.values()\n            ):\n                errors[\"chore_name\"] = \"duplicate_chore\"\n\n            if errors:\n                kids_dict = {\n                    kid_data[\"name\"]: kid_id\n                    for kid_id, kid_data in self._kids_temp.items()\n                }\n                # Re-show the form with the user's current input and errors:\n                default_data = user_input.copy()\n                return self.async_show_form(\n                    step_id=\"chores\",\n                    data_schema=build_chore_schema(kids_dict, default_data),\n                    errors=errors,\n                )\n\n            if user_input.get(\"recurring_frequency\") != FREQUENCY_CUSTOM:\n                user_input.pop(\"custom_interval\", None)\n                user_input.pop(\"custom_interval_unit\", None)\n\n            # If no errors, store the chore\n            self._chores_temp[internal_id] = {\n                \"name\": chore_name,\n                \"default_points\": user_input[\"default_points\"],\n                \"partial_allowed\": user_input[\"partial_allowed\"],\n                \"shared_chore\": user_input[\"shared_chore\"],\n                \"assigned_kids\": user_input[\"assigned_kids\"],\n                \"allow_multiple_claims_per_day\": user_input[\n                    \"allow_multiple_claims_per_day\"\n                ],\n                \"description\": user_input.get(\"chore_description\", \"\"),\n                \"chore_labels\": user_input.get(\"chore_labels\", []),\n                \"icon\": user_input.get(\"icon\", \"\"),\n                \"recurring_frequency\": user_input.get(\"recurring_frequency\", \"none\"),\n                \"custom_interval\": user_input.get(\"custom_interval\"),\n                \"custom_interval_unit\": user_input.get(\"custom_interval_unit\"),\n                \"due_date\": due_date_str,\n                \"applicable_days\": user_input.get(\n                    CONF_APPLICABLE_DAYS, DEFAULT_APPLICABLE_DAYS\n                ),\n                \"notify_on_claim\": user_input.get(\n                    CONF_NOTIFY_ON_CLAIM, DEFAULT_NOTIFY_ON_CLAIM\n                ),\n                \"notify_on_approval\": user_input.get(\n                    CONF_NOTIFY_ON_APPROVAL, DEFAULT_NOTIFY_ON_APPROVAL\n                ),\n                \"notify_on_disapproval\": user_input.get(\n                    CONF_NOTIFY_ON_DISAPPROVAL, DEFAULT_NOTIFY_ON_DISAPPROVAL\n                ),\n                \"internal_id\": internal_id,\n            }\n            LOGGER.debug(\"Added chore: %s with ID: %s\", chore_name, internal_id)\n\n            self._chore_index += 1\n            if self._chore_index >= self._chore_count:\n                return await self.async_step_badge_count()\n            return await self.async_step_chores()\n\n        # Use flow_helpers.build_chore_schema, passing the current kids\n        kids_dict = {\n            kid_data[\"name\"]: kid_id for kid_id, kid_data in self._kids_temp.items()\n        }\n        default_data = {}\n        chore_schema = build_chore_schema(kids_dict, default_data)\n        return self.async_show_form(\n            step_id=\"chores\", data_schema=chore_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # BADGES\n    # --------------------------------------------------------------------------\n    async def async_step_badge_count(self, user_input=None):\n        \"\"\"Ask how many badges to define.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._badge_count = int(user_input[\"badge_count\"])\n                if self._badge_count < 0:\n                    raise ValueError\n                if self._badge_count == 0:\n                    return await self.async_step_reward_count()\n                self._badge_index = 0\n                return await self.async_step_badges()\n            except ValueError:\n                errors[\"base\"] = \"invalid_badge_count\"\n\n        schema = vol.Schema({vol.Required(\"badge_count\", default=0): vol.Coerce(int)})\n        return self.async_show_form(\n            step_id=\"badge_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_badges(self, user_input=None):\n        \"\"\"Collect badge details using internal_id as the primary key.\n\n        Store in self._badges_temp as a dict keyed by internal_id.\n        \"\"\"\n        errors = {}\n        if user_input is not None:\n            badge_name = user_input[\"badge_name\"].strip()\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if not badge_name:\n                errors[\"badge_name\"] = \"invalid_badge_name\"\n            elif any(\n                badge_data[\"name\"] == badge_name\n                for badge_data in self._badges_temp.values()\n            ):\n                errors[\"badge_name\"] = \"duplicate_badge\"\n            else:\n                self._badges_temp[internal_id] = {\n                    \"name\": badge_name,\n                    \"threshold_type\": user_input[\"threshold_type\"],\n                    \"threshold_value\": user_input[\"threshold_value\"],\n                    \"points_multiplier\": user_input[\"points_multiplier\"],\n                    \"icon\": user_input.get(\"icon\", \"\"),\n                    \"internal_id\": internal_id,\n                    \"description\": user_input.get(\"badge_description\", \"\"),\n                    \"badge_labels\": user_input.get(\"badge_labels\", []),\n                }\n                LOGGER.debug(\"Added badge: %s with ID: %s\", badge_name, internal_id)\n\n            self._badge_index += 1\n            if self._badge_index >= self._badge_count:\n                return await self.async_step_reward_count()\n            return await self.async_step_badges()\n\n        badge_schema = build_badge_schema()\n        return self.async_show_form(\n            step_id=\"badges\", data_schema=badge_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # REWARDS\n    # --------------------------------------------------------------------------\n    async def async_step_reward_count(self, user_input=None):\n        \"\"\"Ask how many rewards to define.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._reward_count = int(user_input[\"reward_count\"])\n                if self._reward_count < 0:\n                    raise ValueError\n                if self._reward_count == 0:\n                    return await self.async_step_penalty_count()\n                self._reward_index = 0\n                return await self.async_step_rewards()\n            except ValueError:\n                errors[\"base\"] = \"invalid_reward_count\"\n\n        schema = vol.Schema({vol.Required(\"reward_count\", default=0): vol.Coerce(int)})\n        return self.async_show_form(\n            step_id=\"reward_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_rewards(self, user_input=None):\n        \"\"\"Collect reward details using internal_id as the primary key.\n\n        Store in self._rewards_temp as a dict keyed by internal_id.\n        \"\"\"\n        errors = {}\n        if user_input is not None:\n            reward_name = user_input[\"reward_name\"].strip()\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if not reward_name:\n                errors[\"reward_name\"] = \"invalid_reward_name\"\n            elif any(\n                reward_data[\"name\"] == reward_name\n                for reward_data in self._rewards_temp.values()\n            ):\n                errors[\"reward_name\"] = \"duplicate_reward\"\n            else:\n                self._rewards_temp[internal_id] = {\n                    \"name\": reward_name,\n                    \"cost\": user_input[\"reward_cost\"],\n                    \"description\": user_input.get(\"reward_description\", \"\"),\n                    \"reward_labels\": user_input.get(\"reward_labels\", []),\n                    \"icon\": user_input.get(\"icon\", \"\"),\n                    \"internal_id\": internal_id,\n                }\n                LOGGER.debug(\"Added reward: %s with ID: %s\", reward_name, internal_id)\n\n            self._reward_index += 1\n            if self._reward_index >= self._reward_count:\n                return await self.async_step_penalty_count()\n            return await self.async_step_rewards()\n\n        reward_schema = build_reward_schema()\n        return self.async_show_form(\n            step_id=\"rewards\", data_schema=reward_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # PENALTIES\n    # --------------------------------------------------------------------------\n    async def async_step_penalty_count(self, user_input=None):\n        \"\"\"Ask how many penalties to define.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._penalty_count = int(user_input[\"penalty_count\"])\n                if self._penalty_count < 0:\n                    raise ValueError\n                if self._penalty_count == 0:\n                    return await self.async_step_bonus_count()\n                self._penalty_index = 0\n                return await self.async_step_penalties()\n            except ValueError:\n                errors[\"base\"] = \"invalid_penalty_count\"\n\n        schema = vol.Schema({vol.Required(\"penalty_count\", default=0): vol.Coerce(int)})\n        return self.async_show_form(\n            step_id=\"penalty_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_penalties(self, user_input=None):\n        \"\"\"Collect penalty details using internal_id as the primary key.\n\n        Store in self._penalties_temp as a dict keyed by internal_id.\n        \"\"\"\n        errors = {}\n        if user_input is not None:\n            penalty_name = user_input[\"penalty_name\"].strip()\n            penalty_points = user_input[\"penalty_points\"]\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if not penalty_name:\n                errors[\"penalty_name\"] = \"invalid_penalty_name\"\n            elif any(\n                penalty_data[\"name\"] == penalty_name\n                for penalty_data in self._penalties_temp.values()\n            ):\n                errors[\"penalty_name\"] = \"duplicate_penalty\"\n            else:\n                self._penalties_temp[internal_id] = {\n                    \"name\": penalty_name,\n                    \"description\": user_input.get(\"penalty_description\", \"\"),\n                    \"penalty_labels\": user_input.get(\"penalty_labels\", []),\n                    \"points\": -abs(penalty_points),  # Ensure points are negative\n                    \"icon\": user_input.get(\"icon\", \"\"),\n                    \"internal_id\": internal_id,\n                }\n                LOGGER.debug(\"Added penalty: %s with ID: %s\", penalty_name, internal_id)\n\n            self._penalty_index += 1\n            if self._penalty_index >= self._penalty_count:\n                return await self.async_step_bonus_count()\n            return await self.async_step_penalties()\n\n        penalty_schema = build_penalty_schema()\n        return self.async_show_form(\n            step_id=\"penalties\", data_schema=penalty_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # BONUSES\n    # --------------------------------------------------------------------------\n    async def async_step_bonus_count(self, user_input=None):\n        \"\"\"Ask how many bonuses to define.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._bonus_count = int(user_input[\"bonus_count\"])\n                if self._bonus_count < 0:\n                    raise ValueError\n                if self._bonus_count == 0:\n                    return await self.async_step_achievement_count()\n                self._bonus_index = 0\n                return await self.async_step_bonuses()\n            except ValueError:\n                errors[\"base\"] = \"invalid_bonus_count\"\n\n        schema = vol.Schema({vol.Required(\"bonus_count\", default=0): vol.Coerce(int)})\n        return self.async_show_form(\n            step_id=\"bonus_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_bonuses(self, user_input=None):\n        \"\"\"Collect bonus details using internal_id as the primary key.\n\n        Store in self._bonuses_temp as a dict keyed by internal_id.\n        \"\"\"\n        errors = {}\n        if user_input is not None:\n            bonus_name = user_input[\"bonus_name\"].strip()\n            bonus_points = user_input[\"bonus_points\"]\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if not bonus_name:\n                errors[\"bonus_name\"] = \"invalid_bonus_name\"\n            elif any(\n                bonus_data[\"name\"] == bonus_name\n                for bonus_data in self._bonuses_temp.values()\n            ):\n                errors[\"bonus_name\"] = \"duplicate_bonus\"\n            else:\n                self._bonuses_temp[internal_id] = {\n                    \"name\": bonus_name,\n                    \"description\": user_input.get(\"bonus_description\", \"\"),\n                    \"bonus_labels\": user_input.get(\"bonus_labels\", []),\n                    \"points\": abs(bonus_points),  # Ensure points are positive\n                    \"icon\": user_input.get(\"icon\", \"\"),\n                    \"internal_id\": internal_id,\n                }\n                LOGGER.debug(\"Added bonus '%s' with ID: %s\", bonus_name, internal_id)\n\n            self._bonus_index += 1\n            if self._bonus_index >= self._bonus_count:\n                return await self.async_step_achievement_count()\n            return await self.async_step_bonuses()\n\n        schema = build_bonus_schema()\n        return self.async_show_form(\n            step_id=\"bonuses\", data_schema=schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # ACHIEVEMENTS\n    # --------------------------------------------------------------------------\n    async def async_step_achievement_count(self, user_input=None):\n        \"\"\"Ask how many achievements to define initially.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._achievement_count = int(user_input[\"achievement_count\"])\n                if self._achievement_count < 0:\n                    raise ValueError\n                if self._achievement_count == 0:\n                    return await self.async_step_challenge_count()\n                self._achievement_index = 0\n                return await self.async_step_achievements()\n            except ValueError:\n                errors[\"base\"] = \"invalid_achievement_count\"\n        schema = vol.Schema(\n            {vol.Required(\"achievement_count\", default=0): vol.Coerce(int)}\n        )\n        return self.async_show_form(\n            step_id=\"achievement_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_achievements(self, user_input=None):\n        \"\"\"Collect each achievement's details using internal_id as the key.\"\"\"\n        errors = {}\n\n        if user_input is not None:\n            achievement_name = user_input[\"name\"].strip()\n            if not achievement_name:\n                errors[\"name\"] = \"invalid_achievement_name\"\n            elif any(\n                achievement_data[\"name\"] == achievement_name\n                for achievement_data in self._achievements_temp.values()\n            ):\n                errors[\"name\"] = \"duplicate_achievement\"\n            else:\n                _type = user_input[\"type\"]\n\n                if _type == ACHIEVEMENT_TYPE_STREAK:\n                    chore_id = user_input.get(\"selected_chore_id\")\n                    if not chore_id or chore_id == \"None\":\n                        errors[\"selected_chore_id\"] = \"a_chore_must_be_selected\"\n\n                    final_chore_id = chore_id\n                else:\n                    # Discard chore if not streak\n                    final_chore_id = \"\"\n\n                if not errors:\n                    internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n                    self._achievements_temp[internal_id] = {\n                        \"name\": achievement_name,\n                        \"description\": user_input.get(\"description\", \"\"),\n                        \"achievement_labels\": user_input.get(\"achievement_labels\", []),\n                        \"icon\": user_input.get(\"icon\", \"\"),\n                        \"assigned_kids\": user_input[\"assigned_kids\"],\n                        \"type\": _type,\n                        \"selected_chore_id\": final_chore_id,\n                        \"criteria\": user_input.get(\"criteria\", \"\").strip(),\n                        \"target_value\": user_input[\"target_value\"],\n                        \"reward_points\": user_input[\"reward_points\"],\n                        \"internal_id\": internal_id,\n                        \"progress\": {},\n                    }\n\n                    self._achievement_index += 1\n                    if self._achievement_index >= self._achievement_count:\n                        return await self.async_step_challenge_count()\n                    return await self.async_step_achievements()\n\n        kids_dict = {\n            kid_data[\"name\"]: kid_id for kid_id, kid_data in self._kids_temp.items()\n        }\n        all_chores = self._chores_temp\n        achievement_schema = build_achievement_schema(\n            kids_dict=kids_dict, chores_dict=all_chores, default=None\n        )\n        return self.async_show_form(\n            step_id=\"achievements\", data_schema=achievement_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # CHALLENGES\n    # --------------------------------------------------------------------------\n    async def async_step_challenge_count(self, user_input=None):\n        \"\"\"Ask how many challenges to define initially.\"\"\"\n        errors = {}\n        if user_input is not None:\n            try:\n                self._challenge_count = int(user_input[\"challenge_count\"])\n                if self._challenge_count < 0:\n                    raise ValueError\n                if self._challenge_count == 0:\n                    return await self.async_step_finish()\n                self._challenge_index = 0\n                return await self.async_step_challenges()\n            except ValueError:\n                errors[\"base\"] = \"invalid_challenge_count\"\n        schema = vol.Schema(\n            {vol.Required(\"challenge_count\", default=0): vol.Coerce(int)}\n        )\n        return self.async_show_form(\n            step_id=\"challenge_count\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_challenges(self, user_input=None):\n        \"\"\"Collect each challenge's details using internal_id as the key.\"\"\"\n        errors = {}\n        if user_input is not None:\n            challenge_name = user_input[\"name\"].strip()\n            if not challenge_name:\n                errors[\"name\"] = \"invalid_challenge_name\"\n            elif any(\n                challenge_data[\"name\"] == challenge_name\n                for challenge_data in self._challenges_temp.values()\n            ):\n                errors[\"name\"] = \"duplicate_challenge\"\n            else:\n                _type = user_input[\"type\"]\n\n                if _type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n                    chosen_chore_id = user_input.get(\"selected_chore_id\")\n                    if not chosen_chore_id or chosen_chore_id == \"None\":\n                        errors[\"selected_chore_id\"] = \"a_chore_must_be_selected\"\n                    final_chore_id = chosen_chore_id\n                else:\n                    # Discard chore if not \"CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW\"\n                    final_chore_id = \"\"\n\n                # Process start_date and end_date using the helper:\n                start_date_input = user_input.get(\"start_date\")\n                end_date_input = user_input.get(\"end_date\")\n\n                if start_date_input:\n                    try:\n                        start_date = ensure_utc_datetime(self.hass, start_date_input)\n                        start_dt = dt_util.parse_datetime(start_date)\n                        if start_dt and start_dt < dt_util.utcnow():\n                            errors[\"start_date\"] = \"start_date_in_past\"\n                    except Exception:\n                        errors[\"start_date\"] = \"invalid_start_date\"\n                        start_date = None\n                else:\n                    start_date = None\n\n                if end_date_input:\n                    try:\n                        end_date = ensure_utc_datetime(self.hass, end_date_input)\n                        end_dt = dt_util.parse_datetime(end_date)\n                        if end_dt and end_dt <= dt_util.utcnow():\n                            errors[\"end_date\"] = \"end_date_in_past\"\n                        if start_date:\n                            # Compare start_dt and end_dt if both are valid\n                            if end_dt and start_dt and end_dt <= start_dt:\n                                errors[\"end_date\"] = \"end_date_not_after_start_date\"\n                    except Exception:\n                        errors[\"end_date\"] = \"invalid_end_date\"\n                        end_date = None\n                else:\n                    end_date = None\n\n                if not errors:\n                    internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n                    self._challenges_temp[internal_id] = {\n                        \"name\": challenge_name,\n                        \"description\": user_input.get(\"description\", \"\"),\n                        \"challenge_labels\": user_input.get(\"challenge_labels\", []),\n                        \"icon\": user_input.get(\"icon\", \"\"),\n                        \"assigned_kids\": user_input[\"assigned_kids\"],\n                        \"type\": _type,\n                        \"selected_chore_id\": final_chore_id,\n                        \"criteria\": user_input.get(\"criteria\", \"\").strip(),\n                        \"target_value\": user_input[\"target_value\"],\n                        \"reward_points\": user_input[\"reward_points\"],\n                        \"start_date\": start_date,\n                        \"end_date\": end_date,\n                        \"internal_id\": internal_id,\n                        \"progress\": {},\n                    }\n                    self._challenge_index += 1\n                    if self._challenge_index >= self._challenge_count:\n                        return await self.async_step_finish()\n                    return await self.async_step_challenges()\n\n        kids_dict = {\n            kid_data[\"name\"]: kid_id for kid_id, kid_data in self._kids_temp.items()\n        }\n        all_chores = self._chores_temp\n        default_data = user_input if user_input else None\n        challenge_schema = build_challenge_schema(\n            kids_dict=kids_dict,\n            chores_dict=all_chores,\n            default=default_data,\n        )\n        return self.async_show_form(\n            step_id=\"challenges\", data_schema=challenge_schema, errors=errors\n        )\n\n    # --------------------------------------------------------------------------\n    # FINISH\n    # --------------------------------------------------------------------------\n    async def async_step_finish(self, user_input=None):\n        \"\"\"Finalize summary and create the config entry.\"\"\"\n        if user_input is not None:\n            return self._create_entry()\n\n        # Create a mapping from kid_id to kid_name for easy lookup\n        kid_id_to_name = {\n            kid_id: data[\"name\"] for kid_id, data in self._kids_temp.items()\n        }\n\n        # Enhance parents summary to include associated kids by name\n        parents_summary = []\n        for parent in self._parents_temp.values():\n            associated_kids_names = [\n                kid_id_to_name.get(kid_id, \"Unknown\")\n                for kid_id in parent.get(\"associated_kids\", [])\n            ]\n            if associated_kids_names:\n                kids_str = \", \".join(associated_kids_names)\n                parents_summary.append(f\"{parent['name']} (Kids: {kids_str})\")\n            else:\n                parents_summary.append(parent[\"name\"])\n\n        summary = (\n            f\"\\nKids: {', '.join(kid_data['name'] for kid_data in self._kids_temp.values()) or 'None'}\\n\\n\"\n            f\"Parents: {', '.join(parents_summary) or 'None'}\\n\\n\"\n            f\"Chores: {', '.join(chore_data['name'] for chore_data in self._chores_temp.values()) or 'None'}\\n\\n\"\n            f\"Badges: {', '.join(badge_data['name'] for badge_data in self._badges_temp.values()) or 'None'}\\n\\n\"\n            f\"Rewards: {', '.join(reward_data['name'] for reward_data in self._rewards_temp.values()) or 'None'}\\n\\n\"\n            f\"Penalties: {', '.join(penalty_data['name'] for penalty_data in self._penalties_temp.values()) or 'None'}\\n\\n\"\n            f\"Bonuses: {', '.join(bonus_data['name'] for bonus_data in self._bonuses_temp.values()) or 'None'}\\n\\n\"\n            f\"Achievements: {', '.join(achievement_data['name'] for achievement_data in self._achievements_temp.values()) or 'None'}\\n\\n\"\n            f\"Challenges: {', '.join(challenge_data['name'] for challenge_data in self._challenges_temp.values()) or 'None'}\\n\\n\"\n        )\n        return self.async_show_form(\n            step_id=\"finish\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"summary\": summary},\n        )\n\n    def _create_entry(self):\n        \"\"\"Finalize config entry with data and options using internal_id as keys.\"\"\"\n        entry_data = {}\n        entry_options = {\n            CONF_POINTS_LABEL: self._data.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL),\n            CONF_POINTS_ICON: self._data.get(CONF_POINTS_ICON, DEFAULT_POINTS_ICON),\n            CONF_KIDS: self._kids_temp,\n            CONF_PARENTS: self._parents_temp,\n            CONF_CHORES: self._chores_temp,\n            CONF_BADGES: self._badges_temp,\n            CONF_REWARDS: self._rewards_temp,\n            CONF_PENALTIES: self._penalties_temp,\n            CONF_BONUSES: self._bonuses_temp,\n            CONF_ACHIEVEMENTS: self._achievements_temp,\n            CONF_CHALLENGES: self._challenges_temp,\n        }\n\n        LOGGER.debug(\n            \"Creating entry with data=%s, options=%s\", entry_data, entry_options\n        )\n        return self.async_create_entry(\n            title=\"KidsChores\", data=entry_data, options=entry_options\n        )\n\n    @staticmethod\n    @callback\n    def async_get_options_flow(config_entry):\n        \"\"\"Return the Options Flow.\"\"\"\n        from .options_flow import KidsChoresOptionsFlowHandler\n\n        return KidsChoresOptionsFlowHandler(config_entry)\n"
  },
  {
    "path": "custom_components/kidschores/const.py",
    "content": "# File: const.py\n\"\"\"Constants for the KidsChores integration.\n\nThis file centralizes configuration keys, defaults, labels, domain names,\nevent names, and platform identifiers for consistency across the integration.\nIt also supports localization by defining all labels and UI texts used in sensors,\nservices, and options flow.\n\"\"\"\n\nimport logging\n\nfrom homeassistant.const import Platform\n\n# -------------------- General --------------------\n# Integration Domain and Logging\nDOMAIN = \"kidschores\"\nLOGGER = logging.getLogger(__package__)\n\n# Supported Platforms\nPLATFORMS = [\n    Platform.BUTTON,\n    Platform.CALENDAR,\n    Platform.SELECT,\n    Platform.SENSOR,\n]\n\n# Storage and Versioning\nSTORAGE_KEY = \"kidschores_data\"  # Persistent storage key\nSTORAGE_VERSION = 1  # Storage version\n\n# Update Interval\nUPDATE_INTERVAL = 5  # Update interval for coordinator (in minutes)\n\n# -------------------- Configuration --------------------\n# Configuration Keys\nCONF_ACHIEVEMENTS = \"achievements\"\nCONF_APPLICABLE_DAYS = \"applicable_days\"\nCONF_BADGES = \"badges\"  # Key for badges configuration\nCONF_CHALLENGES = \"challenges\"\nCONF_CHORES = \"chores\"  # Key for chores configuration\nCONF_GLOBAL = \"global\"\nCONF_KIDS = \"kids\"  # Key for kids configuration\nCONF_PARENTS = \"parents\"  # Key for parents configuration\nCONF_PENALTIES = \"penalties\"  # Key for penalties configuration\nCONF_POINTS_ICON = \"points_icon\"\nCONF_POINTS_LABEL = \"points_label\"  # Custom label for points\nCONF_REWARDS = \"rewards\"  # Key for rewards configuration\nCONF_BONUSES = \"bonuses\"\n\n# Options Flow Management\nOPTIONS_FLOW_ACHIEVEMENTS = \"manage_achievements\"  # Edit achivements step\nOPTIONS_FLOW_BADGES = \"manage_badges\"  # Edit badges step\nOPTIONS_FLOW_CHALLENGES = \"manage_challenges\"  # Edit challenges step\nOPTIONS_FLOW_CHORES = \"manage_chores\"  # Edit chores step\nOPTIONS_FLOW_KIDS = \"manage_kids\"  # Edit kids step\nOPTIONS_FLOW_PARENTS = \"manage_parents\"  # Edit parents step\nOPTIONS_FLOW_PENALTIES = \"manage_penalties\"  # Edit penalties step\nOPTIONS_FLOW_REWARDS = \"manage_rewards\"  # Edit rewards step\nOPTIONS_FLOW_BONUSES = \"manage_bonuses\"  # Edit bonuses step\n\n# Validation Keys\nVALIDATION_DUE_DATE = \"due_date\"  # Optional due date for chores\nVALIDATION_PARTIAL_ALLOWED = \"partial_allowed\"  # Allow partial points in chores\nVALIDATION_THRESHOLD_TYPE = \"threshold_type\"  # Badge criteria type\nVALIDATION_THRESHOLD_VALUE = \"threshold_value\"  # Badge criteria value\n\n# Notification configuration keys\nCONF_ENABLE_MOBILE_NOTIFICATIONS = \"enable_mobile_notifications\"\nCONF_MOBILE_NOTIFY_SERVICE = \"mobile_notify_service\"\nCONF_ENABLE_PERSISTENT_NOTIFICATIONS = \"enable_persistent_notifications\"\nCONF_NOTIFY_ON_CLAIM = \"notify_on_claim\"\nCONF_NOTIFY_ON_APPROVAL = \"notify_on_approval\"\nCONF_NOTIFY_ON_DISAPPROVAL = \"notify_on_disapproval\"\nCONF_CHORE_NOTIFY_SERVICE = \"chore_notify_service\"\n\nNOTIFICATION_EVENT = \"mobile_app_notification_action\"\n\n# Achievement types\nACHIEVEMENT_TYPE_STREAK = \"chore_streak\"  # e.g., \"Make bed 20 days in a row\"\nACHIEVEMENT_TYPE_TOTAL = \"chore_total\"  # e.g., \"Complete 100 chores overall\"\nACHIEVEMENT_TYPE_DAILY_MIN = (\n    \"daily_minimum\"  # e.g., \"Complete minimum 5 chores in one day\"\n)\n\n# Challenge types\nCHALLENGE_TYPE_TOTAL_WITHIN_WINDOW = (\n    \"total_within_window\"  # e.g., \"Complete 50 chores in 30 days\"\n)\nCHALLENGE_TYPE_DAILY_MIN = \"daily_minimum\"  # e.g., \"Do 2 chores each day for 14 days\"\n\n\n# -------------------- Defaults --------------------\n# Default Icons\nDEFAULT_ACHIEVEMENTS_ICON = \"mdi:trophy-award\"  # Default icon for achievements\nDEFAULT_BADGE_ICON = \"mdi:shield-star-outline\"  # Default icon for badges\nDEFAULT_CALENDAR_ICON = \"mdi:calendar\"  # Default icon for calendar sensors\nDEFAULT_CHALLENGES_ICON = \"mdi:trophy\"  # Default icon for achievements\nDEFAULT_CHORE_APPROVE_ICON = \"mdi:checkbox-marked-circle-outline\"\nDEFAULT_CHORE_BINARY_ICON = (\n    \"mdi:checkbox-blank-circle-outline\"  # For chore status binary sensor\n)\nDEFAULT_CHORE_CLAIM_ICON = \"mdi:clipboard-check-outline\"\nDEFAULT_CHORE_SENSOR_ICON = (\n    \"mdi:checkbox-blank-circle-outline\"  # For chore status sensor\n)\nDEFAULT_DISAPPROVE_ICON = (\n    \"mdi:close-circle-outline\"  # Default icon for disapprove buttons\n)\nDEFAULT_ICON = \"mdi:star-outline\"  # Default icon for general points display\nDEFAULT_PENALTY_ICON = \"mdi:alert-outline\"  # Default icon for penalties\nDEFAULT_POINTS_ADJUST_MINUS_ICON = \"mdi:minus-circle-outline\"\nDEFAULT_POINTS_ADJUST_PLUS_ICON = \"mdi:plus-circle-outline\"\nDEFAULT_POINTS_ADJUST_MINUS_MULTIPLE_ICON = \"mdi:minus-circle-multiple-outline\"\nDEFAULT_POINTS_ADJUST_PLUS_MULTIPLE_ICON = \"mdi:plus-circle-multiple-outline\"\nDEFAULT_POINTS_ICON = \"mdi:star-outline\"  # Default icon for points\nDEFAULT_STREAK_ICON = \"mdi:blur-linear\"  # Default icon for streaks\nDEFAULT_BONUS_ICON = \"mdi:seal\"  # Default icon for bonuses\nDEFAULT_REWARD_ICON = \"mdi:gift-outline\"  # Default icon for rewards\nDEFAULT_TROPHY_ICON = \"mdi:trophy\"  # For highest-badge sensor fallback\nDEFAULT_TROPHY_OUTLINE = \"mdi:trophy-outline\"\n\n# Default Values\nDEFAULT_APPLICABLE_DAYS = []  # Empty means the chore applies every day.\nDEFAULT_BADGE_THRESHOLD = 50  # Default points threshold for badges\nDEFAULT_MULTIPLE_CLAIMS_PER_DAY = False  # Allow only one chore claim per day\nDEFAULT_PARTIAL_ALLOWED = False  # Partial points not allowed by default\nDEFAULT_POINTS = 5  # Default points awarded for each chore\nDEFAULT_POINTS_MULTIPLIER = 1  # Default points multiplier for badges\nDEFAULT_POINTS_LABEL = \"Points\"  # Default label for points displayed in UI\nDEFAULT_PENALTY_POINTS = 2  # Default points deducted for each penalty\nDEFAULT_BONUS_POINTS = 2  # Default points added for each bonus\nDEFAULT_REMINDER_DELAY = 30  # Default reminder delay in minutes\nDEFAULT_REWARD_COST = 10  # Default cost for each reward\nDEFAULT_DAILY_RESET_TIME = {\n    \"hour\": 0,\n    \"minute\": 0,\n    \"second\": 0,\n}  # Daily reset at midnight\nDEFAULT_MONTHLY_RESET_DAY = 1  # Monthly reset on the 1st day\nDEFAULT_WEEKLY_RESET_DAY = 0  # Weekly reset on Monday (0 = Monday, 6 = Sunday)\nDEFAULT_NOTIFY_ON_CLAIM = True\nDEFAULT_NOTIFY_ON_APPROVAL = True\nDEFAULT_NOTIFY_ON_DISAPPROVAL = True\n\n# -------------------- Recurring Frequencies --------------------\nFREQUENCY_BIWEEKLY = \"biweekly\"\nFREQUENCY_CUSTOM = \"custom\"\nFREQUENCY_DAILY = \"daily\"\nFREQUENCY_MONTHLY = \"monthly\"\nFREQUENCY_NONE = \"none\"\nFREQUENCY_WEEKLY = \"weekly\"\n\n# -------------------- Data Keys --------------------\n# Data Keys for Coordinator and Storage\nDATA_ACHIEVEMENTS = \"achievements\"  # Key for storing achievements data\nDATA_BADGES = \"badges\"  # Key for storing badges data\nDATA_CHALLENGES = \"challenges\"  # Key for storing challenges data\nDATA_CHORES = \"chores\"  # Key for storing chores data\nDATA_KIDS = \"kids\"  # Key for storing kids data in storage\nDATA_PARENTS = \"parents\"  # Key for storing parent data\nDATA_PENDING_CHORE_APPROVALS = \"pending_chore_approvals\"  # Pending chore approvals\nDATA_PENDING_REWARD_APPROVALS = \"pending_reward_approvals\"  # Pending reward approvals\nDATA_PENALTIES = \"penalties\"  # Key for storing penalties data\nDATA_REWARDS = \"rewards\"  # Key for storing rewards data\nDATA_BONUSES = \"bonuses\"  # Key for storing bonuses data\n\n# -------------------- States --------------------\n# Badge Threshold Types\nBADGE_THRESHOLD_TYPE_CHORE_COUNT = (\n    \"chore_count\"  # Badges for completing a number of chores\n)\nBADGE_THRESHOLD_TYPE_POINTS = \"points\"  # Badges awarded for reaching points\n\n# Chore States\nCHORE_STATE_APPROVED = \"approved\"  # Chore fully approved\nCHORE_STATE_APPROVED_IN_PART = \"approved_in_part\"  # Chore approved for some kids\nCHORE_STATE_CLAIMED = \"claimed\"  # Chore claimed by a kid\nCHORE_STATE_CLAIMED_IN_PART = \"claimed_in_part\"  # Chore claimed by some kids\nCHORE_STATE_INDEPENDENT = \"independent\"  # Chore is not shared\nCHORE_STATE_OVERDUE = \"overdue\"  # Chore not completed before the due date\nCHORE_STATE_PARTIAL = \"partial\"  # Chore approved with partial points\nCHORE_STATE_PENDING = \"pending\"  # Default state: chore pending approval\nCHORE_STATE_UNKNOWN = \"unknown\"  # Unknown chore state\n\n\n# Reward States\nREWARD_STATE_APPROVED = \"approved\"  # Reward fully approved\nREWARD_STATE_CLAIMED = \"claimed\"  # Reward claimed by a kid\nREWARD_STATE_NOT_CLAIMED = \"not_claimed\"  # Default state: reward not claimed\nREWARD_STATE_UNKNOWN = \"unknown\"  # Unknown reward state\n\n# -------------------- Events --------------------\n# Event Names\nEVENT_CHORE_COMPLETED = \"kidschores_chore_completed\"  # Event for chore completion\nEVENT_REWARD_REDEEMED = \"kidschores_reward_redeemed\"  # Event for redeeming a reward\n\n# -------------------- Actions --------------------\n# Action titles for notifications\nACTION_TITLE_APPROVE = \"Approve\"\nACTION_TITLE_DISAPPROVE = \"Disapprove\"\nACTION_TITLE_REMIND_30 = \"Remind in 30 mins\"\n\n# Action identifiers\nACTION_APPROVE_CHORE = \"APPROVE_CHORE\"\nACTION_DISAPPROVE_CHORE = \"DISAPPROVE_CHORE\"\nACTION_APPROVE_REWARD = \"APPROVE_REWARD\"\nACTION_DISAPPROVE_REWARD = \"DISAPPROVE_REWARD\"\nACTION_REMIND_30 = \"REMIND_30\"\n\n# -------------------- Sensors --------------------\n# Sensor Attributes\nATTR_ACHIEVEMENT_NAME = \"achievement_name\"\nATTR_ALL_EARNED_BADGES = \"all_earned_badges\"\nATTR_ALLOW_MULTIPLE_CLAIMS_PER_DAY = \"allow_multiple_claims_per_day\"\nATTR_APPLICABLE_DAYS = \"applicable_days\"\nATTR_AWARDED = \"awarded\"\nATTR_ASSIGNED_KIDS = \"assigned_kids\"\nATTR_ASSOCIATED_CHORE = \"associated_chore\"\nATTR_BADGES = \"badges\"\nATTR_CHALLENGE_NAME = \"challenge_name\"\nATTR_CHALLENGE_TYPE = \"challenge_type\"\nATTR_CHORE_APPROVALS_COUNT = \"chore_approvals_count\"\nATTR_CHORE_APPROVALS_TODAY = \"chore_approvals_today\"\nATTR_CHORE_CLAIMS_COUNT = \"chore_claims_count\"\nATTR_CHORE_CURRENT_STREAK = \"chore_current_streak\"\nATTR_CHORE_HIGHEST_STREAK = \"chore_highest_streak\"\nATTR_CHORE_NAME = \"chore_name\"\nATTR_CLAIMED_ON = \"Claimed on\"\nATTR_COST = \"cost\"\nATTR_CRITERIA = \"criteria\"\nATTR_CUSTOM_FREQUENCY_INTERVAL = \"custom_frequency_interval\"\nATTR_CUSTOM_FREQUENCY_UNIT = \"custom_frequency_unit\"\nATTR_DEFAULT_POINTS = \"default_points\"\nATTR_DESCRIPTION = \"description\"\nATTR_DUE_DATE = \"due_date\"\nATTR_END_DATE = \"end_date\"\nATTR_GLOBAL_STATE = \"global_state\"\nATTR_HIGHEST_BADGE_THRESHOLD_VALUE = \"highest_badge_threshold_value\"\nATTR_KID_NAME = \"kid_name\"\nATTR_KID_STATE = \"kid_state\"\nATTR_LABELS = \"labels\"\nATTR_KIDS_EARNED = \"kids_earned\"\nATTR_LAST_DATE = \"last_date\"\nATTR_PARTIAL_ALLOWED = \"partial_allowed\"\nATTR_PENALTY_NAME = \"penalty_name\"\nATTR_PENALTY_POINTS = \"penalty_points\"\nATTR_POINTS_MULTIPLIER = \"points_multiplier\"\nATTR_POINTS_TO_NEXT_BADGE = \"points_to_next_badge\"\nATTR_RAW_PROGRESS = \"raw_progress\"\nATTR_RAW_STREAK = \"raw_streak\"\nATTR_RECURRING_FREQUENCY = \"recurring_frequency\"\nATTR_REDEEMED_ON = \"Redeemed on\"\nATTR_REWARD_APPROVALS_COUNT = \"reward_approvals_count\"\nATTR_REWARD_CLAIMS_COUNT = \"reward_claims_count\"\nATTR_REWARD_NAME = \"reward_name\"\nATTR_REWARD_POINTS = \"reward_points\"\nATTR_BONUS_NAME = \"bonus_name\"\nATTR_BONUS_POINTS = \"bonus_points\"\nATTR_START_DATE = \"start_date\"\nATTR_SHARED_CHORE = \"shared_chore\"\nATTR_TARGET_VALUE = \"target_value\"\nATTR_THRESHOLD_TYPE = \"threshold_type\"\nATTR_TYPE = \"type\"\n\n# Calendar Attributes\nATTR_CAL_SUMMARY = \"summary\"\nATTR_CAL_START = \"start\"\nATTR_CAL_END = \"end\"\nATTR_CAL_ALL_DAY = \"all_day\"\nATTR_CAL_DESCRIPTION = \"description\"\nATTR_CAL_MANUFACTURER = \"manufacturer\"\n\n# Sensor Types\nSENSOR_TYPE_BADGES = \"badges\"  # Sensor tracking earned badges\nSENSOR_TYPE_CHORE_APPROVALS = \"chore_approvals\"  # Chore approvals sensor\nSENSOR_TYPE_CHORE_CLAIMS = \"chore_claims\"  # Chore claims sensor\nSENSOR_TYPE_COMPLETED_DAILY = (\n    \"completed_daily\"  # Sensor tracking daily chores completed\n)\nSENSOR_TYPE_COMPLETED_MONTHLY = (\n    \"completed_monthly\"  # Sensor tracking monthly chores completed\n)\nSENSOR_TYPE_COMPLETED_WEEKLY = (\n    \"completed_weekly\"  # Sensor tracking weekly chores completed\n)\nSENSOR_TYPE_PENALTY_APPLIES = \"penalty_applies\"  # Penalty applies sensor\nSENSOR_TYPE_POINTS = \"points\"  # Sensor tracking total points\nSENSOR_TYPE_PENDING_CHORE_APPROVALS = (\n    \"pending_chore_approvals\"  # Pending chore approvals\n)\nSENSOR_TYPE_PENDING_REWARD_APPROVALS = (\n    \"pending_reward_approvals\"  # Pending reward approvals\n)\nSENSOR_TYPE_REWARD_APPROVALS = \"reward_approvals\"  # Reward approvals sensor\nSENSOR_TYPE_REWARD_CLAIMS = \"reward_claims\"  # Reward claims sensor\nSENSOR_TYPE_BONUS_APPLIES = \"bonus_applies\"  # Bonus applies sensor\n\n\n# -------------------- Services --------------------\n# Custom Services\nSERVICE_APPLY_PENALTY = \"apply_penalty\"  # Apply penalty service\nSERVICE_APPROVE_CHORE = \"approve_chore\"  # Approve chore service\nSERVICE_APPROVE_REWARD = \"approve_reward\"  # Approve reward service\nSERVICE_CLAIM_CHORE = \"claim_chore\"  # Claim chore service\nSERVICE_DISAPPROVE_CHORE = \"disapprove_chore\"  # Disapprove chore service\nSERVICE_DISAPPROVE_REWARD = \"disapprove_reward\"  # Disapprove reward service\nSERVICE_REDEEM_REWARD = \"redeem_reward\"  # Redeem reward service\nSERVICE_RESET_ALL_CHORES = \"reset_all_chores\"  # Reset all chores service\nSERVICE_RESET_ALL_DATA = \"reset_all_data\"  # Reset all data service\nSERVICE_RESET_OVERDUE_CHORES = \"reset_overdue_chores\"  # Reset overdue chores\nSERVICE_SET_CHORE_DUE_DATE = \"set_chore_due_date\"  # Set or reset chores due date\nSERVICE_SKIP_CHORE_DUE_DATE = (\n    \"skip_chore_due_date\"  # Skip chores due date and reschedule\n)\nSERVICE_APPLY_BONUS = \"apply_bonus\"  # Apply bonus service\nSERVICE_RESET_PENALTIES = \"reset_penalties\"  # Reset penalties service\nSERVICE_RESET_BONUSES = \"reset_bonuses\"  # Reset bonuses service\nSERVICE_RESET_REWARDS = \"reset_rewards\"  # Reset rewards service\n\n# Field Names (for consistency across services)\nFIELD_CHORE_ID = \"chore_id\"\nFIELD_CHORE_NAME = \"chore_name\"\nFIELD_DUE_DATE = \"due_date\"\nFIELD_KID_NAME = \"kid_name\"\nFIELD_PARENT_NAME = \"parent_name\"\nFIELD_PENALTY_NAME = \"penalty_name\"\nFIELD_POINTS_AWARDED = \"points_awarded\"\nFIELD_REWARD_NAME = \"reward_name\"\nFIELD_BONUS_NAME = \"bonus_name\"\n\n# -------------------- Labels --------------------\n# Labels for Sensors and UI\nLABEL_BADGES = \"Badges\"\nLABEL_COMPLETED_DAILY = \"Daily Completed Chores\"\nLABEL_COMPLETED_MONTHLY = \"Monthly Completed Chores\"\nLABEL_COMPLETED_WEEKLY = \"Weekly Completed Chores\"\nLABEL_POINTS = \"Points\"\n\n# -------------------- Buttons --------------------\n# Button Prefixes for Dynamic Creation\nBUTTON_DISAPPROVE_CHORE_PREFIX = \"disapprove_chore_button_\"  # Disapprove chore button\nBUTTON_DISAPPROVE_REWARD_PREFIX = (\n    \"disapprove_reward_button_\"  # Disapprove reward button\n)\nBUTTON_PENALTY_PREFIX = (\n    \"penalty_button_\"  # Prefix for dynamically created penalty buttons\n)\nBUTTON_REWARD_PREFIX = \"reward_button_\"  # Prefix for dynamically created reward buttons\nBUTTON_BONUS_PREFIX = \"bonus_button_\"  # Prefix for dynamically created bonus buttons\n\n# -------------------- Errors and Warnings --------------------\nDUE_DATE_NOT_SET = \"Not Set\"\nERROR_CHORE_NOT_FOUND = \"Chore not found.\"  # Error for missing chore\nERROR_CHORE_NOT_FOUND_FMT = \"Chore '{}' not found\"  # Error format for missing chore\nERROR_INVALID_POINTS = \"Invalid points.\"  # Error for invalid points input\nERROR_KID_NOT_FOUND = \"Kid not found.\"  # Error for non-existent kid\nERROR_KID_NOT_FOUND_FMT = \"Kid '{}' not found\"  # Error format for missing kid\nERROR_NOT_AUTHORIZED_ACTION_FMT = \"Not authorized to {}.\"  # Auth error format\nERROR_NOT_AUTHORIZED_FMT = (\n    \"User not authorized to {} for this kid.\"  # Auth error format\n)\nERROR_PENALTY_NOT_FOUND = \"Penalty not found.\"  # Error for missing penalty\nERROR_PENALTY_NOT_FOUND_FMT = (\n    \"Penalty '{}' not found\"  # Error format for missing penalty\n)\nERROR_REWARD_NOT_FOUND = \"Reward not found.\"  # Error for missing reward\nERROR_REWARD_NOT_FOUND_FMT = \"Reward '{}' not found\"  # Error format for missing reward\nERROR_BONUS_NOT_FOUND = \"Bonus not found.\"  # Error for missing bonus\nERROR_BONUS_NOT_FOUND_FMT = \"Bonus '{}' not found\"  # Error format for missing bonus\nERROR_USER_NOT_AUTHORIZED = (\n    \"User is not authorized to perform this action.\"  # Auth error\n)\nMSG_NO_ENTRY_FOUND = \"No KidsChores entry found\"\n\n# Unknown States\nUNKNOWN_CHORE = \"Unknown Chore\"  # Error for unknown chore\nUNKNOWN_KID = \"Unknown Kid\"  # Error for unknown kid\nUNKNOWN_REWARD = \"Unknown Reward\"  # Error for unknown reward\n\n# -------------------- Parent Approval Workflow --------------------\nPARENT_APPROVAL_REQUIRED = True  # Enable parent approval for certain actions\nHA_USERNAME_LINK_ENABLED = True  # Enable linking kids to HA usernames\n\n\n# ---------------------------- Weekdays -----------------------------\nWEEKDAY_OPTIONS = {\n    \"mon\": \"Monday\",\n    \"tue\": \"Tuesday\",\n    \"wed\": \"Wednesday\",\n    \"thu\": \"Thursday\",\n    \"fri\": \"Friday\",\n    \"sat\": \"Saturday\",\n    \"sun\": \"Sunday\",\n}\n"
  },
  {
    "path": "custom_components/kidschores/coordinator.py",
    "content": "# File: coordinator.py\n\"\"\"Coordinator for the KidsChores integration.\n\nHandles data synchronization, chore claiming and approval, badge tracking,\nreward redemption, penalty application, and recurring chore handling.\nManages entities primarily using internal_id for consistency.\n\"\"\"\n\nimport asyncio\nimport uuid\nfrom calendar import monthrange\nfrom datetime import datetime, timedelta\nfrom typing import Any, Optional\n\nfrom homeassistant.auth.models import User\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.exceptions import HomeAssistantError\nfrom homeassistant.helpers.event import async_track_time_change\nfrom homeassistant.helpers import entity_registry as er\nfrom homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed\nfrom homeassistant.util import dt as dt_util\n\n\nfrom .const import (\n    ACHIEVEMENT_TYPE_DAILY_MIN,\n    ACHIEVEMENT_TYPE_STREAK,\n    ACHIEVEMENT_TYPE_TOTAL,\n    ACTION_APPROVE_CHORE,\n    ACTION_APPROVE_REWARD,\n    ACTION_DISAPPROVE_CHORE,\n    ACTION_DISAPPROVE_REWARD,\n    ACTION_REMIND_30,\n    ACTION_TITLE_APPROVE,\n    ACTION_TITLE_DISAPPROVE,\n    ACTION_TITLE_REMIND_30,\n    BADGE_THRESHOLD_TYPE_CHORE_COUNT,\n    BADGE_THRESHOLD_TYPE_POINTS,\n    CHALLENGE_TYPE_DAILY_MIN,\n    CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW,\n    CHORE_STATE_APPROVED,\n    CHORE_STATE_APPROVED_IN_PART,\n    CHORE_STATE_CLAIMED,\n    CHORE_STATE_CLAIMED_IN_PART,\n    CHORE_STATE_INDEPENDENT,\n    CHORE_STATE_OVERDUE,\n    CHORE_STATE_PARTIAL,\n    CHORE_STATE_PENDING,\n    CHORE_STATE_UNKNOWN,\n    CONF_ACHIEVEMENTS,\n    CONF_APPLICABLE_DAYS,\n    CONF_BADGES,\n    CONF_CHALLENGES,\n    CONF_CHORES,\n    CONF_ENABLE_MOBILE_NOTIFICATIONS,\n    CONF_ENABLE_PERSISTENT_NOTIFICATIONS,\n    CONF_KIDS,\n    CONF_MOBILE_NOTIFY_SERVICE,\n    CONF_NOTIFY_ON_APPROVAL,\n    CONF_NOTIFY_ON_CLAIM,\n    CONF_NOTIFY_ON_DISAPPROVAL,\n    CONF_PARENTS,\n    CONF_PENALTIES,\n    CONF_REWARDS,\n    CONF_BONUSES,\n    DATA_ACHIEVEMENTS,\n    DATA_BADGES,\n    DATA_CHALLENGES,\n    DATA_CHORES,\n    DATA_KIDS,\n    DATA_PARENTS,\n    DATA_PENDING_CHORE_APPROVALS,\n    DATA_PENDING_REWARD_APPROVALS,\n    DATA_PENALTIES,\n    DATA_REWARDS,\n    DATA_BONUSES,\n    DEFAULT_APPLICABLE_DAYS,\n    DEFAULT_BADGE_THRESHOLD,\n    DEFAULT_DAILY_RESET_TIME,\n    DEFAULT_ICON,\n    DEFAULT_MONTHLY_RESET_DAY,\n    DEFAULT_MULTIPLE_CLAIMS_PER_DAY,\n    DEFAULT_NOTIFY_ON_APPROVAL,\n    DEFAULT_NOTIFY_ON_CLAIM,\n    DEFAULT_NOTIFY_ON_DISAPPROVAL,\n    DEFAULT_PARTIAL_ALLOWED,\n    DEFAULT_PENALTY_ICON,\n    DEFAULT_PENALTY_POINTS,\n    DEFAULT_POINTS,\n    DEFAULT_POINTS_MULTIPLIER,\n    DEFAULT_REWARD_COST,\n    DEFAULT_REWARD_ICON,\n    DEFAULT_BONUS_ICON,\n    DEFAULT_BONUS_POINTS,\n    DEFAULT_WEEKLY_RESET_DAY,\n    DOMAIN,\n    FREQUENCY_BIWEEKLY,\n    FREQUENCY_CUSTOM,\n    FREQUENCY_DAILY,\n    FREQUENCY_MONTHLY,\n    FREQUENCY_NONE,\n    FREQUENCY_WEEKLY,\n    LOGGER,\n    UPDATE_INTERVAL,\n    WEEKDAY_OPTIONS,\n)\n\nfrom .storage_manager import KidsChoresStorageManager\nfrom .notification_helper import async_send_notification\n\n\nclass KidsChoresDataCoordinator(DataUpdateCoordinator):\n    \"\"\"Coordinator for KidsChores integration.\n\n    Manages data primarily using internal_id for entities.\n    \"\"\"\n\n    def __init__(\n        self,\n        hass: HomeAssistant,\n        config_entry: ConfigEntry,\n        storage_manager: KidsChoresStorageManager,\n    ):\n        \"\"\"Initialize the KidsChoresDataCoordinator.\"\"\"\n        super().__init__(\n            hass,\n            LOGGER,\n            name=f\"{DOMAIN}_coordinator\",\n            update_interval=timedelta(minutes=UPDATE_INTERVAL),\n        )\n        self.config_entry = config_entry\n        self.storage_manager = storage_manager\n        self._data: dict[str, Any] = {}\n\n    # -------------------------------------------------------------------------------------\n    # Migrate Data and Converters\n    # -------------------------------------------------------------------------------------\n\n    def _migrate_datetime(self, dt_str: str) -> str:\n        \"\"\"Convert a datetime string to a UTC-aware ISO string.\"\"\"\n        if not isinstance(dt_str, str):\n            return dt_str\n\n        try:\n            # Try to parse using Home Assistant’s utility first:\n            dt_obj = dt_util.parse_datetime(dt_str)\n            if dt_obj is None:\n                # Fallback using fromisoformat\n                dt_obj = datetime.fromisoformat(dt_str)\n            # If naive, assume local time and make it aware:\n            if dt_obj.tzinfo is None:\n                dt_obj = dt_obj.replace(\n                    tzinfo=dt_util.get_time_zone(self.hass.config.time_zone)\n                )\n            # Convert to UTC\n            dt_obj_utc = dt_util.as_utc(dt_obj)\n            return dt_obj_utc.isoformat()\n        except Exception as err:\n            LOGGER.warning(\"Error migrating datetime '%s': %s\", dt_str, err)\n            return dt_str\n\n    def _migrate_stored_datetimes(self):\n        \"\"\"Walk through stored data and convert known datetime fields to UTC-aware ISO strings.\"\"\"\n        # For each chore, migrate due_date, last_completed, and last_claimed\n        for chore in self._data.get(DATA_CHORES, {}).values():\n            if chore.get(\"due_date\"):\n                chore[\"due_date\"] = self._migrate_datetime(chore[\"due_date\"])\n            if chore.get(\"last_completed\"):\n                chore[\"last_completed\"] = self._migrate_datetime(\n                    chore[\"last_completed\"]\n                )\n            if chore.get(\"last_claimed\"):\n                chore[\"last_claimed\"] = self._migrate_datetime(chore[\"last_claimed\"])\n        # Also, migrate timestamps in pending approvals\n        for approval in self._data.get(DATA_PENDING_CHORE_APPROVALS, []):\n            if approval.get(\"timestamp\"):\n                approval[\"timestamp\"] = self._migrate_datetime(approval[\"timestamp\"])\n        for approval in self._data.get(DATA_PENDING_REWARD_APPROVALS, []):\n            if approval.get(\"timestamp\"):\n                approval[\"timestamp\"] = self._migrate_datetime(approval[\"timestamp\"])\n\n        # Migrate datetime on Challenges\n        for challenge in self._data.get(DATA_CHALLENGES, {}).values():\n            start_date = challenge.get(\"start_date\")\n            if not isinstance(start_date, str) or not start_date.strip():\n                challenge[\"start_date\"] = None\n            else:\n                challenge[\"start_date\"] = self._migrate_datetime(start_date)\n\n            end_date = challenge.get(\"end_date\")\n            if not isinstance(end_date, str) or not end_date.strip():\n                challenge[\"end_date\"] = None\n            else:\n                challenge[\"end_date\"] = self._migrate_datetime(end_date)\n\n    def _migrate_chore_data(self):\n        \"\"\"Migrate each chore's data to include new fields if missing.\n\n        This method iterates over each chore entry in the stored data and ensures\n        that the following keys are present:\n        - CONF_APPLICABLE_DAYS (defaults to DEFAULT_APPLICABLE_DAYS)\n        - CONF_NOTIFY_ON_CLAIM (defaults to DEFAULT_NOTIFY_ON_CLAIM)\n        - CONF_NOTIFY_ON_APPROVAL (defaults to DEFAULT_NOTIFY_ON_APPROVAL)\n        - CONF_NOTIFY_ON_DISAPPROVAL (defaults to DEFAULT_NOTIFY_ON_DISAPPROVAL)\n        \"\"\"\n        chores = self._data.get(DATA_CHORES, {})\n        for chore in chores.values():\n            chore.setdefault(CONF_APPLICABLE_DAYS, DEFAULT_APPLICABLE_DAYS)\n            chore.setdefault(CONF_NOTIFY_ON_CLAIM, DEFAULT_NOTIFY_ON_CLAIM)\n            chore.setdefault(CONF_NOTIFY_ON_APPROVAL, DEFAULT_NOTIFY_ON_APPROVAL)\n            chore.setdefault(CONF_NOTIFY_ON_DISAPPROVAL, DEFAULT_NOTIFY_ON_DISAPPROVAL)\n        LOGGER.info(\"Chore data migration complete.\")\n\n    # -------------------------------------------------------------------------------------\n    # Normalize Lists\n    # -------------------------------------------------------------------------------------\n\n    def _normalize_kid_lists(self, kid_info: dict[str, Any]) -> None:\n        \"Normalize lists and ensuring they are not dict\"\n        for key in [\n            \"claimed_chores\",\n            \"approved_chores\",\n            \"pending_rewards\",\n            \"redeemed_rewards\",\n        ]:\n            if not isinstance(kid_info.get(key), list):\n                kid_info[key] = []\n\n    # -------------------------------------------------------------------------------------\n    # Periodic + First Refresh\n    # -------------------------------------------------------------------------------------\n\n    async def _async_update_data(self):\n        \"\"\"Periodic update.\"\"\"\n        try:\n            # Check overdue chores\n            await self._check_overdue_chores()\n\n            # Notify entities of changes\n            self.async_update_listeners()\n\n            return self._data\n        except Exception as err:\n            raise UpdateFailed(f\"Error updating KidsChores data: {err}\") from err\n\n    async def async_config_entry_first_refresh(self):\n        \"\"\"Load from storage and merge config options.\"\"\"\n        stored_data = self.storage_manager.get_data()\n        if stored_data:\n            self._data = stored_data\n\n            # Migrate any datetime fields in stored data to UTC-aware strings\n            self._migrate_stored_datetimes()\n\n            # Migrate chore data and add new fields\n            self._migrate_chore_data()\n\n        else:\n            self._data = {\n                DATA_KIDS: {},\n                DATA_CHORES: {},\n                DATA_BADGES: {},\n                DATA_REWARDS: {},\n                DATA_PARENTS: {},\n                DATA_PENALTIES: {},\n                DATA_BONUSES: {},\n                DATA_ACHIEVEMENTS: {},\n                DATA_CHALLENGES: {},\n                DATA_PENDING_CHORE_APPROVALS: [],\n                DATA_PENDING_REWARD_APPROVALS: [],\n            }\n\n        if not isinstance(self._data.get(DATA_PENDING_CHORE_APPROVALS), list):\n            self._data[DATA_PENDING_CHORE_APPROVALS] = []\n        if not isinstance(self._data.get(DATA_PENDING_REWARD_APPROVALS), list):\n            self._data[DATA_PENDING_REWARD_APPROVALS] = []\n\n        # Register daily/weekly/monthly resets\n        async_track_time_change(\n            self.hass, self._reset_all_chore_counts, **DEFAULT_DAILY_RESET_TIME\n        )\n\n        # Merge config entry data (options) into the stored data\n        self._initialize_data_from_config()\n\n        # Normalize all kids list fields\n        for kid in self._data.get(DATA_KIDS, {}).values():\n            self._normalize_kid_lists(kid)\n\n        self._persist()\n        await super().async_config_entry_first_refresh()\n\n    # -------------------------------------------------------------------------------------\n    # Data Initialization from Config\n    # -------------------------------------------------------------------------------------\n\n    def _initialize_data_from_config(self):\n        \"\"\"Merge config_entry options with stored data structures using internal_id.\"\"\"\n        options = self.config_entry.options\n\n        # Retrieve configuration dictionaries from config entry options\n        config_sections = {\n            DATA_KIDS: options.get(CONF_KIDS, {}),\n            DATA_PARENTS: options.get(CONF_PARENTS, {}),\n            DATA_CHORES: options.get(CONF_CHORES, {}),\n            DATA_BADGES: options.get(CONF_BADGES, {}),\n            DATA_REWARDS: options.get(CONF_REWARDS, {}),\n            DATA_PENALTIES: options.get(CONF_PENALTIES, {}),\n            DATA_BONUSES: options.get(CONF_BONUSES, {}),\n            DATA_ACHIEVEMENTS: options.get(CONF_ACHIEVEMENTS, {}),\n            DATA_CHALLENGES: options.get(CONF_CHALLENGES, {}),\n        }\n\n        # Ensure minimal structure\n        self._ensure_minimal_structure()\n\n        # Initialize each section using private helper\n        for section_key, data_dict in config_sections.items():\n            init_func = getattr(self, f\"_initialize_{section_key}\", None)\n            if init_func:\n                init_func(data_dict)\n            else:\n                self._data.setdefault(section_key, data_dict)\n                LOGGER.warning(\"No initializer found for section '%s'\", section_key)\n\n        # Recalculate Badges on reload\n        self._recalculate_all_badges()\n\n    def _ensure_minimal_structure(self):\n        \"\"\"Ensure that all necessary data sections are present.\"\"\"\n        for key in [\n            DATA_KIDS,\n            DATA_PARENTS,\n            DATA_CHORES,\n            DATA_BADGES,\n            DATA_REWARDS,\n            DATA_PENALTIES,\n            DATA_BONUSES,\n            DATA_ACHIEVEMENTS,\n            DATA_CHALLENGES,\n        ]:\n            self._data.setdefault(key, {})\n\n        for key in [DATA_PENDING_CHORE_APPROVALS, DATA_PENDING_REWARD_APPROVALS]:\n            if not isinstance(self._data.get(key), list):\n                self._data[key] = []\n\n    # -------------------------------------------------------------------------------------\n    # Helpers to Sync Entities from config\n    # -------------------------------------------------------------------------------------\n\n    def _initialize_kids(self, kids_dict: dict[str, Any]):\n        self._sync_entities(DATA_KIDS, kids_dict, self._create_kid, self._update_kid)\n\n    def _initialize_parents(self, parents_dict: dict[str, Any]):\n        self._sync_entities(\n            DATA_PARENTS, parents_dict, self._create_parent, self._update_parent\n        )\n\n    def _initialize_chores(self, chores_dict: dict[str, Any]):\n        self._sync_entities(\n            DATA_CHORES, chores_dict, self._create_chore, self._update_chore\n        )\n\n    def _initialize_badges(self, badges_dict: dict[str, Any]):\n        self._sync_entities(\n            DATA_BADGES, badges_dict, self._create_badge, self._update_badge\n        )\n\n    def _initialize_rewards(self, rewards_dict: dict[str, Any]):\n        self._sync_entities(\n            DATA_REWARDS, rewards_dict, self._create_reward, self._update_reward\n        )\n\n    def _initialize_penalties(self, penalties_dict: dict[str, Any]):\n        self._sync_entities(\n            DATA_PENALTIES, penalties_dict, self._create_penalty, self._update_penalty\n        )\n\n    def _initialize_achievements(self, achievements_dict: dict[str, Any]):\n        self._sync_entities(\n            DATA_ACHIEVEMENTS,\n            achievements_dict,\n            self._create_achievement,\n            self._update_achievement,\n        )\n\n    def _initialize_challenges(self, challenges_dict: dict[str, Any]):\n        self._sync_entities(\n            DATA_CHALLENGES,\n            challenges_dict,\n            self._create_challenge,\n            self._update_challenge,\n        )\n\n    def _initialize_bonuses(self, bonuses_dict: dict[str, Any]):\n        self._sync_entities(\n            DATA_BONUSES, bonuses_dict, self._create_bonus, self._update_bonus\n        )\n\n    def _sync_entities(\n        self,\n        section: str,\n        config_data: dict[str, Any],\n        create_method,\n        update_method,\n    ):\n        \"\"\"Synchronize entities in a given data section based on config_data.\"\"\"\n        existing_ids = set(self._data[section].keys())\n        config_ids = set(config_data.keys())\n\n        # Identify entities to remove\n        entities_to_remove = existing_ids - config_ids\n        for entity_id in entities_to_remove:\n            # Remove entity from data\n            del self._data[section][entity_id]\n\n            # Remove entity from HA registry\n            self._remove_entities_in_ha(section, entity_id)\n            if section == DATA_CHORES:\n                for kid_id in self.kids_data.keys():\n                    self._remove_kid_chore_entities(kid_id, entity_id)\n\n            # Perform general clean-up\n            self._cleanup_all_links()\n\n            # Remove deleted kids from parents list\n            self._cleanup_parent_assignments()\n\n            # Remove chore approvals on chore delete\n            self._cleanup_pending_chore_approvals()\n\n            # Remove reward approvals on reward delete\n            if section == DATA_REWARDS:\n                self._cleanup_pending_reward_approvals()\n\n        # Add or update entities\n        for entity_id, entity_body in config_data.items():\n            if entity_id not in self._data[section]:\n                create_method(entity_id, entity_body)\n            else:\n                update_method(entity_id, entity_body)\n\n        # Remove orphaned shared chore sensors.\n        if section == DATA_CHORES:\n            self.hass.async_create_task(self._remove_orphaned_shared_chore_sensors())\n\n        # Remove orphaned achievement and challenges sensors\n        self.hass.async_create_task(self._remove_orphaned_achievement_entities())\n        self.hass.async_create_task(self._remove_orphaned_challenge_entities())\n\n    def _cleanup_all_links(self) -> None:\n        \"\"\"Run all cross-entity cleanup routines.\"\"\"\n        self._cleanup_deleted_kid_references()\n        self._cleanup_deleted_chore_references()\n        self._cleanup_deleted_chore_in_achievements()\n        self._cleanup_deleted_chore_in_challenges()\n\n    def _remove_entities_in_ha(self, section: str, item_id: str):\n        \"\"\"Remove all platform entities whose unique_id references the given item_id.\"\"\"\n        ent_reg = er.async_get(self.hass)\n        for entity_entry in list(ent_reg.entities.values()):\n            if str(item_id) in str(entity_entry.unique_id):\n                ent_reg.async_remove(entity_entry.entity_id)\n                LOGGER.debug(\n                    \"Auto-removed entity '%s' with unique_id '%s' from registry\",\n                    entity_entry.entity_id,\n                    entity_entry.unique_id,\n                )\n\n    async def _remove_orphaned_shared_chore_sensors(self):\n        \"\"\"Remove SharedChoreGlobalStateSensor entities for chores no longer marked as shared.\"\"\"\n        ent_reg = er.async_get(self.hass)\n        prefix = f\"{self.config_entry.entry_id}_\"\n        suffix = \"_global_state\"\n        for entity_entry in list(ent_reg.entities.values()):\n            if (\n                entity_entry.domain == \"sensor\"\n                and entity_entry.unique_id.startswith(prefix)\n                and entity_entry.unique_id.endswith(suffix)\n            ):\n                chore_id = entity_entry.unique_id[len(prefix) : -len(suffix)]\n                chore_info = self.chores_data.get(chore_id)\n                if not chore_info or not chore_info.get(\"shared_chore\", False):\n                    ent_reg.async_remove(entity_entry.entity_id)\n                    LOGGER.debug(\n                        \"Removed orphaned SharedChoreGlobalStateSensor: %s\",\n                        entity_entry.entity_id,\n                    )\n\n    async def _remove_orphaned_achievement_entities(self) -> None:\n        \"\"\"Remove achievement progress entities for kids that are no longer assigned.\"\"\"\n        ent_reg = er.async_get(self.hass)\n        prefix = f\"{self.config_entry.entry_id}_\"\n        suffix = \"_achievement_progress\"\n        for entity_entry in list(ent_reg.entities.values()):\n            if (\n                entity_entry.domain == \"sensor\"\n                and entity_entry.unique_id.startswith(prefix)\n                and entity_entry.unique_id.endswith(suffix)\n            ):\n                core_id = entity_entry.unique_id[len(prefix) : -len(suffix)]\n                parts = core_id.split(\"_\", 1)\n                if len(parts) != 2:\n                    continue\n\n                kid_id, achievement_id = parts\n                achievement = self._data.get(DATA_ACHIEVEMENTS, {}).get(achievement_id)\n                if not achievement or kid_id not in achievement.get(\n                    \"assigned_kids\", []\n                ):\n                    ent_reg.async_remove(entity_entry.entity_id)\n                    LOGGER.debug(\n                        \"Removed orphaned achievement progress sensor '%s' because kid '%s' is not assigned to achievement '%s'\",\n                        entity_entry.entity_id,\n                        kid_id,\n                        achievement_id,\n                    )\n\n    async def _remove_orphaned_challenge_entities(self) -> None:\n        \"\"\"Remove challenge progress sensor entities for kids no longer assigned.\"\"\"\n        ent_reg = er.async_get(self.hass)\n        prefix = f\"{self.config_entry.entry_id}_\"\n        suffix = \"_challenge_progress\"\n        for entity_entry in list(ent_reg.entities.values()):\n            if (\n                entity_entry.domain == \"sensor\"\n                and entity_entry.unique_id.startswith(prefix)\n                and entity_entry.unique_id.endswith(suffix)\n            ):\n                core_id = entity_entry.unique_id[len(prefix) : -len(suffix)]\n                parts = core_id.split(\"_\", 1)\n                if len(parts) != 2:\n                    continue\n\n                kid_id, challenge_id = parts\n                challenge = self._data.get(DATA_CHALLENGES, {}).get(challenge_id)\n                if not challenge or kid_id not in challenge.get(\"assigned_kids\", []):\n                    ent_reg.async_remove(entity_entry.entity_id)\n                    LOGGER.debug(\n                        \"Removed orphaned challenge progress sensor '%s' because kid '%s' is not assigned to challenge '%s'\",\n                        entity_entry.entity_id,\n                        kid_id,\n                        challenge_id,\n                    )\n\n    def _remove_kid_chore_entities(self, kid_id: str, chore_id: str) -> None:\n        \"\"\"Remove all kid-specific chore entities for a given kid and chore.\"\"\"\n        ent_reg = er.async_get(self.hass)\n        for entity_entry in list(ent_reg.entities.values()):\n            if (kid_id in entity_entry.unique_id) and (\n                chore_id in entity_entry.unique_id\n            ):\n                ent_reg.async_remove(entity_entry.entity_id)\n                LOGGER.debug(\n                    \"Removed kid-specific entity '%s' for kid '%s' and chore '%s'\",\n                    entity_entry.entity_id,\n                    kid_id,\n                    chore_id,\n                )\n\n    def _cleanup_chore_from_kid(self, kid_id: str, chore_id: str) -> None:\n        \"\"\"Remove references to a specific chore from a kid's data.\"\"\"\n        kid = self.kids_data.get(kid_id)\n        if not kid:\n            return\n\n        # Remove from lists if present\n        for key in [\"claimed_chores\", \"approved_chores\"]:\n            if chore_id in kid.get(key, []):\n                kid[key] = [c for c in kid[key] if c != chore_id]\n                LOGGER.debug(\n                    \"Removed chore '%s' from kid '%s' list '%s'\", chore_id, kid_id, key\n                )\n\n        # Remove from dictionary fields if present\n        for dict_key in [\"chore_claims\", \"chore_approvals\"]:\n            if chore_id in kid.get(dict_key, {}):\n                kid[dict_key].pop(chore_id)\n                LOGGER.debug(\n                    \"Removed chore '%s' from kid '%s' dict '%s'\",\n                    chore_id,\n                    kid_id,\n                    dict_key,\n                )\n\n        # Remove from chore streaks if present\n        if \"chore_streaks\" in kid and chore_id in kid[\"chore_streaks\"]:\n            kid[\"chore_streaks\"].pop(chore_id)\n            LOGGER.debug(\n                \"Removed chore streak for chore '%s' from kid '%s'\", chore_id, kid_id\n            )\n\n        # Remove any pending chore approvals for this kid and chore\n        self._data[DATA_PENDING_CHORE_APPROVALS] = [\n            ap\n            for ap in self._data.get(DATA_PENDING_CHORE_APPROVALS, [])\n            if not (ap.get(\"kid_id\") == kid_id and ap.get(\"chore_id\") == chore_id)\n        ]\n\n    def _cleanup_pending_chore_approvals(self) -> None:\n        \"\"\"Remove any pending chore approvals for chore IDs that no longer exist.\"\"\"\n        valid_chore_ids = set(self._data.get(DATA_CHORES, {}).keys())\n        self._data[DATA_PENDING_CHORE_APPROVALS] = [\n            ap\n            for ap in self._data.get(DATA_PENDING_CHORE_APPROVALS, [])\n            if ap.get(\"chore_id\") in valid_chore_ids\n        ]\n\n    def _cleanup_pending_reward_approvals(self) -> None:\n        \"\"\"Remove any pending reward approvals for reward IDs that no longer exist.\"\"\"\n        valid_reward_ids = set(self._data.get(DATA_REWARDS, {}).keys())\n        self._data[DATA_PENDING_REWARD_APPROVALS] = [\n            approval\n            for approval in self._data.get(DATA_PENDING_REWARD_APPROVALS, [])\n            if approval.get(\"reward_id\") in valid_reward_ids\n        ]\n\n    def _cleanup_deleted_kid_references(self) -> None:\n        \"\"\"Remove references to kids that no longer exist from other sections.\"\"\"\n        valid_kid_ids = set(self.kids_data.keys())\n\n        # Remove deleted kid IDs from all chore assignments\n        for chore in self._data.get(DATA_CHORES, {}).values():\n            if \"assigned_kids\" in chore:\n                original = chore[\"assigned_kids\"]\n                filtered = [kid for kid in original if kid in valid_kid_ids]\n                if filtered != original:\n                    chore[\"assigned_kids\"] = filtered\n                    LOGGER.debug(\n                        \"Cleaned up assigned_kids in chore '%s'\", chore.get(\"name\")\n                    )\n\n        # Remove progress in achievements and challenges\n        for section in [DATA_ACHIEVEMENTS, DATA_CHALLENGES]:\n            for entity in self._data.get(section, {}).values():\n                progress = entity.get(\"progress\", {})\n                keys_to_remove = [kid for kid in progress if kid not in valid_kid_ids]\n                for kid in keys_to_remove:\n                    del progress[kid]\n                    LOGGER.debug(\n                        \"Removed progress for deleted kid '%s' in section '%s'\",\n                        kid,\n                        section,\n                    )\n                if \"assigned_kids\" in entity:\n                    original_assigned = entity[\"assigned_kids\"]\n                    filtered_assigned = [\n                        kid for kid in original_assigned if kid in valid_kid_ids\n                    ]\n                    if filtered_assigned != original_assigned:\n                        entity[\"assigned_kids\"] = filtered_assigned\n                        LOGGER.debug(\n                            \"Cleaned up assigned_kids in %s '%s'\",\n                            section,\n                            entity.get(\"name\"),\n                        )\n\n    def _cleanup_deleted_chore_references(self) -> None:\n        \"\"\"Remove references to chores that no longer exist from kid data.\"\"\"\n        valid_chore_ids = set(self.chores_data.keys())\n        for kid in self.kids_data.values():\n            # Clean up list fields\n            for key in [\"claimed_chores\", \"approved_chores\"]:\n                if key in kid:\n                    original = kid[key]\n                    filtered = [chore for chore in original if chore in valid_chore_ids]\n                    if filtered != original:\n                        kid[key] = filtered\n\n            # Clean up dictionary fields\n            for dict_key in [\"chore_claims\", \"chore_approvals\"]:\n                if dict_key in kid:\n                    kid[dict_key] = {\n                        chore: count\n                        for chore, count in kid[dict_key].items()\n                        if chore in valid_chore_ids\n                    }\n\n            # Clean up chore streaks\n            if \"chore_streaks\" in kid:\n                for chore in list(kid[\"chore_streaks\"].keys()):\n                    if chore not in valid_chore_ids:\n                        del kid[\"chore_streaks\"][chore]\n                        LOGGER.debug(\n                            \"Removed chore streak for deleted chore '%s'\", chore\n                        )\n\n    def _cleanup_parent_assignments(self) -> None:\n        \"\"\"Remove any kid IDs from parent's 'associated_kids' that no longer exist.\"\"\"\n        valid_kid_ids = set(self.kids_data.keys())\n        for parent in self._data.get(DATA_PARENTS, {}).values():\n            original = parent.get(\"associated_kids\", [])\n            filtered = [kid_id for kid_id in original if kid_id in valid_kid_ids]\n            if filtered != original:\n                parent[\"associated_kids\"] = filtered\n                LOGGER.debug(\n                    \"Cleaned up associated_kids for parent '%s'. New list: %s\",\n                    parent.get(\"name\"),\n                    filtered,\n                )\n\n    def _cleanup_deleted_chore_in_achievements(self) -> None:\n        \"\"\"Clear selected_chore_id in achievements if the chore no longer exists.\"\"\"\n        valid_chore_ids = set(self.chores_data.keys())\n        for achievement in self._data.get(DATA_ACHIEVEMENTS, {}).values():\n            selected = achievement.get(\"selected_chore_id\")\n            if selected and selected not in valid_chore_ids:\n                achievement[\"selected_chore_id\"] = \"\"\n                LOGGER.debug(\n                    \"Cleared selected_chore_id in achievement '%s'\",\n                    achievement.get(\"name\"),\n                )\n\n    def _cleanup_deleted_chore_in_challenges(self) -> None:\n        \"\"\"Clear selected_chore_id in challenges if the chore no longer exists.\"\"\"\n        valid_chore_ids = set(self.chores_data.keys())\n        for challenge in self._data.get(DATA_CHALLENGES, {}).values():\n            selected = challenge.get(\"selected_chore_id\")\n            if selected and selected not in valid_chore_ids:\n                challenge[\"selected_chore_id\"] = \"\"\n                LOGGER.debug(\n                    \"Cleared selected_chore_id in challenge '%s'\", challenge.get(\"name\")\n                )\n\n    # -------------------------------------------------------------------------------------\n    # Create/Update Entities\n    # (Kids, Parents, Chores, Badges, Rewards, Penalties, Achievements and Challenges)\n    # -------------------------------------------------------------------------------------\n\n    # -- Kids\n    def _create_kid(self, kid_id: str, kid_data: dict[str, Any]):\n        self._data[DATA_KIDS][kid_id] = {\n            \"name\": kid_data.get(\"name\", \"\"),\n            \"points\": kid_data.get(\"points\", 0.0),\n            \"badges\": kid_data.get(\"badges\", []),\n            \"claimed_chores\": kid_data.get(\"claimed_chores\", []),\n            \"approved_chores\": kid_data.get(\"approved_chores\", []),\n            \"completed_chores_today\": kid_data.get(\"completed_chores_today\", 0),\n            \"completed_chores_weekly\": kid_data.get(\"completed_chores_weekly\", 0),\n            \"completed_chores_monthly\": kid_data.get(\"completed_chores_monthly\", 0),\n            \"completed_chores_total\": kid_data.get(\"completed_chores_total\", 0),\n            \"ha_user_id\": kid_data.get(\"ha_user_id\"),\n            \"internal_id\": kid_id,\n            \"points_multiplier\": kid_data.get(\"points_multiplier\", 1.0),\n            \"reward_claims\": kid_data.get(\"reward_claims\", {}),\n            \"reward_approvals\": kid_data.get(\"reward_approvals\", {}),\n            \"chore_claims\": kid_data.get(\"chore_claims\", {}),\n            \"chore_approvals\": kid_data.get(\"chore_approvals\", {}),\n            \"penalty_applies\": kid_data.get(\"penalty_applies\", {}),\n            \"bonus_applies\": kid_data.get(\"bonus_applies\", {}),\n            \"pending_rewards\": kid_data.get(\"pending_rewards\", []),\n            \"redeemed_rewards\": kid_data.get(\"redeemed_rewards\", []),\n            \"points_earned_today\": kid_data.get(\"points_earned_today\", 0.0),\n            \"points_earned_weekly\": kid_data.get(\"points_earned_weekly\", 0.0),\n            \"points_earned_monthly\": kid_data.get(\"points_earned_monthly\", 0.0),\n            \"max_points_ever\": kid_data.get(\"max_points_ever\", 0.0),\n            \"enable_notifications\": kid_data.get(\"enable_notifications\", True),\n            \"mobile_notify_service\": kid_data.get(\"mobile_notify_service\", \"\"),\n            \"use_persistent_notifications\": kid_data.get(\n                \"use_persistent_notifications\", True\n            ),\n            \"chore_streaks\": {},\n            \"overall_chore_streak\": 0,\n            \"last_chore_date\": None,\n            \"overdue_chores\": [],\n            \"overdue_notifications\": {},\n        }\n\n        self._normalize_kid_lists(self._data[DATA_KIDS][kid_id])\n\n        LOGGER.debug(\n            \"Added new kid '%s' with ID: %s\",\n            self._data[DATA_KIDS][kid_id][\"name\"],\n            kid_id,\n        )\n\n    def _update_kid(self, kid_id: str, kid_data: dict[str, Any]):\n        kid_info = self._data[DATA_KIDS][kid_id]\n        # Overwrite or set default if not present\n        kid_info[\"name\"] = kid_data.get(\"name\", kid_info[\"name\"])\n        kid_info[\"ha_user_id\"] = kid_data.get(\"ha_user_id\", kid_info[\"ha_user_id\"])\n        kid_info.setdefault(\"reward_claims\", kid_data.get(\"reward_claims\", {}))\n        kid_info.setdefault(\"reward_approvals\", kid_data.get(\"reward_approvals\", {}))\n        kid_info.setdefault(\"chore_claims\", kid_data.get(\"chore_claims\", {}))\n        kid_info.setdefault(\"chore_approvals\", kid_data.get(\"chore_approvals\", {}))\n        kid_info.setdefault(\"penalty_applies\", kid_data.get(\"penalty_applies\", {}))\n        kid_info.setdefault(\"bonus_applies\", kid_data.get(\"bonus_applies\", {}))\n        kid_info.setdefault(\"pending_rewards\", kid_data.get(\"pending_rewards\", []))\n        kid_info.setdefault(\"redeemed_rewards\", kid_data.get(\"redeemed_rewards\", []))\n        kid_info.setdefault(\n            \"points_earned_today\", kid_data.get(\"points_earned_today\", 0.0)\n        )\n        kid_info.setdefault(\n            \"points_earned_weekly\", kid_data.get(\"points_earned_weekly\", 0.0)\n        )\n        kid_info.setdefault(\n            \"points_earned_monthly\", kid_data.get(\"points_earned_monthly\", 0.0)\n        )\n        kid_info.setdefault(\"max_points_ever\", kid_data.get(\"max_points_ever\", 0.0))\n        kid_info.setdefault(\"points_multiplier\", kid_data.get(\"points_multiplier\", 1.0))\n        kid_info[\"enable_notifications\"] = kid_data.get(\n            \"enable_notifications\", kid_info.get(\"enable_notifications\", True)\n        )\n        kid_info[\"mobile_notify_service\"] = kid_data.get(\n            \"mobile_notify_service\", kid_info.get(\"mobile_notify_service\", \"\")\n        )\n        kid_info[\"use_persistent_notifications\"] = kid_data.get(\n            \"use_persistent_notifications\",\n            kid_info.get(\"use_persistent_notifications\", True),\n        )\n        kid_info.setdefault(\"chore_streaks\", {})\n        kid_info.setdefault(\"overall_chore_streak\", 0)\n        kid_info.setdefault(\"last_chore_date\", None)\n        kid_info.setdefault(\"overdue_chores\", [])\n        kid_info.setdefault(\"overdue_notifications\", {})\n\n        self._normalize_kid_lists(self._data[DATA_KIDS][kid_id])\n\n        LOGGER.debug(\"Updated kid '%s' with ID: %s\", kid_info[\"name\"], kid_id)\n\n    # -- Parents\n    def _create_parent(self, parent_id: str, parent_data: dict[str, Any]):\n        associated_kids_ids = []\n        for kid_id in parent_data.get(\"associated_kids\", []):\n            if kid_id in self.kids_data:\n                associated_kids_ids.append(kid_id)\n            else:\n                LOGGER.warning(\n                    \"Parent '%s': Kid ID '%s' not found. Skipping assignment to parent\",\n                    parent_data.get(\"name\", parent_id),\n                    kid_id,\n                )\n\n        self._data[DATA_PARENTS][parent_id] = {\n            \"name\": parent_data.get(\"name\", \"\"),\n            \"ha_user_id\": parent_data.get(\"ha_user_id\", \"\"),\n            \"associated_kids\": associated_kids_ids,\n            \"enable_notifications\": parent_data.get(\"enable_notifications\", True),\n            \"mobile_notify_service\": parent_data.get(\"mobile_notify_service\", \"\"),\n            \"use_persistent_notifications\": parent_data.get(\n                \"use_persistent_notifications\", True\n            ),\n            \"internal_id\": parent_id,\n        }\n        LOGGER.debug(\n            \"Added new parent '%s' with ID: %s\",\n            self._data[DATA_PARENTS][parent_id][\"name\"],\n            parent_id,\n        )\n\n    def _update_parent(self, parent_id: str, parent_data: dict[str, Any]):\n        parent_info = self._data[DATA_PARENTS][parent_id]\n        parent_info[\"name\"] = parent_data.get(\"name\", parent_info[\"name\"])\n        parent_info[\"ha_user_id\"] = parent_data.get(\n            \"ha_user_id\", parent_info[\"ha_user_id\"]\n        )\n\n        # Update associated_kids\n        updated_kids = []\n        for kid_id in parent_data.get(\"associated_kids\", []):\n            if kid_id in self.kids_data:\n                updated_kids.append(kid_id)\n            else:\n                LOGGER.warning(\n                    \"Parent '%s': Kid ID '%s' not found. Skipping assignment\",\n                    parent_info[\"name\"],\n                    kid_id,\n                )\n        parent_info[\"associated_kids\"] = updated_kids\n        parent_info[\"enable_notifications\"] = parent_data.get(\n            \"enable_notifications\", parent_info.get(\"enable_notifications\", True)\n        )\n        parent_info[\"mobile_notify_service\"] = parent_data.get(\n            \"mobile_notify_service\", parent_info.get(\"mobile_notify_service\", \"\")\n        )\n        parent_info[\"use_persistent_notifications\"] = parent_data.get(\n            \"use_persistent_notifications\",\n            parent_info.get(\"use_persistent_notifications\", True),\n        )\n\n        LOGGER.debug(\"Updated parent '%s' with ID: %s\", parent_info[\"name\"], parent_id)\n\n    # -- Chores\n    def _create_chore(self, chore_id: str, chore_data: dict[str, Any]):\n        assigned_kids_ids = []\n        for kid_name in chore_data.get(\"assigned_kids\", []):\n            kid_id = self._get_kid_id_by_name(kid_name)\n            if kid_id:\n                assigned_kids_ids.append(kid_id)\n            else:\n                LOGGER.warning(\n                    \"Chore '%s': Kid name '%s' not found. Skipping assignment\",\n                    chore_data.get(\"name\", chore_id),\n                    kid_name,\n                )\n\n        # If chore is recurring, set due_date to creation date if not set\n        freq = chore_data.get(\"recurring_frequency\", FREQUENCY_NONE)\n        if freq != FREQUENCY_NONE and not chore_data.get(\"due_date\"):\n            now_local = dt_util.utcnow().astimezone(\n                dt_util.get_time_zone(self.hass.config.time_zone)\n            )\n            # Force the time to 23:59:00 (and zero microseconds)\n            default_due = now_local.replace(hour=23, minute=59, second=0, microsecond=0)\n            chore_data[\"due_date\"] = default_due.isoformat()\n            LOGGER.debug(\n                \"Chore '%s' has freq '%s' but no due_date. Defaulting to 23:59 local time: %s\",\n                chore_data.get(\"name\", chore_id),\n                freq,\n                chore_data[\"due_date\"],\n            )\n\n        self._data[DATA_CHORES][chore_id] = {\n            \"name\": chore_data.get(\"name\", \"\"),\n            \"state\": chore_data.get(\"state\", CHORE_STATE_PENDING),\n            \"default_points\": chore_data.get(\"default_points\", DEFAULT_POINTS),\n            \"allow_multiple_claims_per_day\": chore_data.get(\n                \"allow_multiple_claims_per_day\", DEFAULT_MULTIPLE_CLAIMS_PER_DAY\n            ),\n            \"partial_allowed\": chore_data.get(\n                \"partial_allowed\", DEFAULT_PARTIAL_ALLOWED\n            ),\n            \"description\": chore_data.get(\"description\", \"\"),\n            \"chore_labels\": chore_data.get(\"chore_labels\", []),\n            \"icon\": chore_data.get(\"icon\", DEFAULT_ICON),\n            \"shared_chore\": chore_data.get(\"shared_chore\", False),\n            \"assigned_kids\": assigned_kids_ids,\n            \"recurring_frequency\": chore_data.get(\n                \"recurring_frequency\", FREQUENCY_NONE\n            ),\n            \"custom_interval\": chore_data.get(\"custom_interval\")\n            if chore_data.get(\"recurring_frequency\") == FREQUENCY_CUSTOM\n            else None,\n            \"custom_interval_unit\": chore_data.get(\"custom_interval_unit\")\n            if chore_data.get(\"recurring_frequency\") == FREQUENCY_CUSTOM\n            else None,\n            \"due_date\": chore_data.get(\"due_date\"),\n            \"last_completed\": chore_data.get(\"last_completed\"),\n            \"last_claimed\": chore_data.get(\"last_claimed\"),\n            \"applicable_days\": chore_data.get(\"applicable_days\", []),\n            \"notify_on_claim\": chore_data.get(\n                \"notify_on_claim\", DEFAULT_NOTIFY_ON_CLAIM\n            ),\n            \"notify_on_approval\": chore_data.get(\n                \"notify_on_approval\", DEFAULT_NOTIFY_ON_APPROVAL\n            ),\n            \"notify_on_disapproval\": chore_data.get(\n                \"notify_on_disapproval\", DEFAULT_NOTIFY_ON_DISAPPROVAL\n            ),\n            \"internal_id\": chore_id,\n        }\n        LOGGER.debug(\n            \"Added new chore '%s' with ID: %s\",\n            self._data[DATA_CHORES][chore_id][\"name\"],\n            chore_id,\n        )\n\n        # Notify Kids of new chore\n        new_name = self._data[DATA_CHORES][chore_id][\"name\"]\n        due_date = self._data[DATA_CHORES][chore_id][\"due_date\"]\n        for kid_id in assigned_kids_ids:\n            due_str = due_date if due_date else \"No due date set\"\n            extra_data = {\"kid_id\": kid_id, \"chore_id\": chore_id}\n            self.hass.async_create_task(\n                self._notify_kid(\n                    kid_id,\n                    title=\"KidsChores: New Chore\",\n                    message=f\"A new chore '{new_name}' was assigned to you! Due: {due_str}\",\n                    extra_data=extra_data,\n                )\n            )\n\n    def _update_chore(self, chore_id: str, chore_data: dict[str, Any]):\n        chore_info = self._data[DATA_CHORES][chore_id]\n        chore_info[\"name\"] = chore_data.get(\"name\", chore_info[\"name\"])\n        chore_info[\"state\"] = chore_data.get(\"state\", chore_info[\"state\"])\n        chore_info[\"default_points\"] = chore_data.get(\n            \"default_points\", chore_info[\"default_points\"]\n        )\n        chore_info[\"allow_multiple_claims_per_day\"] = chore_data.get(\n            \"allow_multiple_claims_per_day\", chore_info[\"allow_multiple_claims_per_day\"]\n        )\n        chore_info[\"partial_allowed\"] = chore_data.get(\n            \"partial_allowed\", chore_info[\"partial_allowed\"]\n        )\n        chore_info[\"description\"] = chore_data.get(\n            \"description\", chore_info[\"description\"]\n        )\n        chore_info[\"chore_labels\"] = chore_data.get(\n            \"chore_labels\", chore_info.get(\"chore_labels\", [])\n        )\n        chore_info[\"icon\"] = chore_data.get(\"icon\", chore_info[\"icon\"])\n        chore_info[\"shared_chore\"] = chore_data.get(\n            \"shared_chore\", chore_info[\"shared_chore\"]\n        )\n\n        assigned_kids_ids = []\n        for kid_name in chore_data.get(\"assigned_kids\", []):\n            kid_id = self._get_kid_id_by_name(kid_name)\n            if kid_id:\n                assigned_kids_ids.append(kid_id)\n            else:\n                LOGGER.warning(\n                    \"Chore '%s': Kid name '%s' not found. Skipping assignment\",\n                    chore_data.get(\"name\", chore_id),\n                    kid_name,\n                )\n        old_assigned = set(chore_info.get(\"assigned_kids\", []))\n        new_assigned = set(assigned_kids_ids)\n        removed_kids = old_assigned - new_assigned\n        for kid in removed_kids:\n            self._remove_kid_chore_entities(kid, chore_id)\n            self._cleanup_chore_from_kid(kid, chore_id)\n\n        # Update the chore's assigned kids list with the new assignments\n        chore_info[\"assigned_kids\"] = list(new_assigned)\n\n        chore_info[\"recurring_frequency\"] = chore_data.get(\n            \"recurring_frequency\", chore_info[\"recurring_frequency\"]\n        )\n        chore_info[\"due_date\"] = chore_data.get(\"due_date\", chore_info[\"due_date\"])\n        chore_info[\"last_completed\"] = chore_data.get(\n            \"last_completed\", chore_info.get(\"last_completed\")\n        )\n        chore_info[\"last_claimed\"] = chore_data.get(\n            \"last_claimed\", chore_info.get(\"last_claimed\")\n        )\n        chore_info[\"applicable_days\"] = chore_data.get(\n            \"applicable_days\", chore_info.get(\"applicable_days\", [])\n        )\n        chore_info[\"notify_on_claim\"] = chore_data.get(\n            \"notify_on_claim\",\n            chore_info.get(\"notify_on_claim\", DEFAULT_NOTIFY_ON_CLAIM),\n        )\n        chore_info[\"notify_on_approval\"] = chore_data.get(\n            \"notify_on_approval\",\n            chore_info.get(\"notify_on_approval\", DEFAULT_NOTIFY_ON_APPROVAL),\n        )\n        chore_info[\"notify_on_disapproval\"] = chore_data.get(\n            \"notify_on_disapproval\",\n            chore_info.get(\"notify_on_disapproval\", DEFAULT_NOTIFY_ON_DISAPPROVAL),\n        )\n        if chore_info[\"recurring_frequency\"] == FREQUENCY_CUSTOM:\n            chore_info[\"custom_interval\"] = chore_data.get(\"custom_interval\")\n            chore_info[\"custom_interval_unit\"] = chore_data.get(\"custom_interval_unit\")\n        else:\n            chore_info[\"custom_interval\"] = None\n            chore_info[\"custom_interval_unit\"] = None\n\n        LOGGER.debug(\"Updated chore '%s' with ID: %s\", chore_info[\"name\"], chore_id)\n\n        self.hass.async_create_task(self._check_overdue_chores())\n\n    # -- Badges\n    def _create_badge(self, badge_id: str, badge_data: dict[str, Any]):\n        self._data[DATA_BADGES][badge_id] = {\n            \"name\": badge_data.get(\"name\", \"\"),\n            \"threshold_type\": badge_data.get(\n                \"threshold_type\", BADGE_THRESHOLD_TYPE_POINTS\n            ),\n            \"threshold_value\": badge_data.get(\n                \"threshold_value\", DEFAULT_BADGE_THRESHOLD\n            ),\n            \"chore_count_type\": badge_data.get(\"chore_count_type\", FREQUENCY_DAILY),\n            \"earned_by\": badge_data.get(\"earned_by\", []),\n            \"points_multiplier\": badge_data.get(\n                \"points_multiplier\", DEFAULT_POINTS_MULTIPLIER\n            ),\n            \"icon\": badge_data.get(\"icon\", DEFAULT_ICON),\n            \"description\": badge_data.get(\"description\", \"\"),\n            \"badge_labels\": badge_data.get(\"badge_labels\", []),\n            \"internal_id\": badge_id,\n        }\n        LOGGER.debug(\n            \"Added new badge '%s' with ID: %s\",\n            self._data[DATA_BADGES][badge_id][\"name\"],\n            badge_id,\n        )\n\n    def _update_badge(self, badge_id: str, badge_data: dict[str, Any]):\n        badge_info = self._data[DATA_BADGES][badge_id]\n        badge_info[\"name\"] = badge_data.get(\"name\", badge_info[\"name\"])\n        badge_info[\"threshold_type\"] = badge_data.get(\n            \"threshold_type\",\n            badge_info.get(\"threshold_type\", BADGE_THRESHOLD_TYPE_POINTS),\n        )\n        badge_info[\"threshold_value\"] = badge_data.get(\n            \"threshold_value\",\n            badge_info.get(\"threshold_value\", DEFAULT_BADGE_THRESHOLD),\n        )\n        badge_info[\"chore_count_type\"] = badge_data.get(\n            \"chore_count_type\", badge_info.get(\"chore_count_type\", FREQUENCY_NONE)\n        )\n        badge_info[\"points_multiplier\"] = badge_data.get(\n            \"points_multiplier\",\n            badge_info.get(\"points_multiplier\", DEFAULT_POINTS_MULTIPLIER),\n        )\n        badge_info[\"icon\"] = badge_data.get(\n            \"icon\", badge_info.get(\"icon\", DEFAULT_ICON)\n        )\n        badge_info[\"description\"] = badge_data.get(\n            \"description\", badge_info.get(\"description\", \"\")\n        )\n        badge_info[\"badge_labels\"] = badge_data.get(\n            \"badge_labels\", badge_info.get(\"badge_labels\", [])\n        )\n\n        LOGGER.debug(\"Updated badge '%s' with ID: %s\", badge_info[\"name\"], badge_id)\n\n    # -- Rewards\n    def _create_reward(self, reward_id: str, reward_data: dict[str, Any]):\n        self._data[DATA_REWARDS][reward_id] = {\n            \"name\": reward_data.get(\"name\", \"\"),\n            \"cost\": reward_data.get(\"cost\", DEFAULT_REWARD_COST),\n            \"description\": reward_data.get(\"description\", \"\"),\n            \"reward_labels\": reward_data.get(\"reward_labels\", []),\n            \"icon\": reward_data.get(\"icon\", DEFAULT_REWARD_ICON),\n            \"internal_id\": reward_id,\n        }\n        LOGGER.debug(\n            \"Added new reward '%s' with ID: %s\",\n            self._data[DATA_REWARDS][reward_id][\"name\"],\n            reward_id,\n        )\n\n    def _update_reward(self, reward_id: str, reward_data: dict[str, Any]):\n        reward_info = self._data[DATA_REWARDS][reward_id]\n        reward_info[\"name\"] = reward_data.get(\"name\", reward_info[\"name\"])\n        reward_info[\"cost\"] = reward_data.get(\"cost\", reward_info[\"cost\"])\n        reward_info[\"description\"] = reward_data.get(\n            \"description\", reward_info[\"description\"]\n        )\n        reward_info[\"reward_labels\"] = reward_data.get(\n            \"reward_labels\", reward_info.get(\"reward_labels\", [])\n        )\n        reward_info[\"icon\"] = reward_data.get(\"icon\", reward_info[\"icon\"])\n        LOGGER.debug(\"Updated reward '%s' with ID: %s\", reward_info[\"name\"], reward_id)\n\n    # -- Penalties\n    def _create_penalty(self, penalty_id: str, penalty_data: dict[str, Any]):\n        self._data[DATA_PENALTIES][penalty_id] = {\n            \"name\": penalty_data.get(\"name\", \"\"),\n            \"points\": penalty_data.get(\"points\", -DEFAULT_PENALTY_POINTS),\n            \"description\": penalty_data.get(\"description\", \"\"),\n            \"penalty_labels\": penalty_data.get(\"penalty_labels\", []),\n            \"icon\": penalty_data.get(\"icon\", DEFAULT_PENALTY_ICON),\n            \"internal_id\": penalty_id,\n        }\n        LOGGER.debug(\n            \"Added new penalty '%s' with ID: %s\",\n            self._data[DATA_PENALTIES][penalty_id][\"name\"],\n            penalty_id,\n        )\n\n    def _update_penalty(self, penalty_id: str, penalty_data: dict[str, Any]):\n        penalty_info = self._data[DATA_PENALTIES][penalty_id]\n        penalty_info[\"name\"] = penalty_data.get(\"name\", penalty_info[\"name\"])\n        penalty_info[\"points\"] = penalty_data.get(\"points\", penalty_info[\"points\"])\n        penalty_info[\"description\"] = penalty_data.get(\n            \"description\", penalty_info[\"description\"]\n        )\n        penalty_info[\"penalty_labels\"] = penalty_data.get(\n            \"penalty_labels\", penalty_info.get(\"penalty_labels\", [])\n        )\n        penalty_info[\"icon\"] = penalty_data.get(\"icon\", penalty_info[\"icon\"])\n        LOGGER.debug(\n            \"Updated penalty '%s' with ID: %s\", penalty_info[\"name\"], penalty_id\n        )\n\n    # -- Bonuses\n    def _create_bonus(self, bonus_id: str, bonus_data: dict[str, Any]):\n        self._data[DATA_BONUSES][bonus_id] = {\n            \"name\": bonus_data.get(\"name\", \"\"),\n            \"points\": bonus_data.get(\"points\", DEFAULT_BONUS_POINTS),\n            \"description\": bonus_data.get(\"description\", \"\"),\n            \"bonus_labels\": bonus_data.get(\"bonus_labels\", []),\n            \"icon\": bonus_data.get(\"icon\", DEFAULT_BONUS_ICON),\n            \"internal_id\": bonus_id,\n        }\n        LOGGER.debug(\n            \"Added new bonus '%s' with ID: %s\",\n            self._data[DATA_BONUSES][bonus_id][\"name\"],\n            bonus_id,\n        )\n\n    def _update_bonus(self, bonus_id: str, bonus_data: dict[str, Any]):\n        bonus_info = self._data[DATA_BONUSES][bonus_id]\n        bonus_info[\"name\"] = bonus_data.get(\"name\", bonus_info[\"name\"])\n        bonus_info[\"points\"] = bonus_data.get(\"points\", bonus_info[\"points\"])\n        bonus_info[\"description\"] = bonus_data.get(\n            \"description\", bonus_info[\"description\"]\n        )\n        bonus_info[\"bonus_labels\"] = bonus_data.get(\n            \"bonus_labels\", bonus_info.get(\"bonus_labels\", [])\n        )\n        bonus_info[\"icon\"] = bonus_data.get(\"icon\", bonus_info[\"icon\"])\n        LOGGER.debug(\"Updated bonus '%s' with ID: %s\", bonus_info[\"name\"], bonus_id)\n\n    # -- Achievements\n    def _create_achievement(\n        self, achievement_id: str, achievement_data: dict[str, Any]\n    ):\n        self._data[DATA_ACHIEVEMENTS][achievement_id] = {\n            \"name\": achievement_data.get(\"name\", \"\"),\n            \"description\": achievement_data.get(\"description\", \"\"),\n            \"achievement_labels\": achievement_data.get(\"achievement_labels\", []),\n            \"icon\": achievement_data.get(\"icon\", \"\"),\n            \"assigned_kids\": achievement_data.get(\"assigned_kids\", []),\n            \"type\": achievement_data.get(\"type\", \"individual\"),\n            \"selected_chore_id\": achievement_data.get(\"selected_chore_id\", \"\"),\n            \"criteria\": achievement_data.get(\"criteria\", \"\"),\n            \"target_value\": achievement_data.get(\"target_value\", 1),\n            \"reward_points\": achievement_data.get(\"reward_points\", 0),\n            \"progress\": achievement_data.get(\"progress\", {}),\n            \"internal_id\": achievement_id,\n        }\n        LOGGER.debug(\n            \"Added new achievement '%s' with ID: %s\",\n            self._data[DATA_ACHIEVEMENTS][achievement_id][\"name\"],\n            achievement_id,\n        )\n\n    def _update_achievement(\n        self, achievement_id: str, achievement_data: dict[str, Any]\n    ):\n        achievement_info = self._data[DATA_ACHIEVEMENTS][achievement_id]\n        achievement_info[\"name\"] = achievement_data.get(\n            \"name\", achievement_info[\"name\"]\n        )\n        achievement_info[\"description\"] = achievement_data.get(\n            \"description\", achievement_info[\"description\"]\n        )\n        achievement_info[\"achievement_labels\"] = achievement_data.get(\n            \"achievement_labels\", achievement_info.get(\"achievement_labels\", [])\n        )\n        achievement_info[\"icon\"] = achievement_data.get(\n            \"icon\", achievement_info[\"icon\"]\n        )\n        achievement_info[\"assigned_kids\"] = achievement_data.get(\n            \"assigned_kids\", achievement_info[\"assigned_kids\"]\n        )\n        achievement_info[\"type\"] = achievement_data.get(\n            \"type\", achievement_info[\"type\"]\n        )\n        achievement_info[\"selected_chore_id\"] = achievement_data.get(\n            \"selected_chore_id\", achievement_info.get(\"selected_chore_id\", \"\")\n        )\n        achievement_info[\"criteria\"] = achievement_data.get(\n            \"criteria\", achievement_info[\"criteria\"]\n        )\n        achievement_info[\"target_value\"] = achievement_data.get(\n            \"target_value\", achievement_info[\"target_value\"]\n        )\n        achievement_info[\"reward_points\"] = achievement_data.get(\n            \"reward_points\", achievement_info[\"reward_points\"]\n        )\n\n        LOGGER.debug(\n            \"Updated achievement '%s' with ID: %s\",\n            achievement_info[\"name\"],\n            achievement_id,\n        )\n\n    # -- Challenges\n    def _create_challenge(self, challenge_id: str, challenge_data: dict[str, Any]):\n        self._data[DATA_CHALLENGES][challenge_id] = {\n            \"name\": challenge_data.get(\"name\", \"\"),\n            \"description\": challenge_data.get(\"description\", \"\"),\n            \"challenge_labels\": challenge_data.get(\"challenge_labels\", []),\n            \"icon\": challenge_data.get(\"icon\", \"\"),\n            \"assigned_kids\": challenge_data.get(\"assigned_kids\", []),\n            \"type\": challenge_data.get(\"type\", \"individual\"),\n            \"selected_chore_id\": challenge_data.get(\"selected_chore_id\", \"\"),\n            \"criteria\": challenge_data.get(\"criteria\", \"\"),\n            \"target_value\": challenge_data.get(\"target_value\", 1),\n            \"reward_points\": challenge_data.get(\"reward_points\", 0),\n            \"start_date\": challenge_data.get(\"start_date\")\n            if challenge_data.get(\"start_date\") not in [None, {}]\n            else None,\n            \"end_date\": challenge_data.get(\"end_date\")\n            if challenge_data.get(\"end_date\") not in [None, {}]\n            else None,\n            \"progress\": challenge_data.get(\"progress\", {}),\n            \"internal_id\": challenge_id,\n        }\n        LOGGER.debug(\n            \"Added new challenge '%s' with ID: %s\",\n            self._data[DATA_CHALLENGES][challenge_id][\"name\"],\n            challenge_id,\n        )\n\n    def _update_challenge(self, challenge_id: str, challenge_data: dict[str, Any]):\n        challenge_info = self._data[DATA_CHALLENGES][challenge_id]\n        challenge_info[\"name\"] = challenge_data.get(\"name\", challenge_info[\"name\"])\n        challenge_info[\"description\"] = challenge_data.get(\n            \"description\", challenge_info[\"description\"]\n        )\n        challenge_info[\"challenge_labels\"] = challenge_data.get(\n            \"challenge_labels\", challenge_info.get(\"challenge_labels\", [])\n        )\n        challenge_info[\"icon\"] = challenge_data.get(\"icon\", challenge_info[\"icon\"])\n        challenge_info[\"assigned_kids\"] = challenge_data.get(\n            \"assigned_kids\", challenge_info[\"assigned_kids\"]\n        )\n        challenge_info[\"type\"] = challenge_data.get(\"type\", challenge_info[\"type\"])\n        challenge_info[\"selected_chore_id\"] = challenge_data.get(\n            \"selected_chore_id\", challenge_info.get(\"selected_chore_id\", \"\")\n        )\n        challenge_info[\"criteria\"] = challenge_data.get(\n            \"criteria\", challenge_info[\"criteria\"]\n        )\n        challenge_info[\"target_value\"] = challenge_data.get(\n            \"target_value\", challenge_info[\"target_value\"]\n        )\n        challenge_info[\"reward_points\"] = challenge_data.get(\n            \"reward_points\", challenge_info[\"reward_points\"]\n        )\n        challenge_info[\"start_date\"] = (\n            challenge_data.get(\"start_date\")\n            if challenge_data.get(\"start_date\") not in [None, {}]\n            else None\n        )\n        challenge_info[\"end_date\"] = (\n            challenge_data.get(\"end_date\")\n            if challenge_data.get(\"end_date\") not in [None, {}]\n            else None\n        )\n        LOGGER.debug(\n            \"Updated challenge '%s' with ID: %s\", challenge_info[\"name\"], challenge_id\n        )\n\n    # -------------------------------------------------------------------------------------\n    # Properties for Easy Access\n    # -------------------------------------------------------------------------------------\n\n    @property\n    def kids_data(self) -> dict[str, Any]:\n        \"\"\"Return the kids data.\"\"\"\n        return self._data.get(DATA_KIDS, {})\n\n    @property\n    def parents_data(self) -> dict[str, Any]:\n        \"\"\"Return the parents data.\"\"\"\n        return self._data.get(DATA_PARENTS, {})\n\n    @property\n    def chores_data(self) -> dict[str, Any]:\n        \"\"\"Return the chores data.\"\"\"\n        return self._data.get(DATA_CHORES, {})\n\n    @property\n    def badges_data(self) -> dict[str, Any]:\n        \"\"\"Return the badges data.\"\"\"\n        return self._data.get(DATA_BADGES, {})\n\n    @property\n    def rewards_data(self) -> dict[str, Any]:\n        \"\"\"Return the rewards data.\"\"\"\n        return self._data.get(DATA_REWARDS, {})\n\n    @property\n    def penalties_data(self) -> dict[str, Any]:\n        \"\"\"Return the penalties data.\"\"\"\n        return self._data.get(DATA_PENALTIES, {})\n\n    @property\n    def achievements_data(self) -> dict[str, Any]:\n        \"\"\"Return the achievements data.\"\"\"\n        return self._data.get(DATA_ACHIEVEMENTS, {})  # New\n\n    @property\n    def challenges_data(self) -> dict[str, Any]:\n        \"\"\"Return the challenges data.\"\"\"\n        return self._data.get(DATA_CHALLENGES, {})\n\n    @property\n    def bonuses_data(self) -> dict[str, Any]:\n        \"\"\"Return the bonuses data.\"\"\"\n        return self._data.get(DATA_BONUSES, {})\n\n    # -------------------------------------------------------------------------------------\n    # Parents: Add, Remove\n    # -------------------------------------------------------------------------------------\n\n    def add_parent(self, parent_def: dict[str, Any]):\n        \"\"\"Add new parent at runtime if needed.\"\"\"\n        parent_name = parent_def.get(\"name\")\n        ha_user_id = parent_def.get(\"ha_user_id\")\n        kid_ids = parent_def.get(\"associated_kids\", [])\n\n        if not parent_name or not ha_user_id:\n            LOGGER.warning(\"Add parent: Parent must have a name and ha_user_id\")\n            return\n\n        if any(p[\"ha_user_id\"] == ha_user_id for p in self.parents_data.values()):\n            LOGGER.warning(\n                \"Add parent: Parent with ha_user_id '%s' already exists\", ha_user_id\n            )\n            return\n\n        valid_kids = []\n        for kid_id in kid_ids:\n            if kid_id in self.kids_data:\n                valid_kids.append(kid_id)\n            else:\n                LOGGER.warning(\n                    \"Add parent: Kid ID '%s' not found. Skipping assignment to parent '%s'\",\n                    kid_id,\n                    parent_name,\n                )\n\n        new_id = str(uuid.uuid4())\n        self.parents_data[new_id] = {\n            \"name\": parent_name,\n            \"ha_user_id\": ha_user_id,\n            \"associated_kids\": valid_kids,\n            \"internal_id\": new_id,\n        }\n        LOGGER.debug(\"Added new parent '%s' with ID: %s\", parent_name, new_id)\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def remove_parent(self, parent_id: str):\n        \"\"\"Remove a parent by ID.\"\"\"\n        if parent_id in self.parents_data:\n            parent_name = self.parents_data[parent_id][\"name\"]\n            del self.parents_data[parent_id]\n            LOGGER.debug(\"Removed parent '%s' with ID: %s\", parent_name, parent_id)\n            self._persist()\n            self.async_set_updated_data(self._data)\n        else:\n            LOGGER.warning(\"Remove parent: Parent ID '%s' not found\", parent_id)\n\n    # -------------------------------------------------------------------------------------\n    # Chores: Claim, Approve, Disapprove, Compute Global State for Shared Chores\n    # -------------------------------------------------------------------------------------\n\n    def claim_chore(self, kid_id: str, chore_id: str, user_name: str):\n        \"\"\"Kid claims chore => state=claimed; parent must then approve.\"\"\"\n        if chore_id not in self.chores_data:\n            LOGGER.warning(\"Chore ID '%s' not found for claim\", chore_id)\n            raise HomeAssistantError(f\"Chore with ID '{chore_id}' not found.\")\n\n        chore_info = self.chores_data[chore_id]\n        if kid_id not in chore_info.get(\"assigned_kids\", []):\n            LOGGER.warning(\n                \"Claim chore: Chore ID '%s' not assigned to kid ID '%s'\",\n                chore_id,\n                kid_id,\n            )\n            raise HomeAssistantError(\n                f\"Chore '{chore_info.get('name')}' is not assigned to kid '{self.kids_data[kid_id]['name']}'.\"\n            )\n\n        if kid_id not in self.kids_data:\n            LOGGER.warning(\"Kid ID '%s' not found\", kid_id)\n            raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n        kid_info = self.kids_data.get(kid_id)\n\n        self._normalize_kid_lists(kid_info)\n\n        allow_multiple = chore_info.get(\"allow_multiple_claims_per_day\", False)\n        if allow_multiple:\n            # If already approved, remove it so the new claim can trigger a new approval flow\n            kid_info[\"approved_chores\"] = [\n                item for item in kid_info.get(\"approved_chores\", []) if item != chore_id\n            ]\n\n        if not allow_multiple:\n            if chore_id in kid_info.get(\n                \"claimed_chores\", []\n            ) or chore_id in kid_info.get(\"approved_chores\", []):\n                error_message = f\"Chore '{chore_info['name']}' has already been claimed today and multiple claims are not allowed.\"\n                LOGGER.warning(error_message)\n                raise HomeAssistantError(error_message)\n\n        self._process_chore_state(kid_id, chore_id, CHORE_STATE_CLAIMED)\n\n        # Send a notification to the parents that a kid claimed a chore\n        if chore_info.get(CONF_NOTIFY_ON_CLAIM, DEFAULT_NOTIFY_ON_CLAIM):\n            actions = [\n                {\n                    \"action\": f\"{ACTION_APPROVE_CHORE}|{kid_id}|{chore_id}\",\n                    \"title\": ACTION_TITLE_APPROVE,\n                },\n                {\n                    \"action\": f\"{ACTION_DISAPPROVE_CHORE}|{kid_id}|{chore_id}\",\n                    \"title\": ACTION_TITLE_DISAPPROVE,\n                },\n                {\n                    \"action\": f\"{ACTION_REMIND_30}|{kid_id}|{chore_id}\",\n                    \"title\": ACTION_TITLE_REMIND_30,\n                },\n            ]\n            # Pass extra context so the event handler can route the action.\n            extra_data = {\n                \"kid_id\": kid_id,\n                \"chore_id\": chore_id,\n            }\n            self.hass.async_create_task(\n                self._notify_parents(\n                    kid_id,\n                    title=\"KidsChores: Chore Claimed\",\n                    message=f\"'{self.kids_data[kid_id]['name']}' claimed chore '{self.chores_data[chore_id]['name']}'\",\n                    actions=actions,\n                    extra_data=extra_data,\n                )\n            )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def approve_chore(\n        self,\n        parent_name: str,\n        kid_id: str,\n        chore_id: str,\n        points_awarded: Optional[float] = None,\n    ):\n        \"\"\"Approve a chore for kid_id if assigned.\"\"\"\n        if chore_id not in self.chores_data:\n            raise HomeAssistantError(f\"Chore with ID '{chore_id}' not found.\")\n\n        chore_info = self.chores_data[chore_id]\n        if kid_id not in chore_info.get(\"assigned_kids\", []):\n            raise HomeAssistantError(\n                f\"Chore '{chore_info.get('name')}' is not assigned to kid '{self.kids_data[kid_id]['name']}'.\"\n            )\n\n        if kid_id not in self.kids_data:\n            raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n        kid_info = self.kids_data.get(kid_id)\n\n        allow_multiple = chore_info.get(\"allow_multiple_claims_per_day\", False)\n        if not allow_multiple:\n            if chore_id in kid_info.get(\"approved_chores\", []):\n                error_message = f\"Chore '{chore_info['name']}' has already been approved today; multiple approvals not allowed.\"\n                LOGGER.warning(error_message)\n                raise HomeAssistantError(error_message)\n\n        default_points = chore_info.get(\"default_points\", DEFAULT_POINTS)\n        multiplier = kid_info.get(\"points_multiplier\", 1.0)\n        awarded_points = (\n            points_awarded * multiplier\n            if points_awarded is not None\n            else default_points * multiplier\n        )\n\n        self._process_chore_state(\n            kid_id, chore_id, CHORE_STATE_APPROVED, points_awarded=awarded_points\n        )\n\n        # Remove to avoid awarding duplicated points\n        # old_points = float(kid_info[\"points\"])\n        # new_points = old_points + awarded_points\n        # self.update_kid_points(kid_id, new_points)\n\n        # increment completed chores counters\n        kid_info[\"completed_chores_today\"] += 1\n        kid_info[\"completed_chores_weekly\"] += 1\n        kid_info[\"completed_chores_monthly\"] += 1\n        kid_info[\"completed_chores_total\"] += 1\n\n        # Track today’s approvals for chores that allow multiple claims.\n        if chore_info.get(\"allow_multiple_claims_per_day\", False):\n            kid_info.setdefault(\"today_chore_approvals\", {})\n            kid_info[\"today_chore_approvals\"][chore_id] = (\n                kid_info[\"today_chore_approvals\"].get(chore_id, 0) + 1\n            )\n\n        chore_info[\"last_completed\"] = dt_util.utcnow().isoformat()\n\n        today = dt_util.as_local(dt_util.utcnow()).date()\n        self._update_chore_streak_for_kid(kid_id, chore_id, today)\n        self._update_overall_chore_streak(kid_id, today)\n\n        # remove from pending approvals\n        self._data[DATA_PENDING_CHORE_APPROVALS] = [\n            ap\n            for ap in self._data[DATA_PENDING_CHORE_APPROVALS]\n            if not (ap[\"kid_id\"] == kid_id and ap[\"chore_id\"] == chore_id)\n        ]\n\n        # increment chore approvals\n        if chore_id in kid_info[\"chore_approvals\"]:\n            kid_info[\"chore_approvals\"][chore_id] += 1\n        else:\n            kid_info[\"chore_approvals\"][chore_id] = 1\n\n        # Manage Achievements\n        today = dt_util.as_local(dt_util.utcnow()).date()\n        for achievement_id, achievement in self.achievements_data.items():\n            if achievement.get(\"type\") == ACHIEVEMENT_TYPE_STREAK:\n                selected_chore_id = achievement.get(\"selected_chore_id\")\n                if selected_chore_id == chore_id:\n                    # Get or create the progress dict for this kid\n                    progress = achievement.setdefault(\"progress\", {}).setdefault(\n                        kid_id,\n                        {\"current_streak\": 0, \"last_date\": None, \"awarded\": False},\n                    )\n                    self._update_streak_progress(progress, today)\n\n        # Manage Challenges\n        today_iso = dt_util.as_local(dt_util.utcnow()).date().isoformat()\n        for challenge_id, challenge in self.challenges_data.items():\n            if challenge.get(\"type\") == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n                # (Challenge update logic for total-within-window remains here)\n                start_date_raw = challenge.get(\"start_date\")\n                if isinstance(start_date_raw, str):\n                    start_date = dt_util.parse_datetime(start_date_raw)\n                    if start_date and start_date.tzinfo is None:\n                        start_date = start_date.replace(tzinfo=dt_util.UTC)\n                else:\n                    start_date = None\n\n                end_date_raw = challenge.get(\"end_date\")\n                if isinstance(end_date_raw, str):\n                    end_date = dt_util.parse_datetime(end_date_raw)\n                    if end_date and end_date.tzinfo is None:\n                        end_date = end_date.replace(tzinfo=dt_util.UTC)\n                else:\n                    end_date = None\n\n                now = dt_util.utcnow()\n\n                if start_date and end_date and start_date <= now <= end_date:\n                    progress = challenge.setdefault(\"progress\", {}).setdefault(\n                        kid_id, {\"count\": 0, \"awarded\": False}\n                    )\n                    progress[\"count\"] += 1\n\n            elif challenge.get(\"type\") == CHALLENGE_TYPE_DAILY_MIN:\n                # Only update if the challenge is tracking a specific chore.\n                selected_chore = challenge.get(\"selected_chore_id\")\n                if not selected_chore:\n                    LOGGER.warning(\n                        \"Challenge '%s' of type daily_min has no selected_chore_id set. Skipping progress update.\",\n                        challenge.get(\"name\"),\n                    )\n                    continue\n                if selected_chore != chore_id:\n                    continue\n                if kid_id in challenge.get(\"assigned_kids\", []):\n                    progress = challenge.setdefault(\"progress\", {}).setdefault(\n                        kid_id, {\"daily_counts\": {}, \"awarded\": False}\n                    )\n                    progress[\"daily_counts\"][today_iso] = (\n                        progress[\"daily_counts\"].get(today_iso, 0) + 1\n                    )\n\n        # Send a notification to the kid that chore was approved\n        if chore_info.get(CONF_NOTIFY_ON_APPROVAL, DEFAULT_NOTIFY_ON_APPROVAL):\n            extra_data = {\"kid_id\": kid_id, \"chore_id\": chore_id}\n            self.hass.async_create_task(\n                self._notify_kid(\n                    kid_id,\n                    title=\"KidsChores: Chore Approved\",\n                    message=f\"Your chore '{chore_info['name']}' was approved. You earned {awarded_points} points.\",\n                    extra_data=extra_data,\n                )\n            )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def disapprove_chore(self, parent_name: str, kid_id: str, chore_id: str):\n        \"\"\"Disapprove a chore for kid_id.\"\"\"\n        chore_info = self.chores_data.get(chore_id)\n        if not chore_info:\n            raise HomeAssistantError(f\"Chore with ID '{chore_id}' not found.\")\n\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n        self._process_chore_state(kid_id, chore_id, CHORE_STATE_PENDING)\n\n        # Send a notification to the kid that chore was disapproved\n        if chore_info.get(CONF_NOTIFY_ON_DISAPPROVAL, DEFAULT_NOTIFY_ON_DISAPPROVAL):\n            extra_data = {\"kid_id\": kid_id, \"chore_id\": chore_id}\n            self.hass.async_create_task(\n                self._notify_kid(\n                    kid_id,\n                    title=\"KidsChores: Chore Disapproved\",\n                    message=f\"Your chore '{chore_info['name']}' was disapproved.\",\n                    extra_data=extra_data,\n                )\n            )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def update_chore_state(self, chore_id: str, state: str):\n        \"\"\"Manually override a chore's state.\"\"\"\n        chore_info = self.chores_data.get(chore_id)\n        if not chore_info:\n            LOGGER.warning(\"Update chore state: Chore ID '%s' not found\", chore_id)\n            return\n        # Set state for all kids assigned to the chore:\n        for kid_id in chore_info.get(\"assigned_kids\", []):\n            if kid_id:\n                self._process_chore_state(kid_id, chore_id, state)\n        self._persist()\n        self.async_set_updated_data(self._data)\n        LOGGER.debug(f\"Chore ID '{chore_id}' state manually updated to '{state}'\")\n\n    # -------------------------------------------------------------------------------------\n    # Chore State Processing: Centralized Function\n    # The most critical thing to understand when working on this function is that\n    # chore_info[\"state\"] is actually the global state of the chore. The individual chore\n    # state per kid is always calculated based on whether they have any claimed, approved, or\n    # overdue chores listed for them.\n    #\n    # Global state will only match if a single kid is assigned to the chore, or all kids\n    # assigned are in the same state.\n    # -------------------------------------------------------------------------------------\n\n    def _process_chore_state(\n        self,\n        kid_id: str,\n        chore_id: str,\n        new_state: str,\n        *,\n        points_awarded: Optional[float] = None,\n    ) -> None:\n        LOGGER.debug(\n            \"Entering _process_chore_state with kid_id=%s, chore_id=%s, new_state=%s, points_awarded=%s\",\n            kid_id,\n            chore_id,\n            new_state,\n            points_awarded,\n        )\n\n        \"\"\"Centralized function to update a chore’s state for a given kid.\"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        chore_info = self.chores_data.get(chore_id)\n\n        if not kid_info or not chore_info:\n            LOGGER.warning(\n                \"State change skipped: Kid '%s' or Chore '%s' not found\",\n                kid_id,\n                chore_id,\n            )\n            return\n\n        # Clear any overdue tracking.\n        kid_info.setdefault(\"overdue_chores\", [])\n        kid_info.setdefault(\"overdue_notifications\", {})\n\n        # Remove all instances of the chore from overdue lists.\n        kid_info[\"overdue_chores\"] = [\n            entry for entry in kid_info.get(\"overdue_chores\", []) if entry != chore_id\n        ]\n\n        if chore_id in kid_info[\"overdue_notifications\"]:\n            kid_info[\"overdue_notifications\"].pop(chore_id)\n\n        if new_state == CHORE_STATE_CLAIMED:\n            # Remove all previous approvals in case of duplicate, add to claimed.\n            kid_info[\"approved_chores\"] = [\n                item for item in kid_info.get(\"approved_chores\", []) if item != chore_id\n            ]\n\n            kid_info.setdefault(\"claimed_chores\", [])\n\n            if chore_id not in kid_info[\"claimed_chores\"]:\n                kid_info[\"claimed_chores\"].append(chore_id)\n\n            chore_info[\"last_claimed\"] = dt_util.utcnow().isoformat()\n\n            self._data.setdefault(DATA_PENDING_CHORE_APPROVALS, []).append(\n                {\n                    \"kid_id\": kid_id,\n                    \"chore_id\": chore_id,\n                    \"timestamp\": dt_util.utcnow().isoformat(),\n                }\n            )\n\n        elif new_state == CHORE_STATE_APPROVED:\n            # Remove all claims for chores in case of duplicates, add to approvals.\n            kid_info[\"claimed_chores\"] = [\n                item for item in kid_info.get(\"claimed_chores\", []) if item != chore_id\n            ]\n\n            kid_info.setdefault(\"approved_chores\", [])\n\n            if chore_id not in kid_info[\"approved_chores\"]:\n                kid_info[\"approved_chores\"].append(chore_id)\n\n            chore_info[\"last_completed\"] = dt_util.utcnow().isoformat()\n\n            if points_awarded is not None:\n                current_points = float(kid_info.get(\"points\", 0))\n                self.update_kid_points(kid_id, current_points + points_awarded)\n\n            today = dt_util.as_local(dt_util.utcnow()).date()\n\n            self._update_chore_streak_for_kid(kid_id, chore_id, today)\n            self._update_overall_chore_streak(kid_id, today)\n\n            self._data[DATA_PENDING_CHORE_APPROVALS] = [\n                ap\n                for ap in self._data.get(DATA_PENDING_CHORE_APPROVALS, [])\n                if not (ap.get(\"kid_id\") == kid_id and ap.get(\"chore_id\") == chore_id)\n            ]\n\n        elif new_state == CHORE_STATE_PENDING:\n            # Remove the chore from both claimed and approved lists.\n            for field in [\"claimed_chores\", \"approved_chores\"]:\n                if chore_id in kid_info.get(field, []):\n                    kid_info[field] = [c for c in kid_info[field] if c != chore_id]\n\n            # Remove from pending approvals.\n            self._data[DATA_PENDING_CHORE_APPROVALS] = [\n                ap\n                for ap in self._data.get(DATA_PENDING_CHORE_APPROVALS, [])\n                if not (ap.get(\"kid_id\") == kid_id and ap.get(\"chore_id\") == chore_id)\n            ]\n\n        elif new_state == CHORE_STATE_OVERDUE:\n            # Mark as overdue.\n            kid_info.setdefault(\"overdue_chores\", [])\n\n            if chore_id not in kid_info[\"overdue_chores\"]:\n                kid_info[\"overdue_chores\"].append(chore_id)\n\n            kid_info.setdefault(\"overdue_notifications\", {})\n            kid_info[\"overdue_notifications\"][chore_id] = dt_util.utcnow().isoformat()\n\n        # Compute and update the chore's global state.\n        # Given the process above is handling everything properly for each kid, computing the global state straightforward.\n        # This process needs run every time a chore state changes, so it no longer warrants a separate function.\n        assigned_kids = chore_info.get(\"assigned_kids\", [])\n\n        if len(assigned_kids) == 1:\n            # if only one kid is assigned to the chore, update the chore state to new state 1:1\n            chore_info[\"state\"] = new_state\n        elif len(assigned_kids) > 1:\n            # For chores assigned to multiple kids, you have to figure out the global state\n            count_pending = count_claimed = count_approved = count_overdue = 0\n            for kid_id in assigned_kids:\n                kid_info = self.kids_data.get(kid_id, {})\n                if chore_id in kid_info.get(\"overdue_chores\", []):\n                    count_overdue += 1\n                elif chore_id in kid_info.get(\"approved_chores\", []):\n                    count_approved += 1\n                elif chore_id in kid_info.get(\"claimed_chores\", []):\n                    count_claimed += 1\n                else:\n                    count_pending += 1\n            total = len(assigned_kids)\n\n            # If all kids are in the same state, update the chore state to new state 1:1\n            if (\n                count_pending == total\n                or count_claimed == total\n                or count_approved == total\n                or count_overdue == total\n            ):\n                chore_info[\"state\"] = new_state\n\n            # For shared chores, recompute global state of a partial if they aren't all in the same state as checked above\n            elif chore_info.get(\"shared_chore\", False):\n                if count_overdue > 0:\n                    chore_info[\"state\"] = CHORE_STATE_OVERDUE\n                elif count_approved > 0:\n                    chore_info[\"state\"] = CHORE_STATE_APPROVED_IN_PART\n                elif count_claimed > 0:\n                    chore_info[\"state\"] = CHORE_STATE_CLAIMED_IN_PART\n                else:\n                    chore_info[\"state\"] = CHORE_STATE_UNKNOWN\n\n            # For non-shared chores multiple assign it will be independent if they aren't all in the same state as checked above.\n            elif chore_info.get(\"shared_chore\", False) is False:\n                chore_info[\"state\"] = CHORE_STATE_INDEPENDENT\n\n        else:\n            chore_info[\"state\"] = CHORE_STATE_UNKNOWN\n\n        LOGGER.debug(\n            \"Chore '%s' global state computed as '%s'\",\n            chore_id,\n            chore_info[\"state\"],\n        )\n\n    # -------------------------------------------------------------------------------------\n    # Kids: Update Points\n    # -------------------------------------------------------------------------------------\n\n    def update_kid_points(self, kid_id: str, new_points: float):\n        \"\"\"Set a kid's points to 'new_points', updating daily/weekly/monthly counters.\"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            LOGGER.warning(\"Update kid points: Kid ID '%s' not found\", kid_id)\n            return\n\n        old_points = float(kid_info[\"points\"])\n        delta = new_points - old_points\n        if delta == 0:\n            LOGGER.debug(\"No change in points for kid '%s'. Skipping updates\", kid_id)\n            return\n\n        kid_info[\"points\"] = new_points\n        kid_info[\"points_earned_today\"] += delta\n        kid_info[\"points_earned_weekly\"] += delta\n        kid_info[\"points_earned_monthly\"] += delta\n\n        # Update Max Points Ever\n        if new_points > kid_info.get(\"max_points_ever\", 0):\n            kid_info[\"max_points_ever\"] = new_points\n\n        # Check Badges\n        self._check_badges_for_kid(kid_id)\n        self._check_achievements_for_kid(kid_id)\n        self._check_challenges_for_kid(kid_id)\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n        LOGGER.debug(\n            \"update_kid_points: Kid '%s' changed from %.2f to %.2f (delta=%.2f)\",\n            kid_id,\n            old_points,\n            new_points,\n            delta,\n        )\n\n    # -------------------------------------------------------------------------------------\n    # Rewards: Redeem, Approve, Disapprove\n    # -------------------------------------------------------------------------------------\n\n    def redeem_reward(self, parent_name: str, kid_id: str, reward_id: str):\n        \"\"\"Kid claims a reward => mark as pending approval (no deduction yet).\"\"\"\n        reward = self.rewards_data.get(reward_id)\n        if not reward:\n            raise HomeAssistantError(f\"Reward with ID '{reward_id}' not found.\")\n\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n        cost = reward.get(\"cost\", 0.0)\n        if kid_info[\"points\"] < cost:\n            raise HomeAssistantError(\n                f\"'{kid_info['name']}' does not have enough points ({cost} needed).\"\n            )\n\n        kid_info.setdefault(\"pending_rewards\", []).append(reward_id)\n        kid_info.setdefault(\"redeemed_rewards\", [])\n\n        # Add to pending approvals\n        self._data[DATA_PENDING_REWARD_APPROVALS].append(\n            {\n                \"kid_id\": kid_id,\n                \"reward_id\": reward_id,\n                \"timestamp\": dt_util.utcnow().isoformat(),\n            }\n        )\n\n        # increment reward_claims counter\n        if reward_id in kid_info[\"reward_claims\"]:\n            kid_info[\"reward_claims\"][reward_id] += 1\n        else:\n            kid_info[\"reward_claims\"][reward_id] = 1\n\n        # Send a notification to the parents that a kid claimed a reward\n        actions = [\n            {\n                \"action\": f\"{ACTION_APPROVE_REWARD}|{kid_id}|{reward_id}\",\n                \"title\": ACTION_TITLE_APPROVE,\n            },\n            {\n                \"action\": f\"{ACTION_DISAPPROVE_REWARD}|{kid_id}|{reward_id}\",\n                \"title\": ACTION_TITLE_DISAPPROVE,\n            },\n            {\n                \"action\": f\"{ACTION_REMIND_30}|{kid_id}|{reward_id}\",\n                \"title\": ACTION_TITLE_REMIND_30,\n            },\n        ]\n        extra_data = {\"kid_id\": kid_id, \"reward_id\": reward_id}\n        self.hass.async_create_task(\n            self._notify_parents(\n                kid_id,\n                title=\"KidsChores: Reward Claimed\",\n                message=f\"'{kid_info['name']}' claimed reward '{reward['name']}'\",\n                actions=actions,\n                extra_data=extra_data,\n            )\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def approve_reward(self, parent_name: str, kid_id: str, reward_id: str):\n        \"\"\"Parent approves the reward => deduct points.\"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n        reward = self.rewards_data.get(reward_id)\n        if not reward:\n            raise HomeAssistantError(f\"Reward with ID '{reward_id}' not found.\")\n\n        cost = reward.get(\"cost\", 0.0)\n        if reward_id in kid_info.get(\"pending_rewards\", []):\n            if kid_info[\"points\"] < cost:\n                raise HomeAssistantError(\n                    f\"'{kid_info['name']}' does not have enough points to redeem '{reward['name']}'.\"\n                )\n\n            # Deduct\n            new_points = float(kid_info[\"points\"]) - cost\n            self.update_kid_points(kid_id, new_points)\n\n            kid_info[\"pending_rewards\"].remove(reward_id)\n            kid_info[\"redeemed_rewards\"].append(reward_id)\n        else:\n            # direct approval scenario\n            if kid_info[\"points\"] < cost:\n                raise HomeAssistantError(\n                    f\"'{kid_info['name']}' does not have enough points to redeem '{reward['name']}'.\"\n                )\n            kid_info[\"points\"] -= cost\n            kid_info[\"redeemed_rewards\"].append(reward_id)\n\n        self._check_badges_for_kid(kid_id)\n\n        # remove 1 claim from pending approvals\n        approvals = self._data[DATA_PENDING_REWARD_APPROVALS]\n        for i, ap in enumerate(approvals):\n            if ap[\"kid_id\"] == kid_id and ap[\"reward_id\"] == reward_id:\n                del approvals[i]  # Remove only the first match\n                break  # Stop after the first removal\n\n        # increment reward_approvals\n        if reward_id in kid_info[\"reward_approvals\"]:\n            kid_info[\"reward_approvals\"][reward_id] += 1\n        else:\n            kid_info[\"reward_approvals\"][reward_id] = 1\n\n        # Send a notification to the kid that reward was approved\n        extra_data = {\"kid_id\": kid_id, \"reward_id\": reward_id}\n        self.hass.async_create_task(\n            self._notify_kid(\n                kid_id,\n                title=\"KidsChores: Reward Approved\",\n                message=f\"Your reward '{reward['name']}' was approved.\",\n                extra_data=extra_data,\n            )\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def disapprove_reward(self, parent_name: str, kid_id: str, reward_id: str):\n        \"\"\"Disapprove a reward for kid_id.\"\"\"\n\n        reward = self.rewards_data.get(reward_id)\n        if not reward:\n            raise HomeAssistantError(f\"Reward with ID '{reward_id}' not found.\")\n\n        # remove from pending approvals\n        self._data[DATA_PENDING_REWARD_APPROVALS] = [\n            ap\n            for ap in self._data[DATA_PENDING_REWARD_APPROVALS]\n            if not (ap[\"kid_id\"] == kid_id and ap[\"reward_id\"] == reward_id)\n        ]\n\n        kid_info = self.kids_data.get(kid_id)\n        if kid_info and reward_id in kid_info.get(\"pending_rewards\", []):\n            kid_info[\"pending_rewards\"].remove(reward_id)\n\n        # Send a notification to the kid that reward was disapproved\n        extra_data = {\"kid_id\": kid_id, \"reward_id\": reward_id}\n        self.hass.async_create_task(\n            self._notify_kid(\n                kid_id,\n                title=\"KidsChores: Reward Disapproved\",\n                message=f\"Your reward '{reward['name']}' was disapproved.\",\n                extra_data=extra_data,\n            )\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # -------------------------------------------------------------------------------------\n    # Badges: Add, Check, Award\n    # -------------------------------------------------------------------------------------\n\n    def add_badge(self, badge_def: dict[str, Any]):\n        \"\"\"Add new badge at runtime if needed.\"\"\"\n        badge_name = badge_def.get(\"name\")\n        if not badge_name:\n            LOGGER.warning(\"Add badge: Badge must have a name\")\n            return\n        if any(b[\"name\"] == badge_name for b in self.badges_data.values()):\n            LOGGER.warning(\"Add badge: Badge '%s' already exists\", badge_name)\n            return\n        internal_id = str(uuid.uuid4())\n        self.badges_data[internal_id] = {\n            \"name\": badge_name,\n            \"threshold_type\": badge_def.get(\n                \"threshold_type\", BADGE_THRESHOLD_TYPE_POINTS\n            ),\n            \"threshold_value\": badge_def.get(\n                \"threshold_value\", DEFAULT_BADGE_THRESHOLD\n            ),\n            \"chore_count_type\": badge_def.get(\"chore_count_type\", FREQUENCY_DAILY),\n            \"earned_by\": [],\n            \"points_multiplier\": badge_def.get(\n                \"points_multiplier\", DEFAULT_POINTS_MULTIPLIER\n            ),\n            \"icon\": badge_def.get(\"icon\", DEFAULT_ICON),\n            \"description\": badge_def.get(\"description\", \"\"),\n            \"internal_id\": internal_id,\n        }\n        LOGGER.debug(\"Added new badge '%s' with ID: %s\", badge_name, internal_id)\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def _check_badges_for_kid(self, kid_id: str):\n        \"\"\"Evaluate all badge thresholds for kid.\"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            return\n\n        for badge_id, badge_data in self.badges_data.items():\n            if kid_id in badge_data.get(\"earned_by\", []):\n                continue  # already earned\n            threshold_type = badge_data.get(\"threshold_type\")\n            threshold_val = badge_data.get(\"threshold_value\", 0)\n            if threshold_type == BADGE_THRESHOLD_TYPE_POINTS:\n                if kid_info[\"points\"] >= threshold_val:\n                    self._award_badge(kid_id, badge_id)\n            elif threshold_type == BADGE_THRESHOLD_TYPE_CHORE_COUNT:\n                ctype = badge_data.get(\"chore_count_type\", FREQUENCY_DAILY)\n                if ctype == \"total\":\n                    ccount = kid_info.get(\"completed_chores_total\", 0)\n                else:\n                    ccount = kid_info.get(f\"completed_chores_{ctype}\", 0)\n                if ccount >= threshold_val:\n                    self._award_badge(kid_id, badge_id)\n\n    def _award_badge(self, kid_id: str, badge_id: str):\n        \"\"\"Add the badge to kid's 'earned_by' and kid's 'badges' list.\"\"\"\n        badge = self.badges_data.get(badge_id)\n        if not badge:\n            LOGGER.error(\n                \"Attempted to award non-existent badge ID '%s' to kid ID '%s'\",\n                badge_id,\n                kid_id,\n            )\n            return\n\n        if kid_id in badge.get(\"earned_by\", []):\n            return  # already earned\n\n        badge.setdefault(\"earned_by\", []).append(kid_id)\n        kid_info = self.kids_data.get(kid_id, {})\n        if badge[\"name\"] not in kid_info.get(\"badges\", []):\n            kid_info.setdefault(\"badges\", []).append(badge[\"name\"])\n            self._update_kid_multiplier(kid_id)\n\n            badge_name = badge[\"name\"]\n            kid_name = kid_info[\"name\"]\n\n            # Send a notification to the kid and parents that a new badge was earned\n            extra_data = {\"kid_id\": kid_id, \"badge_id\": badge_id}\n            self.hass.async_create_task(\n                self._notify_kid(\n                    kid_id,\n                    title=\"KidsChores: Badge Earned\",\n                    message=f\"You earned a new badge: '{badge_name}'!\",\n                    extra_data=extra_data,\n                )\n            )\n            self.hass.async_create_task(\n                self._notify_parents(\n                    kid_id,\n                    title=\"KidsChores: Badge Earned\",\n                    message=f\"'{kid_name}' earned a new badge: '{badge_name}'.\",\n                    extra_data=extra_data,\n                )\n            )\n\n            self._persist()\n            self.async_set_updated_data(self._data)\n\n    def _update_kid_multiplier(self, kid_id: str):\n        \"\"\"Update the kid's points multiplier based on highest badge achieved.\"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            return\n        earned_badges = [\n            b for b in self.badges_data.values() if kid_id in b.get(\"earned_by\", [])\n        ]\n        if not earned_badges:\n            kid_info[\"points_multiplier\"] = 1.0\n            return\n        highest_mult = max(b.get(\"points_multiplier\", 1.0) for b in earned_badges)\n        kid_info[\"points_multiplier\"] = highest_mult\n\n    def _recalculate_all_badges(self):\n        \"\"\"Global re-check of all badges for all kids.\"\"\"\n        LOGGER.info(\"Starting global badge recalculation\")\n\n        ## Clear current references\n        # for _, badge_info in self.badges_data.items():\n        #    badge_info[\"earned_by\"] = []\n        # for _, kid_info in self.kids_data.items():\n        #    kid_info[\"badges\"] = []\n\n        # Re-check thresholds\n        for badge_id, badge_info in self.badges_data.items():\n            ttype = badge_info.get(\"threshold_type\", BADGE_THRESHOLD_TYPE_POINTS)\n            tval = badge_info.get(\"threshold_value\", 0)\n            for kid_id, kid_info in self.kids_data.items():\n                if ttype == BADGE_THRESHOLD_TYPE_POINTS:\n                    if kid_info.get(\"max_points_ever\", 0.0) >= tval:\n                        self._award_badge(kid_id, badge_id)\n                elif ttype == BADGE_THRESHOLD_TYPE_CHORE_COUNT:\n                    ctype = badge_info.get(\"chore_count_type\", FREQUENCY_DAILY)\n                    if ctype == \"total\":\n                        ccount = kid_info.get(\"completed_chores_total\", 0)\n                    else:\n                        ccount = kid_info.get(f\"completed_chores_{ctype}\", 0)\n                    if ccount >= tval:\n                        self._award_badge(kid_id, badge_id)\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n        LOGGER.info(\"Badge recalculation complete\")\n\n    # -------------------------------------------------------------------------------------\n    # Penalties: Apply, Add\n    # -------------------------------------------------------------------------------------\n\n    def apply_penalty(self, parent_name: str, kid_id: str, penalty_id: str):\n        \"\"\"Apply penalty => negative points to reduce kid's points.\"\"\"\n        penalty = self.penalties_data.get(penalty_id)\n        if not penalty:\n            raise HomeAssistantError(f\"Penalty with ID '{penalty_id}' not found.\")\n\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n        penalty_pts = penalty.get(\"points\", 0)\n        new_points = float(kid_info[\"points\"]) + penalty_pts\n        self.update_kid_points(kid_id, new_points)\n\n        # increment penalty_applies\n        if penalty_id in kid_info[\"penalty_applies\"]:\n            kid_info[\"penalty_applies\"][penalty_id] += 1\n        else:\n            kid_info[\"penalty_applies\"][penalty_id] = 1\n\n        # Send a notification to the kid that a penalty was applied\n        extra_data = {\"kid_id\": kid_id, \"penalty_id\": penalty_id}\n        self.hass.async_create_task(\n            self._notify_kid(\n                kid_id,\n                title=\"KidsChores: Penalty Applied\",\n                message=f\"A '{penalty['name']}' penalty was applied. Your points changed by {penalty_pts}.\",\n                extra_data=extra_data,\n            )\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def add_penalty(self, penalty_def: dict[str, Any]):\n        \"\"\"Add new penalty at runtime if needed.\"\"\"\n        penalty_name = penalty_def.get(\"name\")\n        if not penalty_name:\n            LOGGER.warning(\"Add penalty: Penalty must have a name\")\n            return\n        if any(p[\"name\"] == penalty_name for p in self.penalties_data.values()):\n            LOGGER.warning(\"Add penalty: Penalty '%s' already exists\", penalty_name)\n            return\n        internal_id = str(uuid.uuid4())\n        self.penalties_data[internal_id] = {\n            \"name\": penalty_name,\n            \"points\": penalty_def.get(\"points\", -DEFAULT_PENALTY_POINTS),\n            \"description\": penalty_def.get(\"description\", \"\"),\n            \"icon\": penalty_def.get(\"icon\", DEFAULT_PENALTY_ICON),\n            \"internal_id\": internal_id,\n        }\n        LOGGER.debug(\"Added new penalty '%s' with ID: %s\", penalty_name, internal_id)\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # -------------------------------------------------------------------------\n    # Bonuses: Apply, Add\n    # -------------------------------------------------------------------------\n\n    def apply_bonus(self, parent_name: str, kid_id: str, bonus_id: str):\n        \"\"\"Apply bonus => positive points to increase kid's points.\"\"\"\n        bonus = self.bonuses_data.get(bonus_id)\n        if not bonus:\n            raise HomeAssistantError(f\"Bonus with ID '{bonus_id}' not found.\")\n\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n        bonus_pts = bonus.get(\"points\", 0)\n        new_points = float(kid_info[\"points\"]) + bonus_pts\n        self.update_kid_points(kid_id, new_points)\n\n        # increment bonus_applies\n        if bonus_id in kid_info[\"bonus_applies\"]:\n            kid_info[\"bonus_applies\"][bonus_id] += 1\n        else:\n            kid_info[\"bonus_applies\"][bonus_id] = 1\n\n        # Send a notification to the kid that a bonus was applied\n        extra_data = {\"kid_id\": kid_id, \"bonus_id\": bonus_id}\n        self.hass.async_create_task(\n            self._notify_kid(\n                kid_id,\n                title=\"KidsChores: Bonus Applied\",\n                message=f\"A '{bonus['name']}' bonus was applied. Your points changed by {bonus_pts}.\",\n                extra_data=extra_data,\n            )\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def add_bonus(self, bonus_def: dict[str, Any]):\n        \"\"\"Add new bonus at runtime if needed.\"\"\"\n        bonus_name = bonus_def.get(\"name\")\n        if not bonus_name:\n            LOGGER.warning(\"Add bonus: Bonus must have a name\")\n            return\n        if any(s[\"name\"] == bonus_name for s in self.bonuses_data.values()):\n            LOGGER.warning(\"Add bonus: Bonus '%s' already exists\", bonus_name)\n            return\n        internal_id = str(uuid.uuid4())\n        self.bonuses_data[internal_id] = {\n            \"name\": bonus_name,\n            \"points\": bonus_def.get(\"points\", DEFAULT_BONUS_POINTS),\n            \"description\": bonus_def.get(\"description\", \"\"),\n            \"icon\": bonus_def.get(\"icon\", DEFAULT_BONUS_ICON),\n            \"internal_id\": internal_id,\n        }\n        LOGGER.debug(\"Added new bonus '%s' with ID: %s\", bonus_name, internal_id)\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # -------------------------------------------------------------------------\n    # Achievements: Check, Award\n    # -------------------------------------------------------------------------\n    def _check_achievements_for_kid(self, kid_id: str):\n        \"\"\"Evaluate all achievement criteria for a given kid.\n\n        For each achievement not already awarded, check its type and update progress accordingly.\n        \"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            return\n\n        now_date = dt_util.as_local(dt_util.utcnow()).date()\n\n        for achievement_id, achievement in self._data[DATA_ACHIEVEMENTS].items():\n            progress = achievement.setdefault(\"progress\", {})\n            if kid_id in progress and progress[kid_id].get(\"awarded\", False):\n                continue\n\n            ach_type = achievement.get(\"type\")\n            target = achievement.get(\"target_value\", 1)\n\n            # For a streak achievement, update a streak counter:\n            if ach_type == ACHIEVEMENT_TYPE_STREAK:\n                progress = progress.setdefault(\n                    kid_id, {\"current_streak\": 0, \"last_date\": None, \"awarded\": False}\n                )\n\n                self._update_streak_progress(progress, now_date)\n                if progress[\"current_streak\"] >= target:\n                    self._award_achievement(kid_id, achievement_id)\n\n            # For a total achievement, simply compare total completed chores:\n            elif ach_type == ACHIEVEMENT_TYPE_TOTAL:\n                # Get per–kid progress for this achievement.\n                progress = achievement.setdefault(\"progress\", {}).setdefault(\n                    kid_id, {\"baseline\": None, \"current_value\": 0, \"awarded\": False}\n                )\n\n                # Set the baseline so that we only count chores done after deployment.\n                if \"baseline\" not in progress or progress[\"baseline\"] is None:\n                    progress[\"baseline\"] = kid_info.get(\"completed_chores_total\", 0)\n\n                # Calculate progress as (current total minus baseline)\n                current_total = kid_info.get(\"completed_chores_total\", 0)\n\n                progress[\"current_value\"] = current_total\n\n                effective_target = progress[\"baseline\"] + target\n\n                if current_total >= effective_target:\n                    self._award_achievement(kid_id, achievement_id)\n\n            # For daily minimum achievement, compare total daily chores:\n            elif ach_type == ACHIEVEMENT_TYPE_DAILY_MIN:\n                # Initialize progress for this achievement if missing.\n                progress = achievement.setdefault(\"progress\", {}).setdefault(\n                    kid_id, {\"last_awarded_date\": None, \"awarded\": False}\n                )\n\n                today = dt_util.as_local(dt_util.utcnow()).date().isoformat()\n\n                # Only award bonus if not awarded today AND the kid's daily count meets the threshold.\n                if (\n                    progress.get(\"last_awarded_date\") != today\n                    and kid_info.get(\"completed_chores_today\", 0) >= target\n                ):\n                    self._award_achievement(kid_id, achievement_id)\n                    progress[\"last_awarded_date\"] = today\n\n    def _award_achievement(self, kid_id: str, achievement_id: str):\n        \"\"\"Award the achievement to the kid.\n\n        Update the achievement progress to indicate it is earned,\n        and send notifications to both the kid and their parents.\n        \"\"\"\n        achievement = self.achievements_data.get(achievement_id)\n        if not achievement:\n            LOGGER.error(\n                \"Attempted to award non-existent achievement '%s'\", achievement_id\n            )\n            return\n\n        # Get or create the existing progress dictionary for this kid\n        progress_for_kid = achievement.setdefault(\"progress\", {}).get(kid_id)\n        if progress_for_kid is None:\n            # If it doesn't exist, initialize it with baseline from the kid's current total.\n            kid_info = self.kids_data.get(kid_id, {})\n            progress_dict = {\n                \"baseline\": kid_info.get(\"completed_chores_total\", 0),\n                \"current_value\": 0,\n                \"awarded\": False,\n            }\n            achievement[\"progress\"][kid_id] = progress_dict\n            progress_for_kid = progress_dict\n\n        # Mark achievement as earned for the kid by storing progress (e.g. set to target)\n        progress_for_kid[\"awarded\"] = True\n        progress_for_kid[\"current_value\"] = achievement.get(\"target_value\", 1)\n\n        # Award the extra reward points defined in the achievement\n        extra_points = achievement.get(\"reward_points\", 0)\n        kid_info = self.kids_data.get(kid_id)\n        if kid_info is not None:\n            new_points = float(kid_info[\"points\"]) + extra_points\n            self.update_kid_points(kid_id, new_points)\n\n        # Notify kid and parents\n        extra_data = {\"kid_id\": kid_id, \"achievement_id\": achievement_id}\n        self.hass.async_create_task(\n            self._notify_kid(\n                kid_id,\n                title=\"KidsChores: Achievement Earned\",\n                message=f\"You have earned the achievement: '{achievement.get('name')}'.\",\n                extra_data=extra_data,\n            )\n        )\n        self.hass.async_create_task(\n            self._notify_parents(\n                kid_id,\n                title=\"KidsChores: Achievement Earned\",\n                message=f\"{self.kids_data[kid_id]['name']} has earned the achievement: '{achievement.get('name')}'.\",\n                extra_data=extra_data,\n            )\n        )\n        LOGGER.info(\n            \"Awarded achievement '%s' to kid '%s'\", achievement.get(\"name\"), kid_id\n        )\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # -------------------------------------------------------------------------\n    # Challenges: Check, Award\n    # -------------------------------------------------------------------------\n    def _check_challenges_for_kid(self, kid_id: str):\n        \"\"\"Evaluate all challenge criteria for a given kid.\n\n        Checks that the challenge is active and then updates progress.\n        \"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            return\n\n        now = dt_util.utcnow()\n        for challenge_id, challenge in self.challenges_data.items():\n            progress = challenge.setdefault(\"progress\", {})\n            if kid_id in progress and progress[kid_id].get(\"awarded\", False):\n                continue\n\n            # Check challenge window\n            start_date_raw = challenge.get(\"start_date\")\n            if isinstance(start_date_raw, str):\n                start = dt_util.parse_datetime(start_date_raw)\n            else:\n                start = None\n\n            end_date_raw = challenge.get(\"end_date\")\n            if isinstance(end_date_raw, str):\n                end = dt_util.parse_datetime(end_date_raw)\n            else:\n                end = None\n\n            if start and now < start:\n                continue\n            if end and now > end:\n                continue\n\n            target = challenge.get(\"target_value\", 1)\n            challenge_type = challenge.get(\"type\")\n\n            # For a total count challenge:\n            if challenge_type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n                progress = progress.setdefault(kid_id, {\"count\": 0, \"awarded\": False})\n\n                if progress[\"count\"] >= target:\n                    self._award_challenge(kid_id, challenge_id)\n            # For a daily minimum challenge, you might store per-day counts:\n            elif challenge_type == CHALLENGE_TYPE_DAILY_MIN:\n                progress = progress.setdefault(\n                    kid_id, {\"daily_counts\": {}, \"awarded\": False}\n                )\n\n                required_daily = challenge.get(\"required_daily\", 1)\n                start = dt_util.parse_datetime(challenge.get(\"start_date\"))\n                end = dt_util.parse_datetime(challenge.get(\"end_date\"))\n                if start and end:\n                    num_days = (end - start).days + 1\n                    # Verify for each day:\n                    success = True\n                    for n in range(num_days):\n                        day = (start + timedelta(days=n)).date().isoformat()\n                        if progress[\"daily_counts\"].get(day, 0) < required_daily:\n                            success = False\n                            break\n                    if success:\n                        self._award_challenge(kid_id, challenge_id)\n\n    def _award_challenge(self, kid_id: str, challenge_id: str):\n        \"\"\"Award the challenge to the kid.\n\n        Update progress and notify kid/parents.\n        \"\"\"\n        challenge = self.challenges_data.get(challenge_id)\n        if not challenge:\n            LOGGER.error(\"Attempted to award non-existent challenge '%s'\", challenge_id)\n            return\n\n        # Get or create the existing progress dictionary for this kid\n        progress_for_kid = challenge.setdefault(\"progress\", {}).setdefault(\n            kid_id, {\"count\": 0, \"awarded\": False}\n        )\n\n        # Mark challenge as earned for the kid by storing progress\n        progress_for_kid[\"awarded\"] = True\n        progress_for_kid[\"count\"] = challenge.get(\"target_value\", 1)\n\n        # Award extra reward points from the challenge\n        extra_points = challenge.get(\"reward_points\", 0)\n        kid_info = self.kids_data.get(kid_id)\n        if kid_info is not None:\n            new_points = float(kid_info[\"points\"]) + extra_points\n            self.update_kid_points(kid_id, new_points)\n\n        # Notify kid and parents\n        extra_data = {\"kid_id\": kid_id, \"challenge_id\": challenge_id}\n        self.hass.async_create_task(\n            self._notify_kid(\n                kid_id,\n                title=\"KidsChores: Challenge Completed\",\n                message=f\"You have completed the challenge: '{challenge.get('name')}'.\",\n                extra_data=extra_data,\n            )\n        )\n        self.hass.async_create_task(\n            self._notify_parents(\n                kid_id,\n                title=\"KidsChores: Challenge Completed\",\n                message=f\"{self.kids_data[kid_id]['name']} has completed the challenge: '{challenge.get('name')}'.\",\n                extra_data=extra_data,\n            )\n        )\n        LOGGER.info(\"Awarded challenge '%s' to kid '%s'\", challenge.get(\"name\"), kid_id)\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    def _update_streak_progress(self, progress: dict, today: datetime.date):\n        \"\"\"Update a streak progress dict.\n\n        If the last approved date was yesterday, increment the streak.\n        Otherwise, reset to 1.\n        \"\"\"\n        last_date = None\n        if progress.get(\"last_date\"):\n            # Parse the stored ISO string using Home Assistant's dt_util\n            last_dt = dt_util.parse_datetime(progress[\"last_date\"])\n            if last_dt:\n                # Convert to local time and get the date portion\n                last_date = dt_util.as_local(last_dt).date()\n        if last_date == today:\n            # Already updated today – do nothing\n            return\n        elif last_date == today - timedelta(days=1):\n            progress[\"current_streak\"] += 1\n        else:\n            progress[\"current_streak\"] = 1\n        progress[\"last_date\"] = today.isoformat()\n\n    def _update_chore_streak_for_kid(\n        self, kid_id: str, chore_id: str, completion_date: datetime.date\n    ):\n        \"\"\"Update (or initialize) the streak for a specific chore for a kid, and update the max streak achieved so far.\"\"\"\n\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            return\n        # Ensure a streak dictionary exists\n        if \"chore_streaks\" not in kid_info:\n            kid_info[\"chore_streaks\"] = {}\n\n        # Initialize the streak record if not already present\n        streak = kid_info[\"chore_streaks\"].get(\n            chore_id, {\"current_streak\": 0, \"max_streak\": 0, \"last_date\": None}\n        )\n        last_date = None\n        if streak[\"last_date\"]:\n            try:\n                last_date = datetime.fromisoformat(streak[\"last_date\"]).date()\n            except Exception:\n                pass\n\n        if last_date == completion_date - timedelta(days=1):\n            streak[\"current_streak\"] += 1\n        else:\n            streak[\"current_streak\"] = 1\n\n        streak[\"last_date\"] = completion_date.isoformat()\n\n        # Update the maximum streak if the current streak is higher.\n        if streak[\"current_streak\"] > streak.get(\"max_streak\", 0):\n            streak[\"max_streak\"] = streak[\"current_streak\"]\n\n        kid_info[\"chore_streaks\"][chore_id] = streak\n\n    def _update_overall_chore_streak(self, kid_id: str, completion_date: datetime.date):\n        \"\"\"Update the overall streak for a kid (days in a row with at least one approved chore).\"\"\"\n\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            return\n        last_date = None\n        if \"last_chore_date\" in kid_info and kid_info[\"last_chore_date\"]:\n            try:\n                last_date = datetime.fromisoformat(kid_info[\"last_chore_date\"]).date()\n            except Exception:\n                pass\n        if last_date == completion_date - timedelta(days=1):\n            kid_info[\"overall_chore_streak\"] = (\n                kid_info.get(\"overall_chore_streak\", 0) + 1\n            )\n        else:\n            kid_info[\"overall_chore_streak\"] = 1\n        kid_info[\"last_chore_date\"] = completion_date.isoformat()\n\n    # -------------------------------------------------------------------------------------\n    # Recurring / Reset / Overdue\n    # -------------------------------------------------------------------------------------\n\n    async def _check_overdue_chores(self):\n        \"\"\"Check and mark overdue chores if due date is passed.\n\n        Send an overdue notification only if not sent in the last 24 hours.\n        \"\"\"\n        now = dt_util.utcnow()\n        LOGGER.debug(\"Starting overdue check at %s\", now.isoformat())\n\n        for chore_id, chore_info in self.chores_data.items():\n            # LOGGER.debug(\"Checking chore '%s' id '%s' (state=%s)\", chore_info.get(\"name\"), chore_id, chore_info.get(\"state\"))\n\n            # Get the list of assigned kids\n            assigned_kids = chore_info.get(\"assigned_kids\", [])\n            # LOGGER.debug(\"Chore '%s' id '%s' assigned to kids: %s\", chore_info.get(\"name\"), chore_id, assigned_kids,)\n\n            # Check if all assigned kids have either claimed or approved the chore\n            all_kids_claimed_or_approved = all(\n                chore_id in self.kids_data.get(kid_id, {}).get(\"claimed_chores\", [])\n                or chore_id in self.kids_data.get(kid_id, {}).get(\"approved_chores\", [])\n                for kid_id in assigned_kids\n            )\n\n            # Debugging: Log the claim/approval status of each assigned kid\n            for kid_id in assigned_kids:\n                kid_info = self.kids_data.get(kid_id, {})\n                has_claimed = chore_id in kid_info.get(\"claimed_chores\", [])\n                has_approved = chore_id in kid_info.get(\"approved_chores\", [])\n\n                # LOGGER.debug(\"Kid '%s': claimed=%s, approved=%s\", kid_id, has_claimed, has_approved\n\n            # Log the overall result of the check\n            # LOGGER.debug(\"Chore '%s': all_kids_claimed_or_approved=%s\", chore_id, all_kids_claimed_or_approved)\n\n            # Only skip the chore if ALL assigned kids have acted on it\n            if all_kids_claimed_or_approved:\n                # LOGGER.debug(\"Skipping chore '%s': all assigned kids have claimed or approved\", chore_id,)\n                continue\n\n            due_str = chore_info.get(\"due_date\")\n            if not due_str:\n                LOGGER.debug(\n                    \"Chore '%s' has no due_date; checking to confirm it isn't overdue; then skipping if not\",\n                    chore_id,\n                )\n                # If it has no due date, but is overdue, it should be marked as pending\n                if chore_info.get(\"state\") == CHORE_STATE_OVERDUE:\n                    self._process_chore_state(kid_id, chore_id, CHORE_STATE_PENDING)\n                continue\n\n            try:\n                due_date = dt_util.parse_datetime(due_str)\n                if due_date is None:\n                    raise ValueError(\"Parsed datetime is None\")\n                due_date = dt_util.as_utc(due_date)\n                # LOGGER.debug(\"Chore '%s' due_date parsed as %s\", chore_id, due_date.isoformat())\n            except Exception as err:\n                LOGGER.error(\n                    \"Error parsing due_date '%s' for chore '%s': %s\",\n                    due_str,\n                    chore_id,\n                    err,\n                )\n                continue\n\n            # Check for applicable day is no longer required; the scheduling function ensures due_date matches applicable day criteria.\n            # LOGGER.debug(\"Chore '%s': now=%s, due_date=%s\", chore_id, now.isoformat(), due_date.isoformat()\n            if now < due_date:\n                # Not past due date, but before resetting the state back to pending, check if global state is currently overdue\n                for kid_id in assigned_kids:\n                    if chore_id in kid_info.get(\"overdue_chores\", []):\n                        self._process_chore_state(kid_id, chore_id, CHORE_STATE_PENDING)\n                        LOGGER.debug(\n                            \"Chore '%s' status is overdue but not yet due; cleared overdue flags\",\n                            chore_id,\n                        )\n\n                continue\n\n            # Handling for overdue is the same for shared and non-shared chores\n            # Status and global status will be determined by the chore state processor\n            assigned_kids = chore_info.get(\"assigned_kids\", [])\n            for kid_id in assigned_kids:\n                kid_info = self.kids_data.get(kid_id, {})\n\n                # Skip if kid already claimed/approved on the chore.\n                if chore_id in kid_info.get(\n                    \"claimed_chores\", []\n                ) or chore_id in kid_info.get(\"approved_chores\", []):\n                    continue\n\n                # Mark chore as overdue for this kid.\n                self._process_chore_state(kid_id, chore_id, CHORE_STATE_OVERDUE)\n                LOGGER.debug(\n                    \"Marking chore '%s' as overdue for kid '%s'\", chore_id, kid_id\n                )\n\n                # Check notification timestamp.\n                last_notif_str = kid_info[\"overdue_notifications\"].get(chore_id)\n                notify = False\n                if last_notif_str:\n                    try:\n                        last_dt = dt_util.parse_datetime(last_notif_str)\n                        if (\n                            (not last_dt)\n                            or (last_dt < due_date)\n                            or ((now - last_dt) >= timedelta(hours=24))\n                        ):\n                            notify = True\n                        else:\n                            LOGGER.debug(\n                                \"Chore '%s' for kid '%s' already notified within 24 hours\",\n                                chore_id,\n                                kid_id,\n                            )\n                    except Exception as err:\n                        LOGGER.error(\n                            \"Error parsing overdue notification '%s' for chore '%s', kid '%s': %s\",\n                            last_notif_str,\n                            chore_id,\n                            kid_id,\n                            err,\n                        )\n                        notify = True\n                else:\n                    notify = True\n\n                if notify:\n                    kid_info[\"overdue_notifications\"][chore_id] = now.isoformat()\n                    extra_data = {\"kid_id\": kid_id, \"chore_id\": chore_id}\n                    actions = [\n                        {\n                            \"action\": f\"{ACTION_APPROVE_CHORE}|{kid_id}|{chore_id}\",\n                            \"title\": ACTION_TITLE_APPROVE,\n                        },\n                        {\n                            \"action\": f\"{ACTION_DISAPPROVE_CHORE}|{kid_id}|{chore_id}\",\n                            \"title\": ACTION_TITLE_DISAPPROVE,\n                        },\n                        {\n                            \"action\": f\"{ACTION_REMIND_30}|{kid_id}|{chore_id}\",\n                            \"title\": ACTION_TITLE_REMIND_30,\n                        },\n                    ]\n                    LOGGER.debug(\n                        \"Sending overdue notification for chore '%s' to kid '%s'\",\n                        chore_id,\n                        kid_id,\n                    )\n                    self.hass.async_create_task(\n                        self._notify_kid(\n                            kid_id,\n                            title=\"KidsChores: Chore Overdue\",\n                            message=f\"Your chore '{chore_info.get('name', 'Unnamed Chore')}' is overdue\",\n                            extra_data=extra_data,\n                        )\n                    )\n                    self.hass.async_create_task(\n                        self._notify_parents(\n                            kid_id,\n                            title=\"KidsChores: Chore Overdue\",\n                            message=f\"{self._get_kid_name_by_id(kid_id)}'s chore '{chore_info.get('name', 'Unnamed Chore')}' is overdue\",\n                            actions=actions,\n                            extra_data=extra_data,\n                        )\n                    )\n        LOGGER.debug(\"Overdue check completed\")\n\n    async def _reset_all_chore_counts(self, now: datetime):\n        \"\"\"Trigger resets based on the current time for all frequencies.\"\"\"\n        await self._handle_recurring_chore_resets(now)\n        await self._reset_daily_reward_statuses()\n        await self._check_overdue_chores()\n\n        for kid in self.kids_data.values():\n            kid[\"today_chore_approvals\"] = {}\n\n    async def _handle_recurring_chore_resets(self, now: datetime):\n        \"\"\"Handle recurring resets for daily, weekly, and monthly frequencies.\"\"\"\n\n        await self._reschedule_recurring_chores(now)\n\n        # Daily\n        if now.hour == DEFAULT_DAILY_RESET_TIME.get(\"hour\", 0):\n            await self._reset_chore_counts(FREQUENCY_DAILY, now)\n\n        # Weekly\n        if now.weekday() == DEFAULT_WEEKLY_RESET_DAY:\n            await self._reset_chore_counts(FREQUENCY_WEEKLY, now)\n\n        # Monthly\n        days_in_month = monthrange(now.year, now.month)[1]\n        reset_day = min(DEFAULT_MONTHLY_RESET_DAY, days_in_month)\n        if now.day == reset_day:\n            await self._reset_chore_counts(FREQUENCY_MONTHLY, now)\n\n    async def _reset_chore_counts(self, frequency: str, now: datetime):\n        \"\"\"Reset chore counts and statuses based on the recurring frequency.\"\"\"\n        # Reset counters on kids\n        for kid_info in self.kids_data.values():\n            if frequency == FREQUENCY_DAILY:\n                kid_info[\"completed_chores_today\"] = 0\n                kid_info[\"points_earned_today\"] = 0.0\n            elif frequency == FREQUENCY_WEEKLY:\n                kid_info[\"completed_chores_weekly\"] = 0\n                kid_info[\"points_earned_weekly\"] = 0.0\n            elif frequency == FREQUENCY_MONTHLY:\n                kid_info[\"completed_chores_monthly\"] = 0\n                kid_info[\"points_earned_monthly\"] = 0.0\n\n        LOGGER.info(f\"{frequency.capitalize()} chore counts have been reset\")\n\n        # If daily reset -> reset statuses\n        if frequency == FREQUENCY_DAILY:\n            await self._reset_daily_chore_statuses([frequency])\n        elif frequency == FREQUENCY_WEEKLY:\n            await self._reset_daily_chore_statuses([frequency, FREQUENCY_WEEKLY])\n\n    async def _reschedule_recurring_chores(self, now: datetime):\n        \"\"\"For chores with the given recurring frequency, reschedule due date if they are approved and past due.\"\"\"\n\n        for chore_id, chore_info in self.chores_data.items():\n            # Only consider chores with a recurring frequency (any of the three) and a defined due_date:\n            if chore_info.get(\"recurring_frequency\") not in (\n                FREQUENCY_DAILY,\n                FREQUENCY_WEEKLY,\n                FREQUENCY_BIWEEKLY,\n                FREQUENCY_MONTHLY,\n                FREQUENCY_CUSTOM,\n            ):\n                continue\n            if not chore_info.get(\"due_date\"):\n                continue\n\n            try:\n                due_date = dt_util.parse_datetime(\n                    chore_info[\"due_date\"]\n                ) or datetime.fromisoformat(chore_info[\"due_date\"])\n            except Exception as e:\n                LOGGER.warning(\"Error parsing due_date for chore '%s': %s\", chore_id, e)\n                continue\n\n            # If the due date is in the past and the chore is approved or approved_in_part\n            if now > due_date and chore_info.get(\"state\") in [\n                CHORE_STATE_APPROVED,\n                CHORE_STATE_APPROVED_IN_PART,\n            ]:\n                # Reschedule the chore\n                self._reschedule_next_due_date(chore_info)\n                LOGGER.debug(\n                    \"Rescheduled recurring chore '%s'\", chore_info.get(\"name\", chore_id)\n                )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n        LOGGER.debug(\"Daily rescheduling of recurring chores complete\")\n\n    async def _reset_daily_chore_statuses(self, target_freqs: list[str]):\n        \"\"\"Reset chore statuses and clear approved/claimed chores for chores with these freq.\"\"\"\n        LOGGER.info(\"Executing _reset_daily_chore_statuses\")\n\n        now = dt_util.utcnow()\n        for chore_id, chore_info in self.chores_data.items():\n            frequency = chore_info.get(\"recurring_frequency\", FREQUENCY_NONE)\n            # Only consider chores whose frequency is either in target_freqs or FREQUENCY_NONE.\n            if frequency in target_freqs or frequency == FREQUENCY_NONE:\n                due_date_str = chore_info.get(\"due_date\")\n                if due_date_str:\n                    try:\n                        due_date = dt_util.parse_datetime(\n                            due_date_str\n                        ) or datetime.fromisoformat(due_date_str)\n                        # If the due date has not yet been reached, skip resetting this chore.\n                        if now < due_date:\n                            continue\n                    except Exception as e:\n                        LOGGER.warning(\n                            \"Error parsing due_date '%s' for chore '%s': %s\",\n                            due_date_str,\n                            chore_id,\n                            e,\n                        )\n                # If no due date or the due date has passed, then reset the chore state\n                if chore_info[\"state\"] not in [\n                    CHORE_STATE_PENDING,\n                    CHORE_STATE_OVERDUE,\n                ]:\n                    previous_state = chore_info[\"state\"]\n                    for kid_id in chore_info.get(\"assigned_kids\", []):\n                        if kid_id:\n                            self._process_chore_state(\n                                kid_id, chore_id, CHORE_STATE_PENDING\n                            )\n                    LOGGER.debug(\n                        \"Resetting chore '%s' from '%s' to '%s'\",\n                        chore_id,\n                        previous_state,\n                        CHORE_STATE_PENDING,\n                    )\n\n        # clear pending chore approvals\n        target_chore_ids = [\n            chore_id\n            for chore_id, chore_info in self.chores_data.items()\n            if chore_info.get(\"recurring_frequency\") in target_freqs\n        ]\n        self._data[DATA_PENDING_CHORE_APPROVALS] = [\n            ap\n            for ap in self._data[DATA_PENDING_CHORE_APPROVALS]\n            if ap[\"chore_id\"] not in target_chore_ids\n        ]\n\n        self._persist()\n\n    async def _reset_daily_reward_statuses(self):\n        \"\"\"Reset all kids' reward states daily.\"\"\"\n        # Remove from global pending reward approvals\n        self._data[DATA_PENDING_REWARD_APPROVALS] = []\n        LOGGER.debug(\"Cleared all pending reward approvals globally\")\n\n        # For each kid, clear pending/approved reward lists to reflect daily reset\n        for kid_id, kid_info in self.kids_data.items():\n            kid_info[\"pending_rewards\"] = []\n            kid_info[\"redeemed_rewards\"] = []\n\n            LOGGER.debug(\n                \"Cleared daily reward statuses for kid ID '%s' (%s)\",\n                kid_id,\n                kid_info.get(\"name\", \"Unknown\"),\n            )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n        LOGGER.info(\"Daily reward statuses have been reset\")\n\n    def _reschedule_next_due_date(self, chore_info: dict[str, Any]):\n        \"\"\"Reschedule the next due date based on the recurring frequency.\"\"\"\n        freq = chore_info.get(\"recurring_frequency\", FREQUENCY_NONE)\n        if freq == FREQUENCY_CUSTOM:\n            custom_interval = chore_info.get(\"custom_interval\")\n            custom_unit = chore_info.get(\"custom_interval_unit\")\n            if custom_interval is None or custom_unit not in [\n                \"days\",\n                \"weeks\",\n                \"months\",\n            ]:\n                LOGGER.warning(\n                    \"Custom frequency set but custom_interval or unit invalid for chore '%s'\",\n                    chore_info.get(\"name\"),\n                )\n                return\n\n        due_date_str = chore_info.get(\"due_date\")\n        if not freq or freq == FREQUENCY_NONE or not due_date_str:\n            LOGGER.debug(\n                \"Skipping reschedule: recurring_frequency=%s, due_date=%s\",\n                freq,\n                due_date_str,\n            )\n            return\n        try:\n            original_due = dt_util.parse_datetime(due_date_str)\n            if not original_due:\n                original_due = datetime.fromisoformat(due_date_str)\n        except ValueError:\n            LOGGER.warning(\"Unable to parse due_date '%s'\", due_date_str)\n            return\n\n        applicable_days = chore_info.get(CONF_APPLICABLE_DAYS, DEFAULT_APPLICABLE_DAYS)\n        weekday_mapping = {i: key for i, key in enumerate(WEEKDAY_OPTIONS.keys())}\n        # Convert next_due to local time for proper weekday checking\n        now = dt_util.utcnow()\n        now_local = dt_util.as_local(now)\n        next_due = original_due\n        next_due_local = dt_util.as_local(next_due)\n\n        # Track first iteration to allow one advancement for future dates\n        first_iteration = True\n        # Ensure the next due date is advanced even if it's already scheduled in the future\n        # Handle past due_date by looping until we find a future date that is also on an applicable day\n        while (\n            first_iteration\n            or next_due_local <= now_local\n            or (\n                applicable_days\n                and weekday_mapping[next_due_local.weekday()] not in applicable_days\n            )\n        ):\n            # If next_due is still in the past, increment by the full frequency period\n            if first_iteration or (next_due_local <= now_local):\n                if freq == FREQUENCY_DAILY:\n                    next_due += timedelta(days=1)\n                elif freq == FREQUENCY_WEEKLY:\n                    next_due += timedelta(weeks=1)\n                elif freq == FREQUENCY_BIWEEKLY:\n                    next_due += timedelta(weeks=2)\n                elif freq == FREQUENCY_MONTHLY:\n                    next_due = self._add_months(next_due, 1)\n                elif freq == FREQUENCY_CUSTOM:\n                    if custom_unit == \"days\":\n                        next_due += timedelta(days=custom_interval)\n                    elif custom_unit == \"weeks\":\n                        next_due += timedelta(weeks=custom_interval)\n                    elif custom_unit == \"months\":\n                        next_due = self._add_months(next_due, custom_interval)\n            else:\n                # Next due is in the future but not on an applicable day,\n                # so just add one day until it falls on an applicable day.\n                next_due += timedelta(days=1)\n\n            # After first loop, only move forward if necessary\n            first_iteration = False\n\n            # Update the local time reference for the new next_due\n            next_due_local = dt_util.as_local(next_due)\n\n            LOGGER.debug(\n                \"Rescheduling chore: Original Due: %s, New Attempt: %s (Local: %s), Now: %s (Local: %s), Weekday: %s, Applicable Days: %s\",\n                original_due,\n                next_due,\n                next_due_local,\n                now,\n                now_local,\n                weekday_mapping[next_due_local.weekday()],\n                applicable_days,\n            )\n\n        chore_info[\"due_date\"] = next_due.isoformat()\n        chore_id = chore_info.get(\"internal_id\")\n\n        # Update config_entry.options for this chore so that the new due_date is visible in Options\n        self.hass.async_create_task(\n            self._update_chore_due_date_in_config(\n                chore_id, chore_info[\"due_date\"], None, None, None\n            )\n        )\n        # Reset the chore state to Pending\n        for kid_id in chore_info.get(\"assigned_kids\", []):\n            if kid_id:\n                self._process_chore_state(kid_id, chore_id, CHORE_STATE_PENDING)\n\n        LOGGER.info(\n            \"Chore '%s' rescheduled: Original due date %s, Final new due date (local) %s\",\n            chore_info.get(\"name\", chore_id),\n            dt_util.as_local(original_due).isoformat(),\n            next_due_local.isoformat(),\n        )\n\n    # Removed the _add_one_month method since _add_months method will handle all cases including adding one month.\n    def _add_months(self, dt_in: datetime, months: int) -> datetime:\n        \"\"\"Add a specified number of months to a datetime, preserving the day if possible.\"\"\"\n        total_month = dt_in.month + months\n        year = dt_in.year + (total_month - 1) // 12\n        month = ((total_month - 1) % 12) + 1\n        day = dt_in.day\n        days_in_new_month = monthrange(year, month)[1]\n        if day > days_in_new_month:\n            day = days_in_new_month\n        return dt_in.replace(year=year, month=month, day=day)\n\n    # Set Chore Due Date\n    def set_chore_due_date(self, chore_id: str, due_date: Optional[datetime]) -> None:\n        \"\"\"Set the due date of a chore.\"\"\"\n        # Retrieve the chore data; raise error if not found.\n        chore_info = self.chores_data.get(chore_id)\n        if chore_info is None:\n            raise HomeAssistantError(f\"Chore with ID '{chore_id}' not found.\")\n\n        # Convert the due_date to an ISO-formatted string if provided; otherwise use None.\n        new_due_date = due_date.isoformat() if due_date else None\n\n        # Update the chore's due date. If the key is missing, add it.\n        try:\n            chore_info[\"due_date\"] = new_due_date\n        except KeyError as err:\n            raise HomeAssistantError(\n                f\"Missing 'due_date' key in chore data for '{chore_id}': {err}\"\n            )\n\n        # If the due date is cleared (None), then remove any recurring frequency\n        # and custom interval settings unless the frequency is none, daily, or weekly.\n        if new_due_date is None:\n            # FREQUENCY_DAILY, FREQUENCY_WEEKLY, and FREQUENCY_NONE are all OK without a due_date\n            current_frequency = chore_info.get(\"recurring_frequency\")\n            if chore_info.get(\"recurring_frequency\") not in (\n                FREQUENCY_NONE,\n                FREQUENCY_DAILY,\n                FREQUENCY_WEEKLY,\n            ):\n                LOGGER.debug(\n                    \"Removing frequency for chore '%s': current frequency '%s' is does not work with a due date of None\",\n                    chore_id,\n                    current_frequency,\n                )\n                chore_info[\"recurring_frequency\"] = FREQUENCY_NONE\n                chore_info.pop(\"custom_interval\", None)\n                chore_info.pop(\"custom_interval_unit\", None)\n\n        # Update config_entry.options so that the new due date is visible in Options.\n        # Use new_due_date here to ensure we’re passing the updated value.\n        self.hass.async_create_task(\n            self._update_chore_due_date_in_config(\n                chore_id,\n                chore_info.get(\"due_date\"),\n                chore_info.get(\"recurring_frequency\"),\n                chore_info.get(\"custom_interval\"),\n                chore_info.get(\"custom_interval_unit\"),\n            )\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # Skip Chore Due Date\n    def skip_chore_due_date(self, chore_id: str) -> None:\n        \"\"\"Skip the current due date of a recurring chore and reschedule it.\"\"\"\n        chore = self.chores_data.get(chore_id)\n        if not chore:\n            raise HomeAssistantError(f\"Chore with ID '{chore_id}' not found.\")\n\n        if chore.get(\"recurring_frequency\", FREQUENCY_NONE) == FREQUENCY_NONE:\n            raise HomeAssistantError(\n                f\"Chore '{chore.get('name', chore_id)}' does not have a recurring frequency.\"\n            )\n        if not chore.get(\"due_date\"):\n            raise HomeAssistantError(\n                f\"Chore '{chore.get('name', chore_id)}' does not have a due date set.\"\n            )\n\n        # Compute the next due date and update the chore options/config.\n        self._reschedule_next_due_date(chore)\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # Reset Overdue Chores\n    def reset_overdue_chores(\n        self, chore_id: Optional[str] = None, kid_id: Optional[str] = None\n    ) -> None:\n        \"\"\"Reset overdue chore(s) to Pending state and reschedule.\"\"\"\n\n        if chore_id:\n            # Specific chore reset (with or without kid_id)\n            chore = self.chores_data.get(chore_id)\n            if not chore:\n                raise HomeAssistantError(f\"Chore with ID '{chore_id}' not found.\")\n\n            # Reschedule happens at the chore level, so it is not necessary to check for kid_id\n            # _rescheduled_next_due_date will also handle setting the status to Pending\n            self._reschedule_next_due_date(chore)\n\n        elif kid_id:\n            # Kid-only reset: reset all overdue chores for the specified kid.\n            # Note that reschedule happens at the chore level, so it chores assigned to this kid that are multi assigned\n            # will show as reset for those other kids\n            kid = self.kids_data.get(kid_id)\n            if not kid:\n                raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n            for cid, chore in self.chores_data.items():\n                if kid_id in chore.get(\"assigned_kids\", []):\n                    if cid in kid.get(\"overdue_chores\", []):\n                        # Reschedule chore which will also set status to Pending\n                        self._reschedule_next_due_date(chore)\n        else:\n            # Global reset: Reset all chores that are overdue.\n            for kid_id, kid in self.kids_data.items():\n                for cid, chore in self.chores_data.items():\n                    if kid_id in chore.get(\"assigned_kids\", []):\n                        if cid in kid.get(\"overdue_chores\", []):\n                            # Reschedule chore which will also set status to Pending\n                            self._reschedule_next_due_date(chore)\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # -------------------------------------------------------------------------------------\n    # Penalties: Reset\n    # -------------------------------------------------------------------------------------\n\n    def reset_penalties(\n        self, kid_id: Optional[str] = None, penalty_id: Optional[str] = None\n    ) -> None:\n        \"\"\"Reset penalties based on provided kid_id and penalty_id.\"\"\"\n\n        if penalty_id and kid_id:\n            # Reset a specific penalty for a specific kid\n            kid_info = self.kids_data.get(kid_id)\n            if not kid_info:\n                LOGGER.error(\"Reset Penalties: Kid with ID '%s' not found.\", kid_id)\n                raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n            if penalty_id not in kid_info.get(\"penalty_applies\", {}):\n                LOGGER.error(\n                    \"Reset Penalties: Penalty '%s' does not apply to kid '%s'.\",\n                    penalty_id,\n                    kid_id,\n                )\n                raise HomeAssistantError(\n                    f\"Penalty '{penalty_id}' does not apply to kid '{kid_id}'.\"\n                )\n\n            kid_info[\"penalty_applies\"].pop(penalty_id, None)\n\n        elif penalty_id:\n            # Reset a specific penalty for all kids\n            found = False\n            for kid_info in self.kids_data.values():\n                if penalty_id in kid_info.get(\"penalty_applies\", {}):\n                    found = True\n                    kid_info[\"penalty_applies\"].pop(penalty_id, None)\n\n            if not found:\n                LOGGER.warning(\n                    \"Reset Penalties: Penalty '%s' not found in any kid's data.\",\n                    penalty_id,\n                )\n\n        elif kid_id:\n            # Reset all penalties for a specific kid\n            kid_info = self.kids_data.get(kid_id)\n            if not kid_info:\n                LOGGER.error(\"Reset Penalties: Kid with ID '%s' not found.\", kid_id)\n                raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n            kid_info[\"penalty_applies\"].clear()\n\n        else:\n            # Reset all penalties for all kids\n            LOGGER.info(\"Reset Penalties: Resetting all penalties for all kids.\")\n            for kid_info in self.kids_data.values():\n                kid_info[\"penalty_applies\"].clear()\n\n        LOGGER.debug(\n            \"Penalties reset completed (kid_id=%s, penalty_id=%s)\", kid_id, penalty_id\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # -------------------------------------------------------------------------------------\n    # Bonuses: Reset\n    # -------------------------------------------------------------------------------------\n\n    def reset_bonuses(\n        self, kid_id: Optional[str] = None, bonus_id: Optional[str] = None\n    ) -> None:\n        \"\"\"Reset bonuses based on provided kid_id and bonus_id.\"\"\"\n\n        if bonus_id and kid_id:\n            # Reset a specific bonus for a specific kid\n            kid_info = self.kids_data.get(kid_id)\n            if not kid_info:\n                LOGGER.error(\"Reset Bonuses: Kid with ID '%s' not found.\", kid_id)\n                raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n            if bonus_id not in kid_info.get(\"bonus_applies\", {}):\n                LOGGER.error(\n                    \"Reset Bonuses: Bonus '%s' does not apply to kid '%s'.\",\n                    bonus_id,\n                    kid_id,\n                )\n                raise HomeAssistantError(\n                    f\"Bonus '{bonus_id}' does not apply to kid '{kid_id}'.\"\n                )\n\n            kid_info[\"bonus_applies\"].pop(bonus_id, None)\n\n        elif bonus_id:\n            # Reset a specific bonus for all kids\n            found = False\n            for kid_info in self.kids_data.values():\n                if bonus_id in kid_info.get(\"bonus_applies\", {}):\n                    found = True\n                    kid_info[\"bonus_applies\"].pop(bonus_id, None)\n\n            if not found:\n                LOGGER.warning(\n                    \"Reset Bonuses: Bonus '%s' not found in any kid's data.\", bonus_id\n                )\n\n        elif kid_id:\n            # Reset all bonuses for a specific kid\n            kid_info = self.kids_data.get(kid_id)\n            if not kid_info:\n                LOGGER.error(\"Reset Bonuses: Kid with ID '%s' not found.\", kid_id)\n                raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n            kid_info[\"bonus_applies\"].clear()\n\n        else:\n            # Reset all bonuses for all kids\n            LOGGER.info(\"Reset Bonuses: Resetting all bonuses for all kids.\")\n            for kid_info in self.kids_data.values():\n                kid_info[\"bonus_applies\"].clear()\n\n        LOGGER.debug(\n            \"Bonuses reset completed (kid_id=%s, bonus_id=%s)\", kid_id, bonus_id\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # -------------------------------------------------------------------------------------\n    # Rewards: Reset\n    # This function resets reward-related data for a specified kid and/or reward by\n    # clearing claims, approvals, redeemed and pending rewards, and removing associated\n    # pending reward approvals from the global data.\n    # -------------------------------------------------------------------------------------\n\n    def reset_rewards(\n        self, kid_id: Optional[str] = None, reward_id: Optional[str] = None\n    ) -> None:\n        \"\"\"Reset rewards based on provided kid_id and reward_id.\"\"\"\n\n        if reward_id and kid_id:\n            # Reset a specific reward for a specific kid\n            kid_info = self.kids_data.get(kid_id)\n            if not kid_info:\n                LOGGER.error(\"Reset Rewards: Kid with ID '%s' not found.\", kid_id)\n                raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n            kid_info[\"reward_claims\"].pop(reward_id, None)\n            kid_info[\"reward_approvals\"].pop(reward_id, None)\n            kid_info[\"redeemed_rewards\"] = [\n                reward for reward in kid_info[\"redeemed_rewards\"] if reward != reward_id\n            ]\n            kid_info[\"pending_rewards\"] = [\n                reward for reward in kid_info[\"pending_rewards\"] if reward != reward_id\n            ]\n\n            # Remove open claims from pending approvals for this kid and reward.\n            self._data[DATA_PENDING_REWARD_APPROVALS] = [\n                ap\n                for ap in self._data[DATA_PENDING_REWARD_APPROVALS]\n                if not (ap[\"kid_id\"] == kid_id and ap[\"reward_id\"] == reward_id)\n            ]\n\n        elif reward_id:\n            # Reset a specific reward for all kids\n            found = False\n            for kid_info in self.kids_data.values():\n                if reward_id in kid_info.get(\"reward_claims\", {}):\n                    found = True\n                    kid_info[\"reward_claims\"].pop(reward_id, None)\n                if reward_id in kid_info.get(\"reward_approvals\", {}):\n                    found = True\n                    kid_info[\"reward_approvals\"].pop(reward_id, None)\n                kid_info[\"redeemed_rewards\"] = [\n                    reward\n                    for reward in kid_info[\"redeemed_rewards\"]\n                    if reward != reward_id\n                ]\n                kid_info[\"pending_rewards\"] = [\n                    reward\n                    for reward in kid_info[\"pending_rewards\"]\n                    if reward != reward_id\n                ]\n            # Remove open claims from pending approvals for this reward (all kids).\n            self._data[DATA_PENDING_REWARD_APPROVALS] = [\n                ap\n                for ap in self._data[DATA_PENDING_REWARD_APPROVALS]\n                if ap[\"reward_id\"] != reward_id\n            ]\n            if not found:\n                LOGGER.warning(\n                    \"Reset Rewards: Reward '%s' not found in any kid's data.\",\n                    reward_id,\n                )\n\n        elif kid_id:\n            # Reset all rewards for a specific kid\n            kid_info = self.kids_data.get(kid_id)\n            if not kid_info:\n                LOGGER.error(\"Reset Rewards: Kid with ID '%s' not found.\", kid_id)\n                raise HomeAssistantError(f\"Kid with ID '{kid_id}' not found.\")\n\n            kid_info[\"reward_claims\"].clear()\n            kid_info[\"reward_approvals\"].clear()\n            kid_info[\"redeemed_rewards\"].clear()\n            kid_info[\"pending_rewards\"].clear()\n\n            # Remove open claims from pending approvals for that kid.\n            self._data[DATA_PENDING_REWARD_APPROVALS] = [\n                ap\n                for ap in self._data[DATA_PENDING_REWARD_APPROVALS]\n                if ap[\"kid_id\"] != kid_id\n            ]\n\n        else:\n            # Reset all rewards for all kids\n            LOGGER.info(\"Reset Rewards: Resetting all rewards for all kids.\")\n            for kid_info in self.kids_data.values():\n                kid_info[\"reward_claims\"].clear()\n                kid_info[\"reward_approvals\"].clear()\n                kid_info[\"redeemed_rewards\"].clear()\n                kid_info[\"pending_rewards\"].clear()\n\n            # Clear all pending reward approvals.\n            self._data[DATA_PENDING_REWARD_APPROVALS].clear()\n\n        LOGGER.debug(\n            \"Rewards reset completed (kid_id=%s, reward_id=%s)\", kid_id, reward_id\n        )\n\n        self._persist()\n        self.async_set_updated_data(self._data)\n\n    # Persist new due dates on config entries\n    # This is not being used currently, but was refactored so it calls a new function _update_chore_due_date_in_config\n    # which can be used to update a single chore's due date and frequency.  New function can be used in multiple places.\n\n    async def _update_all_chore_due_dates_in_config(self) -> None:\n        \"\"\"Update due dates for all chores in config_entry.options.\"\"\"\n        tasks = []\n        for chore_id, chore_info in self.chores_data.items():\n            if \"due_date\" in chore_info:\n                tasks.append(\n                    self._update_chore_due_date_in_config(\n                        chore_id,\n                        chore_info.get(\"due_date\"),\n                        recurring_frequency=chore_info.get(\"recurring_frequency\"),\n                        custom_interval=chore_info.get(\"custom_interval\"),\n                        custom_interval_unit=chore_info.get(\"custom_interval_unit\"),\n                    )\n                )\n\n        # Run all updates concurrently\n        if tasks:\n            await asyncio.gather(*tasks)\n\n    # Persist new due dates on config entries\n    async def _update_chore_due_date_in_config(\n        self,\n        chore_id: str,\n        due_date: Optional[str],\n        recurring_frequency: Optional[str] = None,\n        custom_interval: Optional[int] = None,\n        custom_interval_unit: Optional[str] = None,\n    ) -> None:\n        \"\"\"Update the due date and frequency fields for a specific chore in config_entry.options.\n\n        - due_date should be an ISO-formatted string (or None).\n        - If a frequency is passed, then that value is set.\n        If the frequency is FREQUENCY_CUSTOM, custom_interval and custom_interval_unit are required.\n        If the frequency is not custom, any custom interval settings are cleared.\n        - If no frequency is passed, then do not change the frequency or custom interval settings.\n        \"\"\"\n        updated_options = dict(self.config_entry.options)\n        chores_conf = dict(updated_options.get(DATA_CHORES, {}))\n\n        # Get existing options for the chore.\n        existing_options = dict(chores_conf.get(chore_id, {}))\n\n        # Update due_date: set if provided; otherwise remove.\n        if due_date is not None:\n            existing_options[\"due_date\"] = due_date\n        else:\n            existing_options.pop(\"due_date\", None)\n\n        # If a frequency is passed, update it.\n        if recurring_frequency is not None:\n            existing_options[\"recurring_frequency\"] = recurring_frequency\n            if recurring_frequency == FREQUENCY_CUSTOM:\n                # For custom frequency, custom_interval and custom_interval_unit are required.\n                if custom_interval is None or custom_interval_unit is None:\n                    raise HomeAssistantError(\n                        \"For custom frequency, both custom_interval and custom_interval_unit are required.\"\n                    )\n                existing_options[\"custom_interval\"] = custom_interval\n                existing_options[\"custom_interval_unit\"] = custom_interval_unit\n            else:\n                # For non-custom frequencies, clear any custom interval settings.\n                existing_options.pop(\"custom_interval\", None)\n                existing_options.pop(\"custom_interval_unit\", None)\n        # If no frequency is passed, leave the frequency and custom fields unchanged.\n\n        chores_conf[chore_id] = existing_options\n        updated_options[DATA_CHORES] = chores_conf\n\n        new_data = dict(self.config_entry.data)\n        new_data[\"last_change\"] = dt_util.utcnow().isoformat()\n\n        update_result = self.hass.config_entries.async_update_entry(\n            self.config_entry, data=new_data, options=updated_options\n        )\n        if asyncio.iscoroutine(update_result):\n            await update_result\n\n    # -------------------------------------------------------------------------------------\n    # Notifications\n    # -------------------------------------------------------------------------------------\n\n    async def send_kc_notification(\n        self,\n        user_id: Optional[str],\n        title: str,\n        message: str,\n        notification_id: str,\n    ) -> None:\n        \"\"\"Send a persistent notification to a user if possible; fallback to a general persistent notification if the user is not found or not set.\"\"\"\n        hass = self.hass\n        if not user_id:\n            # If no user_id is provided, use a general notification\n            LOGGER.debug(\n                \"No user_id provided. Sending a general persistent notification\"\n            )\n            await hass.services.async_call(\n                \"persistent_notification\",\n                \"create\",\n                {\n                    \"title\": title,\n                    \"message\": message,\n                    \"notification_id\": notification_id,\n                },\n                blocking=True,\n            )\n            return\n\n        try:\n            user_obj: User = await hass.auth.async_get_user(user_id)\n            if not user_obj:\n                LOGGER.warning(\n                    \"User with ID '%s' not found. Sending fallback persistent notification\",\n                    user_id,\n                )\n                await hass.services.async_call(\n                    \"persistent_notification\",\n                    \"create\",\n                    {\n                        \"title\": title,\n                        \"message\": message,\n                        \"notification_id\": notification_id,\n                    },\n                    blocking=True,\n                )\n                return\n\n            await hass.services.async_call(\n                \"persistent_notification\",\n                \"create\",\n                {\n                    \"title\": title,\n                    \"message\": message,\n                    \"notification_id\": notification_id,\n                },\n                blocking=True,\n            )\n        except Exception as err:\n            LOGGER.warning(\n                \"Failed to send user-specific notification to user_id='%s': %s. Fallback to persistent_notification\",\n                user_id,\n                err,\n            )\n            await hass.services.async_call(\n                \"persistent_notification\",\n                \"create\",\n                {\n                    \"title\": title,\n                    \"message\": message,\n                    \"notification_id\": notification_id,\n                },\n                blocking=True,\n            )\n\n    async def _notify_kid(\n        self,\n        kid_id: str,\n        title: str,\n        message: str,\n        actions: Optional[list[dict[str, str]]] = None,\n        extra_data: Optional[dict] = None,\n    ) -> None:\n        \"\"\"Notify a kid using their configured notification settings.\"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            return\n        if not kid_info.get(\"enable_notifications\", True):\n            LOGGER.debug(\"Notifications disabled for kid '%s'\", kid_id)\n            return\n        mobile_enabled = kid_info.get(CONF_ENABLE_MOBILE_NOTIFICATIONS, True)\n        persistent_enabled = kid_info.get(CONF_ENABLE_PERSISTENT_NOTIFICATIONS, True)\n        mobile_notify_service = kid_info.get(CONF_MOBILE_NOTIFY_SERVICE, \"\")\n        if mobile_enabled and mobile_notify_service:\n            await async_send_notification(\n                self.hass,\n                mobile_notify_service,\n                title,\n                message,\n                actions=actions,\n                extra_data=extra_data,\n                use_persistent=persistent_enabled,\n            )\n        elif persistent_enabled:\n            await self.hass.services.async_call(\n                \"persistent_notification\",\n                \"create\",\n                {\n                    \"title\": title,\n                    \"message\": message,\n                    \"notification_id\": f\"kid_{kid_id}\",\n                },\n                blocking=True,\n            )\n        else:\n            LOGGER.debug(\"No notification method configured for kid '%s'\", kid_id)\n\n    async def _notify_parents(\n        self,\n        kid_id: str,\n        title: str,\n        message: str,\n        actions: Optional[list[dict[str, str]]] = None,\n        extra_data: Optional[dict] = None,\n    ) -> None:\n        \"\"\"Notify all parents associated with a kid using their settings.\"\"\"\n        for parent_id, parent_info in self.parents_data.items():\n            if kid_id not in parent_info.get(\"associated_kids\", []):\n                continue\n            if not parent_info.get(\"enable_notifications\", True):\n                LOGGER.debug(\"Notifications disabled for parent '%s'\", parent_id)\n                continue\n            mobile_enabled = parent_info.get(CONF_ENABLE_MOBILE_NOTIFICATIONS, True)\n            persistent_enabled = parent_info.get(\n                CONF_ENABLE_PERSISTENT_NOTIFICATIONS, True\n            )\n            mobile_notify_service = parent_info.get(CONF_MOBILE_NOTIFY_SERVICE, \"\")\n            if mobile_enabled and mobile_notify_service:\n                await async_send_notification(\n                    self.hass,\n                    mobile_notify_service,\n                    title,\n                    message,\n                    actions=actions,\n                    extra_data=extra_data,\n                    use_persistent=persistent_enabled,\n                )\n            elif persistent_enabled:\n                await self.hass.services.async_call(\n                    \"persistent_notification\",\n                    \"create\",\n                    {\n                        \"title\": title,\n                        \"message\": message,\n                        \"notification_id\": f\"parent_{parent_id}\",\n                    },\n                    blocking=True,\n                )\n            else:\n                LOGGER.debug(\n                    \"No notification method configured for parent '%s'\", parent_id\n                )\n\n    async def remind_in_minutes(\n        self,\n        kid_id: str,\n        minutes: int,\n        *,\n        chore_id: Optional[str] = None,\n        reward_id: Optional[str] = None,\n    ) -> None:\n        \"\"\"\n        Wait for the specified number of minutes and then resend the parent's\n        notification if the chore or reward is still pending approval.\n\n        If a chore_id is provided, the method checks the corresponding chore’s state.\n        If a reward_id is provided, it checks whether that reward is still pending.\n        \"\"\"\n        LOGGER.info(\n            \"Scheduling reminder for kid '%s', chore '%s', reward '%s' in %d minutes\",\n            kid_id,\n            chore_id,\n            reward_id,\n            minutes,\n        )\n        await asyncio.sleep(minutes * 60)\n\n        kid_info = self.kids_data.get(kid_id)\n        if not kid_info:\n            LOGGER.warning(\"Kid with ID '%s' not found during reminder check\", kid_id)\n            return\n\n        if chore_id:\n            chore_info = self.chores_data.get(chore_id)\n            if not chore_info:\n                LOGGER.warning(\n                    \"Chore with ID '%s' not found during reminder check\", chore_id\n                )\n                return\n            # Only resend if the chore is still in a pending-like state.\n            if chore_info.get(\"state\") not in [\n                CHORE_STATE_PENDING,\n                CHORE_STATE_CLAIMED,\n                CHORE_STATE_OVERDUE,\n            ]:\n                LOGGER.info(\n                    \"Chore '%s' is no longer pending approval; no reminder sent\",\n                    chore_id,\n                )\n                return\n            actions = [\n                {\n                    \"action\": f\"{ACTION_APPROVE_CHORE}|{kid_id}|{chore_id}\",\n                    \"title\": ACTION_TITLE_APPROVE,\n                },\n                {\n                    \"action\": f\"{ACTION_DISAPPROVE_CHORE}|{kid_id}|{chore_id}\",\n                    \"title\": ACTION_TITLE_DISAPPROVE,\n                },\n                {\n                    \"action\": f\"{ACTION_REMIND_30}|{kid_id}|{chore_id}\",\n                    \"title\": ACTION_TITLE_REMIND_30,\n                },\n            ]\n            extra_data = {\"kid_id\": kid_id, \"chore_id\": chore_id}\n            await self._notify_parents(\n                kid_id,\n                title=\"KidsChores: Reminder for Pending Chore\",\n                message=f\"Reminder: {kid_info.get('name', 'A kid')} has '{chore_info.get('name', 'Unnamed Chore')}' chore pending approval.\",\n                actions=actions,\n                extra_data=extra_data,\n            )\n            LOGGER.info(\"Resent reminder for chore '%s' for kid '%s'\", chore_id, kid_id)\n        elif reward_id:\n            # Check if the reward is still pending approval.\n            if reward_id not in kid_info.get(\"pending_rewards\", []):\n                LOGGER.info(\n                    \"Reward '%s' is no longer pending approval for kid '%s'; no reminder sent\",\n                    reward_id,\n                    kid_id,\n                )\n                return\n            actions = [\n                {\n                    \"action\": f\"{ACTION_APPROVE_REWARD}|{kid_id}|{reward_id}\",\n                    \"title\": ACTION_TITLE_APPROVE,\n                },\n                {\n                    \"action\": f\"{ACTION_DISAPPROVE_REWARD}|{kid_id}|{reward_id}\",\n                    \"title\": ACTION_TITLE_DISAPPROVE,\n                },\n                {\n                    \"action\": f\"{ACTION_REMIND_30}|{kid_id}|{reward_id}\",\n                    \"title\": ACTION_TITLE_REMIND_30,\n                },\n            ]\n            extra_data = {\"kid_id\": kid_id, \"reward_id\": reward_id}\n            reward = self.rewards_data.get(reward_id, {})\n            reward_name = reward.get(\"name\", \"the reward\")\n            await self._notify_parents(\n                kid_id,\n                title=\"KidsChores: Reminder for Pending Reward\",\n                message=f\"Reminder: {kid_info.get('name', 'A kid')} has '{reward_name}' reward pending approval.\",\n                actions=actions,\n                extra_data=extra_data,\n            )\n            LOGGER.info(\n                \"Resent reminder for reward '%s' for kid '%s'\", reward_id, kid_id\n            )\n        else:\n            LOGGER.warning(\"No chore_id or reward_id provided for reminder action\")\n\n    # -------------------------------------------------------------------------------------\n    # Storage\n    # -------------------------------------------------------------------------------------\n\n    def _persist(self):\n        \"\"\"Save to persistent storage.\"\"\"\n        self.storage_manager.set_data(self._data)\n        self.hass.add_job(self.storage_manager.async_save)\n\n    # -------------------------------------------------------------------------------------\n    # Internal Helper for kid <-> name lookups\n    # -------------------------------------------------------------------------------------\n\n    def _get_kid_id_by_name(self, kid_name: str) -> Optional[str]:\n        \"\"\"Help function to get kid_id by kid_name.\"\"\"\n        for kid_id, k_info in self.kids_data.items():\n            if k_info.get(\"name\") == kid_name:\n                return kid_id\n        return None\n\n    def _get_kid_name_by_id(self, kid_id: str) -> Optional[str]:\n        \"\"\"Help function to get kid_name by kid_id.\"\"\"\n        kid_info = self.kids_data.get(kid_id)\n        if kid_info:\n            return kid_info.get(\"name\")\n        return None\n"
  },
  {
    "path": "custom_components/kidschores/flow_helpers.py",
    "content": "# File: flow_helpers.py\n\"\"\"Helpers for the KidsChores integration's Config and Options flow.\n\nProvides schema builders and input-processing logic for internal_id-based management.\n\"\"\"\n\nimport datetime\nimport uuid\nimport voluptuous as vol\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers import selector, config_validation as cv\nfrom homeassistant.util import dt as dt_util\n\nfrom .const import (\n    ACHIEVEMENT_TYPE_DAILY_MIN,\n    ACHIEVEMENT_TYPE_STREAK,\n    ACHIEVEMENT_TYPE_TOTAL,\n    CHALLENGE_TYPE_DAILY_MIN,\n    CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW,\n    CONF_APPLICABLE_DAYS,\n    CONF_ENABLE_MOBILE_NOTIFICATIONS,\n    CONF_ENABLE_PERSISTENT_NOTIFICATIONS,\n    CONF_MOBILE_NOTIFY_SERVICE,\n    CONF_NOTIFY_ON_APPROVAL,\n    CONF_NOTIFY_ON_CLAIM,\n    CONF_NOTIFY_ON_DISAPPROVAL,\n    CONF_POINTS_LABEL,\n    CONF_POINTS_ICON,\n    DEFAULT_APPLICABLE_DAYS,\n    DEFAULT_NOTIFY_ON_APPROVAL,\n    DEFAULT_NOTIFY_ON_CLAIM,\n    DEFAULT_NOTIFY_ON_DISAPPROVAL,\n    DEFAULT_POINTS_MULTIPLIER,\n    DEFAULT_POINTS_LABEL,\n    DEFAULT_POINTS_ICON,\n    DOMAIN,\n    FREQUENCY_BIWEEKLY,\n    FREQUENCY_CUSTOM,\n    FREQUENCY_DAILY,\n    FREQUENCY_MONTHLY,\n    FREQUENCY_NONE,\n    FREQUENCY_WEEKLY,\n    WEEKDAY_OPTIONS,\n)\n\n\ndef build_points_schema(\n    default_label=DEFAULT_POINTS_LABEL, default_icon=DEFAULT_POINTS_ICON\n):\n    \"\"\"Build a schema for points label & icon.\"\"\"\n    return vol.Schema(\n        {\n            vol.Required(CONF_POINTS_LABEL, default=default_label): str,\n            vol.Optional(\n                CONF_POINTS_ICON, default=default_icon\n            ): selector.IconSelector(),\n        }\n    )\n\n\ndef build_kid_schema(\n    hass,\n    users,\n    default_kid_name=\"\",\n    default_ha_user_id=None,\n    internal_id=None,\n    default_enable_mobile_notifications=False,\n    default_mobile_notify_service=None,\n    default_enable_persistent_notifications=False,\n):\n    \"\"\"Build a Voluptuous schema for adding/editing a Kid, keyed by internal_id in the dict.\"\"\"\n    user_options = [{\"value\": \"\", \"label\": \"None\"}] + [\n        {\"value\": user.id, \"label\": user.name} for user in users\n    ]\n    notify_options = [{\"value\": \"\", \"label\": \"None\"}] + _get_notify_services(hass)\n\n    return vol.Schema(\n        {\n            vol.Required(\"kid_name\", default=default_kid_name): str,\n            vol.Optional(\n                \"ha_user\", default=default_ha_user_id or \"\"\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=user_options,\n                    mode=selector.SelectSelectorMode.DROPDOWN,\n                    multiple=False,\n                )\n            ),\n            vol.Required(\n                CONF_ENABLE_MOBILE_NOTIFICATIONS,\n                default=default_enable_mobile_notifications,\n            ): selector.BooleanSelector(),\n            vol.Optional(\n                CONF_MOBILE_NOTIFY_SERVICE, default=default_mobile_notify_service or \"\"\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=notify_options,\n                    mode=selector.SelectSelectorMode.DROPDOWN,\n                    multiple=False,\n                )\n            ),\n            vol.Required(\n                CONF_ENABLE_PERSISTENT_NOTIFICATIONS,\n                default=default_enable_persistent_notifications,\n            ): selector.BooleanSelector(),\n            vol.Required(\"internal_id\", default=internal_id or str(uuid.uuid4())): str,\n        }\n    )\n\n\ndef build_parent_schema(\n    hass,\n    users,\n    kids_dict,\n    default_parent_name=\"\",\n    default_ha_user_id=None,\n    default_associated_kids=None,\n    default_enable_mobile_notifications=False,\n    default_mobile_notify_service=None,\n    default_enable_persistent_notifications=False,\n    internal_id=None,\n):\n    \"\"\"Build a Voluptuous schema for adding/editing a Parent, keyed by internal_id in the dict.\"\"\"\n    user_options = [{\"value\": \"\", \"label\": \"None\"}] + [\n        {\"value\": user.id, \"label\": user.name} for user in users\n    ]\n    kid_options = [\n        {\"value\": kid_id, \"label\": kid_name} for kid_name, kid_id in kids_dict.items()\n    ]\n    notify_options = [{\"value\": \"\", \"label\": \"None\"}] + _get_notify_services(hass)\n\n    return vol.Schema(\n        {\n            vol.Required(\"parent_name\", default=default_parent_name): str,\n            vol.Optional(\n                \"ha_user_id\", default=default_ha_user_id or \"\"\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=user_options,\n                    mode=selector.SelectSelectorMode.DROPDOWN,\n                    multiple=False,\n                )\n            ),\n            vol.Optional(\n                \"associated_kids\", default=default_associated_kids or []\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=kid_options,\n                    translation_key=\"associated_kids\",\n                    multiple=True,\n                )\n            ),\n            vol.Required(\n                CONF_ENABLE_MOBILE_NOTIFICATIONS,\n                default=default_enable_mobile_notifications,\n            ): selector.BooleanSelector(),\n            vol.Optional(\n                CONF_MOBILE_NOTIFY_SERVICE, default=default_mobile_notify_service or \"\"\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=notify_options,\n                    mode=selector.SelectSelectorMode.DROPDOWN,\n                    multiple=False,\n                )\n            ),\n            vol.Required(\n                CONF_ENABLE_PERSISTENT_NOTIFICATIONS,\n                default=default_enable_persistent_notifications,\n            ): selector.BooleanSelector(),\n            vol.Required(\"internal_id\", default=internal_id or str(uuid.uuid4())): str,\n        }\n    )\n\n\ndef build_chore_schema(kids_dict, default=None):\n    \"\"\"Build a schema for chores, referencing existing kids by name.\n\n    Uses internal_id for entity management.\n    \"\"\"\n    default = default or {}\n    chore_name_default = default.get(\"name\", \"\")\n    internal_id_default = default.get(\"internal_id\", str(uuid.uuid4()))\n\n    kid_choices = {k: k for k in kids_dict}\n\n    return vol.Schema(\n        {\n            vol.Required(\"chore_name\", default=chore_name_default): str,\n            vol.Optional(\n                \"chore_description\", default=default.get(\"description\", \"\")\n            ): str,\n            vol.Optional(\n                \"chore_labels\", default=default.get(\"chore_labels\", [])\n            ): selector.LabelSelector(selector.LabelSelectorConfig(multiple=True)),\n            vol.Required(\n                \"default_points\", default=default.get(\"default_points\", 5)\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Required(\n                \"assigned_kids\", default=default.get(\"assigned_kids\", [])\n            ): cv.multi_select(kid_choices),\n            vol.Required(\n                \"shared_chore\", default=default.get(\"shared_chore\", False)\n            ): selector.BooleanSelector(),\n            vol.Required(\n                \"allow_multiple_claims_per_day\",\n                default=default.get(\"allow_multiple_claims_per_day\", False),\n            ): selector.BooleanSelector(),\n            vol.Required(\n                \"partial_allowed\", default=default.get(\"partial_allowed\", False)\n            ): selector.BooleanSelector(),\n            vol.Optional(\n                \"icon\", default=default.get(\"icon\", \"\")\n            ): selector.IconSelector(),\n            vol.Required(\n                \"recurring_frequency\",\n                default=default.get(\"recurring_frequency\", FREQUENCY_NONE),\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=[\n                        FREQUENCY_NONE,\n                        FREQUENCY_DAILY,\n                        FREQUENCY_WEEKLY,\n                        FREQUENCY_BIWEEKLY,\n                        FREQUENCY_MONTHLY,\n                        FREQUENCY_CUSTOM,\n                    ],\n                    translation_key=\"recurring_frequency\",\n                )\n            ),\n            vol.Optional(\n                \"custom_interval\", default=default.get(\"custom_interval\", None)\n            ): vol.Any(\n                None,\n                selector.NumberSelector(\n                    selector.NumberSelectorConfig(\n                        mode=selector.NumberSelectorMode.BOX, min=1, step=1\n                    )\n                ),\n            ),\n            vol.Optional(\n                \"custom_interval_unit\",\n                default=default.get(\"custom_interval_unit\", None),\n            ): vol.Any(\n                None,\n                selector.SelectSelector(\n                    selector.SelectSelectorConfig(\n                        options=[\"\", \"days\", \"weeks\", \"months\"],\n                        translation_key=\"custom_interval_unit\",\n                        multiple=False,\n                        mode=selector.SelectSelectorMode.DROPDOWN,\n                    )\n                ),\n            ),\n            vol.Optional(\n                CONF_APPLICABLE_DAYS,\n                default=default.get(CONF_APPLICABLE_DAYS, DEFAULT_APPLICABLE_DAYS),\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=[\n                        {\"value\": key, \"label\": WEEKDAY_OPTIONS[key]}\n                        for key in WEEKDAY_OPTIONS\n                    ],\n                    multiple=True,\n                    translation_key=\"applicable_days\",\n                )\n            ),\n            vol.Optional(\"due_date\", default=default.get(\"due_date\")): vol.Any(\n                None, selector.DateTimeSelector()\n            ),\n            vol.Optional(\n                CONF_NOTIFY_ON_CLAIM,\n                default=default.get(CONF_NOTIFY_ON_CLAIM, DEFAULT_NOTIFY_ON_CLAIM),\n            ): selector.BooleanSelector(),\n            vol.Optional(\n                CONF_NOTIFY_ON_APPROVAL,\n                default=default.get(\n                    CONF_NOTIFY_ON_APPROVAL, DEFAULT_NOTIFY_ON_APPROVAL\n                ),\n            ): selector.BooleanSelector(),\n            vol.Optional(\n                CONF_NOTIFY_ON_DISAPPROVAL,\n                default=default.get(\n                    CONF_NOTIFY_ON_DISAPPROVAL, DEFAULT_NOTIFY_ON_DISAPPROVAL\n                ),\n            ): selector.BooleanSelector(),\n            vol.Required(\"internal_id\", default=internal_id_default): str,\n        }\n    )\n\n\ndef build_badge_schema(default=None):\n    \"\"\"Build a schema for badges, keyed by internal_id in the dict.\"\"\"\n    default = default or {}\n    badge_name_default = default.get(\"name\", \"\")\n    internal_id_default = default.get(\"internal_id\", str(uuid.uuid4()))\n    points_multiplier_default = default.get(\n        \"points_multiplier\", DEFAULT_POINTS_MULTIPLIER\n    )\n\n    return vol.Schema(\n        {\n            vol.Required(\"badge_name\", default=badge_name_default): str,\n            vol.Optional(\n                \"badge_description\", default=default.get(\"description\", \"\")\n            ): str,\n            vol.Optional(\n                \"badge_labels\", default=default.get(\"badge_labels\", [])\n            ): selector.LabelSelector(selector.LabelSelectorConfig(multiple=True)),\n            vol.Required(\n                \"threshold_type\",\n                default=default.get(\"threshold_type\", \"points\"),\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=[\"points\", \"chore_count\"],\n                    translation_key=\"threshold_type\",\n                )\n            ),\n            vol.Required(\n                \"threshold_value\", default=default.get(\"threshold_value\", 10)\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Required(\n                \"points_multiplier\",\n                default=points_multiplier_default,\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX, step=0.01, min=1.0\n                )\n            ),\n            vol.Optional(\n                \"icon\", default=default.get(\"icon\", \"\")\n            ): selector.IconSelector(),\n            vol.Required(\"internal_id\", default=internal_id_default): str,\n        }\n    )\n\n\ndef build_reward_schema(default=None):\n    \"\"\"Build a schema for rewards, keyed by internal_id in the dict.\"\"\"\n    default = default or {}\n    reward_name_default = default.get(\"name\", \"\")\n    internal_id_default = default.get(\"internal_id\", str(uuid.uuid4()))\n\n    return vol.Schema(\n        {\n            vol.Required(\"reward_name\", default=reward_name_default): str,\n            vol.Optional(\n                \"reward_description\", default=default.get(\"description\", \"\")\n            ): str,\n            vol.Optional(\n                \"reward_labels\", default=default.get(\"reward_labels\", [])\n            ): selector.LabelSelector(selector.LabelSelectorConfig(multiple=True)),\n            vol.Required(\n                \"reward_cost\", default=default.get(\"cost\", 10.0)\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Optional(\n                \"icon\", default=default.get(\"icon\", \"\")\n            ): selector.IconSelector(),\n            vol.Required(\"internal_id\", default=internal_id_default): str,\n        }\n    )\n\n\ndef build_achievement_schema(kids_dict, chores_dict, default=None):\n    \"\"\"Build a schema for achievements, keyed by internal_id.\"\"\"\n    default = default or {}\n    achievement_name_default = default.get(\"name\", \"\")\n    internal_id_default = default.get(\"internal_id\", str(uuid.uuid4()))\n\n    kid_options = [\n        {\"value\": kid_id, \"label\": kid_name} for kid_name, kid_id in kids_dict.items()\n    ]\n\n    chore_options = [{\"value\": \"\", \"label\": \"None\"}]\n    for chore_id, chore_data in chores_dict.items():\n        chore_name = chore_data.get(\"name\", f\"Chore {chore_id[:6]}\")\n        chore_options.append({\"value\": chore_id, \"label\": chore_name})\n\n    default_selected_chore = default.get(\"selected_chore_id\", \"\")\n    if not default_selected_chore or default_selected_chore not in [\n        option[\"value\"] for option in chore_options\n    ]:\n        pass\n\n    default_criteria = default.get(\"criteria\", \"\")\n    default_assigned_kids = default.get(\"assigned_kids\", [])\n    if not isinstance(default_assigned_kids, list):\n        default_assigned_kids = [default_assigned_kids]\n\n    return vol.Schema(\n        {\n            vol.Required(\"name\", default=achievement_name_default): str,\n            vol.Optional(\"description\", default=default.get(\"description\", \"\")): str,\n            vol.Optional(\n                \"achievement_labels\", default=default.get(\"achievement_labels\", [])\n            ): selector.LabelSelector(selector.LabelSelectorConfig(multiple=True)),\n            vol.Optional(\n                \"icon\", default=default.get(\"icon\", \"\")\n            ): selector.IconSelector(),\n            vol.Required(\n                \"assigned_kids\", default=default_assigned_kids\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=kid_options,\n                    translation_key=\"assigned_kids\",\n                    multiple=True,\n                )\n            ),\n            vol.Required(\n                \"type\", default=default.get(\"type\", ACHIEVEMENT_TYPE_STREAK)\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=[\n                        {\"value\": ACHIEVEMENT_TYPE_STREAK, \"label\": \"Chore Streak\"},\n                        {\"value\": ACHIEVEMENT_TYPE_TOTAL, \"label\": \"Chore Total\"},\n                        {\n                            \"value\": ACHIEVEMENT_TYPE_DAILY_MIN,\n                            \"label\": \"Daily Minimum Chores\",\n                        },\n                    ],\n                    mode=selector.SelectSelectorMode.DROPDOWN,\n                )\n            ),\n            # If type == \"chore_streak\", let the user choose the chore to track:\n            vol.Optional(\n                \"selected_chore_id\", default=default_selected_chore\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=chore_options,\n                    mode=selector.SelectSelectorMode.DROPDOWN,\n                    multiple=False,\n                )\n            ),\n            # For non-streak achievements the user can type criteria freely:\n            vol.Optional(\"criteria\", default=default_criteria): str,\n            vol.Required(\n                \"target_value\", default=default.get(\"target_value\", 1)\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Required(\n                \"reward_points\", default=default.get(\"reward_points\", 0)\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Required(\"internal_id\", default=internal_id_default): str,\n        }\n    )\n\n\ndef build_challenge_schema(kids_dict, chores_dict, default=None):\n    \"\"\"Build a schema for challenges, keyed by internal_id.\"\"\"\n    default = default or {}\n    challenge_name_default = default.get(\"name\", \"\")\n    internal_id_default = default.get(\"internal_id\", str(uuid.uuid4()))\n\n    kid_options = [\n        {\"value\": kid_id, \"label\": kid_name} for kid_name, kid_id in kids_dict.items()\n    ]\n\n    chore_options = [{\"value\": \"\", \"label\": \"\"}]\n    for chore_id, chore_data in chores_dict.items():\n        chore_name = chore_data.get(\"name\", f\"Chore {chore_id[:6]}\")\n        chore_options.append({\"value\": chore_id, \"label\": chore_name})\n\n    default_selected_chore = default.get(\"selected_chore_id\", \"\")\n    available_values = [option[\"value\"] for option in chore_options]\n    if default_selected_chore not in available_values:\n        default_selected_chore = \"\"\n\n    default_criteria = default.get(\"criteria\", \"\")\n    default_assigned_kids = default.get(\"assigned_kids\", [])\n    if not isinstance(default_assigned_kids, list):\n        default_assigned_kids = [default_assigned_kids]\n\n    return vol.Schema(\n        {\n            vol.Required(\"name\", default=challenge_name_default): str,\n            vol.Optional(\"description\", default=default.get(\"description\", \"\")): str,\n            vol.Optional(\n                \"challenge_labels\", default=default.get(\"challenge_labels\", [])\n            ): selector.LabelSelector(selector.LabelSelectorConfig(multiple=True)),\n            vol.Optional(\n                \"icon\", default=default.get(\"icon\", \"\")\n            ): selector.IconSelector(),\n            vol.Required(\n                \"assigned_kids\", default=default_assigned_kids\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=kid_options,\n                    translation_key=\"assigned_kids\",\n                    multiple=True,\n                )\n            ),\n            vol.Required(\n                \"type\", default=default.get(\"type\", CHALLENGE_TYPE_DAILY_MIN)\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=[\n                        {\n                            \"value\": CHALLENGE_TYPE_DAILY_MIN,\n                            \"label\": \"Minimum Chores per Day\",\n                        },\n                        {\n                            \"value\": CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW,\n                            \"label\": \"Total Chores within Period\",\n                        },\n                    ],\n                    mode=selector.SelectSelectorMode.DROPDOWN,\n                )\n            ),\n            # If type == \"chore_streak\", let the user choose the chore to track:\n            vol.Optional(\n                \"selected_chore_id\", default=default_selected_chore\n            ): selector.SelectSelector(\n                selector.SelectSelectorConfig(\n                    options=chore_options,\n                    mode=selector.SelectSelectorMode.DROPDOWN,\n                    multiple=False,\n                )\n            ),\n            # For non-streak achievements the user can type criteria freely:\n            vol.Optional(\"criteria\", default=default_criteria): str,\n            vol.Required(\n                \"target_value\", default=default.get(\"target_value\", 1)\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Required(\n                \"reward_points\", default=default.get(\"reward_points\", 0)\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Required(\n                \"start_date\", default=default.get(\"start_date\")\n            ): selector.DateTimeSelector(),\n            vol.Required(\n                \"end_date\", default=default.get(\"end_date\")\n            ): selector.DateTimeSelector(),\n            vol.Required(\"internal_id\", default=internal_id_default): str,\n        }\n    )\n\n\ndef build_penalty_schema(default=None):\n    \"\"\"Build a schema for penalties, keyed by internal_id in the dict.\n\n    Stores penalty_points as positive in the form, converted to negative internally.\n    \"\"\"\n    default = default or {}\n    penalty_name_default = default.get(\"name\", \"\")\n    internal_id_default = default.get(\"internal_id\", str(uuid.uuid4()))\n\n    # Display penalty points as positive for user input\n    display_points = abs(default.get(\"points\", 1)) if default else 1\n\n    return vol.Schema(\n        {\n            vol.Required(\"penalty_name\", default=penalty_name_default): str,\n            vol.Optional(\n                \"penalty_description\", default=default.get(\"description\", \"\")\n            ): str,\n            vol.Optional(\n                \"penalty_labels\", default=default.get(\"penalty_labels\", [])\n            ): selector.LabelSelector(selector.LabelSelectorConfig(multiple=True)),\n            vol.Required(\n                \"penalty_points\", default=display_points\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Optional(\n                \"icon\", default=default.get(\"icon\", \"\")\n            ): selector.IconSelector(),\n            vol.Required(\"internal_id\", default=internal_id_default): str,\n        }\n    )\n\n\ndef build_bonus_schema(default=None):\n    \"\"\"Build a schema for bonuses, keyed by internal_id in the dict.\n\n    Stores bonus_points as positive in the form, converted to negative internally.\n    \"\"\"\n    default = default or {}\n    bonus_name_default = default.get(\"name\", \"\")\n    internal_id_default = default.get(\"internal_id\", str(uuid.uuid4()))\n\n    # Display bonus points as positive for user input\n    display_points = abs(default.get(\"points\", 1)) if default else 1\n\n    return vol.Schema(\n        {\n            vol.Required(\"bonus_name\", default=bonus_name_default): str,\n            vol.Optional(\n                \"bonus_description\", default=default.get(\"description\", \"\")\n            ): str,\n            vol.Optional(\n                \"bonus_labels\", default=default.get(\"bonus_labels\", [])\n            ): selector.LabelSelector(selector.LabelSelectorConfig(multiple=True)),\n            vol.Required(\n                \"bonus_points\", default=display_points\n            ): selector.NumberSelector(\n                selector.NumberSelectorConfig(\n                    mode=selector.NumberSelectorMode.BOX,\n                    min=0,\n                    step=0.1,\n                )\n            ),\n            vol.Optional(\n                \"icon\", default=default.get(\"icon\", \"\")\n            ): selector.IconSelector(),\n            vol.Required(\"internal_id\", default=internal_id_default): str,\n        }\n    )\n\n\n# ----------------- HELPERS -----------------\n\n\n# Penalty points are stored as negative internally, but displayed as positive in the form.\ndef process_penalty_form_input(user_input: dict) -> dict:\n    \"\"\"Ensure penalty points are negative internally.\"\"\"\n    data = dict(user_input)\n    data[\"points\"] = -abs(data[\"penalty_points\"])\n    return data\n\n\n# Get notify services from HA\ndef _get_notify_services(hass: HomeAssistant) -> list[dict[str, str]]:\n    \"\"\"Return a list of all notify.* services as [{'value': 'notify.foo', 'label': 'notify.foo'}, ...].\"\"\"\n    services_list = []\n    all_services = hass.services.async_services()\n    if \"notify\" in all_services:\n        for service_name in all_services[\"notify\"].keys():\n            fullname = f\"notify.{service_name}\"\n            services_list.append({\"value\": fullname, \"label\": fullname})\n    return services_list\n\n\n# Ensure aware datetime objects\ndef ensure_utc_datetime(hass: HomeAssistant, dt_value: any) -> str:\n    \"\"\"Convert a datetime input (or a datetime string) into an ISO string that is timezone aware (in UTC).\n\n    If dt_value is naive, assume it is in the local timezone.\n    \"\"\"\n    # Convert dt_value to a datetime object if necessary\n    if not isinstance(dt_value, datetime.datetime):\n        dt_value = dt_util.parse_datetime(dt_value)\n        if dt_value is None:\n            raise ValueError(f\"Unable to parse datetime from {dt_value}\")\n\n    # If the datetime is naive, assume local time using hass.config.time_zone\n    if dt_value.tzinfo is None:\n        local_tz = dt_util.get_time_zone(hass.config.time_zone)\n        dt_value = dt_value.replace(tzinfo=local_tz)\n\n    # Convert to UTC and return the ISO string\n    return dt_util.as_utc(dt_value).isoformat()\n"
  },
  {
    "path": "custom_components/kidschores/kc_helpers.py",
    "content": "# File: kc_helpers.py\n\"\"\"KidsChores helper functions and shared logic.\"\"\"\n\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.auth.models import User\nfrom homeassistant.helpers.label_registry import async_get\nfrom typing import Optional\n\nfrom .const import LOGGER, DOMAIN\nfrom .coordinator import KidsChoresDataCoordinator\n\n\n# -------- Get Coordinator --------\ndef _get_kidschores_coordinator(\n    hass: HomeAssistant,\n) -> KidsChoresDataCoordinator | None:\n    \"\"\"Retrieve KidsChores coordinator from hass.data.\"\"\"\n    domain_entries = hass.data.get(DOMAIN, {})\n    if not domain_entries:\n        return None\n\n    entry_id = next(iter(domain_entries), None)\n    if not entry_id:\n        return None\n\n    data = domain_entries.get(entry_id)\n    if not data or \"coordinator\" not in data:\n        return None\n\n    return data[\"coordinator\"]\n\n\n# -------- Authorization for General Actions --------\nasync def is_user_authorized_for_global_action(\n    hass: HomeAssistant,\n    user_id: str,\n    action: str,\n) -> bool:\n    \"\"\"Check if the user is allowed to do a global action (penalty, reward, points adjust) that doesn't require a specific kid_id.\n\n    By default:\n      - Admin users => authorized\n      - Everyone else => not authorized\n\n    \"\"\"\n    if not user_id:\n        return False  # no user context => not authorized\n\n    user: User = await hass.auth.async_get_user(user_id)\n    if not user:\n        LOGGER.warning(\"%s: Invalid user ID '%s'\", action, user_id)\n        return False\n\n    if user.is_admin:\n        return True\n\n    # Allow non-admin users if they are registered as a parent in KidsChores.\n    coordinator = _get_kidschores_coordinator(hass)\n    if coordinator:\n        for parent in coordinator.parents_data.values():\n            if parent.get(\"ha_user_id\") == user.id:\n                return True\n\n    LOGGER.warning(\n        \"%s: Non-admin user '%s' is not authorized in this logic\", action, user.name\n    )\n    return False\n\n\n# -------- Authorization for Kid-Specific Actions --------\nasync def is_user_authorized_for_kid(\n    hass: HomeAssistant,\n    user_id: str,\n    kid_id: str,\n) -> bool:\n    \"\"\"Check if user is authorized to manage chores/rewards/etc. for the given kid.\n\n    By default:\n      - Admin => authorized\n      - If kid_info['ha_user_id'] == user.id => authorized\n      - Otherwise => not authorized\n    \"\"\"\n    if not user_id:\n        return False\n\n    user: User = await hass.auth.async_get_user(user_id)\n    if not user:\n        LOGGER.warning(\"Authorization: Invalid user ID '%s'\", user_id)\n        return False\n\n    # Admin => automatically allowed\n    if user.is_admin:\n        return True\n\n    # Allow non-admin users if they are registered as a parent in KidsChores.\n    coordinator = _get_kidschores_coordinator(hass)\n    if coordinator:\n        for parent in coordinator.parents_data.values():\n            if parent.get(\"ha_user_id\") == user.id:\n                return True\n\n    coordinator: KidsChoresDataCoordinator = _get_kidschores_coordinator(hass)\n    if not coordinator:\n        LOGGER.warning(\"Authorization: No KidsChores coordinator found\")\n        return False\n\n    kid_info = coordinator.kids_data.get(kid_id)\n    if not kid_info:\n        LOGGER.warning(\n            \"Authorization: Kid ID '%s' not found in coordinator data\", kid_id\n        )\n        return False\n\n    linked_ha_id = kid_info.get(\"ha_user_id\")\n    if linked_ha_id and linked_ha_id == user.id:\n        return True\n\n    LOGGER.warning(\n        \"Authorization: Non-admin user '%s' attempted to manage kid '%s' but is not linked\",\n        user.name,\n        kid_info.get(\"name\"),\n    )\n    return False\n\n\n# ------------------ Helper Functions ------------------\ndef _get_kid_id_by_name(self, kid_name: str) -> Optional[str]:\n    \"\"\"Help function to get kid_id by kid_name.\"\"\"\n    for kid_id, kid_info in self.kids_data.items():\n        if kid_info.get(\"name\") == kid_name:\n            return kid_id\n    return None\n\n\ndef _get_kid_name_by_id(self, kid_id: str) -> Optional[str]:\n    \"\"\"Help function to get kid_name by kid_id.\"\"\"\n    kid_info = self.kids_data.get(kid_id)\n    if kid_info:\n        return kid_info.get(\"name\")\n    return None\n\n\ndef get_friendly_label(hass, label_name: str) -> str:\n    registry = async_get(hass)\n    entries = registry.async_list_labels()\n    label_entry = registry.async_get_label(label_name)\n    return label_entry.name if label_entry else label_name\n"
  },
  {
    "path": "custom_components/kidschores/manifest.json",
    "content": "{\n  \"domain\": \"kidschores\",\n  \"name\": \"KidsChores\",\n  \"codeowners\": [\"@ad-ha\"],\n  \"config_flow\": true,\n  \"dependencies\": [],\n  \"documentation\": \"https://github.com/ad-ha/kidschores-ha\",\n  \"iot_class\": \"local_polling\",\n  \"issue_tracker\": \"https://github.com/ad-ha/kidschores-ha/issues\",\n  \"requirements\": [],\n  \"version\": \"0.3.0\"\n}\n"
  },
  {
    "path": "custom_components/kidschores/notification_action_handler.py",
    "content": "# File: notification_action_handler.py\n\"\"\"Handle notification actions from HA companion notifications.\"\"\"\n\nfrom homeassistant.core import HomeAssistant, Event\nfrom homeassistant.exceptions import HomeAssistantError\n\nfrom .const import (\n    NOTIFICATION_EVENT,\n    ACTION_APPROVE_CHORE,\n    ACTION_APPROVE_REWARD,\n    ACTION_DISAPPROVE_CHORE,\n    ACTION_DISAPPROVE_REWARD,\n    ACTION_REMIND_30,\n    DEFAULT_REMINDER_DELAY,\n    LOGGER,\n)\nfrom .coordinator import KidsChoresDataCoordinator\n\n\nasync def async_handle_notification_action(hass: HomeAssistant, event: Event) -> None:\n    \"\"\"Handle notification actions from HA companion notifications.\"\"\"\n\n    action_field = event.data.get(\"action\")\n    if not action_field:\n        LOGGER.error(\"No action found in event data: %s\", event.data)\n        return\n\n    parts = action_field.split(\"|\")\n    base_action = parts[0]\n    kid_id = None\n    chore_id = None\n    reward_id = None\n\n    # Decide what to expect based on the base action.\n    if base_action in (ACTION_APPROVE_REWARD, ACTION_DISAPPROVE_REWARD):\n        if len(parts) < 3:\n            LOGGER.error(\"Not enough context in reward action field: %s\", action_field)\n            return\n        kid_id = parts[1]\n        reward_id = parts[2]\n    elif base_action in (\n        ACTION_APPROVE_CHORE,\n        ACTION_DISAPPROVE_CHORE,\n        ACTION_REMIND_30,\n    ):\n        if len(parts) < 3:\n            LOGGER.error(\"Not enough context in chore action field: %s\", action_field)\n            return\n        kid_id = parts[1]\n        chore_id = parts[2]\n    else:\n        LOGGER.error(\"Unknown base action: %s\", base_action)\n        return\n\n    # Parent name may be provided in the event data or use a default.\n    parent_name = event.data.get(\"parent_name\", \"ParentOrAdmin\")\n\n    if not kid_id or not base_action:\n        LOGGER.error(\"Notification action event missing required data: %s\", event.data)\n        return\n\n    # Retrieve the coordinator.\n    domain_data = hass.data.get(\"kidschores\", {})\n    if not domain_data:\n        LOGGER.error(\"No KidsChores data found in hass.data\")\n        return\n    entry_id = next(iter(domain_data))\n    coordinator: KidsChoresDataCoordinator = domain_data[entry_id].get(\"coordinator\")\n    if not coordinator:\n        LOGGER.error(\"No coordinator found in KidsChores data\")\n        return\n\n    try:\n        if base_action == ACTION_APPROVE_CHORE:\n            await coordinator.approve_chore(\n                parent_name=parent_name,\n                kid_id=kid_id,\n                chore_id=chore_id,\n            )\n        elif base_action == ACTION_DISAPPROVE_CHORE:\n            await coordinator.disapprove_chore(\n                parent_name=parent_name,\n                kid_id=kid_id,\n                chore_id=chore_id,\n            )\n        elif base_action == ACTION_APPROVE_REWARD:\n            await coordinator.approve_reward(\n                parent_name=parent_name,\n                kid_id=kid_id,\n                reward_id=reward_id,\n            )\n        elif base_action == ACTION_DISAPPROVE_REWARD:\n            await coordinator.disapprove_reward(\n                parent_name=parent_name,\n                kid_id=kid_id,\n                reward_id=reward_id,\n            )\n        elif base_action == ACTION_REMIND_30:\n            await coordinator.remind_in_minutes(\n                kid_id=kid_id,\n                chore_id=chore_id,\n                reward_id=reward_id,\n                minutes=DEFAULT_REMINDER_DELAY,\n            )\n        else:\n            LOGGER.error(\"Received unknown notification action: %s\", base_action)\n    except HomeAssistantError as err:\n        LOGGER.error(\"Error processing notification action %s: %s\", base_action, err)\n"
  },
  {
    "path": "custom_components/kidschores/notification_helper.py",
    "content": "# File: notification_helper.py\n\"\"\"Sends notifications using Home Assistant's notify services.\n\nThis module implements a helper for sending notifications in the KidsChores integration.\nIt supports sending notifications via Home Assistant’s notify services (HA Companion notifications)\nand includes an optional payload of actions. For actionable notifications, you must encode extra\ncontext (like kid_id and chore_id) directly into the action string.\nAll texts and labels are referenced from constants.\n\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.exceptions import HomeAssistantError\n\nfrom .const import DOMAIN, LOGGER\n\n\nasync def async_send_notification(\n    hass: HomeAssistant,\n    notify_service: str,\n    title: str,\n    message: str,\n    actions: Optional[list[dict[str, str]]] = None,\n    extra_data: Optional[dict[str, str]] = None,\n    use_persistent: bool = False,\n) -> None:\n    \"\"\"Send a notification using the specified notify service.\"\"\"\n\n    payload = {\"title\": title, \"message\": message}\n\n    if actions:\n        payload.setdefault(\"data\", {})[\"actions\"] = actions\n\n    if extra_data:\n        payload.setdefault(\"data\", {}).update(extra_data)\n\n    try:\n        if \".\" not in notify_service:\n            domain = \"notify\"\n            service = notify_service\n        else:\n            domain, service = notify_service.split(\".\", 1)\n        await hass.services.async_call(domain, service, payload, blocking=True)\n        LOGGER.debug(\"Notification sent via '%s': %s\", notify_service, payload)\n\n    except Exception as err:\n        LOGGER.error(\n            \"Failed to send notification via '%s': %s. Payload: %s\",\n            notify_service,\n            err,\n            payload,\n        )\n        raise HomeAssistantError(\n            f\"Failed to send notification via '{notify_service}': {err}\"\n        ) from err\n"
  },
  {
    "path": "custom_components/kidschores/options_flow.py",
    "content": "# File: options_flow.py\n\"\"\"Options Flow for the KidsChores integration, managing entities by internal_id.\n\nHandles add/edit/delete operations with entities referenced internally by internal_id.\nEnsures consistency and reloads the integration upon changes.\n\"\"\"\n\nimport datetime\nimport uuid\nimport voluptuous as vol\nfrom homeassistant import config_entries\nfrom homeassistant.helpers import selector\nfrom homeassistant.util import dt as dt_util\n\nfrom .const import (\n    ACHIEVEMENT_TYPE_STREAK,\n    CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW,\n    CONF_APPLICABLE_DAYS,\n    CONF_ACHIEVEMENTS,\n    CONF_BADGES,\n    CONF_CHALLENGES,\n    CONF_CHORES,\n    CONF_KIDS,\n    CONF_NOTIFY_ON_APPROVAL,\n    CONF_NOTIFY_ON_CLAIM,\n    CONF_NOTIFY_ON_DISAPPROVAL,\n    CONF_PARENTS,\n    CONF_PENALTIES,\n    CONF_POINTS_ICON,\n    CONF_POINTS_LABEL,\n    CONF_REWARDS,\n    CONF_BONUSES,\n    DEFAULT_APPLICABLE_DAYS,\n    DEFAULT_NOTIFY_ON_APPROVAL,\n    DEFAULT_NOTIFY_ON_CLAIM,\n    DEFAULT_NOTIFY_ON_DISAPPROVAL,\n    DEFAULT_POINTS_ICON,\n    DEFAULT_POINTS_LABEL,\n    FREQUENCY_CUSTOM,\n    DOMAIN,\n    LOGGER,\n)\nfrom .flow_helpers import (\n    build_points_schema,\n    build_kid_schema,\n    build_parent_schema,\n    build_chore_schema,\n    build_badge_schema,\n    build_reward_schema,\n    build_penalty_schema,\n    build_achievement_schema,\n    build_challenge_schema,\n    ensure_utc_datetime,\n    build_bonus_schema,\n)\n\n\ndef _ensure_str(value):\n    \"\"\"Convert anything to string safely.\"\"\"\n    if isinstance(value, dict):\n        # Attempt to get a known key or fallback\n        return str(value.get(\"value\", next(iter(value.values()), \"\")))\n    return str(value)\n\n\nclass KidsChoresOptionsFlowHandler(config_entries.OptionsFlow):\n    \"\"\"Options Flow for adding/editing/deleting kids, chores, badges, rewards, penalties, and bonuses.\n\n    Manages entities via internal_id for consistency and historical data preservation.\n    \"\"\"\n\n    def __init__(self, config_entry: config_entries.ConfigEntry):\n        \"\"\"Initialize the options flow.\"\"\"\n        self._entry_options = {}\n        self._action = None\n        self._entity_type = None\n\n    async def async_step_init(self, user_input=None):\n        \"\"\"Display the main menu for the Options Flow.\n\n        Add/Edit/Delete kid, chore, badge, reward, penalty, or done.\n        \"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        if user_input is not None:\n            selection = user_input[\"menu_selection\"]\n            if selection.startswith(\"manage_\"):\n                self._entity_type = selection.replace(\"manage_\", \"\")\n                # If user chose manage_points\n                if self._entity_type == \"points\":\n                    return await self.async_step_manage_points()\n                # Else manage other entities\n                return await self.async_step_manage_entity()\n            elif selection == \"done\":\n                return self.async_abort(reason=\"setup_complete\")\n\n        main_menu = [\n            \"manage_points\",\n            \"manage_kid\",\n            \"manage_parent\",\n            \"manage_chore\",\n            \"manage_badge\",\n            \"manage_reward\",\n            \"manage_penalty\",\n            \"manage_bonus\",\n            \"manage_achievement\",\n            \"manage_challenge\",\n            \"done\",\n        ]\n\n        return self.async_show_form(\n            step_id=\"init\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(\"menu_selection\"): selector.SelectSelector(\n                        selector.SelectSelectorConfig(\n                            options=main_menu,\n                            mode=selector.SelectSelectorMode.LIST,\n                            translation_key=\"main_menu\",\n                        )\n                    )\n                }\n            ),\n        )\n\n    async def async_step_manage_points(self, user_input=None):\n        \"\"\"Let user edit the points label/icon after initial setup.\"\"\"\n        if user_input is not None:\n            new_label = user_input.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL)\n            new_icon = user_input.get(CONF_POINTS_ICON, DEFAULT_POINTS_ICON)\n\n            self._entry_options = dict(self.config_entry.options)\n            self._entry_options[CONF_POINTS_LABEL] = new_label\n            self._entry_options[CONF_POINTS_ICON] = new_icon\n            LOGGER.debug(\n                \"Before saving points, entry_options = %s\", self._entry_options\n            )\n            await self._update_and_reload()\n\n            return await self.async_step_init()\n\n        # Get existing values from entry options\n        current_label = self._entry_options.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL)\n        current_icon = self._entry_options.get(CONF_POINTS_ICON, DEFAULT_POINTS_ICON)\n\n        # Build the form\n        points_schema = build_points_schema(\n            default_label=current_label, default_icon=current_icon\n        )\n\n        return self.async_show_form(\n            step_id=\"manage_points\",\n            data_schema=points_schema,\n            description_placeholders={},\n        )\n\n    async def async_step_manage_entity(self, user_input=None):\n        \"\"\"Handle the management actions for a selected entity type.\n\n        Presents add/edit/delete options for the selected entity.\n        \"\"\"\n        if user_input is not None:\n            self._action = user_input[\"manage_action\"]\n            # Route to the corresponding step based on action\n            if self._action == \"add\":\n                return await getattr(self, f\"async_step_add_{self._entity_type}\")()\n            elif self._action in [\"edit\", \"delete\"]:\n                return await self.async_step_select_entity()\n            elif self._action == \"back\":\n                return await self.async_step_init()\n\n        # Define manage action choices\n        manage_action_choices = [\n            \"add\",\n            \"edit\",\n            \"delete\",\n            \"back\",  # Option to go back to the main menu\n        ]\n\n        return self.async_show_form(\n            step_id=\"manage_entity\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(\"manage_action\"): selector.SelectSelector(\n                        selector.SelectSelectorConfig(\n                            options=manage_action_choices,\n                            mode=selector.SelectSelectorMode.LIST,\n                            translation_key=\"manage_actions\",\n                        )\n                    )\n                }\n            ),\n            description_placeholders={\"entity_type\": self._entity_type},\n        )\n\n    async def async_step_select_entity(self, user_input=None):\n        \"\"\"Select an entity (kid, chore, etc.) to edit or delete based on internal_id.\"\"\"\n        if self._action not in [\"edit\", \"delete\"]:\n            LOGGER.error(\"Invalid action '%s' for select_entity step\", self._action)\n            return self.async_abort(reason=\"invalid_action\")\n\n        entity_dict = self._get_entity_dict()\n        entity_names = [data[\"name\"] for data in entity_dict.values()]\n\n        if user_input is not None:\n            selected_name = _ensure_str(user_input[\"entity_name\"])\n            internal_id = next(\n                (\n                    eid\n                    for eid, data in entity_dict.items()\n                    if data[\"name\"] == selected_name\n                ),\n                None,\n            )\n            if not internal_id:\n                LOGGER.error(\"Selected entity '%s' not found\", selected_name)\n                return self.async_abort(reason=\"invalid_entity\")\n\n            # Store internal_id in context for later use\n            self.context[\"internal_id\"] = internal_id\n\n            # Route to the corresponding edit/delete step\n            return await getattr(\n                self, f\"async_step_{self._action}_{self._entity_type}\"\n            )()\n\n        if not entity_names:\n            return self.async_abort(reason=f\"no_{self._entity_type}s\")\n\n        return self.async_show_form(\n            step_id=\"select_entity\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(\"entity_name\"): selector.SelectSelector(\n                        selector.SelectSelectorConfig(\n                            options=entity_names,\n                            mode=selector.SelectSelectorMode.DROPDOWN,\n                            sort=True,\n                        )\n                    )\n                }\n            ),\n            description_placeholders={\n                \"entity_type\": self._entity_type,\n                \"action\": self._action,\n            },\n        )\n\n    def _get_entity_dict(self):\n        \"\"\"Retrieve the appropriate entity dictionary based on entity_type.\"\"\"\n        entity_type_to_conf = {\n            \"kid\": CONF_KIDS,\n            \"parent\": CONF_PARENTS,\n            \"chore\": CONF_CHORES,\n            \"badge\": CONF_BADGES,\n            \"reward\": CONF_REWARDS,\n            \"penalty\": CONF_PENALTIES,\n            \"achievement\": CONF_ACHIEVEMENTS,\n            \"challenge\": CONF_CHALLENGES,\n            \"bonus\": CONF_BONUSES,\n        }\n        key = entity_type_to_conf.get(self._entity_type)\n        if key is None:\n            LOGGER.error(\n                \"Unknown entity_type '%s'. Cannot retrieve entity dictionary\",\n                self._entity_type,\n            )\n            return {}\n        return self._entry_options.get(key, {})\n\n    # ------------------ ADD ENTITY ------------------\n    async def async_step_add_kid(self, user_input=None):\n        \"\"\"Add a new kid.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        kids_dict = self._entry_options.setdefault(CONF_KIDS, {})\n\n        if user_input is not None:\n            kid_name = user_input[\"kid_name\"].strip()\n            ha_user_id = user_input.get(\"ha_user\") or \"\"\n            enable_mobile_notifications = user_input.get(\n                \"enable_mobile_notifications\", True\n            )\n            notify_service = user_input.get(\"mobile_notify_service\") or \"\"\n            enable_persist = user_input.get(\"enable_persistent_notifications\", True)\n\n            if any(kid_data[\"name\"] == kid_name for kid_data in kids_dict.values()):\n                errors[\"kid_name\"] = \"duplicate_kid\"\n            else:\n                internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n                kids_dict[internal_id] = {\n                    \"name\": kid_name,\n                    \"ha_user_id\": ha_user_id,\n                    \"enable_notifications\": enable_mobile_notifications,\n                    \"mobile_notify_service\": notify_service,\n                    \"use_persistent_notifications\": enable_persist,\n                    \"internal_id\": internal_id,\n                }\n                self._entry_options[CONF_KIDS] = kids_dict\n\n                LOGGER.debug(\"Added kid '%s' with ID: %s\", kid_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        # Retrieve HA users for linking\n        users = await self.hass.auth.async_get_users()\n        schema = build_kid_schema(\n            self.hass,\n            users=users,\n            default_kid_name=\"\",\n            default_ha_user_id=None,\n            default_enable_mobile_notifications=False,\n            default_mobile_notify_service=None,\n            default_enable_persistent_notifications=False,\n        )\n        return self.async_show_form(\n            step_id=\"add_kid\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_add_parent(self, user_input=None):\n        \"\"\"Add a new parent.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        parents_dict = self._entry_options.setdefault(CONF_PARENTS, {})\n\n        if user_input is not None:\n            parent_name = user_input[\"parent_name\"].strip()\n            ha_user_id = user_input.get(\"ha_user_id\") or \"\"\n            associated_kids = user_input.get(\"associated_kids\", [])\n            enable_mobile_notifications = user_input.get(\n                \"enable_mobile_notifications\", True\n            )\n            notify_service = user_input.get(\"mobile_notify_service\") or \"\"\n            enable_persist = user_input.get(\"enable_persistent_notifications\", True)\n\n            if any(\n                parent_data[\"name\"] == parent_name\n                for parent_data in parents_dict.values()\n            ):\n                errors[\"parent_name\"] = \"duplicate_parent\"\n            else:\n                internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n                parents_dict[internal_id] = {\n                    \"name\": parent_name,\n                    \"ha_user_id\": ha_user_id,\n                    \"associated_kids\": associated_kids,\n                    \"enable_notifications\": enable_mobile_notifications,\n                    \"mobile_notify_service\": notify_service,\n                    \"use_persistent_notifications\": enable_persist,\n                    \"internal_id\": internal_id,\n                }\n                self._entry_options[CONF_PARENTS] = parents_dict\n\n                LOGGER.debug(\"Added parent '%s' with ID: %s\", parent_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        # Retrieve HA users and existing kids for linking\n        users = await self.hass.auth.async_get_users()\n        kids_dict = {\n            kid_data[\"name\"]: kid_id\n            for kid_id, kid_data in self._entry_options.get(CONF_KIDS, {}).items()\n        }\n\n        parent_schema = build_parent_schema(\n            self.hass,\n            users=users,\n            kids_dict=kids_dict,\n            default_parent_name=\"\",\n            default_ha_user_id=None,\n            default_associated_kids=[],\n            default_enable_mobile_notifications=False,\n            default_mobile_notify_service=None,\n            default_enable_persistent_notifications=False,\n            internal_id=None,\n        )\n        return self.async_show_form(\n            step_id=\"add_parent\", data_schema=parent_schema, errors=errors\n        )\n\n    async def async_step_add_chore(self, user_input=None):\n        \"\"\"Add a new chore.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        chores_dict = self._entry_options.setdefault(CONF_CHORES, {})\n\n        if user_input is not None:\n            chore_name = user_input[\"chore_name\"].strip()\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if user_input.get(\"due_date\"):\n                raw_due = user_input[\"due_date\"]\n                try:\n                    due_date_str = ensure_utc_datetime(self.hass, raw_due)\n                    due_dt = dt_util.parse_datetime(due_date_str)\n                    if due_dt and due_dt < dt_util.utcnow():\n                        errors[\"due_date\"] = \"due_date_in_past\"\n                except ValueError:\n                    errors[\"due_date\"] = \"invalid_due_date\"\n                    due_date_str = None\n            else:\n                due_date_str = None\n\n            if any(\n                chore_data[\"name\"] == chore_name for chore_data in chores_dict.values()\n            ):\n                errors[\"chore_name\"] = \"duplicate_chore\"\n\n            if errors:\n                kids_dict = {\n                    data[\"name\"]: eid\n                    for eid, data in self._entry_options.get(CONF_KIDS, {}).items()\n                }\n                schema = build_chore_schema(kids_dict, default=user_input)\n                return self.async_show_form(\n                    step_id=\"add_chore\", data_schema=schema, errors=errors\n                )\n\n            if user_input.get(\"recurring_frequency\") != FREQUENCY_CUSTOM:\n                user_input.pop(\"custom_interval\", None)\n                user_input.pop(\"custom_interval_unit\", None)\n\n            chores_dict[internal_id] = {\n                \"name\": chore_name,\n                \"default_points\": user_input[\"default_points\"],\n                \"partial_allowed\": user_input[\"partial_allowed\"],\n                \"shared_chore\": user_input[\"shared_chore\"],\n                \"allow_multiple_claims_per_day\": user_input[\n                    \"allow_multiple_claims_per_day\"\n                ],\n                \"assigned_kids\": user_input[\"assigned_kids\"],\n                \"description\": user_input.get(\"chore_description\", \"\"),\n                \"chore_labels\": user_input.get(\"chore_labels\", []),\n                \"icon\": user_input.get(\"icon\", \"\"),\n                \"recurring_frequency\": user_input.get(\"recurring_frequency\", \"none\"),\n                \"custom_interval\": user_input.get(\"custom_interval\"),\n                \"custom_interval_unit\": user_input.get(\"custom_interval_unit\"),\n                \"due_date\": due_date_str,\n                \"applicable_days\": user_input.get(\n                    CONF_APPLICABLE_DAYS, DEFAULT_APPLICABLE_DAYS\n                ),\n                \"notify_on_claim\": user_input.get(\n                    CONF_NOTIFY_ON_CLAIM, DEFAULT_NOTIFY_ON_CLAIM\n                ),\n                \"notify_on_approval\": user_input.get(\n                    CONF_NOTIFY_ON_APPROVAL, DEFAULT_NOTIFY_ON_APPROVAL\n                ),\n                \"notify_on_disapproval\": user_input.get(\n                    CONF_NOTIFY_ON_DISAPPROVAL, DEFAULT_NOTIFY_ON_DISAPPROVAL\n                ),\n                \"internal_id\": internal_id,\n            }\n            self._entry_options[CONF_CHORES] = chores_dict\n\n            LOGGER.debug(\"Added chore '%s' with ID: %s\", chore_name, internal_id)\n            LOGGER.debug(\n                \"Final stored 'due_date' for chore '%s': %s\",\n                chore_name,\n                due_date_str,\n            )\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        # Use flow_helpers.build_chore_schema, passing current kids\n        kids_dict = {\n            data[\"name\"]: eid\n            for eid, data in self._entry_options.get(CONF_KIDS, {}).items()\n        }\n        schema = build_chore_schema(kids_dict)\n        return self.async_show_form(\n            step_id=\"add_chore\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_add_badge(self, user_input=None):\n        \"\"\"Add a new badge.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        badges_dict = self._entry_options.setdefault(CONF_BADGES, {})\n\n        if user_input is not None:\n            badge_name = user_input[\"badge_name\"].strip()\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if any(\n                badge_data[\"name\"] == badge_name for badge_data in badges_dict.values()\n            ):\n                errors[\"badge_name\"] = \"duplicate_badge\"\n            else:\n                badges_dict[internal_id] = {\n                    \"name\": badge_name,\n                    \"threshold_type\": user_input[\"threshold_type\"],\n                    \"threshold_value\": user_input[\"threshold_value\"],\n                    \"points_multiplier\": user_input[\"points_multiplier\"],\n                    \"icon\": user_input.get(\"icon\", \"\"),\n                    \"internal_id\": internal_id,\n                    \"description\": user_input.get(\"badge_description\", \"\"),\n                    \"badge_labels\": user_input.get(\"badge_labels\", []),\n                }\n                self._entry_options[CONF_BADGES] = badges_dict\n\n                LOGGER.debug(\"Added badge '%s' with ID: %s\", badge_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        schema = build_badge_schema()\n        return self.async_show_form(\n            step_id=\"add_badge\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_add_reward(self, user_input=None):\n        \"\"\"Add a new reward.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        rewards_dict = self._entry_options.setdefault(CONF_REWARDS, {})\n\n        if user_input is not None:\n            reward_name = user_input[\"reward_name\"].strip()\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if any(\n                reward_data[\"name\"] == reward_name\n                for reward_data in rewards_dict.values()\n            ):\n                errors[\"reward_name\"] = \"duplicate_reward\"\n            else:\n                rewards_dict[internal_id] = {\n                    \"name\": reward_name,\n                    \"cost\": user_input[\"reward_cost\"],\n                    \"description\": user_input.get(\"reward_description\", \"\"),\n                    \"reward_labels\": user_input.get(\"reward_labels\", []),\n                    \"icon\": user_input.get(\"icon\", \"\"),\n                    \"internal_id\": internal_id,\n                }\n                self._entry_options[CONF_REWARDS] = rewards_dict\n\n                LOGGER.debug(\"Added reward '%s' with ID: %s\", reward_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        schema = build_reward_schema()\n        return self.async_show_form(\n            step_id=\"add_reward\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_add_penalty(self, user_input=None):\n        \"\"\"Add a new penalty.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        penalties_dict = self._entry_options.setdefault(CONF_PENALTIES, {})\n\n        if user_input is not None:\n            penalty_name = user_input[\"penalty_name\"].strip()\n            penalty_points = user_input[\"penalty_points\"]\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if any(\n                penalty_data[\"name\"] == penalty_name\n                for penalty_data in penalties_dict.values()\n            ):\n                errors[\"penalty_name\"] = \"duplicate_penalty\"\n            else:\n                penalties_dict[internal_id] = {\n                    \"name\": penalty_name,\n                    \"description\": user_input.get(\"penalty_description\", \"\"),\n                    \"penalty_labels\": user_input.get(\"penalty_labels\", []),\n                    \"points\": -abs(penalty_points),  # Ensure points are negative\n                    \"icon\": user_input.get(\"icon\", \"\"),\n                    \"internal_id\": internal_id,\n                }\n                self._entry_options[CONF_PENALTIES] = penalties_dict\n\n                LOGGER.debug(\n                    \"Added penalty '%s' with ID: %s\", penalty_name, internal_id\n                )\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        schema = build_penalty_schema()\n        return self.async_show_form(\n            step_id=\"add_penalty\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_add_bonus(self, user_input=None):\n        \"\"\"Add a new bonus.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        bonuses_dict = self._entry_options.setdefault(CONF_BONUSES, {})\n\n        if user_input is not None:\n            bonus_name = user_input[\"bonus_name\"].strip()\n            bonus_points = user_input[\"bonus_points\"]\n            internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n\n            if any(\n                bonus_data[\"name\"] == bonus_name for bonus_data in bonuses_dict.values()\n            ):\n                errors[\"bonus_name\"] = \"duplicate_bonus\"\n            else:\n                bonuses_dict[internal_id] = {\n                    \"name\": bonus_name,\n                    \"description\": user_input.get(\"bonus_description\", \"\"),\n                    \"bonus_labels\": user_input.get(\"bonus_labels\", []),\n                    \"points\": abs(bonus_points),  # Ensure points are positive\n                    \"icon\": user_input.get(\"icon\", \"\"),\n                    \"internal_id\": internal_id,\n                }\n                self._entry_options[CONF_BONUSES] = bonuses_dict\n\n                LOGGER.debug(\"Added bonus '%s' with ID: %s\", bonus_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        schema = build_bonus_schema()\n        return self.async_show_form(\n            step_id=\"add_bonus\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_add_achievement(self, user_input=None):\n        \"\"\"Add a new achievement.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        achievements_dict = self._entry_options.setdefault(CONF_ACHIEVEMENTS, {})\n\n        chores_dict = self._entry_options.get(CONF_CHORES, {})\n\n        if user_input is not None:\n            achievement_name = user_input[\"name\"].strip()\n            if any(\n                data[\"name\"] == achievement_name for data in achievements_dict.values()\n            ):\n                errors[\"name\"] = \"duplicate_achievement\"\n            else:\n                _type = user_input[\"type\"]\n\n                chore_id = \"\"\n\n                if _type == ACHIEVEMENT_TYPE_STREAK:\n                    c = user_input.get(\"selected_chore_id\") or \"\"\n                    if not c or c == \"None\":\n                        errors[\"selected_chore_id\"] = \"a_chore_must_be_selected\"\n                    chore_id = c\n\n                if not errors:\n                    internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n                    achievements_dict[internal_id] = {\n                        \"name\": achievement_name,\n                        \"description\": user_input.get(\"description\", \"\"),\n                        \"achievement_labels\": user_input.get(\"achievement_labels\", []),\n                        \"icon\": user_input.get(\"icon\", \"\"),\n                        \"assigned_kids\": user_input[\"assigned_kids\"],\n                        \"type\": _type,\n                        \"selected_chore_id\": chore_id,\n                        \"criteria\": user_input.get(\"criteria\", \"\").strip(),\n                        \"target_value\": user_input[\"target_value\"],\n                        \"reward_points\": user_input[\"reward_points\"],\n                        \"internal_id\": internal_id,\n                        \"progress\": {},\n                    }\n                    self._entry_options[\"achievements\"] = achievements_dict\n                    LOGGER.debug(\n                        \"Added achievement '%s' with ID: %s\",\n                        achievement_name,\n                        internal_id,\n                    )\n                    await self._update_and_reload()\n                    return await self.async_step_init()\n\n        kids_dict = {\n            kid_data[\"name\"]: kid_id\n            for kid_id, kid_data in self._entry_options.get(CONF_KIDS, {}).items()\n        }\n        achievement_schema = build_achievement_schema(\n            kids_dict=kids_dict, chores_dict=chores_dict, default=None\n        )\n        return self.async_show_form(\n            step_id=\"add_achievement\", data_schema=achievement_schema, errors=errors\n        )\n\n    async def async_step_add_challenge(self, user_input=None):\n        \"\"\"Add a new challenge.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        challenges_dict = self._entry_options.setdefault(CONF_CHALLENGES, {})\n\n        chores_dict = self._entry_options.get(CONF_CHORES, {})\n\n        if user_input is not None:\n            challenge_name = user_input[\"name\"].strip()\n            if any(data[\"name\"] == challenge_name for data in challenges_dict.values()):\n                errors[\"name\"] = \"duplicate_challenge\"\n            else:\n                _type = user_input[\"type\"]\n\n                chore_id = \"\"\n                if _type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n                    c = user_input.get(\"selected_chore_id\") or \"\"\n                    if not c or c == \"None\":\n                        errors[\"selected_chore_id\"] = \"a_chore_must_be_selected\"\n                    chore_id = c\n\n                # Process start_date and end_date using the helper:\n                start_date_input = user_input.get(\"start_date\")\n                end_date_input = user_input.get(\"end_date\")\n\n                if start_date_input:\n                    try:\n                        start_date = ensure_utc_datetime(self.hass, start_date_input)\n                        start_dt = dt_util.parse_datetime(start_date)\n                        if start_dt and start_dt < dt_util.utcnow():\n                            errors[\"start_date\"] = \"start_date_in_past\"\n                    except Exception:\n                        errors[\"start_date\"] = \"invalid_start_date\"\n                        start_date = None\n                else:\n                    start_date = None\n\n                if end_date_input:\n                    try:\n                        end_date = ensure_utc_datetime(self.hass, end_date_input)\n                        end_dt = dt_util.parse_datetime(end_date)\n                        if end_dt and end_dt <= dt_util.utcnow():\n                            errors[\"end_date\"] = \"end_date_in_past\"\n                        if start_date:\n                            sdt = dt_util.parse_datetime(start_date)\n                            if sdt and end_dt and end_dt <= sdt:\n                                errors[\"end_date\"] = \"end_date_not_after_start_date\"\n                    except Exception:\n                        errors[\"end_date\"] = \"invalid_end_date\"\n                        end_date = None\n                else:\n                    end_date = None\n\n                if not errors:\n                    internal_id = user_input.get(\"internal_id\", str(uuid.uuid4()))\n                    challenges_dict[internal_id] = {\n                        \"name\": challenge_name,\n                        \"description\": user_input.get(\"description\", \"\"),\n                        \"challenge_labels\": user_input.get(\"challenge_labels\", []),\n                        \"icon\": user_input.get(\"icon\", \"\"),\n                        \"assigned_kids\": user_input[\"assigned_kids\"],\n                        \"type\": _type,\n                        \"selected_chore_id\": chore_id,\n                        \"criteria\": user_input.get(\"criteria\", \"\").strip(),\n                        \"target_value\": user_input[\"target_value\"],\n                        \"reward_points\": user_input[\"reward_points\"],\n                        \"start_date\": start_date,\n                        \"end_date\": end_date,\n                        \"internal_id\": internal_id,\n                        \"progress\": {},\n                    }\n                    self._entry_options[CONF_CHALLENGES] = challenges_dict\n                    LOGGER.debug(\n                        \"Added challenge '%s' with ID: %s\", challenge_name, internal_id\n                    )\n                    await self._update_and_reload()\n                    return await self.async_step_init()\n\n        kids_dict = {\n            kid_data[\"name\"]: kid_id\n            for kid_id, kid_data in self._entry_options.get(CONF_KIDS, {}).items()\n        }\n        challenge_schema = build_challenge_schema(\n            kids_dict=kids_dict, chores_dict=chores_dict, default=user_input\n        )\n        return self.async_show_form(\n            step_id=\"add_challenge\", data_schema=challenge_schema, errors=errors\n        )\n\n    # ------------------ EDIT ENTITY ------------------\n    async def async_step_edit_kid(self, user_input=None):\n        \"\"\"Edit an existing kid.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        kids_dict = self._entry_options.get(CONF_KIDS, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in kids_dict:\n            LOGGER.error(\"Edit kid: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_kid\")\n\n        kid_data = kids_dict[internal_id]\n\n        if user_input is not None:\n            new_name = user_input[\"kid_name\"].strip()\n            ha_user_id = user_input.get(\"ha_user\") or \"\"\n            enable_notifications = user_input.get(\"enable_mobile_notifications\", True)\n            mobile_notify_service = user_input.get(\"mobile_notify_service\") or \"\"\n            use_persistent = user_input.get(\"enable_persistent_notifications\", True)\n\n            # Check for duplicate names excluding current kid\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in kids_dict.items()\n            ):\n                errors[\"kid_name\"] = \"duplicate_kid\"\n            else:\n                kid_data[\"name\"] = new_name\n                kid_data[\"ha_user_id\"] = ha_user_id\n                kid_data[\"enable_notifications\"] = enable_notifications\n                kid_data[\"mobile_notify_service\"] = mobile_notify_service\n                kid_data[\"use_persistent_notifications\"] = use_persistent\n\n                self._entry_options[CONF_KIDS] = kids_dict\n\n                LOGGER.debug(\"Edited kid '%s' with ID: %s\", new_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        # Retrieve HA users for linking\n        users = await self.hass.auth.async_get_users()\n        schema = build_kid_schema(\n            self.hass,\n            users=users,\n            default_kid_name=kid_data[\"name\"],\n            default_ha_user_id=kid_data.get(\"ha_user_id\"),\n            default_enable_mobile_notifications=kid_data.get(\n                \"enable_notifications\", True\n            ),\n            default_mobile_notify_service=kid_data.get(\"mobile_notify_service\"),\n            default_enable_persistent_notifications=kid_data.get(\n                \"use_persistent_notifications\", True\n            ),\n            internal_id=internal_id,\n        )\n        return self.async_show_form(\n            step_id=\"edit_kid\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_edit_parent(self, user_input=None):\n        \"\"\"Edit an existing parent.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        parents_dict = self._entry_options.get(CONF_PARENTS, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in parents_dict:\n            LOGGER.error(\"Edit parent: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_parent\")\n\n        parent_data = parents_dict[internal_id]\n\n        if user_input is not None:\n            new_name = user_input[\"parent_name\"].strip()\n            ha_user_id = user_input.get(\"ha_user_id\") or \"\"\n            associated_kids = user_input.get(\"associated_kids\", [])\n            enable_notifications = user_input.get(\"enable_mobile_notifications\", True)\n            mobile_notify_service = user_input.get(\"mobile_notify_service\") or \"\"\n            use_persistent = user_input.get(\"enable_persistent_notifications\", True)\n\n            # Check for duplicate names excluding current parent\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in parents_dict.items()\n            ):\n                errors[\"parent_name\"] = \"duplicate_parent\"\n            else:\n                parent_data[\"name\"] = new_name\n                parent_data[\"ha_user_id\"] = ha_user_id\n                parent_data[\"associated_kids\"] = associated_kids\n                parent_data[\"enable_notifications\"] = enable_notifications\n                parent_data[\"mobile_notify_service\"] = mobile_notify_service\n                parent_data[\"use_persistent_notifications\"] = use_persistent\n\n                self._entry_options[CONF_PARENTS] = parents_dict\n\n                LOGGER.debug(\"Edited parent '%s' with ID: %s\", new_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        # Retrieve HA users and existing kids for linking\n        users = await self.hass.auth.async_get_users()\n        kids_dict = {\n            kid_data[\"name\"]: kid_id\n            for kid_id, kid_data in self._entry_options.get(CONF_KIDS, {}).items()\n        }\n\n        parent_schema = build_parent_schema(\n            self.hass,\n            users=users,\n            kids_dict=kids_dict,\n            default_parent_name=parent_data[\"name\"],\n            default_ha_user_id=parent_data.get(\"ha_user_id\"),\n            default_associated_kids=parent_data.get(\"associated_kids\", []),\n            default_enable_mobile_notifications=parent_data.get(\n                \"enable_notifications\", True\n            ),\n            default_mobile_notify_service=parent_data.get(\"mobile_notify_service\"),\n            default_enable_persistent_notifications=parent_data.get(\n                \"use_persistent_notifications\", True\n            ),\n            internal_id=internal_id,\n        )\n        return self.async_show_form(\n            step_id=\"edit_parent\", data_schema=parent_schema, errors=errors\n        )\n\n    async def async_step_edit_chore(self, user_input=None):\n        \"\"\"Edit an existing chore.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        chores_dict = self._entry_options.get(CONF_CHORES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in chores_dict:\n            LOGGER.error(\"Edit chore: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_chore\")\n\n        chore_data = chores_dict[internal_id]\n\n        if user_input is not None:\n            new_name = user_input[\"chore_name\"].strip()\n            raw_due = user_input.get(\"due_date\")\n\n            # Check for duplicate names excluding current chore\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in chores_dict.items()\n            ):\n                errors[\"chore_name\"] = \"duplicate_chore\"\n            else:\n                if user_input.get(\"recurring_frequency\") != FREQUENCY_CUSTOM:\n                    user_input.pop(\"custom_interval\", None)\n                    user_input.pop(\"custom_interval_unit\", None)\n\n                chore_data[\"name\"] = new_name\n                chore_data[\"description\"] = user_input.get(\"chore_description\", \"\")\n                chore_data[\"chore_labels\"] = user_input.get(\"chore_labels\", [])\n                chore_data[\"default_points\"] = user_input[\"default_points\"]\n                chore_data[\"shared_chore\"] = user_input[\"shared_chore\"]\n                chore_data[\"partial_allowed\"] = user_input[\"partial_allowed\"]\n                chore_data[\"allow_multiple_claims_per_day\"] = user_input[\n                    \"allow_multiple_claims_per_day\"\n                ]\n                chore_data[\"assigned_kids\"] = user_input[\"assigned_kids\"]\n                chore_data[\"icon\"] = user_input.get(\"icon\", \"\")\n                chore_data[\"recurring_frequency\"] = user_input.get(\n                    \"recurring_frequency\", \"none\"\n                )\n                chore_data[\"custom_interval\"] = user_input.get(\"custom_interval\")\n                chore_data[\"custom_interval_unit\"] = user_input.get(\n                    \"custom_interval_unit\"\n                )\n                if raw_due:\n                    try:\n                        if isinstance(raw_due, datetime.datetime):\n                            parsed_due = raw_due\n                        else:\n                            parsed_due = dt_util.parse_datetime(\n                                raw_due\n                            ) or datetime.datetime.fromisoformat(raw_due)\n                        due_utc = dt_util.as_utc(parsed_due)\n                        if due_utc < dt_util.utcnow():\n                            errors[\"due_date\"] = \"due_date_in_past\"\n                        else:\n                            chore_data[\"due_date\"] = due_utc.isoformat()\n                    except Exception:\n                        errors[\"due_date\"] = \"invalid_due_date\"\n                else:\n                    chore_data[\"due_date\"] = None\n                    LOGGER.debug(\"No date/time provided; defaulting to None\")\n\n                chore_data[\"applicable_days\"] = user_input.get(\"applicable_days\", [])\n                chore_data[\"notify_on_claim\"] = user_input.get(\"notify_on_claim\", True)\n                chore_data[\"notify_on_approval\"] = user_input.get(\n                    \"notify_on_approval\", True\n                )\n                chore_data[\"notify_on_disapproval\"] = user_input.get(\n                    \"notify_on_disapproval\", True\n                )\n\n            if errors:\n                kids_dict = {\n                    data[\"name\"]: eid\n                    for eid, data in self._entry_options.get(CONF_KIDS, {}).items()\n                }\n                default_data = user_input.copy()\n                return self.async_show_form(\n                    step_id=\"edit_chore\",\n                    data_schema=build_chore_schema(\n                        kids_dict, default={**chore_data, **default_data}\n                    ),\n                    errors=errors,\n                )\n\n            self._entry_options[CONF_CHORES] = chores_dict\n\n            LOGGER.debug(\"Edited chore '%s' with ID: %s\", new_name, internal_id)\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        # Use flow_helpers.build_chore_schema, passing current kids\n        kids_dict = {\n            data[\"name\"]: eid\n            for eid, data in self._entry_options.get(CONF_KIDS, {}).items()\n        }\n\n        # Convert stored string to datetime for DateTimeSelector\n        existing_due_str = chore_data.get(\"due_date\")\n        existing_due_date = None\n\n        if existing_due_str:\n            try:\n                # Attempt to parse using dt_util or fallback to fromisoformat\n                parsed_date = dt_util.parse_datetime(\n                    existing_due_str\n                ) or datetime.datetime.fromisoformat(existing_due_str)\n                # Convert to the required format for DateTimeSelector\n                existing_due_date = dt_util.as_local(parsed_date).strftime(\n                    \"%Y-%m-%d %H:%M:%S\"\n                )\n                LOGGER.debug(\n                    \"Processed existing_due_date for DateTimeSelector: %s\",\n                    existing_due_date,\n                )\n            except ValueError as e:\n                LOGGER.error(\n                    \"Failed to parse existing_due_date '%s': %s\", existing_due_str, e\n                )\n\n        schema = build_chore_schema(\n            kids_dict, default={**chore_data, \"due_date\": existing_due_date}\n        )\n        return self.async_show_form(\n            step_id=\"edit_chore\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_edit_badge(self, user_input=None):\n        \"\"\"Edit an existing badge.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        badges_dict = self._entry_options.get(CONF_BADGES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in badges_dict:\n            LOGGER.error(\"Edit badge: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_badge\")\n\n        badge_data = badges_dict[internal_id]\n\n        if user_input is not None:\n            new_name = user_input[\"badge_name\"].strip()\n\n            # Check for duplicate names excluding current badge\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in badges_dict.items()\n            ):\n                errors[\"badge_name\"] = \"duplicate_badge\"\n            else:\n                badge_data[\"name\"] = new_name\n                badge_data[\"threshold_type\"] = user_input[\"threshold_type\"]\n                badge_data[\"threshold_value\"] = user_input[\"threshold_value\"]\n                badge_data[\"points_multiplier\"] = user_input[\"points_multiplier\"]\n                badge_data[\"icon\"] = user_input.get(\"icon\", \"\")\n                badge_data[\"description\"] = user_input[\"badge_description\"]\n                badge_data[\"badge_labels\"] = user_input.get(\"badge_labels\", [])\n\n                self._entry_options[CONF_BADGES] = badges_dict\n\n                LOGGER.debug(\"Edited badge '%s' with ID: %s\", new_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        schema = build_badge_schema(default=badge_data)\n        return self.async_show_form(\n            step_id=\"edit_badge\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_edit_reward(self, user_input=None):\n        \"\"\"Edit an existing reward.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        rewards_dict = self._entry_options.get(CONF_REWARDS, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in rewards_dict:\n            LOGGER.error(\"Edit reward: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_reward\")\n\n        reward_data = rewards_dict[internal_id]\n\n        if user_input is not None:\n            new_name = user_input[\"reward_name\"].strip()\n\n            # Check for duplicate names excluding current reward\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in rewards_dict.items()\n            ):\n                errors[\"reward_name\"] = \"duplicate_reward\"\n            else:\n                reward_data[\"name\"] = new_name\n                reward_data[\"cost\"] = user_input[\"reward_cost\"]\n                reward_data[\"description\"] = user_input.get(\"reward_description\", \"\")\n                reward_data[\"reward_labels\"] = user_input.get(\"reward_labels\", [])\n                reward_data[\"icon\"] = user_input.get(\"icon\", \"\")\n\n                self._entry_options[CONF_REWARDS] = rewards_dict\n\n                LOGGER.debug(\"Edited reward '%s' with ID: %s\", new_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        schema = build_reward_schema(default=reward_data)\n        return self.async_show_form(\n            step_id=\"edit_reward\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_edit_penalty(self, user_input=None):\n        \"\"\"Edit an existing penalty.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        penalties_dict = self._entry_options.get(CONF_PENALTIES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in penalties_dict:\n            LOGGER.error(\"Edit penalty: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_penalty\")\n\n        penalty_data = penalties_dict[internal_id]\n\n        if user_input is not None:\n            new_name = user_input[\"penalty_name\"].strip()\n            penalty_points = user_input[\"penalty_points\"]\n\n            # Check for duplicate names excluding current penalty\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in penalties_dict.items()\n            ):\n                errors[\"penalty_name\"] = \"duplicate_penalty\"\n            else:\n                penalty_data[\"name\"] = new_name\n                penalty_data[\"description\"] = user_input.get(\"penalty_description\", \"\")\n                penalty_data[\"penalty_labels\"] = user_input.get(\"penalty_labels\", [])\n                penalty_data[\"points\"] = -abs(\n                    penalty_points\n                )  # Ensure points are negative\n                penalty_data[\"icon\"] = user_input.get(\"icon\", \"\")\n\n                self._entry_options[CONF_PENALTIES] = penalties_dict\n\n                LOGGER.debug(\"Edited penalty '%s' with ID: %s\", new_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        # Prepare data for schema (convert points to positive for display)\n        display_data = dict(penalty_data)\n        display_data[\"penalty_points\"] = abs(display_data[\"points\"])\n        schema = build_penalty_schema(default=display_data)\n        return self.async_show_form(\n            step_id=\"edit_penalty\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_edit_bonus(self, user_input=None):\n        \"\"\"Edit an existing bonus.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        bonuses_dict = self._entry_options.get(CONF_BONUSES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in bonuses_dict:\n            LOGGER.error(\"Edit bonus: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_bonus\")\n\n        bonus_data = bonuses_dict[internal_id]\n\n        if user_input is not None:\n            new_name = user_input[\"bonus_name\"].strip()\n            bonus_points = user_input[\"bonus_points\"]\n\n            # Check for duplicate names excluding current bonus\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in bonuses_dict.items()\n            ):\n                errors[\"bonus_name\"] = \"duplicate_bonus\"\n            else:\n                bonus_data[\"name\"] = new_name\n                bonus_data[\"description\"] = user_input.get(\"bonus_description\", \"\")\n                bonus_data[\"bonus_labels\"] = user_input.get(\"bonus_labels\", [])\n                bonus_data[\"points\"] = abs(bonus_points)  # Ensure points are positive\n                bonus_data[\"icon\"] = user_input.get(\"icon\", \"\")\n\n                self._entry_options[CONF_BONUSES] = bonuses_dict\n\n                LOGGER.debug(\"Edited bonus '%s' with ID: %s\", new_name, internal_id)\n                await self._update_and_reload()\n                return await self.async_step_init()\n\n        # Prepare data for schema (convert points to positive for display)\n        display_data = dict(bonus_data)\n        display_data[\"bonus_points\"] = abs(display_data[\"points\"])\n        schema = build_bonus_schema(default=display_data)\n        return self.async_show_form(\n            step_id=\"edit_bonus\", data_schema=schema, errors=errors\n        )\n\n    async def async_step_edit_achievement(self, user_input=None):\n        \"\"\"Edit an existing achievement.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        errors = {}\n        achievements_dict = self._entry_options.get(CONF_ACHIEVEMENTS, {})\n\n        internal_id = self.context.get(\"internal_id\")\n        if not internal_id or internal_id not in achievements_dict:\n            LOGGER.error(\"Edit achievement: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_achievement\")\n\n        achievement_data = achievements_dict[internal_id]\n\n        if user_input is not None:\n            new_name = user_input[\"name\"].strip()\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in achievements_dict.items()\n            ):\n                errors[\"name\"] = \"duplicate_achievement\"\n            else:\n                _type = user_input[\"type\"]\n\n                chore_id = \"\"\n                if _type == ACHIEVEMENT_TYPE_STREAK:\n                    c = user_input.get(\"selected_chore_id\") or \"\"\n                    if not c or c == \"None\":\n                        errors[\"selected_chore_id\"] = \"a_chore_must_be_selected\"\n                    chore_id = c\n\n                if not errors:\n                    achievement_data[\"name\"] = new_name\n                    achievement_data[\"description\"] = user_input.get(\"description\", \"\")\n                    achievement_data[\"achievement_labels\"] = user_input.get(\n                        \"achievement_labels\", []\n                    )\n                    achievement_data[\"icon\"] = user_input.get(\"icon\", \"\")\n                    achievement_data[\"assigned_kids\"] = user_input[\"assigned_kids\"]\n                    achievement_data[\"type\"] = _type\n                    achievement_data[\"selected_chore_id\"] = chore_id\n                    achievement_data[\"criteria\"] = user_input.get(\n                        \"criteria\", \"\"\n                    ).strip()\n                    achievement_data[\"target_value\"] = user_input[\"target_value\"]\n                    achievement_data[\"reward_points\"] = user_input[\"reward_points\"]\n                    achievements_dict[internal_id] = achievement_data\n                    self._entry_options[CONF_ACHIEVEMENTS] = achievements_dict\n                    LOGGER.debug(\n                        \"Edited achievement '%s' with ID: %s\", new_name, internal_id\n                    )\n                    await self._update_and_reload()\n                    return await self.async_step_init()\n\n        kids_dict = {\n            kid_data[\"name\"]: kid_id\n            for kid_id, kid_data in self._entry_options.get(CONF_KIDS, {}).items()\n        }\n        chores_dict = self._entry_options.get(CONF_CHORES, {})\n\n        achievement_schema = build_achievement_schema(\n            kids_dict=kids_dict, chores_dict=chores_dict, default=achievement_data\n        )\n        return self.async_show_form(\n            step_id=\"edit_achievement\", data_schema=achievement_schema, errors=errors\n        )\n\n    async def async_step_edit_challenge(self, user_input=None):\n        \"\"\"Edit an existing challenge.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n        errors = {}\n        challenges_dict = self._entry_options.get(CONF_CHALLENGES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in challenges_dict:\n            LOGGER.error(\"Edit challenge: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_challenge\")\n\n        challenge_data = challenges_dict[internal_id]\n\n        if user_input is None:\n            kids_dict = {\n                data[\"name\"]: kid_id\n                for kid_id, data in self._entry_options.get(CONF_KIDS, {}).items()\n            }\n            chores_dict = self._entry_options.get(CONF_CHORES, {})\n            # Convert stored start/end dates to a display format (e.g. local time string)\n            default_data = {\n                **challenge_data,\n                \"start_date\": challenge_data.get(\"start_date\")\n                and dt_util.as_local(\n                    dt_util.parse_datetime(challenge_data[\"start_date\"])\n                ).strftime(\"%Y-%m-%d %H:%M:%S\"),\n                \"end_date\": challenge_data.get(\"end_date\")\n                and dt_util.as_local(\n                    dt_util.parse_datetime(challenge_data[\"end_date\"])\n                ).strftime(\"%Y-%m-%d %H:%M:%S\"),\n            }\n            schema = build_challenge_schema(\n                kids_dict=kids_dict, chores_dict=chores_dict, default=default_data\n            )\n            return self.async_show_form(\n                step_id=\"edit_challenge\", data_schema=schema, errors=errors\n            )\n\n        start_date_input = user_input.get(\"start_date\")\n        if start_date_input:\n            try:\n                new_start_date = ensure_utc_datetime(self.hass, start_date_input)\n                start_dt = dt_util.parse_datetime(new_start_date)\n                if start_dt and start_dt < dt_util.utcnow():\n                    errors[\"start_date\"] = \"start_date_in_past\"\n            except Exception:\n                errors[\"start_date\"] = \"invalid_start_date\"\n                new_start_date = None\n        else:\n            new_start_date = None\n\n        end_date_input = user_input.get(\"end_date\")\n        if end_date_input:\n            try:\n                new_end_date = ensure_utc_datetime(self.hass, end_date_input)\n                end_dt = dt_util.parse_datetime(new_end_date)\n                if end_dt and end_dt <= dt_util.utcnow():\n                    errors[\"end_date\"] = \"end_date_in_past\"\n                if new_start_date:\n                    sdt = dt_util.parse_datetime(new_start_date)\n                    if sdt and end_dt and end_dt <= sdt:\n                        errors[\"end_date\"] = \"end_date_not_after_start_date\"\n            except Exception:\n                errors[\"end_date\"] = \"invalid_end_date\"\n                new_end_date = None\n        else:\n            new_end_date = None\n\n        if user_input is not None:\n            new_name = user_input[\"name\"].strip()\n            if any(\n                data[\"name\"] == new_name and eid != internal_id\n                for eid, data in challenges_dict.items()\n            ):\n                errors[\"name\"] = \"duplicate_challenge\"\n            else:\n                _type = user_input[\"type\"]\n\n                chore_id = \"\"\n                if _type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n                    c = user_input.get(\"selected_chore_id\") or \"\"\n                    if not c or c == \"None\":\n                        errors[\"selected_chore_id\"] = \"a_chore_must_be_selected\"\n                    chore_id = c\n\n                if not errors:\n                    challenge_data[\"name\"] = new_name\n                    challenge_data[\"description\"] = user_input.get(\"description\", \"\")\n                    challenge_data[\"challenge_labels\"] = user_input.get(\n                        \"challenge_labels\", []\n                    )\n                    challenge_data[\"icon\"] = user_input.get(\"icon\", \"\")\n                    challenge_data[\"assigned_kids\"] = user_input[\"assigned_kids\"]\n                    challenge_data[\"type\"] = _type\n                    challenge_data[\"selected_chore_id\"] = chore_id\n                    challenge_data[\"criteria\"] = user_input.get(\"criteria\", \"\").strip()\n                    challenge_data[\"target_value\"] = user_input[\"target_value\"]\n                    challenge_data[\"reward_points\"] = user_input[\"reward_points\"]\n                    challenge_data[\"start_date\"] = new_start_date\n                    challenge_data[\"end_date\"] = new_end_date\n                    LOGGER.debug(\n                        \"Edited challenge '%s' with ID: %s\", new_name, internal_id\n                    )\n                    await self._update_and_reload()\n                    return await self.async_step_init()\n\n        kids_dict = {\n            kid_data[\"name\"]: kid_id\n            for kid_id, kid_data in self._entry_options.get(CONF_KIDS, {}).items()\n        }\n        chores_dict = self._entry_options.get(CONF_CHORES, {})\n\n        default_data = {\n            **challenge_data,\n            \"start_date\": new_start_date,\n            \"end_date\": new_end_date,\n        }\n        challenge_schema = build_challenge_schema(\n            kids_dict=kids_dict, chores_dict=chores_dict, default=default_data\n        )\n        return self.async_show_form(\n            step_id=\"edit_challenge\", data_schema=challenge_schema, errors=errors\n        )\n\n    # ------------------ DELETE ENTITY ------------------\n    async def async_step_delete_kid(self, user_input=None):\n        \"\"\"Delete a kid.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        kids_dict = self._entry_options.get(CONF_KIDS, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in kids_dict:\n            LOGGER.error(\"Delete kid: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_kid\")\n\n        kid_name = kids_dict[internal_id][\"name\"]\n\n        if user_input is not None:\n            kids_dict.pop(internal_id, None)\n\n            self._entry_options[CONF_KIDS] = kids_dict\n\n            LOGGER.debug(\"Deleted kid '%s' with ID: %s\", kid_name, internal_id)\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_kid\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"kid_name\": kid_name},\n        )\n\n    async def async_step_delete_parent(self, user_input=None):\n        \"\"\"Delete a parent.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        parents_dict = self._entry_options.get(CONF_PARENTS, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in parents_dict:\n            LOGGER.error(\"Delete parent: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_parent\")\n\n        parent_name = parents_dict[internal_id][\"name\"]\n\n        if user_input is not None:\n            parents_dict.pop(internal_id, None)\n\n            self._entry_options[CONF_PARENTS] = parents_dict\n\n            LOGGER.debug(\"Deleted parent '%s' with ID: %s\", parent_name, internal_id)\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_parent\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"parent_name\": parent_name},\n        )\n\n    async def async_step_delete_chore(self, user_input=None):\n        \"\"\"Delete a chore.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        chores_dict = self._entry_options.get(CONF_CHORES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in chores_dict:\n            LOGGER.error(\"Delete chore: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_chore\")\n\n        chore_name = chores_dict[internal_id][\"name\"]\n\n        if user_input is not None:\n            chores_dict.pop(internal_id, None)\n\n            self._entry_options[CONF_CHORES] = chores_dict\n\n            LOGGER.debug(\"Deleted chore '%s' with ID: %s\", chore_name, internal_id)\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_chore\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"chore_name\": chore_name},\n        )\n\n    async def async_step_delete_badge(self, user_input=None):\n        \"\"\"Delete a badge.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        badges_dict = self._entry_options.get(CONF_BADGES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in badges_dict:\n            LOGGER.error(\"Delete badge: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_badge\")\n\n        badge_name = badges_dict[internal_id][\"name\"]\n\n        if user_input is not None:\n            badges_dict.pop(internal_id, None)\n\n            self._entry_options[CONF_BADGES] = badges_dict\n\n            LOGGER.debug(\"Deleted badge '%s' with ID: %s\", badge_name, internal_id)\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_badge\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"badge_name\": badge_name},\n        )\n\n    async def async_step_delete_reward(self, user_input=None):\n        \"\"\"Delete a reward.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        rewards_dict = self._entry_options.get(CONF_REWARDS, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in rewards_dict:\n            LOGGER.error(\"Delete reward: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_reward\")\n\n        reward_name = rewards_dict[internal_id][\"name\"]\n\n        if user_input is not None:\n            rewards_dict.pop(internal_id, None)\n\n            self._entry_options[CONF_REWARDS] = rewards_dict\n\n            LOGGER.debug(\"Deleted reward '%s' with ID: %s\", reward_name, internal_id)\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_reward\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"reward_name\": reward_name},\n        )\n\n    async def async_step_delete_penalty(self, user_input=None):\n        \"\"\"Delete a penalty.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        penalties_dict = self._entry_options.get(CONF_PENALTIES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in penalties_dict:\n            LOGGER.error(\"Delete penalty: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_penalty\")\n\n        penalty_name = penalties_dict[internal_id][\"name\"]\n\n        if user_input is not None:\n            penalties_dict.pop(internal_id, None)\n\n            self._entry_options[CONF_PENALTIES] = penalties_dict\n\n            LOGGER.debug(\"Deleted penalty '%s' with ID: %s\", penalty_name, internal_id)\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_penalty\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"penalty_name\": penalty_name},\n        )\n\n    async def async_step_delete_achievement(self, user_input=None):\n        \"\"\"Delete an achievement.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        achievements_dict = self._entry_options.get(CONF_ACHIEVEMENTS, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in achievements_dict:\n            LOGGER.error(\"Delete achievement: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_achievement\")\n\n        achievement_name = achievements_dict[internal_id][\"name\"]\n        if user_input is not None:\n            achievements_dict.pop(internal_id, None)\n            self._entry_options[CONF_ACHIEVEMENTS] = achievements_dict\n            LOGGER.debug(\n                \"Deleted achievement '%s' with ID: %s\", achievement_name, internal_id\n            )\n\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_achievement\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"achievement_name\": achievement_name},\n        )\n\n    async def async_step_delete_challenge(self, user_input=None):\n        \"\"\"Delete a challenge.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        challenges_dict = self._entry_options.get(CONF_CHALLENGES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in challenges_dict:\n            LOGGER.error(\"Delete challenge: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_challenge\")\n\n        challenge_name = challenges_dict[internal_id][\"name\"]\n        if user_input is not None:\n            challenges_dict.pop(internal_id, None)\n            self._entry_options[CONF_CHALLENGES] = challenges_dict\n            LOGGER.debug(\n                \"Deleted challenge '%s' with ID: %s\", challenge_name, internal_id\n            )\n\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_challenge\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"challenge_name\": challenge_name},\n        )\n\n    async def async_step_delete_bonus(self, user_input=None):\n        \"\"\"Delete a bonus.\"\"\"\n        self._entry_options = dict(self.config_entry.options)\n\n        bonuses_dict = self._entry_options.get(CONF_BONUSES, {})\n        internal_id = self.context.get(\"internal_id\")\n\n        if not internal_id or internal_id not in bonuses_dict:\n            LOGGER.error(\"Delete bonus: Invalid internal_id '%s'\", internal_id)\n            return self.async_abort(reason=\"invalid_bonus\")\n\n        bonus_name = bonuses_dict[internal_id][\"name\"]\n\n        if user_input is not None:\n            bonuses_dict.pop(internal_id, None)\n\n            self._entry_options[CONF_BONUSES] = bonuses_dict\n\n            LOGGER.debug(\"Deleted bonus '%s' with ID: %s\", bonus_name, internal_id)\n            await self._update_and_reload()\n            return await self.async_step_init()\n\n        return self.async_show_form(\n            step_id=\"delete_bonus\",\n            data_schema=vol.Schema({}),\n            description_placeholders={\"bonus_name\": bonus_name},\n        )\n\n    # ------------------ HELPER METHODS ------------------\n    async def _update_and_reload(self):\n        \"\"\"Update the config entry options and reload the integration.\"\"\"\n        new_data = dict(self.config_entry.data)\n        new_data[\"last_change\"] = dt_util.utcnow().isoformat()\n\n        self.hass.config_entries.async_update_entry(\n            self.config_entry, data=new_data, options=self._entry_options\n        )\n        LOGGER.debug(\n            \"Called update_entry. Now reloading entry: %s\", self.config_entry.entry_id\n        )\n        await self.hass.config_entries.async_reload(self.config_entry.entry_id)\n        LOGGER.debug(\"Options updated and integration reloaded\")\n"
  },
  {
    "path": "custom_components/kidschores/select.py",
    "content": "# File: select.py\n\"\"\"Select entities for the KidsChores integration.\n\nAllows the user to pick from all chores, all rewards, or all penalties\nin a global manner. This is useful for automations or scripts where a\nuser wishes to select a chore/reward/penalty dynamically.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Optional\nfrom homeassistant.components.select import SelectEntity\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\n\nfrom .const import DOMAIN, LOGGER\nfrom .coordinator import KidsChoresDataCoordinator\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up the KidsChores select entities from a config entry.\n\n    Creates three global selects:\n      1) ChoresSelect: lists all chore names\n      2) RewardsSelect: lists all reward names\n      3) PenaltiesSelect: lists all penalty names\n\n    \"\"\"\n    data = hass.data[DOMAIN][entry.entry_id]\n    coordinator: KidsChoresDataCoordinator = data[\"coordinator\"]\n\n    # Create one global select entity for each category\n    selects = [\n        ChoresSelect(coordinator, entry),\n        RewardsSelect(coordinator, entry),\n        PenaltiesSelect(coordinator, entry),\n    ]\n\n    for kid_id in coordinator.kids_data.keys():\n        selects.append(ChoresKidSelect(coordinator, entry, kid_id))\n\n    async_add_entities(selects)\n\n\nclass KidsChoresSelectBase(CoordinatorEntity, SelectEntity):\n    \"\"\"Base class for the KidsChores select entities.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kc_select_base\"\n\n    def __init__(self, coordinator: KidsChoresDataCoordinator, entry: ConfigEntry):\n        \"\"\"Initialize the base select entity.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._selected_option: Optional[str] = None\n\n    @property\n    def current_option(self) -> Optional[str]:\n        \"\"\"Return the currently selected option (chore/reward/penalty name).\n\n        None if nothing has been selected.\n        \"\"\"\n        return self._selected_option\n\n    async def async_select_option(self, option: str) -> None:\n        \"\"\"When the user selects an option from the dropdown, store it.\n\n        By default, no further action is taken.\n        \"\"\"\n        self._selected_option = option\n        LOGGER.debug(\n            \"%s: User selected option '%s'\",\n            self._attr_name,\n            option,\n        )\n        self.async_write_ha_state()\n\n\nclass ChoresSelect(KidsChoresSelectBase):\n    \"\"\"Global select entity listing all defined chores by name.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chores_select\"\n\n    def __init__(self, coordinator: KidsChoresDataCoordinator, entry: ConfigEntry):\n        \"\"\"Initialize the Chores select entity.\"\"\"\n        super().__init__(coordinator, entry)\n        self._attr_unique_id = f\"{entry.entry_id}_chores_select\"\n        self._attr_name = \"KidsChores: All Chores\"\n        self.entity_id = f\"select.kc_all_chores\"\n\n    @property\n    def options(self) -> list[str]:\n        \"\"\"Return a list of chore names from the coordinator.\n\n        If no chores exist, returns an empty list.\n        \"\"\"\n        return [\n            chore_info.get(\"name\", f\"Chore {chore_id}\")\n            for chore_id, chore_info in self.coordinator.chores_data.items()\n        ]\n\n\nclass RewardsSelect(KidsChoresSelectBase):\n    \"\"\"Global select entity listing all defined rewards by name.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"rewards_select\"\n\n    def __init__(self, coordinator: KidsChoresDataCoordinator, entry: ConfigEntry):\n        \"\"\"Initialize the Rewards select entity.\"\"\"\n        super().__init__(coordinator, entry)\n        self._attr_unique_id = f\"{entry.entry_id}_rewards_select\"\n        self._attr_name = \"KidsChores: All Rewards\"\n        self.entity_id = f\"select.kc_all_rewards\"\n\n    @property\n    def options(self) -> list[str]:\n        \"\"\"Return a list of reward names from the coordinator.\n\n        If no rewards exist, returns an empty list.\n        \"\"\"\n        return [\n            reward_info.get(\"name\", f\"Reward {reward_id}\")\n            for reward_id, reward_info in self.coordinator.rewards_data.items()\n        ]\n\n\nclass PenaltiesSelect(KidsChoresSelectBase):\n    \"\"\"Global select entity listing all defined penalties by name.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"penalties_select\"\n\n    def __init__(self, coordinator: KidsChoresDataCoordinator, entry: ConfigEntry):\n        \"\"\"Initialize the Penalties select entity.\"\"\"\n        super().__init__(coordinator, entry)\n        self._attr_unique_id = f\"{entry.entry_id}_penalties_select\"\n        self._attr_name = \"KidsChores: All Penalties\"\n        self.entity_id = f\"select.kc_all_penalties\"\n\n    @property\n    def options(self) -> list[str]:\n        \"\"\"Return a list of penalty names from the coordinator.\n\n        If no penalties exist, returns an empty list.\n        \"\"\"\n        return [\n            penalty_info.get(\"name\", f\"Penalty {penalty_id}\")\n            for penalty_id, penalty_info in self.coordinator.penalties_data.items()\n        ]\n\n\nclass ChoresKidSelect(KidsChoresSelectBase):\n    \"\"\"Select entity listing only the chores assigned to a specific kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chores_kid_select\"\n\n    def __init__(\n        self, coordinator: KidsChoresDataCoordinator, entry: ConfigEntry, kid_id: str\n    ):\n        \"\"\"Initialize the ChoresKidSelect.\"\"\"\n        super().__init__(coordinator, entry)\n        self._kid_id = kid_id\n        kid_name = coordinator.kids_data.get(kid_id, {}).get(\"name\", f\"Kid {kid_id}\")\n        self._attr_unique_id = f\"{entry.entry_id}_chores_select_{kid_id}\"\n        self._attr_name = f\"KidsChores: Chores for {kid_name}\"\n        self.entity_id = f\"select.kc_{kid_name}_chore_list\"\n\n    @property\n    def options(self) -> list[str]:\n        \"\"\"Return a list of chore names assigned to this kid, with a 'None' option.\"\"\"\n        # Start with a \"None\" entry\n        options = [\"None\"]\n        for chore_id, chore in self.coordinator.chores_data.items():\n            if self._kid_id in chore.get(\"assigned_kids\", []):\n                options.append(chore.get(\"name\", f\"Chore {chore_id}\"))\n        return options\n\n\nclass BonusesSelect(KidsChoresSelectBase):\n    \"\"\"Global select entity listing all defined bonuses by name.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"bonuses_select\"\n\n    def __init__(self, coordinator: KidsChoresDataCoordinator, entry: ConfigEntry):\n        \"\"\"Initialize the Bonuses select entity.\"\"\"\n        super().__init__(coordinator, entry)\n        self._attr_unique_id = f\"{entry.entry_id}_bonuses_select\"\n        self._attr_name = \"KidsChores: All Bonuses\"\n        self.entity_id = f\"select.kc_all_bonuses\"\n\n    @property\n    def options(self) -> list[str]:\n        \"\"\"Return a list of bonus names from the coordinator.\n\n        If no bonuses exist, returns an empty list.\n        \"\"\"\n        return [\n            bonus_info.get(\"name\", f\"Bonus {bonus_id}\")\n            for bonus_id, bonus_info in self.coordinator.bonuses_data.items()\n        ]\n"
  },
  {
    "path": "custom_components/kidschores/sensor.py",
    "content": "# File: sensor.py\n\"\"\"Sensors for the KidsChores integration.\n\nThis file defines all sensor entities for each Kid, Chore, Reward, and Badge.\n\nAvailable Sensors:\n01. KidPointsSensor .................... Kid's current total points\n02. KidPointsEarnedDailySensor ......... Points earned by the kid today\n03. KidPointsEarnedWeeklySensor ........ Points earned by the kid this week\n04. KidPointsEarnedMonthlySensor ....... Points earned by the kid this month\n05. KidMaxPointsEverSensor ............. The highest points total the kid has ever reached\n06. CompletedChoresDailySensor ......... Chores completed by the kid today\n07. CompletedChoresWeeklySensor ........ Chores completed by the kid this week\n08. CompletedChoresMonthlySensor ....... Chores completed by the kid this month\n09. CompletedChoresTotalSensor ......... Total chores completed by the kid\n10.* KidBadgesSensor .................... Number of badges the kid currently has - DEPRECATE\n11. KidHighestBadgeSensor .............. The highest (threshold) badge the kid holds\n12. BadgeSensor ........................ One sensor per badge, showing its threshold & who earned it\n13. ChoreStatusSensor .................. Shows current state (pending/claimed/approved, etc.) for each (kid, chore)\n14. SharedChoreGlobalStateSensor ....... Shows current global state for shared chores\n15. RewardStatusSensor ................. Shows current state (not claimed/claimed/approved) for each (kid, reward)\n16. PenaltyAppliesSensor ............... Tracks how many times each penalty was applied for each kid\n17.* RewardClaimsSensor ................. Number of times a reward was claimed by a kid - DEPRECATE\n18.* RewardApprovalsSensor .............. Number of times a reward was approved for a kid - DEPRECATE\n19.* ChoreClaimsSensor .................. Number of times a chore was claimed by a kid - DEPRECATE\n20.* ChoreApprovalsSensor ............... Number of times a chore was approved for a kid - DEPRECATE\n21. PendingChoreApprovalsSensor ........ Lists chores that are awaiting approval\n22. PendingRewardApprovalsSensor ....... Lists rewards that are awaiting approval\n23. AchievementSensor .................. Shows the achievement name, target value, reward points, and number of kids that have earned it\n24. ChallengeSensor .................... Shows the challenge name, target, reward, and number of kids that have completed it\n25. AchievementProgressSensor .......... Progress (in %) toward an achievement per kid\n26. ChallengeProgressSensor ............ Progress (in %) toward a challenge per kid\n27. KidHighestStreakSensor ............. The highest current streak (in days) among streak-type achievements for a kid\n28.* ChoreStreakSensor .................. Current streak (in days) for a kid for a specific chore - DEPRECATE\n\"\"\"\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import PERCENTAGE, UnitOfTime\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.components.sensor import SensorEntity\nfrom homeassistant.exceptions import HomeAssistantError\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\nfrom homeassistant.util import dt as dt_util\n\nfrom .const import (\n    ACHIEVEMENT_TYPE_DAILY_MIN,\n    ACHIEVEMENT_TYPE_STREAK,\n    ACHIEVEMENT_TYPE_TOTAL,\n    ATTR_ACHIEVEMENT_NAME,\n    ATTR_ALL_EARNED_BADGES,\n    ATTR_ALLOW_MULTIPLE_CLAIMS_PER_DAY,\n    ATTR_APPLICABLE_DAYS,\n    ATTR_ASSIGNED_KIDS,\n    ATTR_ASSOCIATED_CHORE,\n    ATTR_AWARDED,\n    ATTR_BADGES,\n    ATTR_CHALLENGE_NAME,\n    ATTR_CHALLENGE_TYPE,\n    ATTR_CLAIMED_ON,\n    ATTR_CHORE_APPROVALS_COUNT,\n    ATTR_CHORE_APPROVALS_TODAY,\n    ATTR_CHORE_CLAIMS_COUNT,\n    ATTR_CHORE_CURRENT_STREAK,\n    ATTR_CHORE_HIGHEST_STREAK,\n    ATTR_CHORE_NAME,\n    ATTR_COST,\n    ATTR_CRITERIA,\n    ATTR_CUSTOM_FREQUENCY_INTERVAL,\n    ATTR_CUSTOM_FREQUENCY_UNIT,\n    ATTR_DEFAULT_POINTS,\n    ATTR_DESCRIPTION,\n    ATTR_DUE_DATE,\n    ATTR_END_DATE,\n    ATTR_HIGHEST_BADGE_THRESHOLD_VALUE,\n    ATTR_GLOBAL_STATE,\n    ATTR_KID_NAME,\n    ATTR_KIDS_EARNED,\n    ATTR_LABELS,\n    ATTR_LAST_DATE,\n    ATTR_PARTIAL_ALLOWED,\n    ATTR_PENALTY_NAME,\n    ATTR_PENALTY_POINTS,\n    ATTR_POINTS_MULTIPLIER,\n    ATTR_POINTS_TO_NEXT_BADGE,\n    ATTR_RECURRING_FREQUENCY,\n    ATTR_RAW_PROGRESS,\n    ATTR_RAW_STREAK,\n    ATTR_REDEEMED_ON,\n    ATTR_REWARD_APPROVALS_COUNT,\n    ATTR_REWARD_CLAIMS_COUNT,\n    ATTR_REWARD_NAME,\n    ATTR_REWARD_POINTS,\n    ATTR_START_DATE,\n    ATTR_SHARED_CHORE,\n    ATTR_BONUS_NAME,\n    ATTR_BONUS_POINTS,\n    ATTR_TARGET_VALUE,\n    ATTR_THRESHOLD_TYPE,\n    ATTR_TYPE,\n    CHALLENGE_TYPE_DAILY_MIN,\n    CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW,\n    CHORE_STATE_APPROVED,\n    CHORE_STATE_CLAIMED,\n    CHORE_STATE_OVERDUE,\n    CHORE_STATE_PENDING,\n    CHORE_STATE_UNKNOWN,\n    CONF_POINTS_ICON,\n    CONF_POINTS_LABEL,\n    DATA_PENDING_CHORE_APPROVALS,\n    DATA_PENDING_REWARD_APPROVALS,\n    DEFAULT_ACHIEVEMENTS_ICON,\n    DEFAULT_BADGE_ICON,\n    DEFAULT_CHALLENGES_ICON,\n    DEFAULT_CHORE_SENSOR_ICON,\n    DEFAULT_PENALTY_ICON,\n    DEFAULT_PENALTY_POINTS,\n    DEFAULT_POINTS_ICON,\n    DEFAULT_POINTS_LABEL,\n    DEFAULT_REWARD_COST,\n    DEFAULT_REWARD_ICON,\n    DEFAULT_BONUS_ICON,\n    DEFAULT_BONUS_POINTS,\n    DEFAULT_STREAK_ICON,\n    DEFAULT_TROPHY_ICON,\n    DEFAULT_TROPHY_OUTLINE,\n    DOMAIN,\n    DUE_DATE_NOT_SET,\n    FREQUENCY_CUSTOM,\n    LABEL_POINTS,\n    REWARD_STATE_APPROVED,\n    REWARD_STATE_CLAIMED,\n    REWARD_STATE_NOT_CLAIMED,\n    UNKNOWN_CHORE,\n    UNKNOWN_KID,\n    UNKNOWN_REWARD,\n)\nfrom .coordinator import KidsChoresDataCoordinator\nfrom .kc_helpers import get_friendly_label\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant, entry: ConfigEntry, async_add_entities\n):\n    \"\"\"Set up sensors for KidsChores integration.\"\"\"\n    data = hass.data[DOMAIN][entry.entry_id]\n    coordinator: KidsChoresDataCoordinator = data[\"coordinator\"]\n\n    points_label = entry.options.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL)\n    points_icon = entry.options.get(CONF_POINTS_ICON, DEFAULT_POINTS_ICON)\n    entities = []\n\n    # Sensor to detail number of Chores pending approval\n    entities.append(PendingChoreApprovalsSensor(coordinator, entry))\n\n    # Sensor to detail number of Rewards pending approval\n    entities.append(PendingRewardApprovalsSensor(coordinator, entry))\n\n    # For each kid, add standard sensors\n    for kid_id, kid_info in coordinator.kids_data.items():\n        kid_name = kid_info.get(\"name\", f\"Kid {kid_id}\")\n\n        # Points counter sensor\n        entities.append(\n            KidPointsSensor(\n                coordinator, entry, kid_id, kid_name, points_label, points_icon\n            )\n        )\n        entities.append(\n            CompletedChoresTotalSensor(coordinator, entry, kid_id, kid_name)\n        )\n\n        # Chores completed by each Kid during the day\n        entities.append(\n            CompletedChoresDailySensor(coordinator, entry, kid_id, kid_name)\n        )\n\n        # Chores completed by each Kid during the week\n        entities.append(\n            CompletedChoresWeeklySensor(coordinator, entry, kid_id, kid_name)\n        )\n\n        # Chores completed by each Kid during the month\n        entities.append(\n            CompletedChoresMonthlySensor(coordinator, entry, kid_id, kid_name)\n        )\n\n        # Badges Obtained by each Kid\n        entities.append(KidBadgesSensor(coordinator, entry, kid_id, kid_name))\n\n        # Kid Highest Badge\n        entities.append(KidHighestBadgeSensor(coordinator, entry, kid_id, kid_name))\n\n        # Poimts obtained per Kid during the day\n        entities.append(\n            KidPointsEarnedDailySensor(\n                coordinator, entry, kid_id, kid_name, points_label, points_icon\n            )\n        )\n\n        # Poimts obtained per Kid during the week\n        entities.append(\n            KidPointsEarnedWeeklySensor(\n                coordinator, entry, kid_id, kid_name, points_label, points_icon\n            )\n        )\n\n        # Poimts obtained per Kid during the month\n        entities.append(\n            KidPointsEarnedMonthlySensor(\n                coordinator, entry, kid_id, kid_name, points_label, points_icon\n            )\n        )\n\n        # Maximum Points ever obtained ny a kid\n        entities.append(\n            KidMaxPointsEverSensor(\n                coordinator, entry, kid_id, kid_name, points_label, points_icon\n            )\n        )\n\n        # Reward Claims and Approvals\n        for reward_id, reward_info in coordinator.rewards_data.items():\n            reward_name = reward_info.get(\"name\", f\"Reward {reward_id}\")\n            entities.append(\n                RewardClaimsSensor(\n                    coordinator, entry, kid_id, kid_name, reward_id, reward_name\n                )\n            )\n\n            # Rewards Approval Sensor\n            entities.append(\n                RewardApprovalsSensor(\n                    coordinator, entry, kid_id, kid_name, reward_id, reward_name\n                )\n            )\n\n        # Chore Claims and Approvals\n        for chore_id, chore_info in coordinator.chores_data.items():\n            if kid_id not in chore_info.get(\"assigned_kids\", []):\n                continue\n            chore_name = chore_info.get(\"name\", f\"Chore {chore_id}\")\n            entities.append(\n                ChoreClaimsSensor(\n                    coordinator, entry, kid_id, kid_name, chore_id, chore_name\n                )\n            )\n\n            # Chore Approvals Sensor\n            entities.append(\n                ChoreApprovalsSensor(\n                    coordinator, entry, kid_id, kid_name, chore_id, chore_name\n                )\n            )\n\n            # Chore Streak per Kid\n            entities.append(\n                ChoreStreakSensor(\n                    coordinator, entry, kid_id, kid_name, chore_id, chore_name\n                )\n            )\n\n        # Penalty Applies\n        for penalty_id, penalty_info in coordinator.penalties_data.items():\n            penalty_name = penalty_info.get(\"name\", f\"Penalty {penalty_id}\")\n            entities.append(\n                PenaltyAppliesSensor(\n                    coordinator, entry, kid_id, kid_name, penalty_id, penalty_name\n                )\n            )\n\n        # Bonus Applies\n        for bonus_id, bonus_info in coordinator.bonuses_data.items():\n            bonus_name = bonus_info.get(\"name\", f\"Bonus {bonus_id}\")\n            entities.append(\n                BonusAppliesSensor(\n                    coordinator, entry, kid_id, kid_name, bonus_id, bonus_name\n                )\n            )\n\n        # Achivement Progress per Kid\n        for achievement_id, achievement in coordinator.achievements_data.items():\n            if kid_id in achievement.get(\"assigned_kids\", []):\n                achievement_name = achievement.get(\n                    \"name\", f\"Achievement {achievement_id}\"\n                )\n                entities.append(\n                    AchievementProgressSensor(\n                        coordinator,\n                        entry,\n                        kid_id,\n                        kid_name,\n                        achievement_id,\n                        achievement_name,\n                    )\n                )\n\n        # Challenge Progress per Kid\n        for challenge_id, challenge in coordinator.challenges_data.items():\n            if kid_id in challenge.get(\"assigned_kids\", []):\n                challenge_name = challenge.get(\"name\", f\"Challenge {challenge_id}\")\n                entities.append(\n                    ChallengeProgressSensor(\n                        coordinator,\n                        entry,\n                        kid_id,\n                        kid_name,\n                        challenge_id,\n                        challenge_name,\n                    )\n                )\n\n        # Highest Streak Sensor per Kid\n        entities.append(KidHighestStreakSensor(coordinator, entry, kid_id, kid_name))\n\n    # For each chore assigned to each kid, add a ChoreStatusSensor\n    for chore_id, chore_info in coordinator.chores_data.items():\n        chore_name = chore_info.get(\"name\", f\"Chore {chore_id}\")\n        assigned_kids_ids = chore_info.get(\"assigned_kids\", [])\n        for kid_id in assigned_kids_ids:\n            kid_name = coordinator._get_kid_name_by_id(kid_id) or f\"Kid {kid_id}\"\n            entities.append(\n                ChoreStatusSensor(\n                    coordinator, entry, kid_id, kid_name, chore_id, chore_name\n                )\n            )\n\n    # For each shared chore, add a global state sensor\n    for chore_id, chore_info in coordinator.chores_data.items():\n        if chore_info.get(\"shared_chore\", False):\n            chore_name = chore_info.get(\"name\", f\"Chore {chore_id}\")\n            entities.append(\n                SharedChoreGlobalStateSensor(coordinator, entry, chore_id, chore_name)\n            )\n\n    # For each Reward, add a RewardStatusSensor\n    for reward_id, reward_info in coordinator.rewards_data.items():\n        reward_name = reward_info.get(\"name\", f\"Reward {reward_id}\")\n\n        # For each kid, create the reward status sensor\n        for kid_id, kid_info in coordinator.kids_data.items():\n            kid_name = kid_info.get(\"name\", f\"Kid {kid_id}\")\n            entities.append(\n                RewardStatusSensor(\n                    coordinator, entry, kid_id, kid_name, reward_id, reward_name\n                )\n            )\n\n    # For each Badge, add a BadgeSensor\n    for badge_id, badge_info in coordinator.badges_data.items():\n        badge_name = badge_info.get(\"name\", f\"Badge {badge_id}\")\n        entities.append(BadgeSensor(coordinator, entry, badge_id, badge_name))\n\n    # For each Achievement, add an AchievementSensor\n    for achievement_id, achievement in coordinator.achievements_data.items():\n        achievement_name = achievement.get(\"name\", f\"Achievement {achievement_id}\")\n        entities.append(\n            AchievementSensor(coordinator, entry, achievement_id, achievement_name)\n        )\n\n    # For each Challenge, add a ChallengeSensor\n    for challenge_id, challenge in coordinator.challenges_data.items():\n        challenge_name = challenge.get(\"name\", f\"Challenge {challenge_id}\")\n        entities.append(\n            ChallengeSensor(coordinator, entry, challenge_id, challenge_name)\n        )\n\n    async_add_entities(entities)\n\n\n# ------------------------------------------------------------------------------------------\nclass ChoreStatusSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor for chore status: pending/claimed/approved/etc.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chore_status_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, chore_id, chore_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._chore_id = chore_id\n        self._chore_name = chore_name\n        self._entry = entry\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{chore_id}_status\"\n        self.entity_id = f\"sensor.kc_{kid_name}_chore_status_{chore_name}\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"chore_name\": chore_name,\n        }\n\n    @property\n    def native_value(self):\n        \"\"\"Return the chore's state based on shared or individual tracking.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        # The status of the kids chore should always be their own status, it's only global status that would show independent or in-part\n        if self._chore_id in kid_info.get(\"approved_chores\", []):\n            return CHORE_STATE_APPROVED\n        elif self._chore_id in kid_info.get(\"claimed_chores\", []):\n            return CHORE_STATE_CLAIMED\n        elif self._chore_id in kid_info.get(\"overdue_chores\", []):\n            return CHORE_STATE_OVERDUE\n        else:\n            return CHORE_STATE_PENDING\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include points, description, etc.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        shared = chore_info.get(\"shared_chore\", False)\n        global_state = chore_info.get(\"state\", CHORE_STATE_UNKNOWN)\n\n        assigned_kids_ids = chore_info.get(\"assigned_kids\", [])\n        assigned_kids_names = [\n            self.coordinator._get_kid_name_by_id(k_id) or f\"Kid {k_id}\"\n            for k_id in assigned_kids_ids\n        ]\n\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        chore_streak_data = kid_info.get(\"chore_streaks\", {}).get(self._chore_id, {})\n        current_streak = chore_streak_data.get(\"current_streak\", 0)\n        highest_streak = chore_streak_data.get(\"max_streak\", 0)\n\n        stored_labels = chore_info.get(\"chore_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_KID_NAME: self._kid_name,\n            ATTR_CHORE_NAME: self._chore_name,\n            ATTR_DESCRIPTION: chore_info.get(\"description\", \"\"),\n            ATTR_CHORE_CLAIMS_COUNT: kid_info.get(\"chore_claims\", {}).get(\n                self._chore_id, 0\n            ),\n            ATTR_CHORE_APPROVALS_COUNT: kid_info.get(\"chore_approvals\", {}).get(\n                self._chore_id, 0\n            ),\n            ATTR_CHORE_CURRENT_STREAK: current_streak,\n            ATTR_CHORE_HIGHEST_STREAK: highest_streak,\n            ATTR_SHARED_CHORE: shared,\n            ATTR_GLOBAL_STATE: global_state,\n            ATTR_RECURRING_FREQUENCY: chore_info.get(\"recurring_frequency\", \"None\"),\n            ATTR_APPLICABLE_DAYS: chore_info.get(\"applicable_days\", []),\n            ATTR_DUE_DATE: chore_info.get(\"due_date\", DUE_DATE_NOT_SET),\n            ATTR_DEFAULT_POINTS: chore_info.get(\"default_points\", 0),\n            ATTR_PARTIAL_ALLOWED: chore_info.get(\"partial_allowed\", False),\n            ATTR_ALLOW_MULTIPLE_CLAIMS_PER_DAY: chore_info.get(\n                \"allow_multiple_claims_per_day\", False\n            ),\n            ATTR_ASSIGNED_KIDS: assigned_kids_names,\n            ATTR_LABELS: friendly_labels,\n        }\n\n        if chore_info.get(\"allow_multiple_claims_per_day\", False):\n            today_approvals = kid_info.get(\"today_chore_approvals\", {}).get(\n                self._chore_id, 0\n            )\n            attributes[ATTR_CHORE_APPROVALS_TODAY] = today_approvals\n\n        if chore_info.get(\"recurring_frequency\") == FREQUENCY_CUSTOM:\n            attributes[ATTR_CUSTOM_FREQUENCY_INTERVAL] = chore_info.get(\n                \"custom_interval\"\n            )\n            attributes[ATTR_CUSTOM_FREQUENCY_UNIT] = chore_info.get(\n                \"custom_interval_unit\"\n            )\n\n        return attributes\n\n    @property\n    def icon(self):\n        \"\"\"Use the chore's custom icon if set, else fallback.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        return chore_info.get(\"icon\", DEFAULT_CHORE_SENSOR_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass KidPointsSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor for a kid's total points balance.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kid_points_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, points_label, points_icon):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._points_label = points_label\n        self._points_icon = points_icon\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_points\"\n        self._attr_state_class = \"measurement\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"points\": self._points_label,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_points\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the kid's total points.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"points\", 0)\n\n    @property\n    def native_unit_of_measurement(self):\n        \"\"\"Return the points label.\"\"\"\n        return self._points_label or LABEL_POINTS\n\n    @property\n    def icon(self):\n        \"\"\"Use the points' custom icon if set, else fallback.\"\"\"\n        return self._points_icon or DEFAULT_POINTS_ICON\n\n\n# ------------------------------------------------------------------------------------------\nclass KidMaxPointsEverSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor showing the maximum points a kid has ever reached.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kid_max_points_ever_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, points_label, points_icon):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._points_label = points_label\n        self._points_icon = points_icon\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_max_points_ever\"\n        self._entry = entry\n        self._attr_translation_placeholders = {\"kid_name\": kid_name}\n        self.entity_id = f\"sensor.kc_{kid_name}_points_max_ever\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the highest points total the kid has ever reached.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"max_points_ever\", 0)\n\n    @property\n    def icon(self):\n        \"\"\"Use the same icon as points or any custom icon you prefer.\"\"\"\n        return self._points_icon or DEFAULT_POINTS_ICON\n\n    @property\n    def native_unit_of_measurement(self):\n        \"\"\"Optionally display the same points label for consistency.\"\"\"\n        return self._points_label or LABEL_POINTS\n\n\n# ------------------------------------------------------------------------------------------\nclass CompletedChoresTotalSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor tracking the total number of chores a kid has completed since integration start.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chores_completed_total_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_completed_total\"\n        self._attr_native_unit_of_measurement = \"chores\"\n        self._attr_icon = \"mdi:clipboard-check-outline\"\n        self._attr_translation_placeholders = {\"kid_name\": kid_name}\n        self.entity_id = f\"sensor.kc_{kid_name}_chores_completed_total\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the total number of chores completed by the kid.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"completed_chores_total\", 0)\n\n\n# ------------------------------------------------------------------------------------------\nclass CompletedChoresDailySensor(CoordinatorEntity, SensorEntity):\n    \"\"\"How many chores kid completed today.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chores_completed_daily_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_completed_daily\"\n        self._attr_native_unit_of_measurement = \"chores\"\n        self._attr_translation_placeholders = {\"kid_name\": kid_name}\n        self.entity_id = f\"sensor.kc_{kid_name}_chores_completed_daily\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of chores completed today.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"completed_chores_today\", 0)\n\n\n# ------------------------------------------------------------------------------------------\nclass CompletedChoresWeeklySensor(CoordinatorEntity, SensorEntity):\n    \"\"\"How many chores kid completed this week.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chores_completed_weekly_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_completed_weekly\"\n        self._attr_native_unit_of_measurement = \"chores\"\n        self._attr_translation_placeholders = {\"kid_name\": kid_name}\n        self.entity_id = f\"sensor.kc_{kid_name}_chores_completed_weekly\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of chores completed this week.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"completed_chores_weekly\", 0)\n\n\n# ------------------------------------------------------------------------------------------\nclass CompletedChoresMonthlySensor(CoordinatorEntity, SensorEntity):\n    \"\"\"How many chores kid completed this month.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chores_completed_monthly_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_completed_monthly\"\n        self._attr_native_unit_of_measurement = \"chores\"\n        self._attr_translation_placeholders = {\"kid_name\": kid_name}\n        self.entity_id = f\"sensor.kc_{kid_name}_chores_completed_monthly\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of chores completed this month.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"completed_chores_monthly\", 0)\n\n\n# DEPRECATE --------------------------------------------------------------------------------\nclass KidBadgesSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor: number of badges earned + attribute with the list.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kid_badges_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_badges\"\n        self._attr_translation_placeholders = {\"kid_name\": kid_name}\n        self.entity_id = f\"sensor.kc_{kid_name}_badges\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of badges the kid has earned.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return len(kid_info.get(\"badges\", []))\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Include the list of badges the kid has earned.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return {ATTR_BADGES: kid_info.get(\"badges\", [])}\n\n\n# ------------------------------------------------------------------------------------------\nclass KidHighestBadgeSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor that returns the \"highest\" badge the kid currently has.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kids_highest_badge_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_highest_badge\"\n        self._attr_translation_placeholders = {\"kid_name\": kid_name}\n        self.entity_id = f\"sensor.kc_{kid_name}_highest_badge\"\n\n    def _find_highest_badge(self):\n        \"\"\"Determine which badge has the highest ranking.\"\"\"\n\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        earned_badge_names = kid_info.get(\"badges\", [])\n\n        highest_badge = None\n        highest_value = -1\n\n        for badge_name in earned_badge_names:\n            # Find badge by name\n            badge_data = next(\n                (\n                    info\n                    for bid, info in self.coordinator.badges_data.items()\n                    if info.get(\"name\") == badge_name\n                ),\n                None,\n            )\n            if not badge_data:\n                continue  # skip if not found or invalid\n\n            threshold_val = badge_data.get(\"threshold_value\", 0)\n            if threshold_val > highest_value:\n                highest_value = threshold_val\n                highest_badge = badge_name\n\n        return highest_badge, highest_value\n\n    @property\n    def native_value(self) -> str:\n        \"\"\"Return the badge name of the highest-threshold badge the kid has earned.\n\n        If the kid has none, return \"None\".\n        \"\"\"\n        highest_badge, _ = self._find_highest_badge()\n        return highest_badge if highest_badge else \"None\"\n\n    @property\n    def icon(self):\n        \"\"\"Return the icon for the highest badge. Fall back if none found.\"\"\"\n        highest_badge, _ = self._find_highest_badge()\n        if highest_badge:\n            badge_data = next(\n                (\n                    info\n                    for bid, info in self.coordinator.badges_data.items()\n                    if info.get(\"name\") == highest_badge\n                ),\n                {},\n            )\n            return badge_data.get(\"icon\", DEFAULT_TROPHY_ICON)\n        return DEFAULT_TROPHY_OUTLINE\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Provide additional details.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        highest_badge, highest_val = self._find_highest_badge()\n\n        current_multiplier = 1.0\n        friendly_labels = []\n\n        if highest_badge:\n            badge_data = next(\n                (\n                    info\n                    for bid, info in self.coordinator.badges_data.items()\n                    if info.get(\"name\") == highest_badge\n                ),\n                {},\n            )\n            current_multiplier = badge_data.get(\"points_multiplier\", 1.0)\n            stored_labels = badge_data.get(\"badge_labels\", [])\n            friendly_labels = [\n                get_friendly_label(self.hass, label) for label in stored_labels\n            ]\n\n        # Compute points needed for next badge:\n        current_points = kid_info.get(\"points\", 0)\n        # Gather thresholds for badges that are higher than current points\n        thresholds = [\n            badge.get(\"threshold_value\", 0)\n            for badge in self.coordinator.badges_data.values()\n            if badge.get(\"threshold_value\", 0) > current_points\n        ]\n        if thresholds:\n            next_threshold = min(thresholds)\n            points_to_next_badge = next_threshold - current_points\n        else:\n            points_to_next_badge = 0\n\n        return {\n            ATTR_KID_NAME: self._kid_name,\n            ATTR_ALL_EARNED_BADGES: kid_info.get(\"badges\", []),\n            ATTR_HIGHEST_BADGE_THRESHOLD_VALUE: highest_val if highest_badge else 0,\n            ATTR_POINTS_MULTIPLIER: current_multiplier,\n            ATTR_POINTS_TO_NEXT_BADGE: points_to_next_badge,\n            ATTR_LABELS: friendly_labels,\n        }\n\n\n# ------------------------------------------------------------------------------------------\nclass BadgeSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor representing a single badge in KidsChores.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"badge_sensor\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        badge_id: str,\n        badge_name: str,\n    ):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._entry = entry\n        self._badge_id = badge_id\n        self._badge_name = badge_name\n        self._attr_unique_id = f\"{entry.entry_id}_{badge_id}_badge_sensor\"\n        self._attr_translation_placeholders = {\"badge_name\": badge_name}\n        self.entity_id = f\"sensor.kc_{badge_name}_badge\"\n\n    @property\n    def native_value(self) -> float:\n        \"\"\"The sensor state is the threshold_value for the badge.\"\"\"\n        badge_info = self.coordinator.badges_data.get(self._badge_id, {})\n        return badge_info.get(\"threshold_value\", 0)\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Provide additional badge data, including which kids currently have it.\"\"\"\n        badge_info = self.coordinator.badges_data.get(self._badge_id, {})\n        threshold_type = badge_info.get(\"threshold_type\", \"points\")\n        points_multiplier = badge_info.get(\"points_multiplier\", 1.0)\n        description = badge_info.get(\"description\", \"\")\n\n        kids_earned_ids = badge_info.get(\"earned_by\", [])\n\n        stored_labels = badge_info.get(\"badge_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        # Convert each kid_id to kid_name\n        kids_earned_names = []\n        for kid_id in kids_earned_ids:\n            kid = self.coordinator.kids_data.get(kid_id)\n            if kid is not None:\n                kids_earned_names.append(kid.get(\"name\", f\"Kid {kid_id}\"))\n            else:\n                kids_earned_names.append(f\"Kid {kid_id} (not found)\")\n\n        return {\n            ATTR_DESCRIPTION: description,\n            ATTR_THRESHOLD_TYPE: threshold_type,\n            ATTR_POINTS_MULTIPLIER: points_multiplier,\n            ATTR_KIDS_EARNED: kids_earned_names,\n            ATTR_LABELS: friendly_labels,\n        }\n\n    @property\n    def icon(self) -> str:\n        \"\"\"Return the badge's custom icon if set, else default.\"\"\"\n        badge_info = self.coordinator.badges_data.get(self._badge_id, {})\n        return badge_info.get(\"icon\", DEFAULT_BADGE_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass PendingChoreApprovalsSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor listing all pending chore approvals.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"pending_chores_approvals_sensor\"\n\n    def __init__(self, coordinator, entry):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._attr_unique_id = f\"{entry.entry_id}_pending_chore_approvals\"\n        self._attr_icon = \"mdi:clipboard-check-outline\"\n        self.entity_id = f\"sensor.kc_global_chore_pending_approvals\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return a summary of pending chore approvals.\"\"\"\n        approvals = self.coordinator._data.get(DATA_PENDING_CHORE_APPROVALS, [])\n        return f\"{len(approvals)} pending chores\"\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return detailed pending chores.\"\"\"\n        approvals = self.coordinator._data.get(DATA_PENDING_CHORE_APPROVALS, [])\n        grouped_by_kid = {}\n\n        for approval in approvals:\n            kid_name = (\n                self.coordinator._get_kid_name_by_id(approval[\"kid_id\"]) or UNKNOWN_KID\n            )\n            chore_info = self.coordinator.chores_data.get(approval[\"chore_id\"], {})\n            chore_name = chore_info.get(\"name\", UNKNOWN_CHORE)\n\n            timestamp = approval[\"timestamp\"]\n\n            if kid_name not in grouped_by_kid:\n                grouped_by_kid[kid_name] = []\n\n            grouped_by_kid[kid_name].append(\n                {\n                    ATTR_CHORE_NAME: chore_name,\n                    ATTR_CLAIMED_ON: timestamp,\n                }\n            )\n\n        return grouped_by_kid\n\n\n# ------------------------------------------------------------------------------------------\nclass PendingRewardApprovalsSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor listing all pending reward approvals.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"pending_rewards_approvals_sensor\"\n\n    def __init__(self, coordinator, entry):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._attr_unique_id = f\"{entry.entry_id}_pending_reward_approvals\"\n        self._attr_icon = \"mdi:gift-open-outline\"\n        self.entity_id = f\"sensor.kc_global_reward_pending_approvals\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return a summary of pending reward approvals.\"\"\"\n        approvals = self.coordinator._data.get(DATA_PENDING_REWARD_APPROVALS, [])\n        return f\"{len(approvals)} pending rewards\"\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return detailed pending rewards.\"\"\"\n        approvals = self.coordinator._data.get(DATA_PENDING_REWARD_APPROVALS, [])\n        grouped_by_kid = {}\n\n        for approval in approvals:\n            kid_name = (\n                self.coordinator._get_kid_name_by_id(approval[\"kid_id\"]) or UNKNOWN_KID\n            )\n            reward_info = self.coordinator.rewards_data.get(approval[\"reward_id\"], {})\n            reward_name = reward_info.get(\"name\", UNKNOWN_REWARD)\n\n            timestamp = approval[\"timestamp\"]\n\n            if kid_name not in grouped_by_kid:\n                grouped_by_kid[kid_name] = []\n\n            grouped_by_kid[kid_name].append(\n                {\n                    ATTR_REWARD_NAME: reward_name,\n                    ATTR_REDEEMED_ON: timestamp,\n                }\n            )\n\n        return grouped_by_kid\n\n\n# DEPRECATE --------------------------------------------------------------------------------\nclass RewardClaimsSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor tracking how many times each reward has been claimed by a kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"reward_claims_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, reward_id, reward_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._reward_id = reward_id\n        self._reward_name = reward_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{reward_id}_reward_claims\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"reward_name\": reward_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_reward_claims_{reward_name}\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of times the reward has been claimed.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"reward_claims\", {}).get(self._reward_id, 0)\n\n    @property\n    def icon(self):\n        \"\"\"Return the chore's custom icon if set, else fallback.\"\"\"\n        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})\n        return reward_info.get(\"icon\", DEFAULT_REWARD_ICON)\n\n\n# DEPRECATE --------------------------------------------------------------------------------\nclass RewardApprovalsSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor tracking how many times each reward has been approved for a kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"reward_approvals_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, reward_id, reward_name):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._reward_id = reward_id\n        self._reward_name = reward_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{reward_id}_reward_approvals\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"reward_name\": reward_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_reward_approvals_{reward_name}\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of times the reward has been approved.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"reward_approvals\", {}).get(self._reward_id, 0)\n\n    @property\n    def icon(self):\n        \"\"\"Return the chore's custom icon if set, else fallback.\"\"\"\n        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})\n        return reward_info.get(\"icon\", DEFAULT_REWARD_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass SharedChoreGlobalStateSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor that shows the global state of a shared chore.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"shared_chore_global_status_sensor\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        chore_id: str,\n        chore_name: str,\n    ):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._chore_id = chore_id\n        self._chore_name = chore_name\n        self._attr_unique_id = f\"{entry.entry_id}_{chore_id}_global_state\"\n        self._attr_translation_placeholders = {\n            \"chore_name\": chore_name,\n        }\n        self.entity_id = f\"sensor.kc_global_chore_status_{chore_name}\"\n\n    @property\n    def native_value(self) -> str:\n        \"\"\"Return the global state for the chore.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        return chore_info.get(\"state\", CHORE_STATE_UNKNOWN)\n\n    @property\n    def extra_state_attributes(self) -> dict:\n        \"\"\"Return additional attributes for the chore.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        assigned_kids_ids = chore_info.get(\"assigned_kids\", [])\n        assigned_kids_names = [\n            self.coordinator._get_kid_name_by_id(k_id) or f\"Kid {k_id}\"\n            for k_id in assigned_kids_ids\n        ]\n\n        stored_labels = chore_info.get(\"chore_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        total_approvals_today = 0\n        for kid_id in assigned_kids_ids:\n            kid_data = self.coordinator.kids_data.get(kid_id, {})\n            total_approvals_today += kid_data.get(\"today_chore_approvals\", {}).get(\n                self._chore_id, 0\n            )\n\n        attributes = {\n            ATTR_CHORE_NAME: self._chore_name,\n            ATTR_DESCRIPTION: chore_info.get(\"description\", \"\"),\n            ATTR_RECURRING_FREQUENCY: chore_info.get(\"recurring_frequency\", \"None\"),\n            ATTR_APPLICABLE_DAYS: chore_info.get(\"applicable_days\", []),\n            ATTR_DUE_DATE: chore_info.get(\"due_date\", \"Not set\"),\n            ATTR_DEFAULT_POINTS: chore_info.get(\"default_points\", 0),\n            ATTR_PARTIAL_ALLOWED: chore_info.get(\"partial_allowed\", False),\n            ATTR_ALLOW_MULTIPLE_CLAIMS_PER_DAY: chore_info.get(\n                \"allow_multiple_claims_per_day\", False\n            ),\n            ATTR_CHORE_APPROVALS_TODAY: total_approvals_today,\n            ATTR_ASSIGNED_KIDS: assigned_kids_names,\n            ATTR_LABELS: friendly_labels,\n        }\n\n        if chore_info.get(\"recurring_frequency\") == FREQUENCY_CUSTOM:\n            attributes[ATTR_CUSTOM_FREQUENCY_INTERVAL] = chore_info.get(\n                \"custom_interval\"\n            )\n            attributes[ATTR_CUSTOM_FREQUENCY_UNIT] = chore_info.get(\n                \"custom_interval_unit\"\n            )\n\n        return attributes\n\n    @property\n    def icon(self) -> str:\n        \"\"\"Return the icon for the chore sensor.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        return chore_info.get(\"icon\", DEFAULT_CHORE_SENSOR_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass RewardStatusSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Shows the status of a reward for a particular kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"reward_status_sensor\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        reward_id: str,\n        reward_name: str,\n    ):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._reward_id = reward_id\n        self._reward_name = reward_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{reward_id}_reward_status\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"reward_name\": reward_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_reward_status_{reward_name}\"\n\n    @property\n    def native_value(self) -> str:\n        \"\"\"Return the current reward status: 'Not Claimed', 'Claimed', or 'Approved'.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        if self._reward_id in kid_info.get(\"pending_rewards\", []):\n            return REWARD_STATE_CLAIMED\n        if self._reward_id in kid_info.get(\"redeemed_rewards\", []):\n            return REWARD_STATE_APPROVED\n        return REWARD_STATE_NOT_CLAIMED\n\n    @property\n    def extra_state_attributes(self) -> dict:\n        \"\"\"Provide extra attributes about the reward.\"\"\"\n        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n\n        stored_labels = reward_info.get(\"reward_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        attributes = {\n            ATTR_KID_NAME: self._kid_name,\n            ATTR_REWARD_NAME: self._reward_name,\n            ATTR_DESCRIPTION: reward_info.get(\"description\", \"\"),\n            ATTR_COST: reward_info.get(\"cost\", DEFAULT_REWARD_COST),\n            ATTR_REWARD_CLAIMS_COUNT: kid_info.get(\"reward_claims\", {}).get(\n                self._reward_id, 0\n            ),\n            ATTR_REWARD_APPROVALS_COUNT: kid_info.get(\"reward_approvals\", {}).get(\n                self._reward_id, 0\n            ),\n            ATTR_LABELS: friendly_labels,\n        }\n\n        return attributes\n\n    @property\n    def icon(self) -> str:\n        \"\"\"Use the reward's custom icon if set, else fallback.\"\"\"\n        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})\n        return reward_info.get(\"icon\", DEFAULT_REWARD_ICON)\n\n\n# DEPRECATE --------------------------------------------------------------------------------\nclass ChoreClaimsSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor tracking how many times each chore has been claimed by a kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chore_claims_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, chore_id, chore_name):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._chore_id = chore_id\n        self._chore_name = chore_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{chore_id}_chore_claims\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"chore_name\": chore_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_chore_claims_{chore_name}\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of times the chore has been claimed.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"chore_claims\", {}).get(self._chore_id, 0)\n\n    @property\n    def icon(self):\n        \"\"\"Return the chore's custom icon if set, else fallback.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        return chore_info.get(\"icon\", DEFAULT_CHORE_SENSOR_ICON)\n\n\n# DEPRECATE --------------------------------------------------------------------------------\nclass ChoreApprovalsSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor tracking how many times each chore has been approved for a kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chore_approvals_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, chore_id, chore_name):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._chore_id = chore_id\n        self._chore_name = chore_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{chore_id}_chore_approvals\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"chore_name\": chore_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_chore_approvals_{chore_name}\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of times the chore has been approved.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"chore_approvals\", {}).get(self._chore_id, 0)\n\n    @property\n    def icon(self):\n        \"\"\"Return the chore's custom icon if set, else fallback.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        return chore_info.get(\"icon\", DEFAULT_CHORE_SENSOR_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass PenaltyAppliesSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor tracking how many times each penalty has been applied to a kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"penalty_applies_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, penalty_id, penalty_name):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._penalty_id = penalty_id\n        self._penalty_name = penalty_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{penalty_id}_penalty_applies\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"penalty_name\": penalty_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_penalties_applied_{penalty_name}\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of times the penalty has been applied.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"penalty_applies\", {}).get(self._penalty_id, 0)\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Expose additional details like penalty points and description.\"\"\"\n        penalty_info = self.coordinator.penalties_data.get(self._penalty_id, {})\n\n        stored_labels = penalty_info.get(\"penalty_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        return {\n            ATTR_KID_NAME: self._kid_name,\n            ATTR_PENALTY_NAME: self._penalty_name,\n            ATTR_DESCRIPTION: penalty_info.get(\"description\", \"\"),\n            ATTR_PENALTY_POINTS: penalty_info.get(\"points\", DEFAULT_PENALTY_POINTS),\n            ATTR_LABELS: friendly_labels,\n        }\n\n    @property\n    def icon(self):\n        \"\"\"Return the chore's custom icon if set, else fallback.\"\"\"\n        penalty_info = self.coordinator.penalties_data.get(self._penalty_id, {})\n        return penalty_info.get(\"icon\", DEFAULT_PENALTY_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass KidPointsEarnedDailySensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor for how many net points a kid earned today.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kid_points_earned_daily_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, points_label, points_icon):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._points_label = points_label\n        self._points_icon = points_icon\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_points_earned_daily\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_points_earned_daily\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return how many net points the kid has earned so far today.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"points_earned_today\", 0)\n\n    @property\n    def native_unit_of_measurement(self):\n        \"\"\"Return the points label.\"\"\"\n        return self._points_label or LABEL_POINTS\n\n    @property\n    def icon(self):\n        \"\"\"Use the points' custom icon if set, else fallback.\"\"\"\n        return self._points_icon or DEFAULT_POINTS_ICON\n\n\n# ------------------------------------------------------------------------------------------\nclass KidPointsEarnedWeeklySensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor for how many net points a kid earned this week.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kid_points_earned_weekly_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, points_label, points_icon):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._points_label = points_label\n        self._points_icon = points_icon\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_points_earned_weekly\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_points_earned_weekly\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return how many net points the kid has earned this week.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"points_earned_weekly\", 0)\n\n    @property\n    def native_unit_of_measurement(self):\n        \"\"\"Return the points label.\"\"\"\n        return self._points_label or LABEL_POINTS\n\n    @property\n    def icon(self):\n        \"\"\"Use the points' custom icon if set, else fallback.\"\"\"\n        return self._points_icon or DEFAULT_POINTS_ICON\n\n\n# ------------------------------------------------------------------------------------------\nclass KidPointsEarnedMonthlySensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor for how many net points a kid earned this month.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kid_points_earned_monthly_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, points_label, points_icon):\n        \"\"\"Initialize the sensor.\"\"\"\n\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._points_label = points_label\n        self._points_icon = points_icon\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_points_earned_monthly\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_points_earned_monthly\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return how many net points the kid has earned this month.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"points_earned_monthly\", 0)\n\n    @property\n    def native_unit_of_measurement(self):\n        \"\"\"Return the points label.\"\"\"\n        return self._points_label or LABEL_POINTS\n\n    @property\n    def icon(self):\n        \"\"\"Use the points' custom icon if set, else fallback.\"\"\"\n        return self._points_icon or DEFAULT_POINTS_ICON\n\n\n# ------------------------------------------------------------------------------------------\nclass AchievementSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor representing an achievement.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"achievement_state_sensor\"\n\n    def __init__(self, coordinator, entry, achievement_id, achievement_name):\n        \"\"\"Initialize the AchievementSensor.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._achievement_id = achievement_id\n        self._achievement_name = achievement_name\n        self._attr_unique_id = f\"{entry.entry_id}_{achievement_id}_achievement\"\n        self._attr_native_unit_of_measurement = PERCENTAGE\n        self._attr_translation_placeholders = {\n            \"achievement_name\": achievement_name,\n        }\n        self.entity_id = f\"sensor.kc_achievement_status_{achievement_name}\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the overall progress percentage toward the achievement.\"\"\"\n\n        achievement = self.coordinator.achievements_data.get(self._achievement_id, {})\n        target = achievement.get(\"target_value\", 1)\n        assigned_kids = achievement.get(\"assigned_kids\", [])\n\n        if not assigned_kids:\n            return 0\n\n        ach_type = achievement.get(\"type\")\n        if ach_type == ACHIEVEMENT_TYPE_TOTAL:\n            total_current = 0\n            total_effective_target = 0\n\n            for kid_id in assigned_kids:\n                progress_data = achievement.get(\"progress\", {}).get(kid_id, {})\n                baseline = (\n                    progress_data.get(\"baseline\", 0)\n                    if isinstance(progress_data, dict)\n                    else 0\n                )\n                current_total = self.coordinator.kids_data.get(kid_id, {}).get(\n                    \"completed_chores_total\", 0\n                )\n                total_current += current_total\n                total_effective_target += baseline + target\n\n            percent = (\n                (total_current / total_effective_target * 100)\n                if total_effective_target > 0\n                else 0\n            )\n\n        elif ach_type == ACHIEVEMENT_TYPE_STREAK:\n            total_current = 0\n\n            for kid_id in assigned_kids:\n                progress_data = achievement.get(\"progress\", {}).get(kid_id, {})\n                total_current += (\n                    progress_data.get(\"current_streak\", 0)\n                    if isinstance(progress_data, dict)\n                    else 0\n                )\n\n            global_target = target * len(assigned_kids)\n\n            percent = (total_current / global_target * 100) if global_target > 0 else 0\n\n        elif ach_type == ACHIEVEMENT_TYPE_DAILY_MIN:\n            total_progress = 0\n\n            for kid_id in assigned_kids:\n                daily = self.coordinator.kids_data.get(kid_id, {}).get(\n                    \"completed_chores_today\", 0\n                )\n                kid_progress = (\n                    100\n                    if daily >= target\n                    else (daily / target * 100)\n                    if target > 0\n                    else 0\n                )\n                total_progress += kid_progress\n\n            percent = total_progress / len(assigned_kids)\n\n        else:\n            percent = 0\n\n        return min(100, round(percent, 1))\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return extra attributes for this achievement.\"\"\"\n        achievement = self.coordinator.achievements_data.get(self._achievement_id, {})\n        progress = achievement.get(\"progress\", {})\n        kids_progress = {}\n\n        earned_by = []\n        for kid_id, data in progress.items():\n            if data.get(\"awarded\", False):\n                kid_name = self.coordinator._get_kid_name_by_id(kid_id) or kid_id\n                earned_by.append(kid_name)\n\n        associated_chore = \"\"\n        selected_chore_id = achievement.get(\"selected_chore_id\")\n        if selected_chore_id:\n            associated_chore = self.coordinator.chores_data.get(\n                selected_chore_id, {}\n            ).get(\"name\", \"\")\n\n        assigned_kids_ids = achievement.get(\"assigned_kids\", [])\n        assigned_kids_names = [\n            self.coordinator._get_kid_name_by_id(k_id) or f\"Kid {k_id}\"\n            for k_id in assigned_kids_ids\n        ]\n        ach_type = achievement.get(\"type\")\n        for kid_id in assigned_kids_ids:\n            kid_name = self.coordinator._get_kid_name_by_id(kid_id) or kid_id\n            progress_data = achievement.get(\"progress\", {}).get(kid_id, {})\n            if ach_type == ACHIEVEMENT_TYPE_TOTAL:\n                kids_progress[kid_name] = progress_data.get(\"current_value\", 0)\n            elif ach_type == ACHIEVEMENT_TYPE_STREAK:\n                kids_progress[kid_name] = progress_data.get(\"current_streak\", 0)\n            elif achievement.get(\"type\") == ACHIEVEMENT_TYPE_DAILY_MIN:\n                kids_progress[kid_name] = self.coordinator.kids_data.get(\n                    kid_id, {}\n                ).get(\"completed_chores_today\", 0)\n            else:\n                kids_progress[kid_name] = 0\n\n        stored_labels = achievement.get(\"achievement_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        return {\n            ATTR_ACHIEVEMENT_NAME: self._achievement_name,\n            ATTR_DESCRIPTION: achievement.get(\"description\", \"\"),\n            ATTR_ASSIGNED_KIDS: assigned_kids_names,\n            ATTR_TYPE: ach_type,\n            ATTR_ASSOCIATED_CHORE: associated_chore,\n            ATTR_CRITERIA: achievement.get(\"criteria\", \"\"),\n            ATTR_TARGET_VALUE: achievement.get(\"target_value\"),\n            ATTR_REWARD_POINTS: achievement.get(\"reward_points\"),\n            ATTR_KIDS_EARNED: earned_by,\n            ATTR_LABELS: friendly_labels,\n        }\n\n    @property\n    def icon(self):\n        \"\"\"Return an icon; you could choose a trophy icon.\"\"\"\n        achievement_info = self.coordinator.achievements_data.get(\n            self._achievement_id, {}\n        )\n        return achievement_info.get(\"icon\", DEFAULT_ACHIEVEMENTS_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass ChallengeSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor representing a challenge.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"challenge_state_sensor\"\n\n    def __init__(self, coordinator, entry, challenge_id, challenge_name):\n        \"\"\"Initialize the ChallengeSensor.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._challenge_id = challenge_id\n        self._challenge_name = challenge_name\n        self._attr_unique_id = f\"{entry.entry_id}_{challenge_id}_challenge\"\n        self._attr_native_unit_of_measurement = PERCENTAGE\n        self._attr_translation_placeholders = {\n            \"challenge_name\": challenge_name,\n        }\n        self.entity_id = f\"sensor.kc_challenge_status_{challenge_name}\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the overall progress percentage toward the challenge.\"\"\"\n\n        challenge = self.coordinator.challenges_data.get(self._challenge_id, {})\n        target = challenge.get(\"target_value\", 1)\n        assigned_kids = challenge.get(\"assigned_kids\", [])\n\n        if not assigned_kids:\n            return 0\n\n        challenge_type = challenge.get(\"type\")\n        total_progress = 0\n\n        for kid_id in assigned_kids:\n            progress_data = challenge.get(\"progress\", {}).get(kid_id, {})\n\n            if challenge_type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n                total_progress += progress_data.get(\"count\", 0)\n\n            elif challenge_type == CHALLENGE_TYPE_DAILY_MIN:\n                if isinstance(progress_data, dict):\n                    daily_counts = progress_data.get(\"daily_counts\", {})\n                    total_progress += sum(daily_counts.values())\n\n                else:\n                    total_progress += 0\n\n            else:\n                total_progress += 0\n\n        global_target = target * len(assigned_kids)\n\n        percent = (total_progress / global_target * 100) if global_target > 0 else 0\n\n        return min(100, round(percent, 1))\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return extra attributes for this challenge.\"\"\"\n        challenge = self.coordinator.challenges_data.get(self._challenge_id, {})\n        progress = challenge.get(\"progress\", {})\n        kids_progress = {}\n        challenge_type = challenge.get(\"type\")\n\n        earned_by = []\n        for kid_id, data in progress.items():\n            if data.get(\"awarded\", False):\n                kid_name = self.coordinator._get_kid_name_by_id(kid_id) or kid_id\n                earned_by.append(kid_name)\n\n        associated_chore = \"\"\n        selected_chore_id = challenge.get(\"selected_chore_id\")\n        if selected_chore_id:\n            associated_chore = self.coordinator.chores_data.get(\n                selected_chore_id, {}\n            ).get(\"name\", \"\")\n\n        assigned_kids_ids = challenge.get(\"assigned_kids\", [])\n        assigned_kids_names = [\n            self.coordinator._get_kid_name_by_id(k_id) or f\"Kid {k_id}\"\n            for k_id in assigned_kids_ids\n        ]\n\n        for kid_id in assigned_kids_ids:\n            kid_name = self.coordinator._get_kid_name_by_id(kid_id) or kid_id\n            progress_data = challenge.get(\"progress\", {}).get(kid_id, {})\n            if challenge_type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n                kids_progress[kid_name] = progress_data.get(\"count\", 0)\n            elif challenge_type == CHALLENGE_TYPE_DAILY_MIN:\n                if isinstance(progress_data, dict):\n                    kids_progress[kid_name] = sum(\n                        progress_data.get(\"daily_counts\", {}).values()\n                    )\n                else:\n                    kids_progress[kid_name] = 0\n            else:\n                kids_progress[kid_name] = 0\n\n        stored_labels = challenge.get(\"challenge_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        return {\n            ATTR_CHALLENGE_NAME: self._challenge_name,\n            ATTR_DESCRIPTION: challenge.get(\"description\", \"\"),\n            ATTR_ASSIGNED_KIDS: assigned_kids_names,\n            ATTR_TYPE: challenge_type,\n            ATTR_ASSOCIATED_CHORE: associated_chore,\n            ATTR_CRITERIA: challenge.get(\"criteria\", \"\"),\n            ATTR_TARGET_VALUE: challenge.get(\"target_value\"),\n            ATTR_REWARD_POINTS: challenge.get(\"reward_points\"),\n            ATTR_START_DATE: challenge.get(\"start_date\"),\n            ATTR_END_DATE: challenge.get(\"end_date\"),\n            ATTR_KIDS_EARNED: earned_by,\n            ATTR_LABELS: friendly_labels,\n        }\n\n    @property\n    def icon(self):\n        \"\"\"Return an icon for challenges (you might want to choose one that fits your theme).\"\"\"\n        challenge_info = self.coordinator.challenges_data.get(self._challenge_id, {})\n        return challenge_info.get(\"icon\", DEFAULT_ACHIEVEMENTS_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass AchievementProgressSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor representing a kid's progress toward a specific achievement.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"achievement_progress_sensor\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        achievement_id: str,\n        achievement_name: str,\n    ):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._achievement_id = achievement_id\n        self._achievement_name = achievement_name\n        self._attr_unique_id = (\n            f\"{entry.entry_id}_{kid_id}_{achievement_id}_achievement_progress\"\n        )\n        self._attr_native_unit_of_measurement = PERCENTAGE\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"achievement_name\": achievement_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_achievement_status_{achievement_name}\"\n\n    @property\n    def native_value(self) -> float:\n        \"\"\"Return the progress percentage toward the achievement.\"\"\"\n        achievement = self.coordinator.achievements_data.get(self._achievement_id, {})\n        target = achievement.get(\"target_value\", 1)\n        ach_type = achievement.get(\"type\")\n\n        if ach_type == ACHIEVEMENT_TYPE_TOTAL:\n            progress_data = achievement.get(\"progress\", {}).get(self._kid_id, {})\n\n            baseline = (\n                progress_data.get(\"baseline\", 0)\n                if isinstance(progress_data, dict)\n                else 0\n            )\n\n            current_total = self.coordinator.kids_data.get(self._kid_id, {}).get(\n                \"completed_chores_total\", 0\n            )\n\n            effective_target = baseline + target\n\n            percent = (\n                (current_total / effective_target * 100) if effective_target > 0 else 0\n            )\n\n        elif ach_type == ACHIEVEMENT_TYPE_STREAK:\n            progress_data = achievement.get(\"progress\", {}).get(self._kid_id, {})\n\n            progress = (\n                progress_data.get(\"current_streak\", 0)\n                if isinstance(progress_data, dict)\n                else 0\n            )\n\n            percent = (progress / target * 100) if target > 0 else 0\n\n        elif ach_type == ACHIEVEMENT_TYPE_DAILY_MIN:\n            daily = self.coordinator.kids_data.get(self._kid_id, {}).get(\n                \"completed_chores_today\", 0\n            )\n\n            percent = (daily / target * 100) if target > 0 else 0\n\n        else:\n            percent = 0\n\n        return min(100, round(percent, 1))\n\n    @property\n    def extra_state_attributes(self) -> dict:\n        \"\"\"Return extra attributes for the achievement progress.\"\"\"\n        achievement = self.coordinator.achievements_data.get(self._achievement_id, {})\n        target = achievement.get(\"target_value\", 1)\n        progress_data = achievement.get(\"progress\", {}).get(self._kid_id, {})\n        awarded = (\n            progress_data.get(\"awarded\", False)\n            if isinstance(progress_data, dict)\n            else False\n        )\n\n        if achievement.get(\"type\") == ACHIEVEMENT_TYPE_TOTAL:\n            raw_progress = (\n                progress_data.get(\"current_value\", 0)\n                if isinstance(progress_data, dict)\n                else 0\n            )\n\n        elif achievement.get(\"type\") == ACHIEVEMENT_TYPE_STREAK:\n            raw_progress = (\n                progress_data.get(\"current_streak\", 0)\n                if isinstance(progress_data, dict)\n                else 0\n            )\n\n        elif achievement.get(\"type\") == ACHIEVEMENT_TYPE_DAILY_MIN:\n            raw_progress = self.coordinator.kids_data.get(self._kid_id, {}).get(\n                \"completed_chores_today\", 0\n            )\n\n        associated_chore = \"\"\n        selected_chore_id = achievement.get(\"selected_chore_id\")\n        if selected_chore_id:\n            associated_chore = self.coordinator.chores_data.get(\n                selected_chore_id, {}\n            ).get(\"name\", \"\")\n\n        assigned_kids_ids = achievement.get(\"assigned_kids\", [])\n        assigned_kids_names = [\n            self.coordinator._get_kid_name_by_id(k_id) or f\"Kid {k_id}\"\n            for k_id in assigned_kids_ids\n        ]\n\n        stored_labels = achievement.get(\"achievement_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        return {\n            ATTR_ACHIEVEMENT_NAME: self._achievement_name,\n            ATTR_DESCRIPTION: achievement.get(\"description\", \"\"),\n            ATTR_ASSIGNED_KIDS: assigned_kids_names,\n            ATTR_TYPE: achievement.get(\"type\"),\n            ATTR_ASSOCIATED_CHORE: associated_chore,\n            ATTR_CRITERIA: achievement.get(\"criteria\", \"\"),\n            ATTR_TARGET_VALUE: target,\n            ATTR_REWARD_POINTS: achievement.get(\"reward_points\"),\n            ATTR_RAW_PROGRESS: raw_progress,\n            ATTR_AWARDED: awarded,\n            ATTR_LABELS: friendly_labels,\n        }\n\n    @property\n    def icon(self) -> str:\n        \"\"\"Return the icon for the achievement.\n\n        Use the icon provided in the achievement data if set, else fallback to default.\n        \"\"\"\n        achievement = self.coordinator.achievements_data.get(self._achievement_id, {})\n        return achievement.get(\"icon\", DEFAULT_ACHIEVEMENTS_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass ChallengeProgressSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor representing a kid's progress toward a specific challenge.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"challenge_progress_sensor\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        challenge_id: str,\n        challenge_name: str,\n    ):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._challenge_id = challenge_id\n        self._challenge_name = challenge_name\n        self._attr_unique_id = (\n            f\"{entry.entry_id}_{kid_id}_{challenge_id}_challenge_progress\"\n        )\n        self._attr_native_unit_of_measurement = PERCENTAGE\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"challenge_name\": challenge_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_challenge_status_{challenge_name}\"\n\n    @property\n    def native_value(self) -> float:\n        \"\"\"Return the challenge progress percentage.\"\"\"\n        challenge = self.coordinator.challenges_data.get(self._challenge_id, {})\n        target = challenge.get(\"target_value\", 1)\n        challenge_type = challenge.get(\"type\")\n        progress_data = challenge.get(\"progress\", {}).get(self._kid_id)\n\n        if challenge_type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n            raw_progress = (\n                progress_data.get(\"count\", 0) if isinstance(progress_data, dict) else 0\n            )\n\n        elif challenge_type == CHALLENGE_TYPE_DAILY_MIN:\n            if isinstance(progress_data, dict):\n                daily_counts = progress_data.get(\"daily_counts\", {})\n                raw_progress = sum(daily_counts.values())\n                # Optionally, compute target as required_daily * number_of_days:\n                start_date = dt_util.parse_datetime(challenge.get(\"start_date\"))\n                end_date = dt_util.parse_datetime(challenge.get(\"end_date\"))\n\n                if start_date and end_date:\n                    num_days = (end_date.date() - start_date.date()).days + 1\n\n                else:\n                    num_days = 1\n                required_daily = challenge.get(\"required_daily\", 1)\n                target = required_daily * num_days\n\n            else:\n                raw_progress = 0\n\n        else:\n            raw_progress = 0\n\n        percent = (raw_progress / target * 100) if target > 0 else 0\n\n        return min(100, round(percent, 1))\n\n    @property\n    def extra_state_attributes(self) -> dict:\n        \"\"\"Return extra attributes for the challenge progress.\"\"\"\n        challenge = self.coordinator.challenges_data.get(self._challenge_id, {})\n        target = challenge.get(\"target_value\", 1)\n        challenge_type = challenge.get(\"type\")\n        progress_data = challenge.get(\"progress\", {}).get(self._kid_id, {})\n        awarded = (\n            progress_data.get(\"awarded\", False)\n            if isinstance(progress_data, dict)\n            else False\n        )\n\n        if challenge_type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:\n            raw_progress = (\n                progress_data.get(\"count\", 0) if isinstance(progress_data, dict) else 0\n            )\n        elif challenge_type == CHALLENGE_TYPE_DAILY_MIN:\n            if isinstance(progress_data, dict):\n                daily_counts = progress_data.get(\"daily_counts\", {})\n                raw_progress = sum(daily_counts.values())\n            else:\n                raw_progress = 0\n        else:\n            raw_progress = 0\n\n        associated_chore = \"\"\n        selected_chore_id = challenge.get(\"selected_chore_id\")\n        if selected_chore_id:\n            associated_chore = self.coordinator.chores_data.get(\n                selected_chore_id, {}\n            ).get(\"name\", \"\")\n\n        assigned_kids_ids = challenge.get(\"assigned_kids\", [])\n        assigned_kids_names = [\n            self.coordinator._get_kid_name_by_id(k_id) or f\"Kid {k_id}\"\n            for k_id in assigned_kids_ids\n        ]\n\n        stored_labels = challenge.get(\"challenge_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        return {\n            ATTR_CHALLENGE_NAME: self._challenge_name,\n            ATTR_DESCRIPTION: challenge.get(\"description\", \"\"),\n            ATTR_ASSIGNED_KIDS: assigned_kids_names,\n            ATTR_TYPE: challenge_type,\n            ATTR_ASSOCIATED_CHORE: associated_chore,\n            ATTR_CRITERIA: challenge.get(\"criteria\", \"\"),\n            ATTR_TARGET_VALUE: target,\n            ATTR_REWARD_POINTS: challenge.get(\"reward_points\"),\n            ATTR_START_DATE: challenge.get(\"start_date\"),\n            ATTR_END_DATE: challenge.get(\"end_date\"),\n            ATTR_RAW_PROGRESS: raw_progress,\n            ATTR_AWARDED: awarded,\n            ATTR_LABELS: friendly_labels,\n        }\n\n    @property\n    def icon(self) -> str:\n        \"\"\"Return the icon for the challenge.\n\n        Use the icon provided in the challenge data if set, else fallback to default.\n        \"\"\"\n        challenge = self.coordinator.challenges_data.get(self._challenge_id, {})\n        return challenge.get(\"icon\", DEFAULT_CHALLENGES_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass KidHighestStreakSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor returning the highest current streak among streak-type achievements for a kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"kid_highest_streak_sensor\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n    ):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_highest_streak\"\n        self._attr_native_unit_of_measurement = UnitOfTime.DAYS\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_highest_streak\"\n\n    @property\n    def native_value(self) -> int:\n        \"\"\"Return the highest current streak among all streak achievements for the kid.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"overall_chore_streak\", 0)\n\n    @property\n    def extra_state_attributes(self) -> dict:\n        \"\"\"Return extra attributes including individual streaks per achievement.\"\"\"\n        streaks = {}\n        for achievement in self.coordinator.achievements_data.values():\n            if achievement.get(\"type\") == ACHIEVEMENT_TYPE_STREAK:\n                achievement_name = achievement.get(\"name\", \"Unnamed Achievement\")\n                progress_for_kid = achievement.get(\"progress\", {}).get(self._kid_id)\n\n                if isinstance(progress_for_kid, dict):\n                    streaks[achievement_name] = progress_for_kid.get(\n                        \"current_streak\", 0\n                    )\n\n                elif isinstance(progress_for_kid, int):\n                    streaks[achievement_name] = progress_for_kid\n\n        return {\"streaks_by_achievement\": streaks}\n\n    @property\n    def icon(self) -> str:\n        \"\"\"Return an icon for 'highest streak'. You can choose any default or allow config overrides.\"\"\"\n        return DEFAULT_STREAK_ICON\n\n\n# ------------------------------------------------------------------------------------------\nclass ChoreStreakSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor returning the current streak for a specific chore for a given kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"chore_streak_sensor\"\n\n    def __init__(\n        self,\n        coordinator: KidsChoresDataCoordinator,\n        entry: ConfigEntry,\n        kid_id: str,\n        kid_name: str,\n        chore_id: str,\n        chore_name: str,\n    ):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._entry = entry\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._chore_id = chore_id\n        self._chore_name = chore_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{chore_id}_streak\"\n        self._attr_native_unit_of_measurement = UnitOfTime.DAYS\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"chore_name\": chore_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_chore_streak_{chore_name}\"\n\n    @property\n    def native_value(self) -> int:\n        \"\"\"Return the current streak (in days) for this kid and chore.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        streaks = kid_info.get(\"chore_streaks\", {})\n        streak_info = streaks.get(self._chore_id, {})\n        return streak_info.get(\"current_streak\", 0)\n\n    @property\n    def extra_state_attributes(self) -> dict:\n        \"\"\"Return extra attributes such as the last approved date for this streak.\"\"\"\n        attributes = {}\n        for achievement in self.coordinator.achievements_data.values():\n            if (\n                achievement.get(\"type\") == ACHIEVEMENT_TYPE_STREAK\n                and achievement.get(\"selected_chore_id\") == self._chore_id\n            ):\n                progress_for_kid = achievement.get(\"progress\", {}).get(self._kid_id)\n\n                if isinstance(progress_for_kid, dict):\n                    attributes[ATTR_LAST_DATE] = progress_for_kid.get(\"last_date\")\n                    attributes[ATTR_RAW_STREAK] = progress_for_kid.get(\n                        \"current_streak\", 0\n                    )\n\n                elif isinstance(progress_for_kid, int):\n                    attributes[ATTR_LAST_DATE] = None\n                    attributes[ATTR_RAW_STREAK] = progress_for_kid\n                break\n        return attributes\n\n    @property\n    def icon(self) -> str:\n        \"\"\"Return the chore's custom icon if set, else fallback.\"\"\"\n        chore_info = self.coordinator.chores_data.get(self._chore_id, {})\n        return chore_info.get(\"icon\", DEFAULT_CHORE_SENSOR_ICON)\n\n\n# ------------------------------------------------------------------------------------------\nclass BonusAppliesSensor(CoordinatorEntity, SensorEntity):\n    \"\"\"Sensor tracking how many times each bonus has been applied to a kid.\"\"\"\n\n    _attr_has_entity_name = True\n    _attr_translation_key = \"bonus_applies_sensor\"\n\n    def __init__(self, coordinator, entry, kid_id, kid_name, bonus_id, bonus_name):\n        \"\"\"Initialize the sensor.\"\"\"\n        super().__init__(coordinator)\n        self._kid_id = kid_id\n        self._kid_name = kid_name\n        self._bonus_id = bonus_id\n        self._bonus_name = bonus_name\n        self._attr_unique_id = f\"{entry.entry_id}_{kid_id}_{bonus_id}_bonus_applies\"\n        self._attr_translation_placeholders = {\n            \"kid_name\": kid_name,\n            \"bonus_name\": bonus_name,\n        }\n        self.entity_id = f\"sensor.kc_{kid_name}_bonuses_applied_{bonus_name}\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the number of times the bonus has been applied.\"\"\"\n        kid_info = self.coordinator.kids_data.get(self._kid_id, {})\n        return kid_info.get(\"bonus_applies\", {}).get(self._bonus_id, 0)\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Expose additional details like bonus points and description.\"\"\"\n        bonus_info = self.coordinator.bonuses_data.get(self._bonus_id, {})\n\n        stored_labels = bonus_info.get(\"bonus_labels\", [])\n        friendly_labels = [\n            get_friendly_label(self.hass, label) for label in stored_labels\n        ]\n\n        return {\n            ATTR_KID_NAME: self._kid_name,\n            ATTR_BONUS_NAME: self._bonus_name,\n            ATTR_DESCRIPTION: bonus_info.get(\"description\", \"\"),\n            ATTR_BONUS_POINTS: bonus_info.get(\"points\", DEFAULT_BONUS_POINTS),\n            ATTR_LABELS: friendly_labels,\n        }\n\n    @property\n    def icon(self):\n        \"\"\"Return the bonus's custom icon if set, else fallback.\"\"\"\n        bonus_info = self.coordinator.bonuses_data.get(self._bonus_id, {})\n        return bonus_info.get(\"icon\", DEFAULT_BONUS_ICON)\n"
  },
  {
    "path": "custom_components/kidschores/services.py",
    "content": "# File: services.py\n\"\"\"Defines custom services for the KidsChores integration.\n\nThese services allow direct actions through scripts or automations.\nIncludes UI editor support with selectors for dropdowns and text inputs.\n\"\"\"\n\nimport asyncio\nimport voluptuous as vol\n\nfrom typing import Optional\nfrom homeassistant.core import HomeAssistant, ServiceCall\nfrom homeassistant.exceptions import HomeAssistantError\nfrom homeassistant.helpers import config_validation as cv\nfrom homeassistant.util import dt as dt_util\n\nfrom .const import (\n    CHORE_STATE_OVERDUE,\n    CHORE_STATE_PENDING,\n    DATA_CHORES,\n    DATA_PENDING_CHORE_APPROVALS,\n    DOMAIN,\n    ERROR_CHORE_NOT_FOUND_FMT,\n    ERROR_KID_NOT_FOUND_FMT,\n    ERROR_NOT_AUTHORIZED_FMT,\n    FIELD_CHORE_ID,\n    FIELD_CHORE_NAME,\n    FIELD_DUE_DATE,\n    FIELD_KID_NAME,\n    FIELD_PARENT_NAME,\n    FIELD_PENALTY_NAME,\n    FIELD_POINTS_AWARDED,\n    FIELD_REWARD_NAME,\n    FIELD_BONUS_NAME,\n    LOGGER,\n    MSG_NO_ENTRY_FOUND,\n    SERVICE_APPLY_PENALTY,\n    SERVICE_APPLY_BONUS,\n    SERVICE_APPROVE_CHORE,\n    SERVICE_APPROVE_REWARD,\n    SERVICE_CLAIM_CHORE,\n    SERVICE_DISAPPROVE_CHORE,\n    SERVICE_DISAPPROVE_REWARD,\n    SERVICE_REDEEM_REWARD,\n    SERVICE_RESET_ALL_CHORES,\n    SERVICE_RESET_ALL_DATA,\n    SERVICE_RESET_OVERDUE_CHORES,\n    SERVICE_RESET_PENALTIES,\n    SERVICE_RESET_BONUSES,\n    SERVICE_RESET_REWARDS,\n    SERVICE_SET_CHORE_DUE_DATE,\n    SERVICE_SKIP_CHORE_DUE_DATE,\n)\nfrom .coordinator import KidsChoresDataCoordinator\nfrom .kc_helpers import is_user_authorized_for_global_action, is_user_authorized_for_kid\nfrom .flow_helpers import ensure_utc_datetime\n\n\n# --- Service Schemas ---\nCLAIM_CHORE_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_KID_NAME): cv.string,\n        vol.Required(FIELD_CHORE_NAME): cv.string,\n    }\n)\n\nAPPROVE_CHORE_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_PARENT_NAME): cv.string,\n        vol.Required(FIELD_KID_NAME): cv.string,\n        vol.Required(FIELD_CHORE_NAME): cv.string,\n        vol.Optional(FIELD_POINTS_AWARDED): vol.Coerce(float),\n    }\n)\n\nDISAPPROVE_CHORE_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_PARENT_NAME): cv.string,\n        vol.Required(FIELD_KID_NAME): cv.string,\n        vol.Required(FIELD_CHORE_NAME): cv.string,\n    }\n)\n\nREDEEM_REWARD_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_PARENT_NAME): cv.string,\n        vol.Required(FIELD_KID_NAME): cv.string,\n        vol.Required(FIELD_REWARD_NAME): cv.string,\n    }\n)\n\nAPPROVE_REWARD_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_PARENT_NAME): cv.string,\n        vol.Required(FIELD_KID_NAME): cv.string,\n        vol.Required(FIELD_REWARD_NAME): cv.string,\n    }\n)\n\nDISAPPROVE_REWARD_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_PARENT_NAME): cv.string,\n        vol.Required(FIELD_KID_NAME): cv.string,\n        vol.Required(FIELD_REWARD_NAME): cv.string,\n    }\n)\n\nAPPLY_PENALTY_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_PARENT_NAME): cv.string,\n        vol.Required(FIELD_KID_NAME): cv.string,\n        vol.Required(FIELD_PENALTY_NAME): cv.string,\n    }\n)\n\nAPPLY_BONUS_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_PARENT_NAME): cv.string,\n        vol.Required(FIELD_KID_NAME): cv.string,\n        vol.Required(FIELD_BONUS_NAME): cv.string,\n    }\n)\n\nRESET_OVERDUE_CHORES_SCHEMA = vol.Schema(\n    {\n        vol.Optional(FIELD_CHORE_ID): cv.string,\n        vol.Optional(FIELD_CHORE_NAME): cv.string,\n        vol.Optional(FIELD_KID_NAME): cv.string,\n    }\n)\n\nRESET_PENALTIES_SCHEMA = vol.Schema(\n    {\n        vol.Optional(FIELD_KID_NAME): cv.string,\n        vol.Optional(FIELD_PENALTY_NAME): cv.string,\n    }\n)\n\nRESET_BONUSES_SCHEMA = vol.Schema(\n    {\n        vol.Optional(FIELD_KID_NAME): cv.string,\n        vol.Optional(FIELD_BONUS_NAME): cv.string,\n    }\n)\n\nRESET_REWARDS_SCHEMA = vol.Schema(\n    {\n        vol.Optional(FIELD_KID_NAME): cv.string,\n        vol.Optional(FIELD_REWARD_NAME): cv.string,\n    }\n)\n\nRESET_ALL_DATA_SCHEMA = vol.Schema({})\n\nRESET_ALL_CHORES_SCHEMA = vol.Schema({})\n\nSET_CHORE_DUE_DATE_SCHEMA = vol.Schema(\n    {\n        vol.Required(FIELD_CHORE_NAME): cv.string,\n        vol.Optional(FIELD_DUE_DATE): vol.Any(cv.string, None),\n    }\n)\n\nSKIP_CHORE_DUE_DATE_SCHEMA = vol.Schema(\n    {\n        vol.Optional(FIELD_CHORE_ID): cv.string,\n        vol.Optional(FIELD_CHORE_NAME): cv.string,\n    }\n)\n\n\ndef async_setup_services(hass: HomeAssistant):\n    \"\"\"Register KidsChores services.\"\"\"\n\n    async def handle_claim_chore(call: ServiceCall):\n        \"\"\"Handle claiming a chore.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Claim Chore: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        user_id = call.context.user_id\n        kid_name = call.data[FIELD_KID_NAME]\n        chore_name = call.data[FIELD_CHORE_NAME]\n\n        # Map kid_name and chore_name to internal_ids\n        kid_id = _get_kid_id_by_name(coordinator, kid_name)\n        if not kid_id:\n            LOGGER.warning(\"Claim Chore: \" + ERROR_KID_NOT_FOUND_FMT, kid_name)\n            raise HomeAssistantError(ERROR_KID_NOT_FOUND_FMT.format(kid_name))\n\n        chore_id = _get_chore_id_by_name(coordinator, chore_name)\n        if not chore_id:\n            LOGGER.warning(\"Claim Chore: \" + ERROR_CHORE_NOT_FOUND_FMT, chore_name)\n            raise HomeAssistantError(ERROR_CHORE_NOT_FOUND_FMT.format(chore_name))\n\n        # Check if user is authorized\n        if user_id and not await is_user_authorized_for_kid(hass, user_id, kid_id):\n            LOGGER.warning(\"Claim Chore: %s\", ERROR_NOT_AUTHORIZED_FMT)\n            raise HomeAssistantError(ERROR_NOT_AUTHORIZED_FMT.format(\"claim chores\"))\n\n        # Process chore claim\n        coordinator.claim_chore(\n            kid_id=kid_id, chore_id=chore_id, user_name=f\"user:{user_id}\"\n        )\n\n        LOGGER.info(\n            \"Chore '%s' claimed by kid '%s' by user '%s'\",\n            chore_name,\n            kid_name,\n            user_id,\n        )\n        await coordinator.async_request_refresh()\n\n    async def handle_approve_chore(call: ServiceCall):\n        \"\"\"Handle approving a claimed chore.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n\n        if not entry_id:\n            LOGGER.warning(\"Approve Chore: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        user_id = call.context.user_id\n        parent_name = call.data[FIELD_PARENT_NAME]\n        kid_name = call.data[FIELD_KID_NAME]\n        chore_name = call.data[FIELD_CHORE_NAME]\n        points_awarded = call.data.get(FIELD_POINTS_AWARDED)\n\n        # Map kid_name and chore_name to internal_ids\n        kid_id = _get_kid_id_by_name(coordinator, kid_name)\n        if not kid_id:\n            LOGGER.warning(\"Approve Chore: Kid '%s' not found\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found\")\n\n        chore_id = _get_chore_id_by_name(coordinator, chore_name)\n        if not chore_id:\n            LOGGER.warning(\"Approve Chore: Chore '%s' not found\", chore_name)\n            raise HomeAssistantError(f\"Chore '{chore_name}' not found\")\n\n        # Check if user is authorized\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Approve Chore: User not authorized\")\n            raise HomeAssistantError(\n                \"You are not authorized to approve chores for this kid.\"\n            )\n\n        # Approve chore and assign points\n        try:\n            coordinator.approve_chore(\n                parent_name=parent_name,\n                kid_id=kid_id,\n                chore_id=chore_id,\n                points_awarded=points_awarded,\n            )\n            LOGGER.info(\n                \"Chore '%s' approved for kid '%s' by parent '%s'. Points Awarded: %s\",\n                chore_name,\n                kid_name,\n                parent_name,\n                points_awarded,\n            )\n            await coordinator.async_request_refresh()\n        except HomeAssistantError as e:\n            LOGGER.error(\"Approve Chore: %s\", e)\n            raise\n        except Exception as e:\n            LOGGER.error(\n                \"Approve Chore: Failed to approve chore '%s' for kid '%s': %s\",\n                chore_name,\n                kid_name,\n                e,\n            )\n            raise HomeAssistantError(\n                f\"Failed to approve chore '{chore_name}' for kid '{kid_name}'.\"\n            )\n\n    async def handle_disapprove_chore(call: ServiceCall):\n        \"\"\"Handle disapproving a chore.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Disapprove Chore: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        parent_name = call.data[FIELD_PARENT_NAME]\n        kid_name = call.data[FIELD_KID_NAME]\n        chore_name = call.data[FIELD_CHORE_NAME]\n\n        # Map kid_name and chore_name to internal_ids\n        kid_id = _get_kid_id_by_name(coordinator, kid_name)\n        if not kid_id:\n            LOGGER.warning(\"Disapprove Chore: Kid '%s' not found\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found\")\n\n        chore_id = _get_chore_id_by_name(coordinator, chore_name)\n        if not chore_id:\n            LOGGER.warning(\"Disapprove Chore: Chore '%s' not found\", chore_name)\n            raise HomeAssistantError(f\"Chore '{chore_name}' not found\")\n\n        # Check if user is authorized\n        user_id = call.context.user_id\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Disapprove Chore: User not authorized\")\n            raise HomeAssistantError(\n                \"You are not authorized to disapprove chores for this kid.\"\n            )\n\n        # Disapprove the chore\n        coordinator.disapprove_chore(\n            parent_name=parent_name,\n            kid_id=kid_id,\n            chore_id=chore_id,\n        )\n        LOGGER.info(\n            \"Chore '%s' disapproved for kid '%s' by parent '%s'\",\n            chore_name,\n            kid_name,\n            parent_name,\n        )\n        await coordinator.async_request_refresh()\n\n    async def handle_redeem_reward(call: ServiceCall):\n        \"\"\"Handle redeeming a reward (claiming without deduction).\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Redeem Reward: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        parent_name = call.data[FIELD_PARENT_NAME]\n        kid_name = call.data[FIELD_KID_NAME]\n        reward_name = call.data[FIELD_REWARD_NAME]\n\n        # Map kid_name and reward_name to internal_ids\n        kid_id = _get_kid_id_by_name(coordinator, kid_name)\n        if not kid_id:\n            LOGGER.warning(\"Redeem Reward: Kid '%s' not found\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found\")\n\n        reward_id = _get_reward_id_by_name(coordinator, reward_name)\n        if not reward_id:\n            LOGGER.warning(\"Redeem Reward: Reward '%s' not found\", reward_name)\n            raise HomeAssistantError(f\"Reward '{reward_name}' not found\")\n\n        # Check if user is authorized\n        user_id = call.context.user_id\n        if user_id and not await is_user_authorized_for_kid(hass, user_id, kid_id):\n            LOGGER.warning(\"Redeem Reward: User not authorized\")\n            raise HomeAssistantError(\n                \"You are not authorized to redeem rewards for this kid.\"\n            )\n\n        # Check if kid has enough points\n        kid_info = coordinator.kids_data.get(kid_id)\n        reward = coordinator.rewards_data.get(reward_id)\n        if not kid_info or not reward:\n            LOGGER.warning(\"Redeem Reward: Invalid kid or reward\")\n            raise HomeAssistantError(\"Invalid kid or reward\")\n\n        if kid_info[\"points\"] < reward.get(\"cost\", 0):\n            LOGGER.warning(\n                \"Redeem Reward: Kid '%s' does not have enough points to redeem reward '%s'\",\n                kid_name,\n                reward_name,\n            )\n            raise HomeAssistantError(\n                f\"Kid '{kid_name}' does not have enough points to redeem '{reward_name}'.\"\n            )\n\n        # Process reward claim without deduction\n        try:\n            coordinator.redeem_reward(\n                parent_name=parent_name, kid_id=kid_id, reward_id=reward_id\n            )\n            LOGGER.info(\n                \"Reward '%s' claimed by kid '%s' and pending approval by parent '%s'\",\n                reward_name,\n                kid_name,\n                parent_name,\n            )\n            await coordinator.async_request_refresh()\n        except HomeAssistantError as e:\n            LOGGER.error(\"Redeem Reward: %s\", e)\n            raise\n        except Exception as e:\n            LOGGER.error(\n                \"Redeem Reward: Failed to claim reward '%s' for kid '%s': %s\",\n                reward_name,\n                kid_name,\n                e,\n            )\n            raise HomeAssistantError(\n                f\"Failed to claim reward '{reward_name}' for kid '{kid_name}'.\"\n            )\n\n    async def handle_approve_reward(call: ServiceCall):\n        \"\"\"Handle approving a reward claimed by a kid.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Approve Reward: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        user_id = call.context.user_id\n        parent_name = call.data[FIELD_PARENT_NAME]\n        kid_name = call.data[FIELD_KID_NAME]\n        reward_name = call.data[FIELD_REWARD_NAME]\n\n        # Map kid_name and reward_name to internal_ids\n        kid_id = _get_kid_id_by_name(coordinator, kid_name)\n        if not kid_id:\n            LOGGER.warning(\"Approve Reward: Kid '%s' not found\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found\")\n\n        reward_id = _get_reward_id_by_name(coordinator, reward_name)\n        if not reward_id:\n            LOGGER.warning(\"Approve Reward: Reward '%s' not found\", reward_name)\n            raise HomeAssistantError(f\"Reward '{reward_name}' not found\")\n\n        # Check if user is authorized\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Approve Reward: User not authorized\")\n            raise HomeAssistantError(\n                \"You are not authorized to approve rewards for this kid.\"\n            )\n\n        # Approve reward redemption and deduct points\n        try:\n            coordinator.approve_reward(\n                parent_name=parent_name, kid_id=kid_id, reward_id=reward_id\n            )\n            LOGGER.info(\n                \"Reward '%s' approved for kid '%s' by parent '%s'\",\n                reward_name,\n                kid_name,\n                parent_name,\n            )\n            await coordinator.async_request_refresh()\n        except HomeAssistantError as e:\n            LOGGER.error(\"Approve Reward: %s\", e)\n            raise\n        except Exception as e:\n            LOGGER.error(\n                \"Approve Reward: Failed to approve reward '%s' for kid '%s': %s\",\n                reward_name,\n                kid_name,\n                e,\n            )\n            raise HomeAssistantError(\n                f\"Failed to approve reward '{reward_name}' for kid '{kid_name}'.\"\n            )\n\n    async def handle_disapprove_reward(call: ServiceCall):\n        \"\"\"Handle disapproving a reward.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Disapprove Reward: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        parent_name = call.data[FIELD_PARENT_NAME]\n        kid_name = call.data[FIELD_KID_NAME]\n        reward_name = call.data[FIELD_REWARD_NAME]\n\n        # Map kid_name and reward_name to internal_ids\n        kid_id = _get_kid_id_by_name(coordinator, kid_name)\n        if not kid_id:\n            LOGGER.warning(\"Disapprove Reward: Kid '%s' not found\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found\")\n\n        reward_id = _get_reward_id_by_name(coordinator, reward_name)\n        if not reward_id:\n            LOGGER.warning(\"Disapprove Reward: Reward '%s' not found\", reward_name)\n            raise HomeAssistantError(f\"Reward '{reward_name}' not found\")\n\n        # Check if user is authorized\n        user_id = call.context.user_id\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Disapprove Reward: User not authorized\")\n            raise HomeAssistantError(\n                \"You are not authorized to disapprove rewards for this kid.\"\n            )\n\n        # Disapprove the reward\n        coordinator.disapprove_reward(\n            parent_name=parent_name,\n            kid_id=kid_id,\n            reward_id=reward_id,\n        )\n        LOGGER.info(\n            \"Reward '%s' disapproved for kid '%s' by parent '%s'\",\n            reward_name,\n            kid_name,\n            parent_name,\n        )\n        await coordinator.async_request_refresh()\n\n    async def handle_apply_penalty(call: ServiceCall):\n        \"\"\"Handle applying a penalty.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Apply Penalty: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        parent_name = call.data[FIELD_PARENT_NAME]\n        kid_name = call.data[FIELD_KID_NAME]\n        penalty_name = call.data[FIELD_PENALTY_NAME]\n\n        # Map kid_name and penalty_name to internal_ids\n        kid_id = _get_kid_id_by_name(coordinator, kid_name)\n        if not kid_id:\n            LOGGER.warning(\"Apply Penalty: Kid '%s' not found\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found\")\n\n        penalty_id = _get_penalty_id_by_name(coordinator, penalty_name)\n        if not penalty_id:\n            LOGGER.warning(\"Apply Penalty: Penalty '%s' not found\", penalty_name)\n            raise HomeAssistantError(f\"Penalty '{penalty_name}' not found\")\n\n        # Check if user is authorized\n        user_id = call.context.user_id\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Apply Penalty: User not authorized\")\n            raise HomeAssistantError(\n                \"You are not authorized to apply penalties for this kid.\"\n            )\n\n        # Apply penalty\n        try:\n            coordinator.apply_penalty(\n                parent_name=parent_name, kid_id=kid_id, penalty_id=penalty_id\n            )\n            LOGGER.info(\n                \"Penalty '%s' applied for kid '%s' by parent '%s'\",\n                penalty_name,\n                kid_name,\n                parent_name,\n            )\n            await coordinator.async_request_refresh()\n        except HomeAssistantError as e:\n            LOGGER.error(\"Apply Penalty: %s\", e)\n            raise\n        except Exception as e:\n            LOGGER.error(\n                \"Apply Penalty: Failed to apply penalty '%s' for kid '%s': %s\",\n                penalty_name,\n                kid_name,\n                e,\n            )\n            raise HomeAssistantError(\n                f\"Failed to apply penalty '{penalty_name}' for kid '{kid_name}'.\"\n            )\n\n    async def handle_reset_penalties(call: ServiceCall):\n        \"\"\"Handle resetting penalties.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Reset Penalties: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n\n        kid_name = call.data.get(FIELD_KID_NAME)\n        penalty_name = call.data.get(FIELD_PENALTY_NAME)\n\n        kid_id = _get_kid_id_by_name(coordinator, kid_name) if kid_name else None\n        penalty_id = (\n            _get_penalty_id_by_name(coordinator, penalty_name) if penalty_name else None\n        )\n\n        if kid_name and not kid_id:\n            LOGGER.warning(\"Reset Penalties: Kid '%s' not found.\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found.\")\n\n        if penalty_name and not penalty_id:\n            LOGGER.warning(\"Reset Penalties: Penalty '%s' not found.\", penalty_name)\n            raise HomeAssistantError(f\"Penalty '{penalty_name}' not found.\")\n\n        # Check if user is authorized\n        user_id = call.context.user_id\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Reset Penalties: User not authorized.\")\n            raise HomeAssistantError(\"You are not authorized to reset penalties.\")\n\n        # Log action based on parameters provided\n        if kid_id is None and penalty_id is None:\n            LOGGER.info(\"Resetting all penalties for all kids.\")\n        elif kid_id is None:\n            LOGGER.info(\"Resetting penalty '%s' for all kids.\", penalty_name)\n        elif penalty_id is None:\n            LOGGER.info(\"Resetting all penalties for kid '%s'.\", kid_name)\n        else:\n            LOGGER.info(\"Resetting penalty '%s' for kid '%s'.\", penalty_name, kid_name)\n\n        # Reset penalties\n        coordinator.reset_penalties(kid_id=kid_id, penalty_id=penalty_id)\n        await coordinator.async_request_refresh()\n\n    async def handle_reset_bonuses(call: ServiceCall):\n        \"\"\"Handle resetting bonuses.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Reset Bonuses: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n\n        kid_name = call.data.get(FIELD_KID_NAME)\n        bonus_name = call.data.get(FIELD_BONUS_NAME)\n\n        kid_id = _get_kid_id_by_name(coordinator, kid_name) if kid_name else None\n        bonus_id = (\n            _get_bonus_id_by_name(coordinator, bonus_name) if bonus_name else None\n        )\n\n        if kid_name and not kid_id:\n            LOGGER.warning(\"Reset Bonuses: Kid '%s' not found.\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found.\")\n\n        if bonus_name and not bonus_id:\n            LOGGER.warning(\"Reset Bonuses: Bonus '%s' not found.\", bonus_name)\n            raise HomeAssistantError(f\"Bonus '{bonus_name}' not found.\")\n\n        # Check if user is authorized\n        user_id = call.context.user_id\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Reset Bonuses: User not authorized.\")\n            raise HomeAssistantError(\"You are not authorized to reset bonuses.\")\n\n        # Log action based on parameters provided\n        if kid_id is None and bonus_id is None:\n            LOGGER.info(\"Resetting all bonuses for all kids.\")\n        elif kid_id is None:\n            LOGGER.info(\"Resetting bonus '%s' for all kids.\", bonus_name)\n        elif bonus_id is None:\n            LOGGER.info(\"Resetting all bonuses for kid '%s'.\", kid_name)\n        else:\n            LOGGER.info(\"Resetting bonus '%s' for kid '%s'.\", bonus_name, kid_name)\n\n        # Reset bonuses\n        coordinator.reset_bonuses(kid_id=kid_id, bonus_id=bonus_id)\n        await coordinator.async_request_refresh()\n\n    async def handle_reset_rewards(call: ServiceCall):\n        \"\"\"Handle resetting rewards counts.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Reset Rewards: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n\n        kid_name = call.data.get(FIELD_KID_NAME)\n        reward_name = call.data.get(FIELD_REWARD_NAME)\n\n        kid_id = _get_kid_id_by_name(coordinator, kid_name) if kid_name else None\n        reward_id = (\n            _get_reward_id_by_name(coordinator, reward_name) if reward_name else None\n        )\n\n        if kid_name and not kid_id:\n            LOGGER.warning(\"Reset Rewards: Kid '%s' not found.\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found.\")\n\n        if reward_name and not reward_id:\n            LOGGER.warning(\"Reset Rewards: Reward '%s' not found.\", reward_name)\n            raise HomeAssistantError(f\"Reward '{reward_name}' not found.\")\n\n        # Check if user is authorized\n        user_id = call.context.user_id\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Reset Rewards: User not authorized.\")\n            raise HomeAssistantError(\"You are not authorized to reset rewards.\")\n\n        # Log action based on parameters provided\n        if kid_id is None and reward_id is None:\n            LOGGER.info(\"Resetting all rewards for all kids.\")\n        elif kid_id is None:\n            LOGGER.info(\"Resetting reward '%s' for all kids.\", reward_name)\n        elif reward_id is None:\n            LOGGER.info(\"Resetting all rewards for kid '%s'.\", kid_name)\n        else:\n            LOGGER.info(\"Resetting reward '%s' for kid '%s'.\", reward_name, kid_name)\n\n        # Reset rewards\n        coordinator.reset_rewards(kid_id=kid_id, reward_id=reward_id)\n        await coordinator.async_request_refresh()\n\n    async def handle_apply_bonus(call: ServiceCall):\n        \"\"\"Handle applying a bonus.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Apply Bonus: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        parent_name = call.data[FIELD_PARENT_NAME]\n        kid_name = call.data[FIELD_KID_NAME]\n        bonus_name = call.data[FIELD_BONUS_NAME]\n\n        # Map kid_name and bonus_name to internal_ids\n        kid_id = _get_kid_id_by_name(coordinator, kid_name)\n        if not kid_id:\n            LOGGER.warning(\"Apply Bonus: Kid '%s' not found\", kid_name)\n            raise HomeAssistantError(f\"Kid '{kid_name}' not found\")\n\n        bonus_id = _get_bonus_id_by_name(coordinator, bonus_name)\n        if not bonus_id:\n            LOGGER.warning(\"Apply Bonus: Bonus '%s' not found\", bonus_name)\n            raise HomeAssistantError(f\"Bonus '{bonus_name}' not found\")\n\n        # Check if user is authorized\n        user_id = call.context.user_id\n        if user_id and not await is_user_authorized_for_global_action(\n            hass, user_id, kid_id\n        ):\n            LOGGER.warning(\"Apply Bonus: User not authorized\")\n            raise HomeAssistantError(\n                \"You are not authorized to apply bonuses for this kid.\"\n            )\n\n        # Apply bonus\n        try:\n            coordinator.apply_bonus(\n                parent_name=parent_name, kid_id=kid_id, bonus_id=bonus_id\n            )\n            LOGGER.info(\n                \"Bonus '%s' applied for kid '%s' by parent '%s'\",\n                bonus_name,\n                kid_name,\n                parent_name,\n            )\n            await coordinator.async_request_refresh()\n        except HomeAssistantError as e:\n            LOGGER.error(\"Apply Bonus: %s\", e)\n            raise\n        except Exception as e:\n            LOGGER.error(\n                \"Apply Bonus: Failed to apply bonus '%s' for kid '%s': %s\",\n                bonus_name,\n                kid_name,\n                e,\n            )\n            raise HomeAssistantError(\n                f\"Failed to apply bonus '{bonus_name}' for kid '{kid_name}'.\"\n            )\n\n    async def handle_reset_all_data(call: ServiceCall):\n        \"\"\"Handle manually resetting ALL data in KidsChores.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Reset All Data: No KidsChores entry found\")\n            return\n\n        data = hass.data[DOMAIN].get(entry_id)\n        if not data:\n            LOGGER.warning(\"Reset All Data: No coordinator data found\")\n            return\n\n        coordinator: KidsChoresDataCoordinator = data[\"coordinator\"]\n\n        # Clear everything from storage\n        await coordinator.storage_manager.async_clear_data()\n\n        # Re-init the coordinator with reload config entry\n        await hass.config_entries.async_reload(entry_id)\n\n        coordinator.async_set_updated_data(coordinator._data)\n        LOGGER.info(\"Manually reset all KidsChores data. Integration is now cleared\")\n\n    async def handle_reset_all_chores(call: ServiceCall):\n        \"\"\"Handle manually resetting all chores to pending, clearing claims/approvals.\"\"\"\n\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Reset All Chores: No KidsChores entry found\")\n            return\n\n        data = hass.data[DOMAIN].get(entry_id)\n        if not data:\n            LOGGER.warning(\"Reset All Chores: No coordinator data found\")\n            return\n\n        coordinator: KidsChoresDataCoordinator = data[\"coordinator\"]\n\n        # Loop over all chores, reset them to pending\n        for chore_id, chore_info in coordinator.chores_data.items():\n            chore_info[\"state\"] = CHORE_STATE_PENDING\n\n        # Remove all chore approvals/claims for each kid\n        for kid_id, kid_info in coordinator.kids_data.items():\n            kid_info[\"claimed_chores\"] = []\n            kid_info[\"approved_chores\"] = []\n            kid_info[\"overdue_chores\"] = []\n            kid_info[\"overdue_notifications\"] = {}\n\n        # Clear the pending approvals queue\n        coordinator._data[DATA_PENDING_CHORE_APPROVALS] = []\n\n        # Persist & notify\n        coordinator._persist()\n        coordinator.async_set_updated_data(coordinator._data)\n        LOGGER.info(\"Manually reset all chores to pending, removed claims/approvals\")\n\n    async def handle_reset_overdue_chores(call: ServiceCall) -> None:\n        \"\"\"Handle resetting overdue chores.\"\"\"\n\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Reset Overdue Chores: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n\n        # Get parameters\n        chore_id = call.data.get(FIELD_CHORE_ID)\n        chore_name = call.data.get(FIELD_CHORE_NAME)\n        kid_name = call.data.get(FIELD_KID_NAME)\n\n        # If chore_id not provided but chore_name is, map it to chore_id.\n        if not chore_id and chore_name:\n            chore_id = _get_chore_id_by_name(coordinator, chore_name)\n\n            if not chore_id:\n                LOGGER.warning(\"Reset Overdue Chores: Chore '%s' not found\", chore_name)\n                raise HomeAssistantError(f\"Chore '{chore_name}' not found.\")\n\n        # If kid_name provided, map it to kid_id.\n        kid_id: Optional[str] = None\n        if kid_name:\n            kid_id = _get_kid_id_by_name(coordinator, kid_name)\n\n            if not kid_id:\n                LOGGER.warning(\"Reset Overdue Chores: Kid '%s' not found\", kid_name)\n                raise HomeAssistantError(f\"Kid '{kid_name}' not found.\")\n\n        coordinator.reset_overdue_chores(chore_id=chore_id, kid_id=kid_id)\n\n        LOGGER.info(\"Reset overdue chores (chore_id=%s, kid_id=%s)\", chore_id, kid_id)\n\n        await coordinator.async_request_refresh()\n        await coordinator._check_overdue_chores()\n\n    async def handle_set_chore_due_date(call: ServiceCall):\n        \"\"\"Handle setting (or clearing) the due date of a chore.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Set Chore Due Date: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n        chore_name = call.data[FIELD_CHORE_NAME]\n        due_date_input = call.data.get(FIELD_DUE_DATE)\n\n        # Look up the chore by name:\n        chore_id = _get_chore_id_by_name(coordinator, chore_name)\n        if not chore_id:\n            LOGGER.warning(\"Set Chore Due Date: Chore '%s' not found\", chore_name)\n            raise HomeAssistantError(ERROR_CHORE_NOT_FOUND_FMT.format(chore_name))\n\n        if due_date_input:\n            try:\n                # Convert the provided date\n                due_date_str = ensure_utc_datetime(hass, due_date_input)\n                due_dt = dt_util.parse_datetime(due_date_str)\n                if due_dt and due_dt < dt_util.utcnow():\n                    raise HomeAssistantError(\"Due date cannot be set in the past.\")\n\n            except Exception as err:\n                LOGGER.error(\n                    \"Set Chore Due Date: Invalid due date '%s': %s\", due_date_input, err\n                )\n                raise HomeAssistantError(\"Invalid due date provided.\")\n\n            # Update the chore’s due_date:\n            coordinator.set_chore_due_date(chore_id, due_dt)\n            LOGGER.info(\n                \"Set due date for chore '%s' (ID: %s) to %s\",\n                chore_name,\n                chore_id,\n                due_date_str,\n            )\n        else:\n            # Clear the due date by setting it to None\n            coordinator.set_chore_due_date(chore_id, None)\n            LOGGER.info(\n                \"Cleared due date for chore '%s' (ID: %s)\", chore_name, chore_id\n            )\n\n        await coordinator.async_request_refresh()\n\n    async def handle_skip_chore_due_date(call: ServiceCall) -> None:\n        \"\"\"Handle skipping the due date on a chore by rescheduling it to the next due date.\"\"\"\n        entry_id = _get_first_kidschores_entry(hass)\n        if not entry_id:\n            LOGGER.warning(\"Skip Chore Due Date: %s\", MSG_NO_ENTRY_FOUND)\n            return\n\n        coordinator: KidsChoresDataCoordinator = hass.data[DOMAIN][entry_id][\n            \"coordinator\"\n        ]\n\n        # Get parameters: either chore_id or chore_name must be provided.\n        chore_id = call.data.get(FIELD_CHORE_ID)\n        chore_name = call.data.get(FIELD_CHORE_NAME)\n\n        if not chore_id and chore_name:\n            chore_id = _get_chore_id_by_name(coordinator, chore_name)\n            if not chore_id:\n                LOGGER.warning(\"Skip Chore Due Date: Chore '%s' not found\", chore_name)\n                raise HomeAssistantError(f\"Chore '{chore_name}' not found.\")\n\n        if not chore_id:\n            raise HomeAssistantError(\n                \"You must provide either a chore_id or chore_name.\"\n            )\n\n        coordinator.skip_chore_due_date(chore_id)\n        LOGGER.info(\"Skipped due date for chore (chore_id=%s)\", chore_id)\n        await coordinator.async_request_refresh()\n\n    # --- Register Services ---\n    hass.services.async_register(\n        DOMAIN, SERVICE_CLAIM_CHORE, handle_claim_chore, schema=CLAIM_CHORE_SCHEMA\n    )\n    hass.services.async_register(\n        DOMAIN, SERVICE_APPROVE_CHORE, handle_approve_chore, schema=APPROVE_CHORE_SCHEMA\n    )\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_DISAPPROVE_CHORE,\n        handle_disapprove_chore,\n        schema=DISAPPROVE_CHORE_SCHEMA,\n    )\n    hass.services.async_register(\n        DOMAIN, SERVICE_REDEEM_REWARD, handle_redeem_reward, schema=REDEEM_REWARD_SCHEMA\n    )\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_APPROVE_REWARD,\n        handle_approve_reward,\n        schema=APPROVE_REWARD_SCHEMA,\n    )\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_DISAPPROVE_REWARD,\n        handle_disapprove_reward,\n        schema=DISAPPROVE_REWARD_SCHEMA,\n    )\n    hass.services.async_register(\n        DOMAIN, SERVICE_APPLY_PENALTY, handle_apply_penalty, schema=APPLY_PENALTY_SCHEMA\n    )\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_RESET_ALL_DATA,\n        handle_reset_all_data,\n        schema=RESET_ALL_DATA_SCHEMA,\n    )\n\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_RESET_ALL_CHORES,\n        handle_reset_all_chores,\n        schema=RESET_ALL_CHORES_SCHEMA,\n    )\n\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_RESET_OVERDUE_CHORES,\n        handle_reset_overdue_chores,\n        schema=RESET_OVERDUE_CHORES_SCHEMA,\n    )\n\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_RESET_PENALTIES,\n        handle_reset_penalties,\n        schema=RESET_PENALTIES_SCHEMA,\n    )\n\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_RESET_BONUSES,\n        handle_reset_bonuses,\n        schema=RESET_BONUSES_SCHEMA,\n    )\n\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_RESET_REWARDS,\n        handle_reset_rewards,\n        schema=RESET_REWARDS_SCHEMA,\n    )\n\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_SET_CHORE_DUE_DATE,\n        handle_set_chore_due_date,\n        schema=SET_CHORE_DUE_DATE_SCHEMA,\n    )\n\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_SKIP_CHORE_DUE_DATE,\n        handle_skip_chore_due_date,\n        schema=SKIP_CHORE_DUE_DATE_SCHEMA,\n    )\n\n    hass.services.async_register(\n        DOMAIN, SERVICE_APPLY_BONUS, handle_apply_bonus, schema=APPLY_BONUS_SCHEMA\n    )\n\n    LOGGER.info(\"KidsChores services have been registered successfully\")\n\n\nasync def async_unload_services(hass: HomeAssistant):\n    \"\"\"Unregister KidsChores services when unloading the integration.\"\"\"\n    services = [\n        SERVICE_CLAIM_CHORE,\n        SERVICE_APPROVE_CHORE,\n        SERVICE_DISAPPROVE_CHORE,\n        SERVICE_REDEEM_REWARD,\n        SERVICE_DISAPPROVE_REWARD,\n        SERVICE_APPLY_PENALTY,\n        SERVICE_APPLY_BONUS,\n        SERVICE_APPROVE_REWARD,\n        SERVICE_RESET_ALL_DATA,\n        SERVICE_RESET_ALL_CHORES,\n        SERVICE_RESET_OVERDUE_CHORES,\n        SERVICE_RESET_PENALTIES,\n        SERVICE_RESET_BONUSES,\n        SERVICE_RESET_REWARDS,\n        SERVICE_SET_CHORE_DUE_DATE,\n        SERVICE_SKIP_CHORE_DUE_DATE,\n    ]\n\n    for service in services:\n        if hass.services.has_service(DOMAIN, service):\n            hass.services.async_remove(DOMAIN, service)\n\n    LOGGER.info(\"KidsChores services have been unregistered\")\n\n\ndef _get_first_kidschores_entry(hass: HomeAssistant) -> Optional[str]:\n    \"\"\"Retrieve the first KidsChores config entry ID.\"\"\"\n    domain_entries = hass.data.get(DOMAIN)\n    if not domain_entries:\n        return None\n    return next(iter(domain_entries.keys()), None)\n\n\ndef _get_kid_id_by_name(\n    coordinator: KidsChoresDataCoordinator, kid_name: str\n) -> Optional[str]:\n    \"\"\"Help function to get kid_id by kid_name.\"\"\"\n    for kid_id, kid_info in coordinator.kids_data.items():\n        if kid_info.get(\"name\") == kid_name:\n            return kid_id\n    return None\n\n\ndef _get_chore_id_by_name(\n    coordinator: KidsChoresDataCoordinator, chore_name: str\n) -> Optional[str]:\n    \"\"\"Help function to get chore_id by chore_name.\"\"\"\n    for chore_id, chore_info in coordinator.chores_data.items():\n        if chore_info.get(\"name\") == chore_name:\n            return chore_id\n    return None\n\n\ndef _get_reward_id_by_name(\n    coordinator: KidsChoresDataCoordinator, reward_name: str\n) -> Optional[str]:\n    \"\"\"Help function to get reward_id by reward_name.\"\"\"\n    for reward_id, reward_info in coordinator.rewards_data.items():\n        if reward_info.get(\"name\") == reward_name:\n            return reward_id\n    return None\n\n\ndef _get_penalty_id_by_name(\n    coordinator: KidsChoresDataCoordinator, penalty_name: str\n) -> Optional[str]:\n    \"\"\"Help function to get penalty_id by penalty_name.\"\"\"\n    for penalty_id, penalty_info in coordinator.penalties_data.items():\n        if penalty_info.get(\"name\") == penalty_name:\n            return penalty_id\n    return None\n\n\ndef _get_bonus_id_by_name(\n    coordinator: KidsChoresDataCoordinator, bonus_name: str\n) -> Optional[str]:\n    \"\"\"Help function to get bonus_id by bonus_name.\"\"\"\n    for bonus_id, bonus_info in coordinator.bonuses_data.items():\n        if bonus_info.get(\"name\") == bonus_name:\n            return bonus_id\n    return None\n"
  },
  {
    "path": "custom_components/kidschores/services.yaml",
    "content": "# File: services.yaml\n#\n# Custom services documentation for the KidsChores integration.\n# These services allow direct actions through scripts or automations.\n# Includes UI editor support with selectors for text inputs and numbers.\n\nclaim_chore:\n  name: \"Claim Chore\"\n  description: \"A kid claims a chore, marking it as 'claimed' for parental approval.\"\n  fields:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The name of the kid claiming the chore.\"\n      example: \"Alice\"\n      required: true\n      selector:\n        text:\n    chore_name:\n      name: \"Chore Name\"\n      description: \"The name of the chore to claim.\"\n      example: \"Wash Dishes\"\n      required: true\n      selector:\n        text:\n\napprove_chore:\n  name: \"Approve Chore\"\n  description: \"Parent approves a chore, awarding points (full or partial).\"\n  fields:\n    parent_name:\n      name: \"Parent Name\"\n      description: \"The parent approving the chore.\"\n      example: \"Mom\"\n      required: true\n      selector:\n        text:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The name of the kid who performed the chore.\"\n      example: \"Alice\"\n      required: true\n      selector:\n        text:\n    chore_name:\n      name: \"Chore Name\"\n      description: \"The name of the chore being approved.\"\n      example: \"Wash Dishes\"\n      required: true\n      selector:\n        text:\n    points_awarded:\n      name: \"Points Awarded\"\n      description: \"Points to award (optional; defaults to the chore's points).\"\n      example: 3\n      required: false\n      selector:\n        number:\n          min: 0\n          max: 1000\n          mode: box\n\ndisapprove_chore:\n  name: \"Disapprove Chore\"\n  description: \"Parent disapproves a chore for a kid, reverting its status.\"\n  fields:\n    parent_name:\n      name: \"Parent Name\"\n      description: \"The parent disapproving the chore.\"\n      example: \"Mom\"\n      required: true\n      selector:\n        text:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The name of the kid whose chore is being disapproved.\"\n      example: \"Bob\"\n      required: true\n      selector:\n        text:\n    chore_name:\n      name: \"Chore Name\"\n      description: \"The name of the chore being disapproved.\"\n      example: \"Clean Room\"\n      required: true\n      selector:\n        text:\n\nredeem_reward:\n  name: \"Redeem Reward\"\n  description: \"A kid redeems a reward, marking it as 'pending' for parental approval.\"\n  fields:\n    parent_name:\n      name: \"Parent Name\"\n      description: \"The parent authorizing the reward redemption.\"\n      example: \"Mom\"\n      required: true\n      selector:\n        text:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The kid redeeming the reward.\"\n      example: \"Alice\"\n      required: true\n      selector:\n        text:\n    reward_name:\n      name: \"Reward Name\"\n      description: \"The name of the reward to redeem.\"\n      example: \"Extra Screen Time\"\n      required: true\n      selector:\n        text:\n\napprove_reward:\n  name: \"Approve Reward\"\n  description: \"Parent approves a reward claimed by a kid, deducting points.\"\n  fields:\n    parent_name:\n      name: \"Parent Name\"\n      description: \"The parent approving the reward.\"\n      example: \"Mom\"\n      required: true\n      selector:\n        text:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The kid who is redeeming the reward.\"\n      example: \"Alice\"\n      required: true\n      selector:\n        text:\n    reward_name:\n      name: \"Reward Name\"\n      description: \"The name of the reward being approved.\"\n      example: \"Extra Screen Time\"\n      required: true\n      selector:\n        text:\n\ndisapprove_reward:\n  name: \"Disapprove Reward\"\n  description: \"Parent disapproves a reward redemption for a kid.\"\n  fields:\n    parent_name:\n      name: \"Parent Name\"\n      description: \"The parent disapproving the reward.\"\n      example: \"Dad\"\n      required: true\n      selector:\n        text:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The kid whose reward redemption is being disapproved.\"\n      example: \"Bob\"\n      required: true\n      selector:\n        text:\n    reward_name:\n      name: \"Reward Name\"\n      description: \"The name of the reward being disapproved.\"\n      example: \"Extra Screen Time\"\n      required: true\n      selector:\n        text:\n\napply_penalty:\n  name: \"Apply Penalty\"\n  description: \"A parent applies a penalty, deducting points from a kid.\"\n  fields:\n    parent_name:\n      name: \"Parent Name\"\n      description: \"The parent applying the penalty.\"\n      example: \"Dad\"\n      required: true\n      selector:\n        text:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The kid receiving the penalty.\"\n      example: \"Bob\"\n      required: true\n      selector:\n        text:\n    penalty_name:\n      name: \"Penalty Name\"\n      description: \"The name of the penalty to apply.\"\n      example: \"Yelling\"\n      required: true\n      selector:\n        text:\n\napply_bonus:\n  name: \"Apply Bonus\"\n  description: \"A parent applies a bonus to award extra points.\"\n  fields:\n    parent_name:\n      name: \"Parent Name\"\n      description: \"The parent applying the bonus.\"\n      example: \"Dad\"\n      required: true\n      selector:\n        text:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The kid receiving the bonus.\"\n      example: \"Bob\"\n      required: true\n      selector:\n        text:\n    bonus_name:\n      name: \"Bonus Name\"\n      description: \"The name of the bonus to apply.\"\n      example: \"Extra Helpful\"\n      required: true\n      selector:\n        text:\n\nreset_all_data:\n  name: \"Reset All Data\"\n  description: \"Completely clears the KidsChores data from storage.\"\n  fields: {}\n\nreset_all_chores:\n  name: \"Reset All Chores\"\n  description: \"Manually reset chores to pending state, removing claims and approvals.\"\n  fields: {}\n\nreset_overdue_chores:\n  name: \"Reset Overdue Chores\"\n  description: >\n    Reset overdue chore(s) back to the Pending state and reschedule them based on\n    their recurring frequency and previous due date. You may optionally provide a\n    chore_id (or chore_name) to reset a specific chore and optionally a kid_name\n    to reset the chore only for that kid.\n  fields:\n    chore_id:\n      name: \"Chore ID\"\n      description: \"The internal ID of the chore to reset (optional if chore_name is provided).\"\n      example: \"abc123\"\n      required: false\n      selector:\n        text:\n    chore_name:\n      name: \"Chore Name\"\n      description: \"The name of the chore to reset (optional if chore_id is provided).\"\n      example: \"Wash Dishes\"\n      required: false\n      selector:\n        text:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"If provided, reset the chore only for this kid.\"\n      example: \"Alice\"\n      required: false\n      selector:\n        text:\n\nset_chore_due_date:\n  name: \"Set Chore Due Date\"\n  description: >\n    Set (or clear) the due date for a chore. Provide the chore name and, if desired,\n    a new due date. If no due date is provided the existing due date will be cleared.\n    The service will reject due dates set in the past.\n  fields:\n    chore_name:\n      name: \"Chore Name\"\n      description: \"The name of the chore to update.\"\n      example: \"Wash Dishes\"\n      required: true\n      selector:\n        text:\n    due_date:\n      name: \"Due Date\"\n      description: >\n        The new due date for the chore. Use the date/time selector to choose a valid\n        date and time (in your local timezone). Leave empty to clear the due date.\n      example: \"2025-03-01T23:59:00Z\"\n      required: false\n      selector:\n        datetime: {}\n\nskip_chore_due_date:\n  name: \"Skip Chore Due Date\"\n  description: >\n    Skip the current due date of a recurring chore. This service immediately\n    reschedules the chore's due date based on its recurring frequency and resets\n    its state to pending. Any pending claims or approvals will be removed.\n  fields:\n    chore_id:\n      name: \"Chore ID\"\n      description: >\n        The internal ID of the chore to update. Optional if you provide a chore name.\n      example: \"abc123\"\n      required: false\n      selector:\n        text:\n    chore_name:\n      name: \"Chore Name\"\n      description: >\n        The name of the chore to update. Optional if you provide a chore ID.\n      example: \"Weekly Laundry\"\n      required: false\n      selector:\n        text:\n\nreset_penalties:\n  name: \"Reset Penalties\"\n  description: >\n    Reset all applied penalties for all kids. Optionally, provide penalty_name to reset a\n    specific penalty across all kids. Use kid_name to reset all penalties for a specific kid.\n    Combine both to reset a specific penalty for a specific kid.\n  fields:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The kid penalites will be reset for.\"\n      example: \"Bob\"\n      required: false\n      selector:\n        text:\n    penalty_name:\n      name: \"Penalty Name\"\n      description: \"The name of the penalty to reset.\"\n      example: \"Yelling\"\n      required: false\n      selector:\n        text:\n\nreset_bonuses:\n  name: \"Reset Bonuses\"\n  description: >\n    Reset all applied bonuses for all kids. Optionally, provide bonus_name to reset a\n    specific bonus across all kids. Use kid_name to reset all bonuses for a specific kid.\n    Combine both to reset a specific bonus for a specific kid.\n  fields:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The kid bonuses will be reset for.\"\n      example: \"Bob\"\n      required: false\n      selector:\n        text:\n    bonus_name:\n      name: \"Bonus Name\"\n      description: \"The name of the bonus to reset.\"\n      example: \"Helping\"\n      required: false\n      selector:\n        text:\n\nreset_rewards:\n  name: \"Reset Rewards\"\n  description: >\n    Reset all reward claim and approval counts for all kids. Optionally, provide reward_name to reset a\n    specific reward counts across all kids. Use kid_name to reset all reward counts for a specific kid.\n    Combine both to reset a specific reward for a specific kid.\n  fields:\n    kid_name:\n      name: \"Kid Name\"\n      description: \"The kid reward counts will be reset for.\"\n      example: \"Bob\"\n      required: false\n      selector:\n        text:\n    reward_name:\n      name: \"Reward Name\"\n      description: \"The name of the reward to reset count.\"\n      example: \"Ice Cream\"\n      required: false\n      selector:\n        text:"
  },
  {
    "path": "custom_components/kidschores/storage_manager.py",
    "content": "# File: storage_manager.py\n\"\"\"Handles persistent data storage for the KidsChores integration.\n\nUses Home Assistant's Storage helper to save and load chore-related data, ensuring\nthe state is preserved across restarts. This includes data for kids, chores,\nbadges, rewards, penalties, and their statuses.\n\"\"\"\n\nimport os\n\nfrom homeassistant.helpers.storage import Store\nfrom .const import (\n    DATA_ACHIEVEMENTS,\n    DATA_BADGES,\n    DATA_BONUSES,\n    DATA_CHALLENGES,\n    DATA_CHORES,\n    DATA_KIDS,\n    DATA_PARENTS,\n    DATA_PENALTIES,\n    DATA_PENDING_CHORE_APPROVALS,\n    DATA_PENDING_REWARD_APPROVALS,\n    DATA_REWARDS,\n    LOGGER,\n    STORAGE_KEY,\n    STORAGE_VERSION,\n)\n\n\nclass KidsChoresStorageManager:\n    \"\"\"Manages loading, saving, and accessing data from Home Assistant's storage.\n\n    Utilizes internal_id as the primary key for all entities.\n    \"\"\"\n\n    def __init__(self, hass, storage_key=STORAGE_KEY):\n        \"\"\"Initialize the storage manager.\n\n        Args:\n            hass: Home Assistant core object.\n            storage_key: Key to identify storage location (default: STORAGE_KEY).\n\n        \"\"\"\n        self.hass = hass\n        self._storage_key = storage_key\n        self._store = Store(hass, STORAGE_VERSION, storage_key)\n        self._data = {}  # In-memory data cache for quick access.\n\n    async def async_initialize(self):\n        \"\"\"Load data from storage during startup.\n\n        If no data exists, initializes with an empty structure.\n        \"\"\"\n        LOGGER.debug(\"KidsChoresStorageManager: Loading data from storage\")\n        existing_data = await self._store.async_load()\n\n        if existing_data is None:\n            # No existing data, create a new default structure.\n            LOGGER.info(\"No existing storage found; initializing new data\")\n            self._data = {\n                DATA_KIDS: {},  # Dictionary of kids keyed by internal_id.\n                DATA_CHORES: {},  # Dictionary of chores keyed by internal_id.\n                DATA_BADGES: {},  # Dictionary of badges keyed by internal_id.\n                DATA_REWARDS: {},  # Dictionary of rewards keyed by internal_id.\n                DATA_PENALTIES: {},  # Dictionary of penalties keyed by internal_id.\n                DATA_BONUSES: {},  # Dictionary of bonuses keyed by internal_id.\n                DATA_PARENTS: {},  # Dictionary of parents keyed by internal_id.\n                DATA_ACHIEVEMENTS: {},  # Dictionary of achievements keyed by internal_id.\n                DATA_CHALLENGES: {},  # Dictionary of challenges keyed by internal_id.\n                DATA_PENDING_CHORE_APPROVALS: [],  # List of pending chore approvals keyed by internal_id.\n                DATA_PENDING_REWARD_APPROVALS: [],  # List of pending rewar approvals keyed by internal_id.\n            }\n        else:\n            # Load existing data into memory.\n            self._data = existing_data\n            LOGGER.info(\"Storage data loaded successfully\")\n\n    @property\n    def data(self):\n        \"\"\"Retrieve the in-memory data cache.\"\"\"\n        return self._data\n\n    def get_data(self):\n        \"\"\"Retrieve the data structure (alternative getter).\"\"\"\n        return self._data\n\n    def set_data(self, new_data: dict):\n        \"\"\"Replace the entire in-memory data structure.\"\"\"\n        self._data = new_data\n\n    def get_kids(self):\n        \"\"\"Retrieve the kids data.\"\"\"\n        return self._data.get(DATA_KIDS, {})\n\n    def get_parents(self):\n        \"\"\"Retrieve the parents data.\"\"\"\n        return self._data.get(DATA_PARENTS, {})\n\n    def get_chores(self):\n        \"\"\"Retrieve the chores data.\"\"\"\n        return self._data.get(DATA_CHORES, {})\n\n    def get_badges(self):\n        \"\"\"Retrieve the badges data.\"\"\"\n        return self._data.get(DATA_BADGES, {})\n\n    def get_rewards(self):\n        \"\"\"Retrieve the rewards data.\"\"\"\n        return self._data.get(DATA_REWARDS, {})\n\n    def get_penalties(self):\n        \"\"\"Retrieve the penalties data.\"\"\"\n        return self._data.get(DATA_PENALTIES, {})\n\n    def get_bonuses(self):\n        \"\"\"Retrieve the bonuses data.\"\"\"\n        return self._data.get(DATA_BONUSES, {})\n\n    def get_achievements(self):\n        \"\"\"Retrieve the achievements data.\"\"\"\n        return self._data.get(DATA_ACHIEVEMENTS, {})\n\n    def get_challenges(self):\n        \"\"\"Retrieve the challenges data.\"\"\"\n        return self._data.get(DATA_CHALLENGES, {})\n\n    def get_pending_chore_approvals(self):\n        \"\"\"Retrieve the pending chore approvals data.\"\"\"\n        return self._data.get(DATA_PENDING_CHORE_APPROVALS, [])\n\n    def get_pending_reward_aprovals(self):\n        \"\"\"Retrieve the pending reward approvals data.\"\"\"\n        return self._data.get(DATA_PENDING_REWARD_APPROVALS, [])\n\n    async def link_user_to_kid(self, user_id, kid_id):\n        \"\"\"Link a Home Assistant user ID to a specific kid by internal_id.\"\"\"\n\n        if \"linked_users\" not in self._data:\n            self._data[\"linked_users\"] = {}\n        self._data[\"linked_users\"][user_id] = kid_id\n        await self._save()\n\n    async def unlink_user(self, user_id):\n        \"\"\"Unlink a Home Assistant user ID from any kid.\"\"\"\n\n        if \"linked_users\" in self._data and user_id in self._data[\"linked_users\"]:\n            del self._data[\"linked_users\"][user_id]\n            await self._save()\n\n    async def get_linked_kids(self):\n        \"\"\"Get all linked users and their associated kids.\"\"\"\n\n        return self._data.get(\"linked_users\", {})\n\n    async def async_save(self):\n        \"\"\"Save the current data structure to storage asynchronously.\"\"\"\n        try:\n            await self._store.async_save(self._data)\n            LOGGER.info(\"Data saved successfully to storage\")\n        except Exception as e:\n            LOGGER.error(\"Failed to save data to storage: %s\", e)\n\n    async def async_clear_data(self):\n        \"\"\"Clear all stored data and reset to default structure.\"\"\"\n\n        LOGGER.warning(\"Clearing all KidsChores data and resetting storage\")\n        self._data = {\n            DATA_KIDS: {},\n            DATA_CHORES: {},\n            DATA_BADGES: {},\n            DATA_REWARDS: {},\n            DATA_PARENTS: {},\n            DATA_PENALTIES: {},\n            DATA_BONUSES: {},\n            DATA_ACHIEVEMENTS: {},\n            DATA_CHALLENGES: {},\n            DATA_PENDING_REWARD_APPROVALS: [],\n            DATA_PENDING_CHORE_APPROVALS: [],\n        }\n        await self.async_save()\n\n    async def async_delete_storage(self) -> None:\n        \"\"\"Delete the storage file completely from disk.\"\"\"\n\n        # First clear in-memory data\n        await self.async_clear_data()\n\n        # Remove the file if it exists\n        if os.path.isfile(self._store._path):\n            try:\n                os.remove(self._store._path)\n                LOGGER.info(\"Storage file removed: %s\", self._store._path)\n            except Exception as e:\n                LOGGER.error(\"Failed to remove storage file: %s\", e)\n        else:\n            LOGGER.info(\"Storage file not found: %s\", self._store._path)\n\n    async def async_update_data(self, key, value):\n        \"\"\"Update a specific section of the data structure.\"\"\"\n\n        if key in self._data:\n            LOGGER.debug(\"Updating data for key: %s\", key)\n            self._data[key] = value\n            await self.async_save()\n        else:\n            LOGGER.warning(\"Attempted to update unknown data key: %s\", key)\n"
  },
  {
    "path": "custom_components/kidschores/translations/en.json",
    "content": "{\n  \"title\": \"KidsChores\",\n  \"config\": {\n    \"step\": {\n      \"intro\": {\n        \"title\": \"Welcome to KidsChores\",\n        \"description\": \"This wizard will guide you through setting up KidsChores.\"\n      },\n      \"points_label\": {\n        \"title\": \"Points Label\",\n        \"description\": \"Choose a label and icon for points.\",\n        \"data\": {\n          \"points_label\": \"Points Label\",\n          \"points_icon\": \"Points Icon\"\n        }\n      },\n      \"kid_count\": {\n        \"title\": \"Number of Kids\",\n        \"description\": \"How many kids do you want to manage?\",\n        \"data\": {\n          \"kid_count\": \"Number of Kids\"\n        }\n      },\n      \"kids\": {\n        \"title\": \"Define Kid\",\n        \"description\": \"Enter the name for each kid.\",\n        \"data\": {\n          \"kid_name\": \"Kid Name\",\n          \"internal_id\": \"Internal ID\",\n          \"ha_user\": \"Home Assistant User\",\n          \"enable_mobile_notifications\": \"Enable Mobile Notifications\",\n          \"mobile_notify_service\": \"Notify Service\",\n          \"enable_persistent_notifications\": \"Enable Persistent Notifications\"\n        }\n      },\n      \"parent_count\": {\n        \"title\": \"Number of Parents\",\n        \"description\": \"How many parents do you want to define initially?\",\n        \"data\": {\n          \"parent_count\": \"Number of Parents\"\n        }\n      },\n      \"parents\": {\n        \"title\": \"Define Parent\",\n        \"description\": \"Enter details for each parent.\",\n        \"data\": {\n          \"parent_name\": \"Parent Name\",\n          \"ha_user_id\": \"Home Assistant User\",\n          \"associated_kids\": \"Associated Kids\",\n          \"enable_mobile_notifications\": \"Enable Mobile Notifications\",\n          \"mobile_notify_service\": \"Notify Service\",\n          \"enable_persistent_notifications\": \"Enable Persistent Notifications\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"chore_count\": {\n        \"title\": \"Number of Chores\",\n        \"description\": \"How many chores do you want to define?\",\n        \"data\": {\n          \"chore_count\": \"Number of Chores\"\n        }\n      },\n      \"chores\": {\n        \"title\": \"Define Chore\",\n        \"description\": \"Enter details for each chore.\",\n        \"data\": {\n          \"chore_name\": \"Chore Name\",\n          \"internal_id\": \"Internal ID\",\n          \"default_points\": \"Default Points\",\n          \"allow_multiple_claims_per_day\": \"Allow Multiple Claims per Day?\",\n          \"partial_allowed\": \"Allow Partial Points?\",\n          \"shared_chore\": \"Shared Chore?\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"chore_description\": \"Description (optional)\",\n          \"chore_labels\": \"Chore Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"recurring_frequency\": \"Recurring Frequency\",\n          \"custom_interval\": \"Custom Recurring Frequency Interval (only use if Custom Recurring Frequency is set)\",\n          \"custom_interval_unit\": \"Custom Recurring Frequency Period\",\n          \"applicable_days\": \"Applicable Days\",\n          \"due_date\": \"Due Date\",\n          \"notify_on_claim\": \"Notify on Claim\",\n          \"notify_on_approval\": \"Notify on Approval\",\n          \"notify_on_disapproval\": \"Notify on Disapproval\"\n        }\n      },\n      \"badge_count\": {\n        \"title\": \"Number of Badges\",\n        \"description\": \"How many badges do you want to define?\",\n        \"data\": {\n          \"badge_count\": \"Badge Count\"\n        }\n      },\n      \"badges\": {\n        \"title\": \"Define Badge\",\n        \"description\": \"Enter details for each badge.\",\n        \"data\": {\n          \"badge_name\": \"Badge Name\",\n          \"internal_id\": \"Internal ID\",\n          \"threshold_type\": \"Threshold Type\",\n          \"threshold_value\": \"Threshold Value\",\n          \"points_multiplier\": \"Points Multiplier\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"badge_description\": \"Description (optional)\",\n          \"badge_labels\": \"Badge Labels\"\n        }\n      },\n      \"reward_count\": {\n        \"title\": \"Number of Rewards\",\n        \"description\": \"How many rewards do you want to define?\",\n        \"data\": {\n          \"reward_count\": \"Reward Count\"\n        }\n      },\n      \"rewards\": {\n        \"title\": \"Define Reward\",\n        \"description\": \"Enter details for each reward.\",\n        \"data\": {\n          \"reward_name\": \"Reward Name\",\n          \"internal_id\": \"Internal ID\",\n          \"reward_cost\": \"Reward Cost\",\n          \"reward_description\": \"Description (optional)\",\n          \"reward_labels\": \"Reward Labels\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"penalty_count\": {\n        \"title\": \"Number of Penalties\",\n        \"description\": \"How many penalties do you want to define?\",\n        \"data\": {\n          \"penalty_count\": \"Penalty Count\"\n        }\n      },\n      \"penalties\": {\n        \"title\": \"Define Penalty\",\n        \"description\": \"Enter details for each penalty.\",\n        \"data\": {\n          \"penalty_name\": \"Penalty Name\",\n          \"penalty_description\": \"Description (optional)\",\n          \"penalty_labels\": \"Penalty Labels\",\n          \"internal_id\": \"Internal ID\",\n          \"penalty_points\": \"Penalty Points (negative)\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"bonus_count\": {\n        \"title\": \"Number of Bonuses\",\n        \"description\": \"How many bonuses do you want to define?\",\n        \"data\": {\n          \"bonus_count\": \"Bonuses Count\"\n        }\n      },\n      \"bonuses\": {\n        \"title\": \"Define Bonus\",\n        \"description\": \"Enter details for each bonus.\",\n        \"data\": {\n          \"bonus_name\": \"Bonus Name\",\n          \"bonus_description\": \"Description (optional)\",\n          \"bonus_labels\": \"Bonus Labels\",\n          \"internal_id\": \"Internal ID\",\n          \"bonus_points\": \"Bonus Points\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"achievement_count\": {\n        \"title\": \"Number of Achievements\",\n        \"description\": \"How many achievements do you want to define?\",\n        \"data\": {\n          \"achievement_count\": \"Achievement Count\"\n        }\n      },\n      \"achievements\": {\n        \"title\": \"Define Achievement\",\n        \"description\": \"Enter details for each achievement.\",\n        \"data\": {\n          \"name\": \"Achievement Name\",\n          \"description\": \"Description (optional)\",\n          \"achievement_labels\": \"Achievement Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"type\": \"Type of Achievement\",\n          \"selected_chore_id\": \"Select Chore Associated\",\n          \"criteria\": \"Criteria (optional)\",\n          \"target_value\": \"Achievement Target\",\n          \"reward_points\": \"Extra Points for Completing Achievement\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"challenge_count\": {\n        \"title\": \"Number of Challenges\",\n        \"description\": \"How many challenges do you want to define?\",\n        \"data\": {\n          \"challenge_count\": \"Challenge Count\"\n        }\n      },\n      \"challenges\": {\n        \"title\": \"Define Challenge\",\n        \"description\": \"Enter details for each challenge.\",\n        \"data\": {\n          \"name\": \"Challenge Name\",\n          \"description\": \"Description (optional)\",\n          \"challenge_labels\": \"Challenge Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"type\": \"Type of Challenge\",\n          \"selected_chore_id\": \"Select Chore Associated with Challenge (Optional)\",\n          \"criteria\": \"Criteria (optional)\",\n          \"target_value\": \"Challenges Target\",\n          \"reward_points\": \"Extra Points for Completing Challenge\",\n          \"start_date\": \"Start Date\",\n          \"end_date\": \"End Date\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"finish\": {\n        \"title\": \"Review & Finish\",\n        \"description\": \"Review the setup:\\n{summary}\\nClick Submit to finalize.\"\n      }\n    },\n    \"error\": {\n      \"a_chore_must_be_selected\": \"A chore must be selected\",\n      \"duplicate_achievement\": \"An achievement with this name already exists\",\n      \"duplicate_badge\": \"A badge with this name already exists\",\n      \"duplicate_challenge\": \"A challenge with this name already exists\",\n      \"duplicate_chore\": \"A chore with this name already exists\",\n      \"duplicate_kid\": \"A kid with this name already exists\",\n      \"duplicate_parent\": \"A parent with this name already exists\",\n      \"duplicate_penalty\": \"A penalty with this name already exists\",\n      \"duplicate_reward\": \"A reward with this name already exists\",\n      \"duplicate_bonus\": \"A bonus with this name already exists\",\n      \"due_date_in_past\": \"Due date must be in the future.\",\n      \"end_date_in_past\": \"End Date must be in the future.\",\n      \"end_date_not_after_start_date\": \"End date must be later than start date\",\n      \"invalid_achievement_count\": \"Invalid achievement count\",\n      \"invalid_achievement_name\": \"Invalid achievement name\",\n      \"invalid_badge\": \"Invalid badge\",\n      \"invalid_badge_count\": \"Invalid badge count\",\n      \"invalid_badge_name\": \"Invalid badge name\",\n      \"invalid_challenge_count\": \"Invalid challenge count\",\n      \"invalid_challenge_name\": \"Invalid challenge name\",\n      \"invalid_chore\": \"Invalid chore\",\n      \"invalid_chore_count\": \"Invalid chore count\",\n      \"invalid_chore_name\": \"Invalid chore name\",\n      \"invalid_due_date\": \"Invalid due date\",\n      \"invalid_end_date\": \"Invalid end date.\",\n      \"invalid_kid_count\": \"Invalid kid count\",\n      \"invalid_kid_name\": \"Invalid kid name\",\n      \"invalid_parent_count\": \"Invalid parent count\",\n      \"invalid_parent_name\": \"Invalid parent name\",\n      \"invalid_penalty\": \"Invalid penalty\",\n      \"invalid_penalty_count\": \"Invalid penalty count\",\n      \"invalid_penalty_name\": \"Invalid penalty name\",\n      \"invalid_reward\": \"Invalid reward\",\n      \"invalid_reward_count\": \"Invalid reward count\",\n      \"invalid_reward_name\": \"Invalid reward name\",\n      \"invalid_bonus\": \"Invalid bonus\",\n      \"invalid_bonus_count\": \"Invalid bonus count\",\n      \"invalid_bonus_name\": \"Invalid bonus name\",\n      \"invalid_start_date\": \"Invalid start date.\",\n      \"start_date_in_past\": \"Start Date must be in the future.\"\n    },\n    \"abort\": {\n      \"single_instance_allowed\": \"Only a single KidsChores instance can be configured.\"\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"KidsChores Options\",\n        \"description\": \"Manage kids, chores, badges, rewards, penalties, bonuses, or finish.\",\n        \"data\": {\n          \"menu_selection\": \"Select an Option\"\n        }\n      },\n      \"manage_entity\": {\n        \"title\": \"Select Action\",\n        \"description\": \"Add, edit or delete options.\",\n        \"data\": {\n          \"manage_action\": \"Select an Action\"\n        }\n      },\n      \"select_entity\": {\n        \"title\": \"Select {entity_type}\",\n        \"description\": \"Select the {entity_type} you want to {action}.\",\n        \"data\": {\n          \"entity_name\": \"Name\"\n        }\n      },\n      \"manage_points\": {\n        \"title\": \"Edit Points Label & Icon\",\n        \"description\": \"Change the label and icon used to represent points.\",\n        \"data\": {\n          \"points_label\": \"Points Label\",\n          \"points_icon\": \"Points Icon\"\n        }\n      },\n      \"add_kid\": {\n        \"title\": \"Add Kid\",\n        \"description\": \"Provide the details for the new kid.\",\n        \"data\": {\n          \"kid_name\": \"Kid Name\",\n          \"ha_user\": \"Home Assistant User\",\n          \"enable_mobile_notifications\": \"Enable Mobile Notifications\",\n          \"mobile_notify_service\": \"Notify Service\",\n          \"enable_persistent_notifications\": \"Enable Persistent Notifications\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"add_parent\": {\n        \"title\": \"Add Parent\",\n        \"description\": \"Provide the details for the new parent.\",\n        \"data\": {\n          \"parent_name\": \"Parent Name\",\n          \"ha_user_id\": \"Home Assistant User\",\n          \"associated_kids\": \"Associated Kids\",\n          \"enable_mobile_notifications\": \"Enable Mobile Notifications\",\n          \"mobile_notify_service\": \"Notify Service\",\n          \"enable_persistent_notifications\": \"Enable Persistent Notifications\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"add_chore\": {\n        \"title\": \"Add Chore\",\n        \"description\": \"Provide the details for the new chore.\",\n        \"data\": {\n          \"chore_name\": \"Chore Name\",\n          \"internal_id\": \"Internal ID\",\n          \"default_points\": \"Default Points\",\n          \"allow_multiple_claims_per_day\": \"Allow Multiple Claims per Day?\",\n          \"partial_allowed\": \"Allow Partial Points?\",\n          \"shared_chore\": \"Shared Chore?\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"chore_description\": \"Description (optional)\",\n          \"chore_labels\": \"Chore Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"recurring_frequency\": \"Recurring Frequency\",\n          \"custom_interval\": \"Custom Recurring Frequency Interval (only use if Custom Recurring Frequency is set)\",\n          \"custom_interval_unit\": \"Custom Recurring Frequency Period\",\n          \"applicable_days\": \"Applicable Days\",\n          \"due_date\": \"Due Date\",\n          \"notify_on_claim\": \"Notify on Claim\",\n          \"notify_on_approval\": \"Notify on Approval\",\n          \"notify_on_disapproval\": \"Notify on Disapproval\"\n        }\n      },\n      \"add_badge\": {\n        \"title\": \"Add Badge\",\n        \"description\": \"Provide the details for the new badge.\",\n        \"data\": {\n          \"badge_name\": \"Badge Name\",\n          \"internal_id\": \"Internal ID\",\n          \"threshold_type\": \"Threshold Type\",\n          \"threshold_value\": \"Threshold Value\",\n          \"points_multiplier\": \"Points Multiplier\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"badge_description\": \"Description (optional)\",\n          \"badge_labels\": \"Badge Labels\"\n        }\n      },\n      \"add_reward\": {\n        \"title\": \"Add Reward\",\n        \"description\": \"Provide the details for the new reward.\",\n        \"data\": {\n          \"reward_name\": \"Reward Name\",\n          \"internal_id\": \"Internal ID\",\n          \"reward_cost\": \"Reward Cost\",\n          \"reward_description\": \"Description (optional)\",\n          \"reward_labels\": \"Reward Labels\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"add_penalty\": {\n        \"title\": \"Add Penalty\",\n        \"description\": \"Provide the details for the new penalty.\",\n        \"data\": {\n          \"penalty_name\": \"Penalty Name\",\n          \"penalty_description\": \"Description (optional)\",\n          \"penalty_labels\": \"Penalty Labels\",\n          \"internal_id\": \"Internal ID\",\n          \"penalty_points\": \"Penalty Points (negative)\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"add_bonus\": {\n        \"title\": \"Add Bonus\",\n        \"description\": \"Provide the details for the new bonus.\",\n        \"data\": {\n          \"bonus_name\": \"Bonus Name\",\n          \"bonus_description\": \"Description (optional)\",\n          \"bonus_labels\": \"Bonus Labels\",\n          \"internal_id\": \"Internal ID\",\n          \"bonus_points\": \"Bonus Points\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"add_achievement\": {\n        \"title\": \"Define Achievement\",\n        \"description\": \"Enter details for each achievement.\",\n        \"data\": {\n          \"name\": \"Achievement Name\",\n          \"description\": \"Description (optional)\",\n          \"achievement_labels\": \"Achievement Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"type\": \"Type of Achievement\",\n          \"selected_chore_id\": \"Select Chore Associated\",\n          \"criteria\": \"Criteria (optional)\",\n          \"target_value\": \"Achievement Target\",\n          \"reward_points\": \"Extra Points for Completing Achievement\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"add_challenge\": {\n        \"title\": \"Define Challenge\",\n        \"description\": \"Enter details for each challenge.\",\n        \"data\": {\n          \"name\": \"Challenge Name\",\n          \"description\": \"Description (optional)\",\n          \"challenge_labels\": \"Challenge Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"type\": \"Type of Challenge\",\n          \"selected_chore_id\": \"Select Chore Associated with Challenge (Optional)\",\n          \"criteria\": \"Criteria (optional)\",\n          \"target_value\": \"Challenges Target\",\n          \"reward_points\": \"Extra Points for Completing Challenge\",\n          \"start_date\": \"Start Date\",\n          \"end_date\": \"End Date\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"edit_kid\": {\n        \"title\": \"Edit Kid\",\n        \"description\": \"Modify the details of the selected kid.\",\n        \"data\": {\n          \"kid_name\": \"Kid Name\",\n          \"ha_user\": \"Home Assistant User\",\n          \"enable_mobile_notifications\": \"Enable Mobile Notifications\",\n          \"mobile_notify_service\": \"Notify Service\",\n          \"enable_persistent_notifications\": \"Enable Persistent Notifications\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"edit_parent\": {\n        \"title\": \"Edit Parent\",\n        \"description\": \"Modify the details of the selected parent.\",\n        \"data\": {\n          \"parent_name\": \"Parent Name\",\n          \"ha_user_id\": \"Home Assistant User\",\n          \"associated_kids\": \"Associated Kids\",\n          \"enable_mobile_notifications\": \"Enable Mobile Notifications\",\n          \"mobile_notify_service\": \"Notify Service\",\n          \"enable_persistent_notifications\": \"Enable Persistent Notifications\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"edit_chore\": {\n        \"title\": \"Edit Chore\",\n        \"description\": \"Modify the details of the selected chore.\",\n        \"data\": {\n          \"chore_name\": \"Chore Name\",\n          \"internal_id\": \"Internal ID\",\n          \"default_points\": \"Default Points\",\n          \"allow_multiple_claims_per_day\": \"Allow Multiple Claims per Day?\",\n          \"partial_allowed\": \"Allow Partial Points?\",\n          \"shared_chore\": \"Shared Chore?\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"chore_description\": \"Description (optional)\",\n          \"chore_labels\": \"Chore Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"recurring_frequency\": \"Recurring Frequency\",\n          \"custom_interval\": \"Custom Recurring Frequency Interval (only use if Custom Recurring Frequency is set)\",\n          \"custom_interval_unit\": \"Custom Recurring Frequency Period\",\n          \"applicable_days\": \"Applicable Days\",\n          \"due_date\": \"Due Date\",\n          \"notify_on_claim\": \"Notify on Claim\",\n          \"notify_on_approval\": \"Notify on Approval\",\n          \"notify_on_disapproval\": \"Notify on Disapproval\"\n        }\n      },\n      \"edit_badge\": {\n        \"title\": \"Edit Badge\",\n        \"description\": \"Modify the details of the selected badge.\",\n        \"data\": {\n          \"badge_name\": \"Badge Name\",\n          \"internal_id\": \"Internal ID\",\n          \"threshold_type\": \"Threshold Type\",\n          \"threshold_value\": \"Threshold Value\",\n          \"points_multiplier\": \"Points Multiplier\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"badge_description\": \"Description (optional)\",\n          \"badge_labels\": \"Badge Labels\"\n        }\n      },\n      \"edit_reward\": {\n        \"title\": \"Edit Reward\",\n        \"description\": \"Modify the details of the selected reward.\",\n        \"data\": {\n          \"reward_name\": \"Reward Name\",\n          \"internal_id\": \"Internal ID\",\n          \"reward_cost\": \"Reward Cost\",\n          \"reward_description\": \"Description (optional)\",\n          \"reward_labels\": \"Reward Labels\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"edit_penalty\": {\n        \"title\": \"Edit Penalty\",\n        \"description\": \"Modify the details of the selected penalty.\",\n        \"data\": {\n          \"penalty_name\": \"Penalty Name\",\n          \"penalty_description\": \"Description (optional)\",\n          \"penalty_labels\": \"Penalty Labels\",\n          \"internal_id\": \"Internal ID\",\n          \"penalty_points\": \"Penalty Points (negative)\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"edit_bonus\": {\n        \"title\": \"Edit Bonus\",\n        \"description\": \"Modify the details of the selected bonus.\",\n        \"data\": {\n          \"bonus_name\": \"Bonus Name\",\n          \"bonus_description\": \"Description (optional)\",\n          \"bonus_labels\": \"Bonus Labels\",\n          \"internal_id\": \"Internal ID\",\n          \"bonus_points\": \"Bonus Points\",\n          \"icon\": \"Icon (mdi:xxx)\"\n        }\n      },\n      \"edit_achievement\": {\n        \"title\": \"Define Achievement\",\n        \"description\": \"Enter details for each achievement.\",\n        \"data\": {\n          \"name\": \"Achievement Name\",\n          \"description\": \"Description (optional)\",\n          \"achievement_labels\": \"Achievement Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"type\": \"Type of Achievement\",\n          \"selected_chore_id\": \"Select Chore Associated\",\n          \"criteria\": \"Criteria (optional)\",\n          \"target_value\": \"Achievement Target\",\n          \"reward_points\": \"Extra Points for Completing Achievement\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"edit_challenge\": {\n        \"title\": \"Define Challenge\",\n        \"description\": \"Enter details for each challenge.\",\n        \"data\": {\n          \"name\": \"Challenge Name\",\n          \"description\": \"Description (optional)\",\n          \"challenge_labels\": \"Challenge Labels\",\n          \"icon\": \"Icon (mdi:xxx)\",\n          \"assigned_kids\": \"Assigned Kids\",\n          \"type\": \"Type of Challenge\",\n          \"selected_chore_id\": \"Select Chore Associated with Challenge (Optional)\",\n          \"criteria\": \"Criteria (optional)\",\n          \"target_value\": \"Challenges Target\",\n          \"reward_points\": \"Extra Points for Completing Challenge\",\n          \"start_date\": \"Start Date\",\n          \"end_date\": \"End Date\",\n          \"internal_id\": \"Internal ID\"\n        }\n      },\n      \"delete_kid\": {\n        \"title\": \"Delete Kid\",\n        \"description\": \"Are you sure you want to delete the kid {kid_name}?\",\n        \"data\": {}\n      },\n      \"delete_parent\": {\n        \"title\": \"Delete Parent\",\n        \"description\": \"Are you sure you want to delete the parent {parent_name}?\",\n        \"data\": {}\n      },\n      \"delete_chore\": {\n        \"title\": \"Delete Chore\",\n        \"description\": \"Are you sure you want to delete the chore {chore_name}?\",\n        \"data\": {}\n      },\n      \"delete_badge\": {\n        \"title\": \"Delete Badge\",\n        \"description\": \"Are you sure you want to delete the badge {badge_name}?\",\n        \"data\": {}\n      },\n      \"delete_reward\": {\n        \"title\": \"Delete Reward\",\n        \"description\": \"Are you sure you want to delete the reward {reward_name}?\",\n        \"data\": {}\n      },\n      \"delete_penalty\": {\n        \"title\": \"Delete Penalty\",\n        \"description\": \"Are you sure you want to delete the penalty {penalty_name}?\",\n        \"data\": {}\n      },\n      \"delete_bonus\": {\n        \"title\": \"Delete Bonus\",\n        \"description\": \"Are you sure you want to delete the bonus {bonus_name}?\",\n        \"data\": {}\n      },\n      \"delete_achievement\": {\n        \"title\": \"Delete Achievement\",\n        \"description\": \"Are you sure you want to delete the achievement {achievement_name}?\",\n        \"data\": {}\n      },\n      \"delete_challenge\": {\n        \"title\": \"Delete Challenge\",\n        \"description\": \"Are you sure you want to delete the challenge {challenge_name}?\",\n        \"data\": {}\n      }\n    },\n    \"error\": {\n      \"a_chore_must_be_selected\": \"A chore must be selected\",\n      \"duplicate_achievement\": \"An achievement with this name already exists\",\n      \"duplicate_badge\": \"A badge with this name already exists\",\n      \"duplicate_challenge\": \"A challenge with this name already exists\",\n      \"duplicate_chore\": \"A chore with this name already exists\",\n      \"duplicate_kid\": \"A kid with this name already exists\",\n      \"duplicate_parent\": \"A parent with this name already exists\",\n      \"duplicate_penalty\": \"A penalty with this name already exists\",\n      \"duplicate_reward\": \"A reward with this name already exists\",\n      \"duplicate_bonus\": \"A bonus with this name already exists\",\n      \"due_date_in_past\": \"Due date must be in the future.\",\n      \"end_date_in_past\": \"End Date must be in the future.\",\n      \"end_date_not_after_start_date\": \"End date must be later than start date\",\n      \"invalid_badge\": \"Invalid badge\",\n      \"invalid_badge_count\": \"Invalid badge count\",\n      \"invalid_chore\": \"Invalid chore\",\n      \"invalid_chore_count\": \"Invalid chore count\",\n      \"invalid_due_date\": \"Invalid due date\",\n      \"invalid_end_date\": \"Invalid end date.\",\n      \"invalid_kid_count\": \"Invalid kid count\",\n      \"invalid_kid_name\": \"Invalid kid name\",\n      \"invalid_penalty\": \"Invalid penalty\",\n      \"invalid_penalty_count\": \"Invalid penalty count\",\n      \"invalid_bonus\": \"Invalid bonus\",\n      \"invalid_bonus_count\": \"Invalid bonus count\",\n      \"invalid_reward\": \"Invalid reward\",\n      \"invalid_reward_count\": \"Invalid reward count\",\n      \"invalid_start_date\": \"Invalid start date.\",\n      \"start_date_in_past\": \"Start Date must be in the future.\"\n    },\n    \"abort\": {\n      \"invalid_action\": \"Invalid Action\",\n      \"invalid_achievement\": \"Invalid Achievement\",\n      \"invalid_badge\": \"Invalid Badge\",\n      \"invalid_challenge\": \"Invalid Challenge\",\n      \"invalid_chore\": \"Invalid Chore\",\n      \"invalid_entity\": \"Invalid Entity\",\n      \"invalid_kid\": \"Invalid Kid\",\n      \"invalid_parent\": \"Invalid Parent\",\n      \"invalid_penalty\": \"Invalid Penalty\",\n      \"invalid_reward\": \"Invalid Reward\",\n      \"invalid_bonus\": \"Invalid Bonus\",\n      \"no_kid\": \"No Kids are setup for edit. Add one entry first.\",\n      \"no_parent\": \"No Parents are setup for edit. Add one entry first.\",\n      \"no_chore\": \"No Chores are setup for edit. Add one entry first.\",\n      \"no_badge\": \"No Badges are setup for edit. Add one entry first.\",\n      \"no_reward\": \"No Rewards are setup for edit. Add one entry first.\",\n      \"no_penalty\": \"No Penalties are setup for edit. Add one entry first.\",\n      \"no_bonus\": \"No Bonuses are setup for edit. Add one entry first.\",\n      \"no_achievement\": \"No Achievements are setup for edit. Add one entry first.\",\n      \"no_challenge\": \"No Challenges are setup for edit. Add one entry first.\",\n      \"setup_complete\": \"Setup Complete\"\n    }\n  },\n  \"selector\": {\n    \"main_menu\": {\n      \"options\": {\n        \"manage_points\": \"Manage Points\",\n        \"manage_kid\": \"Manage Kid\",\n        \"manage_parent\": \"Manage Parent\",\n        \"manage_chore\": \"Manage Chore\",\n        \"manage_badge\": \"Manage Badge\",\n        \"manage_reward\": \"Manage Reward\",\n        \"manage_penalty\": \"Manage Penalty\",\n        \"manage_bonus\": \"Manage Bonus\",\n        \"manage_achievement\": \"Manage Achievement\",\n        \"manage_challenge\": \"Manage Challenge\",\n        \"done\": \"Finish Setup\"\n      }\n    },\n    \"manage_actions\": {\n      \"options\": {\n        \"add\": \"Add\",\n        \"edit\": \"Edit\",\n        \"delete\": \"Delete\",\n        \"back\": \"Back to Main Menu\"\n      }\n    },\n    \"recurring_frequency\": {\n      \"options\": {\n        \"none\": \"None\",\n        \"daily\": \"Daily\",\n        \"weekly\": \"Weekly\",\n        \"biweekly\": \"Biweekly\",\n        \"monthly\": \"Monthly\",\n        \"custom\": \"Custom\"\n      }\n    },\n    \"custom_interval_unit\": {\n      \"options\": {\n        \"days\": \"Days\",\n        \"weeks\": \"Weeks\",\n        \"months\": \"Months\"\n      }\n    },\n    \"applicable_days\": {\n      \"options\": {\n        \"mon\": \"Monday\",\n        \"tue\": \"Tuesday\",\n        \"wed\": \"Wednesday\",\n        \"thu\": \"Thursday\",\n        \"fri\": \"Friday\",\n        \"sat\": \"Saturday\",\n        \"sun\": \"Sunday\"\n      }\n    },\n    \"threshold_type\": {\n      \"options\": {\n        \"points\": \"Points\",\n        \"chore_count\": \"Chore Count\"\n      }\n    }\n  },\n  \"services\": {\n    \"claim_chore\": {\n      \"name\": \"Claim Chore\",\n      \"description\": \"A kid claims a chore, marking it as 'claimed' for parental approval.\",\n      \"fields\": {\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The name of the kid claiming the chore.\",\n          \"example\": \"Alice\"\n        },\n        \"chore_name\": {\n          \"name\": \"Chore Name\",\n          \"description\": \"The name of the chore to claim.\",\n          \"example\": \"Wash Dishes\"\n        }\n      }\n    },\n    \"approve_chore\": {\n      \"name\": \"Approve Chore\",\n      \"description\": \"Parent approves the chore, awarding points.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Parent Name\",\n          \"description\": \"The parent approving the chore.\",\n          \"example\": \"Mom\"\n        },\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The name of the kid who performed the chore.\",\n          \"example\": \"Alice\"\n        },\n        \"chore_name\": {\n          \"name\": \"Chore Name\",\n          \"description\": \"The name of the chore being approved.\",\n          \"example\": \"Wash Dishes\"\n        },\n        \"points_awarded\": {\n          \"name\": \"Points Awarded\",\n          \"description\": \"Points to award (optional; defaults to the chore's points).\",\n          \"example\": 3\n        }\n      }\n    },\n    \"disapprove_chore\": {\n      \"name\": \"Disapprove Chore\",\n      \"description\": \"Parent disapproves a chore for a kid, reverting its status.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Parent Name\",\n          \"description\": \"The parent disapproving the chore.\",\n          \"example\": \"Mom\"\n        },\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The name of the kid whose chore is being disapproved.\",\n          \"example\": \"Alice\"\n        },\n        \"chore_name\": {\n          \"name\": \"Chore Name\",\n          \"description\": \"The name of the chore being disapproved.\",\n          \"example\": \"Clean Room\"\n        }\n      }\n    },\n    \"redeem_reward\": {\n      \"name\": \"Redeem Reward\",\n      \"description\": \"A parent redeems a reward for a kid, deducting points.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Parent Name\",\n          \"description\": \"The parent authorizing the reward redemption.\",\n          \"example\": \"Mom\"\n        },\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid redeeming the reward.\",\n          \"example\": \"Alice\"\n        },\n        \"reward_name\": {\n          \"name\": \"Reward Name\",\n          \"description\": \"The name of the reward to redeem.\",\n          \"example\": \"Extra Screen Time\"\n        }\n      }\n    },\n    \"approve_reward\": {\n      \"name\": \"Approve Reward\",\n      \"description\": \"Parent approves a reward claimed by a kid.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Parent Name\",\n          \"description\": \"The parent approving the reward.\",\n          \"example\": \"Mom\"\n        },\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid who is redeeming the reward.\",\n          \"example\": \"Alice\"\n        },\n        \"reward_name\": {\n          \"name\": \"Reward Name\",\n          \"description\": \"The name of the reward being approved.\",\n          \"example\": \"Extra Screen Time\"\n        }\n      }\n    },\n    \"disapprove_reward\": {\n      \"name\": \"Disapprove Reward\",\n      \"description\": \"Parent disapproves a reward redemption for a kid.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Parent Name\",\n          \"description\": \"The parent disapproving the reward.\",\n          \"example\": \"Dad\"\n        },\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid whose reward redemption is being disapproved.\",\n          \"example\": \"Alice\"\n        },\n        \"reward_name\": {\n          \"name\": \"Reward Name\",\n          \"description\": \"The name of the reward being disapproved.\",\n          \"example\": \"Extra Screen Time\"\n        }\n      }\n    },\n    \"apply_penalty\": {\n      \"name\": \"Apply Penalty\",\n      \"description\": \"A parent applies a penalty to deduct points.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Parent Name\",\n          \"description\": \"The parent applying the penalty.\",\n          \"example\": \"Dad\"\n        },\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid receiving the penalty.\",\n          \"example\": \"Alice\"\n        },\n        \"penalty_name\": {\n          \"name\": \"Penalty Name\",\n          \"description\": \"The name of the penalty to apply.\",\n          \"example\": \"Yelling\"\n        }\n      }\n    },\n    \"apply_bonus\": {\n      \"name\": \"Apply Bonus\",\n      \"description\": \"A parent applies a bonus to a kid, awarding points.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Parent Name\",\n          \"description\": \"The parent applying the bonus.\",\n          \"example\": \"Mom\"\n        },\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid receiving the bonus.\",\n          \"example\": \"Alice\"\n        },\n        \"bonus_name\": {\n          \"name\": \"Bonus Name\",\n          \"description\": \"The name of the bonus to apply.\",\n          \"example\": \"Extra Helpful\"\n        }\n      }\n    },\n    \"reset_all_data\": {\n      \"name\": \"Reset All Data\",\n      \"description\": \"Completely clears the KidsChores data from storage.\"\n    },\n    \"reset_all_chores\": {\n      \"name\": \"Reset All Chores\",\n      \"description\": \"Manually reset chores to pending state, removing claims and approvals.\"\n    },\n    \"reset_overdue_chores\": {\n      \"name\": \"Reset Overdue Chores\",\n      \"description\": \"Reset overdue chore(s) back to the Pending state and reschedule them based on their recurring frequency and previous due date. You may optionally provide a chore_id (or chore_name) to reset a specific chore and optionally a kid_name to reset the chore only for that kid.\",\n      \"fields\": {\n        \"chore_id\": {\n          \"name\": \"Chore ID\",\n          \"description\": \"The internal ID of the chore to reset (optional if chore_name is provided).\",\n          \"example\": \"abc123\"\n        },\n        \"chore_name\": {\n          \"name\": \"Chore Name\",\n          \"description\": \"The name of the chore to reset (optional if chore_id is provided).\",\n          \"example\": \"Wash Dishes\"\n        },\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid receiving the penalty.\",\n          \"example\": \"Alice\"\n        }\n      }\n    },\n    \"set_chore_due_date\": {\n      \"name\": \"Set/Reset Chore Due Date\",\n      \"description\": \"Set (or clear) the due date for a chore. Provide the chore name and, if desired, a new due date. If no due date is provided the existing due date will be cleared. The service will reject due dates set in the past.\",\n      \"fields\": {\n        \"chore_name\": {\n          \"name\": \"Chore Name\",\n          \"description\": \"The name of the chore to update\",\n          \"example\": \"Wash Dishes\"\n        },\n        \"due_date\": {\n          \"name\": \"Due Date\",\n          \"description\": \"The new due date for the chore. Use the date/time selector to choose a valid date and time (in your local timezone). Leave empty to clear the due date.\",\n          \"example\": \"2025-03-01T23:59:00Z\"\n        }\n      }\n    },\n    \"skip_chore_due_date\": {\n      \"name\": \"Skip Chore Due Date\",\n      \"description\": \"Skip the current due date of a recurring chore. This service immediately reschedules the chore's due date based on its recurring frequency and resets its state to pending. Any pending claims or approvals will be removed.\",\n      \"fields\": {\n        \"chore_id\": {\n          \"name\": \"Chore ID\",\n          \"description\": \"The internal ID of the chore to reset (optional if chore_name is provided).\",\n          \"example\": \"abc123\"\n        },\n        \"chore_name\": {\n          \"name\": \"Chore Name\",\n          \"description\": \"The name of the chore to reset (optional if chore_id is provided).\",\n          \"example\": \"Wash Dishes\"\n        }\n      }\n    },\n    \"reset_penalties\": {\n      \"name\": \"Reset Penalties\",\n      \"description\": \"Reset all applied penalties for all kids. Optionally, provide penalty name to reset a specific penalty across all kids. Use kid name to reset all penalties for a specific kid. Combine both to reset a specific penalty for a specific kid.\",\n      \"fields\": {\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid for which penalties will be reset.\",\n          \"example\": \"Alice\"\n        },\n        \"penalty_name\": {\n          \"name\": \"Penalty Name\",\n          \"description\": \"The name of the penalty to reset.\",\n          \"example\": \"Yelling\"\n        }\n      }\n    },\n    \"reset_bonuses\": {\n      \"name\": \"Reset Bonuses\",\n      \"description\": \"Reset all applied bonuses for all kids. Optionally, provide bonus name to reset a specific bonus across all kids. Use kid name to reset all bonuses for a specific kid. Combine both to reset a specific bonus for a specific kid.\",\n      \"fields\": {\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid for which bonuses will be reset.\",\n          \"example\": \"Alice\"\n        },\n        \"bonus_name\": {\n          \"name\": \"Bonus Name\",\n          \"description\": \"The name of the bonus to reset.\",\n          \"example\": \"Helping\"\n        }\n      }\n    },\n    \"reset_rewards\": {\n      \"name\": \"Reset Rewards\",\n      \"description\": \"Reset all reward claim and approval counts for all kids. Optionally, provide reward name to reset a specific reward counts across all kids. Use kid name to reset all reward counts for a specific kid. Combine both to reset a specific reward for a specific kid.\",\n      \"fields\": {\n        \"kid_name\": {\n          \"name\": \"Kid Name\",\n          \"description\": \"The kid for which reward counts will be reset.\",\n          \"example\": \"Alice\"\n        },\n        \"reward_name\": {\n          \"name\": \"Reward Name\",\n          \"description\": \"The name of the reward to reset.\",\n          \"example\": \"Ice Cream\"\n        }\n      }\n    }\n  },\n  \"entity\": {\n    \"sensor\": {\n      \"chore_status_sensor\": {\n        \"name\": \"{kid_name} - Status - {chore_name}\",\n        \"state\": {\n          \"pending\": \"Pending\",\n          \"approved\": \"Approved\",\n          \"claimed\": \"Claimed\",\n          \"overdue\": \"Overdue\",\n          \"unknown\": \"Unknown\",\n          \"none\": \"None\",\n          \"approved_in_part\": \"Approved (in-part)\",\n          \"claimed_in_part\": \"Claimed (in-part)\"\n        },\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Kid Name\"\n          },\n          \"chore_name\": {\n            \"name\": \"Chore Name\"\n          },\n          \"shared_chore\": {\n            \"name\": \"Shared Chore\",\n            \"state\": {\n              \"true\": \"Yes\",\n              \"false\": \"No\"\n            }\n          },\n          \"recurring_frequency\": {\n            \"name\": \"Recurring Frequency\",\n            \"state\": {\n              \"none\": \"None\",\n              \"daily\": \"Daily\",\n              \"weekly\": \"Weekly\",\n              \"biweekly\": \"Biweekly\",\n              \"monthly\": \"Monthly\",\n              \"custom\": \"Custom\"\n            }\n          },\n          \"applicable_days\": {\n            \"name\": \"Applicable Days\",\n            \"state\": {\n              \"mon\": \"Monday\",\n              \"tue\": \"Tuesday\",\n              \"wed\": \"Wednesday\",\n              \"thu\": \"Thursday\",\n              \"fri\": \"Friday\",\n              \"sat\": \"Saturday\",\n              \"sun\": \"Sunday\"\n            }\n          },\n          \"due_date\": {\n            \"name\": \"Due Date\"\n          },\n          \"default_points\": {\n            \"name\": \"Default Points\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"chore_claims_count\": {\n            \"name\": \"Chore Claims Count\"\n          },\n          \"chore_approvals_count\": {\n            \"name\": \"Chore Approvals Count\"\n          },\n          \"chore_current_streak\": {\n            \"name\": \"Chore Current Streak\"\n          },\n          \"chore_highest_streak\": {\n            \"name\": \"Chore Highest Streak\"\n          },\n          \"global_state\": {\n            \"name\": \"Global State\",\n            \"state\": {\n              \"pending\": \"Pending\",\n              \"approved\": \"Approved\",\n              \"claimed\": \"Claimed\",\n              \"overdue\": \"Overdue\",\n              \"unknown\": \"Unknown\",\n              \"none\": \"None\",\n              \"approved_in_part\": \"Approved (in-part)\",\n              \"claimed_in_part\": \"Claimed (in-part)\",\n              \"independent\": \"Independent\"\n            }\n          },\n          \"partial_allowed\": {\n            \"name\": \"Partially Allowed\",\n            \"state\": {\n              \"true\": \"Yes\",\n              \"false\": \"No\"\n            }\n          },\n          \"allow_multiple_claims_per_day\": {\n            \"name\": \"Allow Multiple Claims per Day\",\n            \"state\": {\n              \"true\": \"Yes\",\n              \"false\": \"No\"\n            }\n          },\n          \"assigned_kids\": {\n            \"name\": \"Assigned Kids\"\n          },\n          \"custom_frequency_interval\": {\n            \"name\": \"Custom Frequency\"\n          },\n          \"custom_frequency_unit\": {\n            \"name\": \"Custom Frequency Period\",\n            \"state\": {\n              \"days\": \"Days\",\n              \"weeks\": \"Weeks\",\n              \"months\": \"Months\"\n            }\n          },\n          \"chore_approvals_today\": {\n            \"name\": \"Chore Approvals Today\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"kid_points_sensor\": {\n        \"name\": \"{kid_name} - {points}\"\n      },\n      \"kid_max_points_ever_sensor\": {\n        \"name\": \"{kid_name} - Maximum Points Ever\"\n      },\n      \"chores_completed_total_sensor\": {\n        \"name\": \"{kid_name} - Chores Completed - Total\"\n      },\n      \"chores_completed_daily_sensor\": {\n        \"name\": \"{kid_name} - Chores Completed - Daily\"\n      },\n      \"chores_completed_weekly_sensor\": {\n        \"name\": \"{kid_name} - Chores Completed - Weekly\"\n      },\n      \"chores_completed_monthly_sensor\": {\n        \"name\": \"{kid_name} - Chores Completed - Monthly\"\n      },\n      \"kid_badges_sensor\": {\n        \"name\": \"{kid_name} - Badges Earned\"\n      },\n      \"kids_highest_badge_sensor\": {\n        \"name\": \"{kid_name} - Badge\",\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Kid Name\"\n          },\n          \"all_earned_badges\": {\n            \"name\": \"All Earned Badges\"\n          },\n          \"highest_badge_threshold_value\": {\n            \"name\": \"Highest Badge Threshold\"\n          },\n          \"points_multiplier\": {\n            \"name\": \"Points Multiplier\"\n          },\n          \"points_to_next_badge\": {\n            \"name\": \"Points to Next Badge\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"badge_sensor\": {\n        \"name\": \"Badge - {badge_name}\",\n        \"state_attributes\": {\n          \"threshold_type\": {\n            \"name\": \"Threshold Type\",\n            \"state\": {\n              \"points\": \"Points\",\n              \"chore_count\": \"Chore Count\"\n            }\n          },\n          \"points_multiplier\": {\n            \"name\": \"Points Multiplier\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"kids_earned\": {\n            \"name\": \"Kids Earned\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"pending_chores_approvals_sensor\": {\n        \"name\": \"Pending Chore Approvals\"\n      },\n      \"pending_rewards_approvals_sensor\": {\n        \"name\": \"Pending Reward Approvals\"\n      },\n      \"reward_claims_sensor\": {\n        \"name\": \"{kid_name} - Claims - {reward_name}\"\n      },\n      \"reward_approvals_sensor\": {\n        \"name\": \"{kid_name} - Approvals - {reward_name}\"\n      },\n      \"shared_chore_global_status_sensor\": {\n        \"name\": \"{chore_name} - Global Status\",\n        \"state\": {\n          \"pending\": \"Pending\",\n          \"approved\": \"Approved\",\n          \"claimed\": \"Claimed\",\n          \"overdue\": \"Overdue\",\n          \"unknown\": \"Unknown\",\n          \"none\": \"None\",\n          \"approved_in_part\": \"Approved (in-part)\",\n          \"claimed_in_part\": \"Claimed (in-part)\",\n          \"independent\": \"Independent\"\n        },\n        \"state_attributes\": {\n          \"chore_name\": {\n            \"name\": \"Chore Name\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"recurring_frequency\": {\n            \"name\": \"Recurring Frequency\",\n            \"state\": {\n              \"none\": \"None\",\n              \"daily\": \"Daily\",\n              \"weekly\": \"Weekly\",\n              \"biweekly\": \"Biweekly\",\n              \"monthly\": \"Monthly\",\n              \"custom\": \"Custom\"\n            }\n          },\n          \"applicable_days\": {\n            \"name\": \"Applicable Days\",\n            \"state\": {\n              \"mon\": \"Monday\",\n              \"tue\": \"Tuesday\",\n              \"wed\": \"Wednesday\",\n              \"thu\": \"Thursday\",\n              \"fri\": \"Friday\",\n              \"sat\": \"Saturday\",\n              \"sun\": \"Sunday\"\n            }\n          },\n          \"due_date\": {\n            \"name\": \"Due Date\"\n          },\n          \"default_points\": {\n            \"name\": \"Default Points\"\n          },\n          \"partial_allowed\": {\n            \"name\": \"Partially Allowed\",\n            \"state\": {\n              \"true\": \"Yes\",\n              \"false\": \"No\"\n            }\n          },\n          \"allow_multiple_claims_per_day\": {\n            \"name\": \"Allow Multiple Claims per Day\",\n            \"state\": {\n              \"true\": \"Yes\",\n              \"false\": \"No\"\n            }\n          },\n          \"assigned_kids\": {\n            \"name\": \"Assigned Kids\"\n          },\n          \"chore_approvals_today\": {\n            \"name\": \"Chore Approvals Today\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"reward_status_sensor\": {\n        \"name\": \"{kid_name} - Reward Status - {reward_name}\",\n        \"state\": {\n          \"not_claimed\": \"Not Claimed\",\n          \"approved\": \"Approved\",\n          \"claimed\": \"Claimed\",\n          \"unknown\": \"Unknown\",\n          \"none\": \"None\"\n        },\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Kid Name\"\n          },\n          \"reward_name\": {\n            \"name\": \"Reward Name\"\n          },\n          \"reward_cost\": {\n            \"name\": \"Reward Cost\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"chore_claims_sensor\": {\n        \"name\": \"{kid_name} - Claims - {chore_name}\"\n      },\n      \"chore_approvals_sensor\": {\n        \"name\": \"{kid_name} - Approvals - {chore_name}\"\n      },\n      \"penalty_applies_sensor\": {\n        \"name\": \"{kid_name} - Penalties Applied - {penalty_name}\",\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Kid Name\"\n          },\n          \"penalty_name\": {\n            \"name\": \"Penalty Name\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"penalty_points\": {\n            \"name\": \"Penalty Points\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"bonus_applies_sensor\": {\n        \"name\": \"{kid_name} - Bonus Applies - {bonus_name}\",\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Kid Name\"\n          },\n          \"bonus_name\": {\n            \"name\": \"Bonus Name\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"bonus_points\": {\n            \"name\": \"Bonus Points\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"kid_points_earned_daily_sensor\": {\n        \"name\": \"{kid_name} - Points Earned - Daily\"\n      },\n      \"kid_points_earned_weekly_sensor\": {\n        \"name\": \"{kid_name} - Points Earned - Weekly\"\n      },\n      \"kid_points_earned_monthly_sensor\": {\n        \"name\": \"{kid_name} - Points Earned - Monthly\"\n      },\n      \"achievement_state_sensor\": {\n        \"name\": \"Achievement - {achievement_name}\",\n        \"state_attributes\": {\n          \"achievement_name\": {\n            \"name\": \"Achievement Name\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"assigned_kids\": {\n            \"name\": \"Assigned Kids\"\n          },\n          \"type\": {\n            \"name\": \"Type\",\n            \"state\": {\n              \"chore_total\": \"Chore Total\",\n              \"chore_streak\": \"Chore Streak\",\n              \"daily_minimum\": \"Daily Minimum\"\n            }\n          },\n          \"associated_chore\": {\n            \"name\": \"Associated Chore\"\n          },\n          \"critera\": {\n            \"name\": \"Criteria\"\n          },\n          \"target_value\": {\n            \"name\": \"Target\"\n          },\n          \"reward_points\": {\n            \"name\": \"Reward Points\"\n          },\n          \"kids_earned\": {\n            \"name\": \"Earned by\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"challenge_state_sensor\": {\n        \"name\": \"Challenge - {challenge_name}\",\n        \"state_attributes\": {\n          \"challenge_name\": {\n            \"name\": \"Challenge Name\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"assigned_kids\": {\n            \"name\": \"Assigned Kids\"\n          },\n          \"type\": {\n            \"name\": \"Type\",\n            \"state\": {\n              \"total_within_window\": \"Total Within Window\",\n              \"daily_minimum\": \"Daily Minimum\"\n            }\n          },\n          \"associated_chore\": {\n            \"name\": \"Associated Chore\"\n          },\n          \"critera\": {\n            \"name\": \"Criteria\"\n          },\n          \"target_value\": {\n            \"name\": \"Target\"\n          },\n          \"reward_points\": {\n            \"name\": \"Reward Points\"\n          },\n          \"start_date\": {\n            \"name\": \"Start Date\"\n          },\n          \"end_date\": {\n            \"name\": \"End Date\"\n          },\n          \"kids_earned\": {\n            \"name\": \"Earned by\"\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"achievement_progress_sensor\": {\n        \"name\": \"{kid_name} - Progress - {achievement_name}\",\n        \"state_attributes\": {\n          \"achievement_name\": {\n            \"name\": \"Achievement Name\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"assigned_kids\": {\n            \"name\": \"Assigned Kids\"\n          },\n          \"type\": {\n            \"name\": \"Type\",\n            \"state\": {\n              \"chore_total\": \"Chore Total\",\n              \"chore_streak\": \"Chore Streak\",\n              \"daily_minimum\": \"Daily Minimum\"\n            }\n          },\n          \"associated_chore\": {\n            \"name\": \"Associated Chore\"\n          },\n          \"critera\": {\n            \"name\": \"Criteria\"\n          },\n          \"target_value\": {\n            \"name\": \"Target\"\n          },\n          \"reward_points\": {\n            \"name\": \"Reward Points\"\n          },\n          \"raw_progress\": {\n            \"name\": \"Progress\"\n          },\n          \"awarded\": {\n            \"name\": \"Awarded\",\n            \"state\": {\n              \"true\": \"Yes\",\n              \"false\": \"No\"\n            }\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"challenge_progress_sensor\": {\n        \"name\": \"{kid_name} - Progress - {challenge_name}\",\n        \"state_attributes\": {\n          \"challenge_name\": {\n            \"name\": \"Challenge Name\"\n          },\n          \"description\": {\n            \"name\": \"Description\"\n          },\n          \"assigned_kids\": {\n            \"name\": \"Assigned Kids\"\n          },\n          \"type\": {\n            \"name\": \"Type\",\n            \"state\": {\n              \"total_within_window\": \"Total Within Window\",\n              \"daily_minimum\": \"Daily Minimum\"\n            }\n          },\n          \"associated_chore\": {\n            \"name\": \"Associated Chore\"\n          },\n          \"critera\": {\n            \"name\": \"Criteria\"\n          },\n          \"target_value\": {\n            \"name\": \"Target\"\n          },\n          \"reward_points\": {\n            \"name\": \"Reward Points\"\n          },\n          \"start_date\": {\n            \"name\": \"Start Date\"\n          },\n          \"end_date\": {\n            \"name\": \"End Date\"\n          },\n          \"raw_progress\": {\n            \"name\": \"Progress\"\n          },\n          \"awarded\": {\n            \"name\": \"Awarded\",\n            \"state\": {\n              \"true\": \"Yes\",\n              \"false\": \"No\"\n            }\n          },\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"kid_highest_streak_sensor\": {\n        \"name\": \"{kid_name} - Highest Streak\",\n        \"state_attributes\": {\n          \"streaks_by_achievement\": {\n            \"name\": \"Streaks by Achievement\"\n          }\n        }\n      },\n      \"chore_streak_sensor\": {\n        \"name\": \"{kid_name} - Streak - {chore_name}\",\n        \"state_attributes\": {\n          \"last_date\": {\n            \"name\": \"Last Date\"\n          },\n          \"raw_streak\": {\n            \"name\": \"Current Streak\"\n          }\n        }\n      }\n    },\n    \"button\": {\n      \"claim_chore_button\": {\n        \"name\": \"{kid_name} - Claim Chore - {chore_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"approve_chore_button\": {\n        \"name\": \"{kid_name} - Approve Chore - {chore_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"disapprove_chore_button\": {\n        \"name\": \"{kid_name} - Disapprove Chore - {chore_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"claim_reward_button\": {\n        \"name\": \"{kid_name} - Claim Reward - {reward_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"approve_reward_button\": {\n        \"name\": \"{kid_name} - Approve Reward - {reward_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"disapprove_reward_button\": {\n        \"name\": \"{kid_name} - Disapprove Reward - {reward_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"penalty_button\": {\n        \"name\": \"{kid_name} - Apply Penalty - {penalty_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"bonus_button\": {\n        \"name\": \"{kid_name} - Apply Bonus - {bonus_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Labels\"\n          }\n        }\n      },\n      \"manual_adjustment_button\": {\n        \"name\": \"{kid_name} {sign_label} {points_label}\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/kidschores/translations/es.json",
    "content": "{\n  \"title\": \"KidsChores\",\n  \"config\": {\n    \"step\": {\n      \"intro\": {\n        \"title\": \"Bienvenido a KidsChores\",\n        \"description\": \"Este asistente te guiará en la configuración de KidsChores.\"\n      },\n      \"points_label\": {\n        \"title\": \"Etiqueta de Puntos\",\n        \"description\": \"Elige una etiqueta e ícono para los puntos.\",\n        \"data\": {\n          \"points_label\": \"Etiqueta de Puntos\",\n          \"points_icon\": \"Ícono de Puntos\"\n        }\n      },\n      \"kid_count\": {\n        \"title\": \"Número de Niños\",\n        \"description\": \"¿Cuántos niños deseas gestionar?\",\n        \"data\": {\n          \"kid_count\": \"Número de Niños\"\n        }\n      },\n      \"kids\": {\n        \"title\": \"Definir Niño/a\",\n        \"description\": \"Introduce el nombre para cada niño.\",\n        \"data\": {\n          \"kid_name\": \"Nombre del Niño/a\",\n          \"internal_id\": \"ID Interno\",\n          \"ha_user\": \"Usuario de Home Assistant\",\n          \"enable_mobile_notifications\": \"Habilitar Notificaciones Móviles\",\n          \"mobile_notify_service\": \"Servicio de Notificaciones\",\n          \"enable_persistent_notifications\": \"Habilitar Notificaciones Persistentes\"\n        }\n      },\n      \"parent_count\": {\n        \"title\": \"Número de Padres\",\n        \"description\": \"¿Cuántos padres deseas definir inicialmente?\",\n        \"data\": {\n          \"parent_count\": \"Número de Padres\"\n        }\n      },\n      \"parents\": {\n        \"title\": \"Definir Padre/Madre\",\n        \"description\": \"Introduce los datos de cada padre.\",\n        \"data\": {\n          \"parent_name\": \"Nombre del Padre/Madre\",\n          \"ha_user_id\": \"Usuario de Home Assistant\",\n          \"associated_kids\": \"Niños Asociados\",\n          \"enable_mobile_notifications\": \"Habilitar Notificaciones Móviles\",\n          \"mobile_notify_service\": \"Servicio de Notificaciones\",\n          \"enable_persistent_notifications\": \"Habilitar Notificaciones Persistentes\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"chore_count\": {\n        \"title\": \"Número de Tareas\",\n        \"description\": \"¿Cuántas tareas deseas definir?\",\n        \"data\": {\n          \"chore_count\": \"Número de Tareas\"\n        }\n      },\n      \"chores\": {\n        \"title\": \"Definir Tarea\",\n        \"description\": \"Introduce los datos de cada tarea.\",\n        \"data\": {\n          \"chore_name\": \"Nombre de la Tarea\",\n          \"internal_id\": \"ID Interno\",\n          \"default_points\": \"Puntos Predeterminados\",\n          \"allow_multiple_claims_per_day\": \"¿Permitir múltiples reclamaciones por día?\",\n          \"partial_allowed\": \"¿Permitir puntos parciales?\",\n          \"shared_chore\": \"¿Tarea compartida?\",\n          \"assigned_kids\": \"Niños Asignados\",\n          \"chore_description\": \"Descripción (opcional)\",\n          \"chore_labels\": \"Etiquetas de la Tarea\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"recurring_frequency\": \"Frecuencia recurrente\",\n          \"custom_interval\": \"Intervalo de Frecuencia Recurrente Personalizado (usar solo si se configura Frecuencia Recurrente Personalizada)\",\n          \"custom_interval_unit\": \"Periodo de Frecuencia Recurrente Personalizada\",\n          \"applicable_days\": \"Días aplicables\",\n          \"due_date\": \"Fecha de vencimiento\",\n          \"notify_on_claim\": \"Notificar al reclamar\",\n          \"notify_on_approval\": \"Notificar al aprobar\",\n          \"notify_on_disapproval\": \"Notificar al rechazar\"\n        }\n      },\n      \"badge_count\": {\n        \"title\": \"Número de Insignias\",\n        \"description\": \"¿Cuántas insignias deseas definir?\",\n        \"data\": {\n          \"badge_count\": \"Cantidad de Insignias\"\n        }\n      },\n      \"badges\": {\n        \"title\": \"Definir Insignia\",\n        \"description\": \"Introduce los datos de cada insignia.\",\n        \"data\": {\n          \"badge_name\": \"Nombre de la Insignia\",\n          \"internal_id\": \"ID Interno\",\n          \"threshold_type\": \"Tipo de umbral\",\n          \"threshold_value\": \"Valor del umbral\",\n          \"points_multiplier\": \"Multiplicador de puntos\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"badge_description\": \"Descripción (opcional)\",\n          \"badge_labels\": \"Etiquetas de la Insignia\"\n        }\n      },\n      \"reward_count\": {\n        \"title\": \"Número de Recompensas\",\n        \"description\": \"¿Cuántas recompensas deseas definir?\",\n        \"data\": {\n          \"reward_count\": \"Cantidad de Recompensas\"\n        }\n      },\n      \"rewards\": {\n        \"title\": \"Definir Recompensa\",\n        \"description\": \"Introduce los datos de cada recompensa.\",\n        \"data\": {\n          \"reward_name\": \"Nombre de la Recompensa\",\n          \"internal_id\": \"ID Interno\",\n          \"reward_cost\": \"Costo de la Recompensa\",\n          \"reward_description\": \"Descripción (opcional)\",\n          \"reward_labels\": \"Etiquetas de la Recompensa\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"penalty_count\": {\n        \"title\": \"Número de Penalizaciones\",\n        \"description\": \"¿Cuántas penalizaciones deseas definir?\",\n        \"data\": {\n          \"penalty_count\": \"Cantidad de Penalizaciones\"\n        }\n      },\n      \"penalties\": {\n        \"title\": \"Definir Penalización\",\n        \"description\": \"Introduce los datos de cada penalización.\",\n        \"data\": {\n          \"penalty_name\": \"Nombre de la Penalización\",\n          \"penalty_description\": \"Descripción (opcional)\",\n          \"penalty_labels\": \"Etiquetas de la Penalización\",\n          \"internal_id\": \"ID Interno\",\n          \"penalty_points\": \"Puntos de penalización (negativos)\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"bonus_count\": {\n        \"title\": \"Número de Bonificaciones\",\n        \"description\": \"¿Cuántas bonificaciones deseas definir?\",\n        \"data\": {\n          \"bonus_count\": \"Cantidad de Bonificaciones\"\n        }\n      },\n      \"bonuses\": {\n        \"title\": \"Definir Bonificación\",\n        \"description\": \"Introduce los datos de cada bonificación.\",\n        \"data\": {\n          \"bonus_name\": \"Nombre de la Bonificación\",\n          \"bonus_description\": \"Descripción (opcional)\",\n          \"bonus_labels\": \"Etiquetas de la Bonificación\",\n          \"internal_id\": \"ID Interno\",\n          \"bonus_points\": \"Puntos de bonificación (positivos)\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"achievement_count\": {\n        \"title\": \"Número de Logros\",\n        \"description\": \"¿Cuántos logros deseas definir?\",\n        \"data\": {\n          \"achievement_count\": \"Cantidad de Logros\"\n        }\n      },\n      \"achievements\": {\n        \"title\": \"Definir Logro\",\n        \"description\": \"Introduce los datos de cada logro.\",\n        \"data\": {\n          \"name\": \"Nombre del Logro\",\n          \"description\": \"Descripción (opcional)\",\n          \"achievement_labels\": \"Etiquetas del Logro\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"assigned_kids\": \"Niños asignados\",\n          \"type\": \"Tipo de logro\",\n          \"selected_chore_id\": \"Selecciona la tarea asociada\",\n          \"criteria\": \"Criterios (opcional)\",\n          \"target_value\": \"Objetivo del logro\",\n          \"reward_points\": \"Puntos extra por completar el logro\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"challenge_count\": {\n        \"title\": \"Número de Retos\",\n        \"description\": \"¿Cuántos retos deseas definir?\",\n        \"data\": {\n          \"challenge_count\": \"Cantidad de Retos\"\n        }\n      },\n      \"challenges\": {\n        \"title\": \"Definir Reto\",\n        \"description\": \"Introduce los datos de cada reto.\",\n        \"data\": {\n          \"name\": \"Nombre del Reto\",\n          \"description\": \"Descripción (opcional)\",\n          \"challenge_labels\": \"Etiquetas del Reto\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"assigned_kids\": \"Niños asignados\",\n          \"type\": \"Tipo de reto\",\n          \"selected_chore_id\": \"Selecciona la tarea asociada al reto (opcional)\",\n          \"criteria\": \"Criterios (opcional)\",\n          \"target_value\": \"Objetivo del reto\",\n          \"reward_points\": \"Puntos extra por completar el reto\",\n          \"start_date\": \"Fecha de inicio\",\n          \"end_date\": \"Fecha de finalización\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"finish\": {\n        \"title\": \"Revisar y Finalizar\",\n        \"description\": \"Revisa la configuración:\\n{summary}\\nHaz clic en Enviar para finalizar.\"\n      }\n    },\n    \"error\": {\n      \"a_chore_must_be_selected\": \"Debe seleccionarse una tarea\",\n      \"duplicate_achievement\": \"Ya existe un logro con este nombre\",\n      \"duplicate_badge\": \"Ya existe una insignia con este nombre\",\n      \"duplicate_challenge\": \"Ya existe un desafío con este nombre\",\n      \"duplicate_chore\": \"Ya existe una tarea con este nombre\",\n      \"duplicate_kid\": \"Ya existe un niño con este nombre\",\n      \"duplicate_parent\": \"Ya existe un padre/madre con este nombre\",\n      \"duplicate_penalty\": \"Ya existe una penalización con este nombre\",\n      \"duplicate_reward\": \"Ya existe una recompensa con este nombre\",\n      \"duplicate_bonus\": \"Ya existe una bonificación con este nombre\",\n      \"due_date_in_past\": \"La fecha de vencimiento debe estar en el futuro.\",\n      \"end_date_in_past\": \"La fecha de fin debe estar en el futuro.\",\n      \"end_date_not_after_start_date\": \"La fecha de fin debe ser posterior a la fecha de inicio.\",\n      \"invalid_achievement_count\": \"Cantidad de logros no válida\",\n      \"invalid_achievement_name\": \"Nombre de logro no válido\",\n      \"invalid_badge\": \"Insignia no válida\",\n      \"invalid_badge_count\": \"Cantidad de insignias no válida\",\n      \"invalid_badge_name\": \"Nombre de insignia no válido\",\n      \"invalid_challenge_count\": \"Cantidad de desafíos no válida\",\n      \"invalid_challenge_name\": \"Nombre de desafío no válido\",\n      \"invalid_chore\": \"Tarea no válida\",\n      \"invalid_chore_count\": \"Cantidad de tareas no válida\",\n      \"invalid_chore_name\": \"Nombre de tarea no válido\",\n      \"invalid_due_date\": \"Fecha de vencimiento no válida\",\n      \"invalid_end_date\": \"Fecha de fin no válida.\",\n      \"invalid_kid_count\": \"Cantidad de niños no válida\",\n      \"invalid_kid_name\": \"Nombre de niño no válido\",\n      \"invalid_parent_count\": \"Cantidad de padres no válida\",\n      \"invalid_parent_name\": \"Nombre de padre/madre no válido\",\n      \"invalid_penalty\": \"Penalización no válida\",\n      \"invalid_penalty_count\": \"Cantidad de penalizaciones no válida\",\n      \"invalid_penalty_name\": \"Nombre de penalización no válido\",\n      \"invalid_reward\": \"Recompensa no válida\",\n      \"invalid_reward_count\": \"Cantidad de recompensas no válida\",\n      \"invalid_reward_name\": \"Nombre de recompensa no válido\",\n      \"invalid_start_date\": \"Fecha de inicio no válida.\",\n      \"invalid_bonus\": \"Bonificación no válida\",\n      \"invalid_bonus_count\": \"Cantidad de bonificaciones no válida\",\n      \"invalid_bonus_name\": \"Nombre de bonificación no válido\",\n      \"start_date_in_past\": \"La fecha de inicio debe estar en el futuro.\"\n    },\n    \"abort\": {\n      \"single_instance_allowed\": \"Solo se puede configurar una única instancia de KidsChores.\"\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"Opciones de KidsChores\",\n        \"description\": \"Gestiona niños, tareas, insignias, recompensas, penalizaciones o finaliza.\",\n        \"data\": {\n          \"menu_selection\": \"Selecciona una opción\"\n        }\n      },\n      \"manage_entity\": {\n        \"title\": \"Seleccionar Acción\",\n        \"description\": \"Opciones para añadir, editar o eliminar.\",\n        \"data\": {\n          \"manage_action\": \"Selecciona una acción\"\n        }\n      },\n      \"select_entity\": {\n        \"title\": \"Selecciona {entity_type}\",\n        \"description\": \"Selecciona el {entity_type} que deseas {action}.\",\n        \"data\": {\n          \"entity_name\": \"Nombre\"\n        }\n      },\n      \"manage_points\": {\n        \"title\": \"Editar Etiqueta e Ícono de Puntos\",\n        \"description\": \"Cambia la etiqueta e ícono utilizados para representar los puntos.\",\n        \"data\": {\n          \"points_label\": \"Etiqueta de Puntos\",\n          \"points_icon\": \"Ícono de Puntos\"\n        }\n      },\n      \"add_kid\": {\n        \"title\": \"Añadir Niño/a\",\n        \"description\": \"Proporciona los datos para el nuevo niño.\",\n        \"data\": {\n          \"kid_name\": \"Nombre del Niño/a\",\n          \"ha_user\": \"Usuario de Home Assistant\",\n          \"enable_mobile_notifications\": \"Habilitar Notificaciones Móviles\",\n          \"mobile_notify_service\": \"Servicio de Notificaciones\",\n          \"enable_persistent_notifications\": \"Habilitar Notificaciones Persistentes\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"add_parent\": {\n        \"title\": \"Añadir Padre/Madre\",\n        \"description\": \"Proporciona los datos para el nuevo padre.\",\n        \"data\": {\n          \"parent_name\": \"Nombre del Padre/Madre\",\n          \"ha_user_id\": \"Usuario de Home Assistant\",\n          \"associated_kids\": \"Niños Asociados\",\n          \"enable_mobile_notifications\": \"Habilitar Notificaciones Móviles\",\n          \"mobile_notify_service\": \"Servicio de Notificaciones\",\n          \"enable_persistent_notifications\": \"Habilitar Notificaciones Persistentes\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"add_chore\": {\n        \"title\": \"Añadir Tarea\",\n        \"description\": \"Proporciona los datos de la nueva tarea.\",\n        \"data\": {\n          \"chore_name\": \"Nombre de la Tarea\",\n          \"internal_id\": \"ID Interno\",\n          \"default_points\": \"Puntos Predeterminados\",\n          \"allow_multiple_claims_per_day\": \"¿Permitir múltiples reclamaciones por día?\",\n          \"partial_allowed\": \"¿Permitir puntos parciales?\",\n          \"shared_chore\": \"¿Tarea compartida?\",\n          \"assigned_kids\": \"Niños Asignados\",\n          \"chore_description\": \"Descripción (opcional)\",\n          \"chore_labels\": \"Etiquetas de la Tarea\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"recurring_frequency\": \"Frecuencia recurrente\",\n          \"custom_interval\": \"Intervalo de Frecuencia Recurrente Personalizado (usar solo si se configura Frecuencia Recurrente Personalizada)\",\n          \"custom_interval_unit\": \"Periodo de Frecuencia Recurrente Personalizada\",\n          \"applicable_days\": \"Días aplicables\",\n          \"due_date\": \"Fecha de vencimiento\",\n          \"notify_on_claim\": \"Notificar al reclamar\",\n          \"notify_on_approval\": \"Notificar al aprobar\",\n          \"notify_on_disapproval\": \"Notificar al rechazar\"\n        }\n      },\n      \"add_badge\": {\n        \"title\": \"Añadir Insignia\",\n        \"description\": \"Proporciona los datos de la nueva insignia.\",\n        \"data\": {\n          \"badge_name\": \"Nombre de la Insignia\",\n          \"internal_id\": \"ID Interno\",\n          \"threshold_type\": \"Tipo de umbral\",\n          \"threshold_value\": \"Valor del umbral\",\n          \"points_multiplier\": \"Multiplicador de puntos\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"badge_description\": \"Descripción (opcional)\",\n          \"badge_labels\": \"Etiquetas de la Insignia\"\n        }\n      },\n      \"add_reward\": {\n        \"title\": \"Añadir Recompensa\",\n        \"description\": \"Proporciona los datos de la nueva recompensa.\",\n        \"data\": {\n          \"reward_name\": \"Nombre de la Recompensa\",\n          \"internal_id\": \"ID Interno\",\n          \"reward_cost\": \"Costo de la Recompensa\",\n          \"reward_description\": \"Descripción (opcional)\",\n          \"reward_labels\": \"Etiquetas de la Recompensa\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"add_penalty\": {\n        \"title\": \"Añadir Penalización\",\n        \"description\": \"Proporciona los datos de la nueva penalización.\",\n        \"data\": {\n          \"penalty_name\": \"Nombre de la Penalización\",\n          \"penalty_description\": \"Descripción (opcional)\",\n          \"penalty_labels\": \"Etiquetas de la Penalización\",\n          \"internal_id\": \"ID Interno\",\n          \"penalty_points\": \"Puntos de penalización (negativos)\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"add_bonus\": {\n        \"title\": \"Añadir Bonificación\",\n        \"description\": \"Proporciona los datos de la nueva bonificación.\",\n        \"data\": {\n          \"bonus_name\": \"Nombre de la Bonificación\",\n          \"bonus_description\": \"Descripción (opcional)\",\n          \"bonus_labels\": \"Etiquetas de la Bonificación\",\n          \"internal_id\": \"ID Interno\",\n          \"bonus_points\": \"Puntos de bonificación (positivos)\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"add_achievement\": {\n        \"title\": \"Definir Logro\",\n        \"description\": \"Introduce los datos de cada logro.\",\n        \"data\": {\n          \"name\": \"Nombre del Logro\",\n          \"description\": \"Descripción (opcional)\",\n          \"achievement_labels\": \"Etiquetas del Logro\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"assigned_kids\": \"Niños asignados\",\n          \"type\": \"Tipo de logro\",\n          \"selected_chore_id\": \"Selecciona la tarea asociada\",\n          \"criteria\": \"Criterios (opcional)\",\n          \"target_value\": \"Objetivo del logro\",\n          \"reward_points\": \"Puntos extra por completar el logro\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"add_challenge\": {\n        \"title\": \"Definir Reto\",\n        \"description\": \"Introduce los datos de cada reto.\",\n        \"data\": {\n          \"name\": \"Nombre del Reto\",\n          \"description\": \"Descripción (opcional)\",\n          \"challenge_labels\": \"Etiquetas del Reto\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"assigned_kids\": \"Niños asignados\",\n          \"type\": \"Tipo de reto\",\n          \"selected_chore_id\": \"Selecciona la tarea asociada al reto (opcional)\",\n          \"criteria\": \"Criterios (opcional)\",\n          \"target_value\": \"Objetivo del reto\",\n          \"reward_points\": \"Puntos extra por completar el reto\",\n          \"start_date\": \"Fecha de inicio\",\n          \"end_date\": \"Fecha de finalización\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"edit_kid\": {\n        \"title\": \"Editar Niño/a\",\n        \"description\": \"Modifica los datos del niño seleccionado.\",\n        \"data\": {\n          \"kid_name\": \"Nombre del Niño/a\",\n          \"ha_user\": \"Usuario de Home Assistant\",\n          \"enable_mobile_notifications\": \"Habilitar Notificaciones Móviles\",\n          \"mobile_notify_service\": \"Servicio de Notificaciones\",\n          \"enable_persistent_notifications\": \"Habilitar Notificaciones Persistentes\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"edit_parent\": {\n        \"title\": \"Editar Padre/Madre\",\n        \"description\": \"Modifica los datos del padre seleccionado.\",\n        \"data\": {\n          \"parent_name\": \"Nombre del Padre/Madre\",\n          \"ha_user_id\": \"Usuario de Home Assistant\",\n          \"associated_kids\": \"Niños Asociados\",\n          \"enable_mobile_notifications\": \"Habilitar Notificaciones Móviles\",\n          \"mobile_notify_service\": \"Servicio de Notificaciones\",\n          \"enable_persistent_notifications\": \"Habilitar Notificaciones Persistentes\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"edit_chore\": {\n        \"title\": \"Editar Tarea\",\n        \"description\": \"Modifica los datos de la tarea seleccionada.\",\n        \"data\": {\n          \"chore_name\": \"Nombre de la Tarea\",\n          \"internal_id\": \"ID Interno\",\n          \"default_points\": \"Puntos Predeterminados\",\n          \"allow_multiple_claims_per_day\": \"¿Permitir múltiples reclamaciones por día?\",\n          \"partial_allowed\": \"¿Permitir puntos parciales?\",\n          \"shared_chore\": \"¿Tarea compartida?\",\n          \"assigned_kids\": \"Niños asignados\",\n          \"chore_description\": \"Descripción (opcional)\",\n          \"chore_labels\": \"Etiquetas de la Tarea\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"recurring_frequency\": \"Frecuencia recurrente\",\n          \"custom_interval\": \"Intervalo de Frecuencia Recurrente Personalizado (usar solo si se configura Frecuencia Recurrente Personalizada)\",\n          \"custom_interval_unit\": \"Periodo de Frecuencia Recurrente Personalizada\",\n          \"applicable_days\": \"Días aplicables\",\n          \"due_date\": \"Fecha de vencimiento\",\n          \"notify_on_claim\": \"Notificar al reclamar\",\n          \"notify_on_approval\": \"Notificar al aprobar\",\n          \"notify_on_disapproval\": \"Notificar al rechazar\"\n        }\n      },\n      \"edit_badge\": {\n        \"title\": \"Editar Insignia\",\n        \"description\": \"Modifica los datos de la insignia seleccionada.\",\n        \"data\": {\n          \"badge_name\": \"Nombre de la Insignia\",\n          \"internal_id\": \"ID Interno\",\n          \"threshold_type\": \"Tipo de umbral\",\n          \"threshold_value\": \"Valor del umbral\",\n          \"points_multiplier\": \"Multiplicador de puntos\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"badge_description\": \"Descripción (opcional)\",\n          \"badge_labels\": \"Etiquetas de la Insignia\"\n        }\n      },\n      \"edit_reward\": {\n        \"title\": \"Editar Recompensa\",\n        \"description\": \"Modifica los datos de la recompensa seleccionada.\",\n        \"data\": {\n          \"reward_name\": \"Nombre de la Recompensa\",\n          \"internal_id\": \"ID Interno\",\n          \"reward_cost\": \"Costo de la Recompensa\",\n          \"reward_description\": \"Descripción (opcional)\",\n          \"reward_labels\": \"Etiquetas de la Recompensa\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"edit_penalty\": {\n        \"title\": \"Editar Penalización\",\n        \"description\": \"Modifica los datos de la penalización seleccionada.\",\n        \"data\": {\n          \"penalty_name\": \"Nombre de la Penalización\",\n          \"penalty_description\": \"Descripción (opcional)\",\n          \"penalty_labels\": \"Etiquetas de la Penalización\",\n          \"internal_id\": \"ID Interno\",\n          \"penalty_points\": \"Puntos de penalización (negativos)\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"edit_bonus\": {\n        \"title\": \"Editar Bonificación\",\n        \"description\": \"Modifica los datos de la bonificación seleccionada.\",\n        \"data\": {\n          \"bonus_name\": \"Nombre de la Bonificación\",\n          \"bonus_description\": \"Descripción (opcional)\",\n          \"bonus_labels\": \"Etiquetas de la Bonificación\",\n          \"internal_id\": \"ID Interno\",\n          \"bonus_points\": \"Puntos de bonificación (positivos)\",\n          \"icon\": \"Ícono (mdi:xxx)\"\n        }\n      },\n      \"edit_achievement\": {\n        \"title\": \"Definir Logro\",\n        \"description\": \"Introduce los datos de cada logro.\",\n        \"data\": {\n          \"name\": \"Nombre del Logro\",\n          \"description\": \"Descripción (opcional)\",\n          \"achievement_labels\": \"Etiquetas del Logro\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"assigned_kids\": \"Niños asignados\",\n          \"type\": \"Tipo de logro\",\n          \"selected_chore_id\": \"Selecciona la tarea asociada\",\n          \"criteria\": \"Criterios (opcional)\",\n          \"target_value\": \"Objetivo del logro\",\n          \"reward_points\": \"Puntos extra por completar el logro\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"edit_challenge\": {\n        \"title\": \"Definir Reto\",\n        \"description\": \"Introduce los datos de cada reto.\",\n        \"data\": {\n          \"name\": \"Nombre del Reto\",\n          \"description\": \"Descripción (opcional)\",\n          \"challenge_labels\": \"Etiquetas del Reto\",\n          \"icon\": \"Ícono (mdi:xxx)\",\n          \"assigned_kids\": \"Niños asignados\",\n          \"type\": \"Tipo de reto\",\n          \"selected_chore_id\": \"Selecciona la tarea asociada al reto (opcional)\",\n          \"criteria\": \"Criterios (opcional)\",\n          \"target_value\": \"Objetivo del reto\",\n          \"reward_points\": \"Puntos extra por completar el reto\",\n          \"start_date\": \"Fecha de inicio\",\n          \"end_date\": \"Fecha de finalización\",\n          \"internal_id\": \"ID Interno\"\n        }\n      },\n      \"delete_kid\": {\n        \"title\": \"Eliminar Niño/a\",\n        \"description\": \"¿Estás seguro de que deseas eliminar al niño {kid_name}?\",\n        \"data\": {}\n      },\n      \"delete_parent\": {\n        \"title\": \"Eliminar Padre/Madre\",\n        \"description\": \"¿Estás seguro de que deseas eliminar al padre {parent_name}?\",\n        \"data\": {}\n      },\n      \"delete_chore\": {\n        \"title\": \"Eliminar Tarea\",\n        \"description\": \"¿Estás seguro de que deseas eliminar la tarea {chore_name}?\",\n        \"data\": {}\n      },\n      \"delete_badge\": {\n        \"title\": \"Eliminar Insignia\",\n        \"description\": \"¿Estás seguro de que deseas eliminar la insignia {badge_name}?\",\n        \"data\": {}\n      },\n      \"delete_reward\": {\n        \"title\": \"Eliminar Recompensa\",\n        \"description\": \"¿Estás seguro de que deseas eliminar la recompensa {reward_name}?\",\n        \"data\": {}\n      },\n      \"delete_penalty\": {\n        \"title\": \"Eliminar Penalización\",\n        \"description\": \"¿Estás seguro de que deseas eliminar la penalización {penalty_name}?\",\n        \"data\": {}\n      },\n      \"delete_bonus\": {\n        \"title\": \"Eliminar Bonificación\",\n        \"description\": \"¿Estás seguro de que deseas eliminar la bonificación {bonus_name}?\",\n        \"data\": {}\n      },\n      \"delete_achievement\": {\n        \"title\": \"Eliminar Logro\",\n        \"description\": \"¿Estás seguro de que deseas eliminar el logro {achievement_name}?\",\n        \"data\": {}\n      },\n      \"delete_challenge\": {\n        \"title\": \"Eliminar Reto\",\n        \"description\": \"¿Estás seguro de que deseas eliminar el reto {challenge_name}?\",\n        \"data\": {}\n      }\n    },\n    \"error\": {\n      \"a_chore_must_be_selected\": \"Debe seleccionarse una tarea\",\n      \"duplicate_achievement\": \"Ya existe un logro con este nombre\",\n      \"duplicate_badge\": \"Ya existe una insignia con este nombre\",\n      \"duplicate_challenge\": \"Ya existe un desafío con este nombre\",\n      \"duplicate_chore\": \"Ya existe una tarea con este nombre\",\n      \"duplicate_kid\": \"Ya existe un niño con este nombre\",\n      \"duplicate_parent\": \"Ya existe un padre/madre con este nombre\",\n      \"duplicate_penalty\": \"Ya existe una penalización con este nombre\",\n      \"duplicate_reward\": \"Ya existe una recompensa con este nombre\",\n      \"duplicate_bonus\": \"Ya existe una bonificación con este nombre\",\n      \"due_date_in_past\": \"La fecha de vencimiento debe estar en el futuro.\",\n      \"end_date_in_past\": \"La fecha de fin debe estar en el futuro.\",\n      \"end_date_not_after_start_date\": \"La fecha de fin debe ser posterior a la fecha de inicio.\",\n      \"invalid_badge\": \"Insignia no válida\",\n      \"invalid_badge_count\": \"Cantidad de insignias no válida\",\n      \"invalid_chore\": \"Tarea no válida\",\n      \"invalid_chore_count\": \"Cantidad de tareas no válida\",\n      \"invalid_due_date\": \"Fecha de vencimiento no válida\",\n      \"invalid_end_date\": \"Fecha de fin no válida.\",\n      \"invalid_kid_count\": \"Cantidad de niños no válida\",\n      \"invalid_kid_name\": \"Nombre de niño no válido\",\n      \"invalid_penalty\": \"Penalización no válida\",\n      \"invalid_penalty_count\": \"Cantidad de penalizaciones no válida\",\n      \"invalid_bonus\": \"Bonificación no válida\",\n      \"invalid_bonus_count\": \"Cantidad de bonificaciones no válida\",\n      \"invalid_reward\": \"Recompensa no válida\",\n      \"invalid_reward_count\": \"Cantidad de recompensas no válida\",\n      \"invalid_start_date\": \"Fecha de inicio no válida.\",\n      \"start_date_in_past\": \"La fecha de inicio debe estar en el futuro.\"\n    },\n    \"abort\": {\n      \"invalid_action\": \"Acción no válida\",\n      \"invalid_achievement\": \"Logro no válido\",\n      \"invalid_badge\": \"Insignia no válida\",\n      \"invalid_challenge\": \"Desafío no válido\",\n      \"invalid_chore\": \"Tarea no válida\",\n      \"invalid_entity\": \"Entidad no válida\",\n      \"invalid_kid\": \"Niño no válido\",\n      \"invalid_parent\": \"Padre/Madre no válido\",\n      \"invalid_penalty\": \"Penalización no válida\",\n      \"invalid_reward\": \"Recompensa no válida\",\n      \"invalid_bonus\": \"Bonificación no válida\",\n      \"no_kid\": \"No hay niños configurados. Adiciona una entrada primero.\",\n      \"no_parent\": \"No hay padres configurados. Adiciona una entrada primero.\",\n      \"no_chore\": \"No hay tareas configuradas. Adiciona una entrada primero.\",\n      \"no_badge\": \"No hay insignias configuradas. Adiciona una entrada primero.\",\n      \"no_reward\": \"No hay recompensas configuradas. Adiciona una entrada primero.\",\n      \"no_penalty\": \"No hay penalizaciones configuradas. Adiciona una entrada primero.\",\n      \"no_bonus\": \"No hay bonificaciones configuradas. Adiciona una entrada primero.\",\n      \"no_achievement\": \"No hay logros configurados. Adiciona una entrada primero.\",\n      \"no_challenge\": \"No hay desafíos configurados. Adiciona una entrada primero.\",\n      \"setup_complete\": \"Configuración Completa\"\n    }\n  },\n  \"selector\": {\n    \"main_menu\": {\n      \"options\": {\n        \"manage_points\": \"Gestionar Puntos\",\n        \"manage_kid\": \"Gestionar Niño/a\",\n        \"manage_parent\": \"Gestionar Padre/Madre\",\n        \"manage_chore\": \"Gestionar Tarea\",\n        \"manage_badge\": \"Gestionar Insignia\",\n        \"manage_reward\": \"Gestionar Recompensa\",\n        \"manage_penalty\": \"Gestionar Penalización\",\n        \"manage_bonus\": \"Gestionar Bonificación\",\n        \"manage_achievement\": \"Gestionar Logro\",\n        \"manage_challenge\": \"Gestionar Reto\",\n        \"done\": \"Finalizar Configuración\"\n      }\n    },\n    \"manage_actions\": {\n      \"options\": {\n        \"add\": \"Añadir\",\n        \"edit\": \"Editar\",\n        \"delete\": \"Eliminar\",\n        \"back\": \"Volver al Menú Principal\"\n      }\n    },\n    \"recurring_frequency\": {\n      \"options\": {\n        \"none\": \"Ninguna\",\n        \"daily\": \"Diaria\",\n        \"weekly\": \"Semanal\",\n        \"biweekly\": \"—\",\n        \"monthly\": \"Mensual\",\n        \"custom\": \"Personalizada\"\n      }\n    },\n    \"custom_interval_unit\": {\n      \"options\": {\n        \"days\": \"Días\",\n        \"weeks\": \"Semanas\",\n        \"months\": \"Meses\"\n      }\n    },\n    \"applicable_days\": {\n      \"options\": {\n        \"mon\": \"Lunes\",\n        \"tue\": \"Martes\",\n        \"wed\": \"Miércoles\",\n        \"thu\": \"Jueves\",\n        \"fri\": \"Viernes\",\n        \"sat\": \"Sábado\",\n        \"sun\": \"Domingo\"\n      }\n    },\n    \"threshold_type\": {\n      \"options\": {\n        \"points\": \"Puntos\",\n        \"chore_count\": \"Cantidad de Tareas\"\n      }\n    }\n  },\n  \"services\": {\n    \"claim_chore\": {\n      \"name\": \"Reclamar Tarea\",\n      \"description\": \"Un niño reclama una tarea, marcándola como 'reclamada' para aprobación de los padres.\",\n      \"fields\": {\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El nombre del niño que reclama la tarea.\",\n          \"example\": \"Alice\"\n        },\n        \"chore_name\": {\n          \"name\": \"Nombre de la Tarea\",\n          \"description\": \"El nombre de la tarea a reclamar.\",\n          \"example\": \"Lavar los Platos\"\n        }\n      }\n    },\n    \"approve_chore\": {\n      \"name\": \"Aprobar Tarea\",\n      \"description\": \"El padre aprueba la tarea, otorgando puntos.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Nombre del Padre/Madre\",\n          \"description\": \"El padre que aprueba la tarea.\",\n          \"example\": \"Mamá\"\n        },\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El nombre del niño que realizó la tarea.\",\n          \"example\": \"Alice\"\n        },\n        \"chore_name\": {\n          \"name\": \"Nombre de la Tarea\",\n          \"description\": \"El nombre de la tarea que se está aprobando.\",\n          \"example\": \"Lavar los Platos\"\n        },\n        \"points_awarded\": {\n          \"name\": \"Puntos Otorgados\",\n          \"description\": \"Puntos a otorgar (opcional; por defecto se usan los puntos de la tarea).\",\n          \"example\": 3\n        }\n      }\n    },\n    \"disapprove_chore\": {\n      \"name\": \"Desaprobar Tarea\",\n      \"description\": \"El padre rechaza una tarea para un niño, revirtiendo su estado.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Nombre del Padre/Madre\",\n          \"description\": \"El padre que rechaza la tarea.\",\n          \"example\": \"Mamá\"\n        },\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El nombre del niño cuya tarea se está rechazando.\",\n          \"example\": \"Alice\"\n        },\n        \"chore_name\": {\n          \"name\": \"Nombre de la Tarea\",\n          \"description\": \"El nombre de la tarea que se está rechazando.\",\n          \"example\": \"Limpiar la Habitación\"\n        }\n      }\n    },\n    \"redeem_reward\": {\n      \"name\": \"Canjear Recompensa\",\n      \"description\": \"Un padre canjea una recompensa para un niño, descontando puntos.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Nombre del Padre/Madre\",\n          \"description\": \"El padre que autoriza el canje de la recompensa.\",\n          \"example\": \"Mamá\"\n        },\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El niño que canjea la recompensa.\",\n          \"example\": \"Alice\"\n        },\n        \"reward_name\": {\n          \"name\": \"Nombre de la Recompensa\",\n          \"description\": \"El nombre de la recompensa a canjear.\",\n          \"example\": \"Tiempo Extra de Pantalla\"\n        }\n      }\n    },\n    \"approve_reward\": {\n      \"name\": \"Aprobar Recompensa\",\n      \"description\": \"El padre aprueba una recompensa reclamada por un niño.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Nombre del Padre/Madre\",\n          \"description\": \"El padre que aprueba la recompensa.\",\n          \"example\": \"Mamá\"\n        },\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El niño que está canjeando la recompensa.\",\n          \"example\": \"Alice\"\n        },\n        \"reward_name\": {\n          \"name\": \"Nombre de la Recompensa\",\n          \"description\": \"El nombre de la recompensa que se está aprobando.\",\n          \"example\": \"Tiempo Extra de Pantalla\"\n        }\n      }\n    },\n    \"disapprove_reward\": {\n      \"name\": \"Desaprobar Recompensa\",\n      \"description\": \"El padre rechaza el canje de una recompensa para un niño.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Nombre del Padre/Madre\",\n          \"description\": \"El padre que rechaza la recompensa.\",\n          \"example\": \"Papá\"\n        },\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El niño cuyo canje de recompensa se está rechazando.\",\n          \"example\": \"Alice\"\n        },\n        \"reward_name\": {\n          \"name\": \"Nombre de la Recompensa\",\n          \"description\": \"El nombre de la recompensa que se está rechazando.\",\n          \"example\": \"Tiempo Extra de Pantalla\"\n        }\n      }\n    },\n    \"apply_penalty\": {\n      \"name\": \"Aplicar Penalización\",\n      \"description\": \"Un padre aplica una penalización para descontar puntos.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Nombre del Padre/Madre\",\n          \"description\": \"El padre que aplica la penalización.\",\n          \"example\": \"Papá\"\n        },\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El niño que recibe la penalización.\",\n          \"example\": \"Alice\"\n        },\n        \"penalty_name\": {\n          \"name\": \"Nombre de la Penalización\",\n          \"description\": \"El nombre de la penalización a aplicar.\",\n          \"example\": \"Gritar\"\n        }\n      }\n    },\n    \"apply_bonus\": {\n      \"name\": \"Aplicar Bonificación\",\n      \"description\": \"Un padre aplica una bonificación a un niño, otorgando puntos.\",\n      \"fields\": {\n        \"parent_name\": {\n          \"name\": \"Nombre del Padre/Madre\",\n          \"description\": \"El padre que aplica la bonificación.\",\n          \"example\": \"Mamá\"\n        },\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El niño que recibe la bonificación.\",\n          \"example\": \"Alice\"\n        },\n        \"bonus_name\": {\n          \"name\": \"Nombre de la Bonificación\",\n          \"description\": \"El nombre de la bonificación a aplicar.\",\n          \"example\": \"Ayuda Extra\"\n        }\n      }\n    },\n    \"reset_all_data\": {\n      \"name\": \"Restablecer Todos los Datos\",\n      \"description\": \"Borra por completo los datos de KidsChores del almacenamiento.\"\n    },\n    \"reset_all_chores\": {\n      \"name\": \"Restablecer Todas las Tareas\",\n      \"description\": \"Restablece manualmente las tareas a estado Pendiente, eliminando reclamaciones y aprobaciones.\"\n    },\n    \"reset_overdue_chores\": {\n      \"name\": \"Restablecer Tareas Vencidas\",\n      \"description\": \"Restablece las tareas vencidas a estado Pendiente y las reprograma según su frecuencia recurrente y la fecha de vencimiento anterior. Opcionalmente, puedes proporcionar un chore_id (o chore_name) para restablecer una tarea específica y, opcionalmente, un kid_name para restablecer la tarea solo para ese niño.\",\n      \"fields\": {\n        \"chore_id\": {\n          \"name\": \"ID de la Tarea\",\n          \"description\": \"El ID interno de la tarea a restablecer (opcional si se proporciona chore_name).\",\n          \"example\": \"abc123\"\n        },\n        \"chore_name\": {\n          \"name\": \"Nombre de la Tarea\",\n          \"description\": \"El nombre de la tarea a restablecer (opcional si se proporciona chore_id).\",\n          \"example\": \"Lavar los Platos\"\n        },\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El nombre del niño (opcional).\",\n          \"example\": \"Alice\"\n        }\n      }\n    },\n    \"set_chore_due_date\": {\n      \"name\": \"Establecer/Restablecer Fecha de Vencimiento de la Tarea\",\n      \"description\": \"Establece (o borra) la fecha de vencimiento de una tarea. Proporciona el nombre de la tarea y, si lo deseas, una nueva fecha de vencimiento. Si no se proporciona una fecha, se borrará la existente. El servicio rechazará las fechas en el pasado.\",\n      \"fields\": {\n        \"chore_name\": {\n          \"name\": \"Nombre de la Tarea\",\n          \"description\": \"El nombre de la tarea a actualizar\",\n          \"example\": \"Lavar Platos\"\n        },\n        \"due_date\": {\n          \"name\": \"Fecha de Vencimiento\",\n          \"description\": \"La nueva fecha de vencimiento de la tarea. Usa el selector de fecha/hora para elegir una fecha y hora válidas (en tu zona horaria local). Déjalo vacío para borrar la fecha de vencimiento.\",\n          \"example\": \"2025-03-01T23:59:00Z\"\n        }\n      }\n    },\n    \"skip_chore_due_date\": {\n      \"name\": \"Saltar Fecha de Vencimiento de la Tarea\",\n      \"description\": \"Salta la fecha de vencimiento actual de una tarea recurrente. Este servicio reprograma inmediatamente la fecha de vencimiento de la tarea según su frecuencia de repetición y restablece su estado a pendiente. Se eliminarán todas las reclamaciones o aprobaciones pendientes.\",\n      \"fields\": {\n        \"chore_id\": {\n          \"name\": \"ID de la Tarea\",\n          \"description\": \"El ID interno de la tarea a restablecer (opcional si se proporciona chore_name).\",\n          \"example\": \"abc123\"\n        },\n        \"chore_name\": {\n          \"name\": \"Nombre de la Tarea\",\n          \"description\": \"El nombre de la tarea a restablecer (opcional si se proporciona chore_id).\",\n          \"example\": \"Lavar Platos\"\n        }\n      }\n    },\n    \"reset_penalties\": {\n      \"name\": \"Restablecer Sanciones\",\n      \"description\": \"Restablece todas las sanciones aplicadas para todos los niños. Opcionalmente, proporciona Nombre de la Sanción para restablecer una sanción específica en todos los niños. Usa Nombre del Niño/a para restablecer todas las sanciones de un/a niño/a en particular. Combina ambos para restablecer una sanción específica de un/a niño/a específico/a.\",\n      \"fields\": {\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El niño/a para el cual se restablecerán las sanciones.\",\n          \"example\": \"Alicia\"\n        },\n        \"penalty_name\": {\n          \"name\": \"Nombre de la Sanción\",\n          \"description\": \"El nombre de la sanción a restablecer.\",\n          \"example\": \"Gritar\"\n        }\n      }\n    },\n    \"reset_bonuses\": {\n      \"name\": \"Restablecer Bonificaciones\",\n      \"description\": \"Restablece todas las bonificaciones aplicadas para todos los niños. Opcionalmente, proporciona Nombre de la Bonificación para restablecer una bonificación específica en todos los niños. Usa Nombre del Niño/a para restablecer todas las bonificaciones de un/a niño/a en particular. Combina ambos para restablecer una bonificación específica de un/a niño/a específico/a.\",\n      \"fields\": {\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El niño/a para el cual se restablecerán las bonificaciones.\",\n          \"example\": \"Alicia\"\n        },\n        \"bonus_name\": {\n          \"name\": \"Nombre de la Bonificación\",\n          \"description\": \"El nombre de la bonificación a restablecer.\",\n          \"example\": \"Ayudar\"\n        }\n      }\n    },\n    \"reset_rewards\": {\n      \"name\": \"Restablecer Recompensas\",\n      \"description\": \"Restablece el conteo de reclamaciones y aprobaciones de recompensas para todos los niños. Opcionalmente, proporciona Nombre de la Recompensa para restablecer el conteo de una recompensa específica en todos los niños. Usa Nombre del Niño/a para restablecer el conteo de recompensas de un/a niño/a en particular. Combina ambos para restablecer una recompensa específica de un/a niño/a específico/a.\",\n      \"fields\": {\n        \"kid_name\": {\n          \"name\": \"Nombre del Niño/a\",\n          \"description\": \"El niño para el cual se restablecerán los conteos de recompensas.\",\n          \"example\": \"Alicia\"\n        },\n        \"reward_name\": {\n          \"name\": \"Nombre de la Recompensa\",\n          \"description\": \"El nombre de la recompensa a restablecer.\",\n          \"example\": \"Helado\"\n        }\n      }\n    }\n  },\n  \"entity\": {\n    \"sensor\": {\n      \"chore_status_sensor\": {\n        \"name\": \"{kid_name} - Estado - {chore_name}\",\n        \"state\": {\n          \"pending\": \"Pendiente\",\n          \"approved\": \"Aprobada\",\n          \"claimed\": \"Reclamada\",\n          \"overdue\": \"Vencida\",\n          \"unknown\": \"Desconocida\",\n          \"none\": \"Ninguna\",\n          \"approved_in_part\": \"Aprobada (parcialmente)\",\n          \"claimed_in_part\": \"Reclamada (parcialmente)\"\n        },\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Nombre del Niño/a\"\n          },\n          \"chore_name\": {\n            \"name\": \"Nombre de la Tarea\"\n          },\n          \"shared_chore\": {\n            \"name\": \"Tarea Compartida\",\n            \"state\": {\n              \"true\": \"Sí\",\n              \"false\": \"No\"\n            }\n          },\n          \"recurring_frequency\": {\n            \"name\": \"Frecuencia Recurrente\",\n            \"state\": {\n              \"none\": \"Ninguna\",\n              \"daily\": \"Diaria\",\n              \"weekly\": \"Semanal\",\n              \"biweekly\": \"—\",\n              \"monthly\": \"Mensual\",\n              \"custom\": \"Personalizada\"\n            }\n          },\n          \"applicable_days\": {\n            \"name\": \"Applicable Days\",\n            \"state\": {\n              \"mon\": \"Lunes\",\n              \"tue\": \"Martes\",\n              \"wed\": \"Miércoles\",\n              \"thu\": \"Jueves\",\n              \"fri\": \"Viernes\",\n              \"sat\": \"Sábado\",\n              \"sun\": \"Domingo\"\n            }\n          },\n          \"due_date\": {\n            \"name\": \"Fecha de Vencimiento\"\n          },\n          \"default_points\": {\n            \"name\": \"Puntos Predeterminados\"\n          },\n          \"description\": {\n            \"name\": \"Descripción\"\n          },\n          \"chore_claims_count\": {\n            \"name\": \"—\"\n          },\n          \"chore_approvals_count\": {\n            \"name\": \"—\"\n          },\n          \"chore_current_streak\": {\n            \"name\": \"Racha Actual de la Tarea\"\n          },\n          \"chore_highest_streak\": {\n            \"name\": \"Mejor Racha de la Tarea\"\n          },\n          \"global_state\": {\n            \"name\": \"Estado Global\",\n            \"state\": {\n              \"pending\": \"Pendiente\",\n              \"approved\": \"Aprobada\",\n              \"claimed\": \"Reclamada\",\n              \"overdue\": \"Vencida\",\n              \"unknown\": \"Desconocida\",\n              \"none\": \"Ninguna\",\n              \"approved_in_part\": \"Aprobada (parcialmente)\",\n              \"claimed_in_part\": \"Reclamada (parcialmente)\",\n              \"independent\": \"Independiente\"\n            }\n          },\n          \"partial_allowed\": {\n            \"name\": \"Permitido Parcialmente\",\n            \"state\": {\n              \"true\": \"Sí\",\n              \"false\": \"No\"\n            }\n          },\n          \"allow_multiple_claims_per_day\": {\n            \"name\": \"Permitir Múltiples Reclamaciones por Día\",\n            \"state\": {\n              \"true\": \"Sí\",\n              \"false\": \"No\"\n            }\n          },\n          \"assigned_kids\": {\n            \"name\": \"Niños Asignados\"\n          },\n          \"custom_frequency_interval\": {\n            \"name\": \"Frecuencia Personalizada\"\n          },\n          \"custom_frequency_unit\": {\n            \"name\": \"Periodo de Frecuencia Personalizada\",\n            \"state\": {\n              \"days\": \"Días\",\n              \"weeks\": \"Semanas\",\n              \"months\": \"Meses\"\n            }\n          },\n          \"chore_approvals_today\": {\n            \"name\": \"Aprobaciones de la Tarea Hoy\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"kid_points_sensor\": {\n        \"name\": \"{kid_name} - {points}\"\n      },\n      \"kid_max_points_ever_sensor\": {\n        \"name\": \"{kid_name} - Máximo de Puntos Alcanzados\"\n      },\n      \"chores_completed_total_sensor\": {\n        \"name\": \"{kid_name} - Tareas Completadas - Total\"\n      },\n      \"chores_completed_daily_sensor\": {\n        \"name\": \"{kid_name} - Tareas Completadas - Diarias\"\n      },\n      \"chores_completed_weekly_sensor\": {\n        \"name\": \"{kid_name} - Tareas Completadas - Semanales\"\n      },\n      \"chores_completed_monthly_sensor\": {\n        \"name\": \"{kid_name} - Tareas Completadas - Mensuales\"\n      },\n      \"kid_badges_sensor\": {\n        \"name\": \"{kid_name} - Insignias Obtenidas\"\n      },\n      \"kids_highest_badge_sensor\": {\n        \"name\": \"{kid_name} - Insignia\",\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Nombre del Niño/a\"\n          },\n          \"all_earned_badges\": {\n            \"name\": \"Todas las Insignias Obtenidas\"\n          },\n          \"highest_badge_threshold_value\": {\n            \"name\": \"Mayor Umbral de Insignia\"\n          },\n          \"points_multiplier\": {\n            \"name\": \"Multiplicador de Puntos\"\n          },\n          \"points_to_next_badge\": {\n            \"name\": \"Puntos para la Próxima Insignia\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"badge_sensor\": {\n        \"name\": \"Insignia - {badge_name}\",\n        \"state_attributes\": {\n          \"threshold_type\": {\n            \"name\": \"Tipo de Umbral\",\n            \"state\": {\n              \"points\": \"Puntos\",\n              \"chore_count\": \"Cantidad de Tareas\"\n            }\n          },\n          \"points_multiplier\": {\n            \"name\": \"Multiplicador de Puntos\"\n          },\n          \"descriptionn\": {\n            \"name\": \"Descripción\"\n          },\n          \"kids_earned\": {\n            \"name\": \"Niños que la han Obtenido\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"pending_chores_approvals_sensor\": {\n        \"name\": \"Aprobaciones de Tareas Pendientes\"\n      },\n      \"pending_rewards_approvals_sensor\": {\n        \"name\": \"Aprobaciones de Recompensas Pendientes\"\n      },\n      \"reward_claims_sensor\": {\n        \"name\": \"{kid_name} - Reclamaciones - {reward_name}\"\n      },\n      \"reward_approvals_sensor\": {\n        \"name\": \"{kid_name} - Aprobaciones - {reward_name}\"\n      },\n      \"shared_chore_global_status_sensor\": {\n        \"name\": \"{chore_name} - Estado Global\",\n        \"state\": {\n          \"pending\": \"Pendiente\",\n          \"approved\": \"Aprobada\",\n          \"claimed\": \"Reclamada\",\n          \"overdue\": \"Vencida\",\n          \"unknown\": \"Desconocida\",\n          \"none\": \"Ninguna\",\n          \"approved_in_part\": \"Aprobada (parcialmente)\",\n          \"claimed_in_part\": \"Reclamada (parcialmente)\",\n          \"independent\": \"Independiente\"\n        },\n        \"state_attributes\": {\n          \"chore_name\": {\n            \"name\": \"Nombre de la Tarea\"\n          },\n          \"description\": {\n            \"name\": \"Descripción\"\n          },\n          \"recurring_frequency\": {\n            \"name\": \"Frecuencia Recurrente\",\n            \"state\": {\n              \"none\": \"Ninguna\",\n              \"daily\": \"Diaria\",\n              \"weekly\": \"Semanal\",\n              \"biweekly\": \"—\",\n              \"monthly\": \"Mensual\",\n              \"custom\": \"Personalizada\"\n            }\n          },\n          \"applicable_days\": {\n            \"name\": \"Applicable Days\",\n            \"state\": {\n              \"mon\": \"Lunes\",\n              \"tue\": \"Martes\",\n              \"wed\": \"Miércoles\",\n              \"thu\": \"Jueves\",\n              \"fri\": \"Viernes\",\n              \"sat\": \"Sábado\",\n              \"sun\": \"Domingo\"\n            }\n          },\n          \"due_date\": {\n            \"name\": \"Fecha de Vencimiento\"\n          },\n          \"default_points\": {\n            \"name\": \"Puntos Predeterminados\"\n          },\n          \"partial_allowed\": {\n            \"name\": \"Permitido Parcialmente\",\n            \"state\": {\n              \"true\": \"Sí\",\n              \"false\": \"No\"\n            }\n          },\n          \"allow_multiple_claims_per_day\": {\n            \"name\": \"Permitir Múltiples Reclamaciones por Día\",\n            \"state\": {\n              \"true\": \"Sí\",\n              \"false\": \"No\"\n            }\n          },\n          \"assigned_kids\": {\n            \"name\": \"Niños Asignados\"\n          },\n          \"chore_approvals_today\": {\n            \"name\": \"Aprobaciones de la Tarea Hoy\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"reward_status_sensor\": {\n        \"name\": \"{kid_name} - Estado de Recompensa - {reward_name}\",\n        \"state\": {\n          \"not_claimed\": \"No Reclamada\",\n          \"approved\": \"Aprobada\",\n          \"claimed\": \"Reclamada\",\n          \"unknown\": \"Desconocida\",\n          \"none\": \"Ninguna\"\n        },\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Nombre del Niño/a\"\n          },\n          \"reward_name\": {\n            \"name\": \"Nombre de la Recompensa\"\n          },\n          \"reward_cost\": {\n            \"name\": \"Costo de la Recompensa\"\n          },\n          \"description\": {\n            \"name\": \"Descripción\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"chore_claims_sensor\": {\n        \"name\": \"{kid_name} - Reclamaciones - {chore_name}\"\n      },\n      \"chore_approvals_sensor\": {\n        \"name\": \"{kid_name} - Aprobaciones - {chore_name}\"\n      },\n      \"penalty_applies_sensor\": {\n        \"name\": \"{kid_name} - Penalizaciones Aplicadas - {penalty_name}\",\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Nombre del Niño/a\"\n          },\n          \"penalty_name\": {\n            \"name\": \"Nombre de la Penalización\"\n          },\n          \"descriptionn\": {\n            \"name\": \"Descripción\"\n          },\n          \"penalty_points\": {\n            \"name\": \"Puntos de Penalización\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"bonus_applies_sensor\": {\n        \"name\": \"{kid_name} - Bonus Applies - {bonus_name}\",\n        \"state_attributes\": {\n          \"kid_name\": {\n            \"name\": \"Nombre del Niño/a\"\n          },\n          \"bonus_name\": {\n            \"name\": \"Nombre de la Bonificación\"\n          },\n          \"description\": {\n            \"name\": \"Descripción\"\n          },\n          \"bonus_points\": {\n            \"name\": \"Puntos de Bonificación\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"kid_points_earned_daily_sensor\": {\n        \"name\": \"{kid_name} - Puntos Ganados - Diariamente\"\n      },\n      \"kid_points_earned_weekly_sensor\": {\n        \"name\": \"{kid_name} - Puntos Ganados - Semanalmente\"\n      },\n      \"kid_points_earned_monthly_sensor\": {\n        \"name\": \"{kid_name} - Puntos Ganados - Mensualmente\"\n      },\n      \"achievement_state_sensor\": {\n        \"name\": \"Logro - {achievement_name}\",\n        \"state_attributes\": {\n          \"achievement_name\": {\n            \"name\": \"Nombre del Logro\"\n          },\n          \"description\": {\n            \"name\": \"Descripción\"\n          },\n          \"assigned_kids\": {\n            \"name\": \"Niños Asignados\"\n          },\n          \"type\": {\n            \"name\": \"Tipo\",\n            \"state\": {\n              \"chore_total\": \"Tareas Totales\",\n              \"chore_streak\": \"Racha Tareas\",\n              \"daily_minimum\": \"Mínimo Diario\"\n            }\n          },\n          \"associated_chore\": {\n            \"name\": \"Tarea Asociada\"\n          },\n          \"critera\": {\n            \"name\": \"Criterios\"\n          },\n          \"target_value\": {\n            \"name\": \"Objetivo\"\n          },\n          \"reward_points\": {\n            \"name\": \"Puntos de Recompensa\"\n          },\n          \"kids_earned\": {\n            \"name\": \"Ganado por\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"challenge_state_sensor\": {\n        \"name\": \"Desafío - {challenge_name}\",\n        \"state_attributes\": {\n          \"challenge_name\": {\n            \"name\": \"Nombre del Desafío\"\n          },\n          \"description\": {\n            \"name\": \"Descripción\"\n          },\n          \"assigned_kids\": {\n            \"name\": \"Niños Asignados\"\n          },\n          \"type\": {\n            \"name\": \"Tipo\",\n            \"state\": {\n              \"total_within_window\": \"Total en Periodo\",\n              \"daily_minimum\": \"Minimo Diario\"\n            }\n          },\n          \"associated_chore\": {\n            \"name\": \"Tarea Asociada\"\n          },\n          \"critera\": {\n            \"name\": \"Criterios\"\n          },\n          \"target_value\": {\n            \"name\": \"Objetivo\"\n          },\n          \"reward_points\": {\n            \"name\": \"Puntos de Recompensa\"\n          },\n          \"start_date\": {\n            \"name\": \"Fecha de Inicio\"\n          },\n          \"end_date\": {\n            \"name\": \"Fecha de Finalización\"\n          },\n          \"kids_earned\": {\n            \"name\": \"Ganado por\"\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"achievement_progress_sensor\": {\n        \"name\": \"{kid_name} - Progreso - {achievement_name}\",\n        \"state_attributes\": {\n          \"achievement_name\": {\n            \"name\": \"Nombre del Logro\"\n          },\n          \"description\": {\n            \"name\": \"Descripción\"\n          },\n          \"assigned_kids\": {\n            \"name\": \"Niños Asignados\"\n          },\n          \"type\": {\n            \"name\": \"Tipo\",\n            \"state\": {\n              \"chore_total\": \"Tareas Totales\",\n              \"chore_streak\": \"Racha Tareas\",\n              \"daily_minimum\": \"Mínimo Diario\"\n            }\n          },\n          \"associated_chore\": {\n            \"name\": \"Tarea Asociada\"\n          },\n          \"critera\": {\n            \"name\": \"Criterios\"\n          },\n          \"target_value\": {\n            \"name\": \"Objetivo\"\n          },\n          \"reward_points\": {\n            \"name\": \"Puntos de Recompensa\"\n          },\n          \"raw_progress\": {\n            \"name\": \"Progreso\"\n          },\n          \"awarded\": {\n            \"name\": \"Otorgado\",\n            \"state\": {\n              \"true\": \"Sí\",\n              \"false\": \"No\"\n            }\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"challenge_progress_sensor\": {\n        \"name\": \"{kid_name} - Progreso - {challenge_name}\",\n        \"state_attributes\": {\n          \"challenge_name\": {\n            \"name\": \"Nombre del Desafío\"\n          },\n          \"description\": {\n            \"name\": \"Descripción\"\n          },\n          \"assigned_kids\": {\n            \"name\": \"Niños Asignados\"\n          },\n          \"type\": {\n            \"name\": \"Tipo\",\n            \"state\": {\n              \"total_within_window\": \"Total en Periodo\",\n              \"daily_minimum\": \"Minimo Diario\"\n            }\n          },\n          \"associated_chore\": {\n            \"name\": \"Tarea Asociada\"\n          },\n          \"critera\": {\n            \"name\": \"Criterios\"\n          },\n          \"target_value\": {\n            \"name\": \"Objetivo\"\n          },\n          \"reward_points\": {\n            \"name\": \"Puntos de Recompensa\"\n          },\n          \"start_date\": {\n            \"name\": \"Fecha de Inicio\"\n          },\n          \"end_date\": {\n            \"name\": \"Fecha de Finalización\"\n          },\n          \"raw_progress\": {\n            \"name\": \"Progreso\"\n          },\n          \"awarded\": {\n            \"name\": \"Otorgado\",\n            \"state\": {\n              \"true\": \"Sí\",\n              \"false\": \"No\"\n            }\n          },\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"kid_highest_streak_sensor\": {\n        \"name\": \"{kid_name} - Mayor Racha\",\n        \"state_attributes\": {\n          \"streaks_by_achievement\": {\n            \"name\": \"Rachas por Logro\"\n          }\n        }\n      },\n      \"chore_streak_sensor\": {\n        \"name\": \"{kid_name} - Racha - {chore_name}\",\n        \"state_attributes\": {\n          \"last_date\": {\n            \"name\": \"Última Fecha\"\n          },\n          \"raw_streak\": {\n            \"name\": \"Racha Actual\"\n          }\n        }\n      }\n    },\n    \"button\": {\n      \"claim_chore_button\": {\n        \"name\": \"{kid_name} - Reclamar Tarea - {chore_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"approve_chore_button\": {\n        \"name\": \"{kid_name} - Aprobar Tarea - {chore_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"disapprove_chore_button\": {\n        \"name\": \"{kid_name} - Desaprobar Tarea - {chore_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"claim_reward_button\": {\n        \"name\": \"{kid_name} - Reclamar Recompensa - {reward_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"approve_reward_button\": {\n        \"name\": \"{kid_name} - Aprobar Recompensa - {reward_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"disapprove_reward_button\": {\n        \"name\": \"{kid_name} - Desaprobar Recompensa - {reward_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"penalty_button\": {\n        \"name\": \"{kid_name} - Aplicar Penalización - {penalty_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"bonus_button\": {\n        \"name\": \"{kid_name} - Aplicar Bonificación - {bonus_name}\",\n        \"state_attributes\": {\n          \"labels\": {\n            \"name\": \"Etiquetas\"\n          }\n        }\n      },\n      \"manual_adjustment_button\": {\n        \"name\": \"{kid_name} {sign_label} {points_label}\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n  \"name\": \"KidsChores\",\n  \"homeassistant\": \"2024.12\",\n  \"hacs\": \"1.33.0\",\n  \"render_readme\": true\n}\n"
  }
]