Full Code of ad-ha/kidschores-ha for AI

main d8f5f8817e47 cached
29 files
691.8 KB
152.3k tokens
427 symbols
1 requests
Download .txt
Showing preview only (715K chars total). Download the full file or copy to clipboard to get everything.
Repository: ad-ha/kidschores-ha
Branch: main
Commit: d8f5f8817e47
Files: 29
Total size: 691.8 KB

Directory structure:
gitextract_644k2xfj/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── 01-issue_report.yml
│   │   ├── 02-feature_reques.yml
│   │   └── config.yml
│   └── workflows/
│       ├── hassfest.yaml
│       └── validate.yaml
├── .gitignore
├── LICENSE
├── README.md
├── custom_components/
│   └── kidschores/
│       ├── __init__.py
│       ├── button.py
│       ├── calendar.py
│       ├── config_flow.py
│       ├── const.py
│       ├── coordinator.py
│       ├── flow_helpers.py
│       ├── kc_helpers.py
│       ├── manifest.json
│       ├── notification_action_handler.py
│       ├── notification_helper.py
│       ├── options_flow.py
│       ├── select.py
│       ├── sensor.py
│       ├── services.py
│       ├── services.yaml
│       ├── storage_manager.py
│       └── translations/
│           ├── en.json
│           └── es.json
└── hacs.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: varetas3d
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/ISSUE_TEMPLATE/01-issue_report.yml
================================================
---
name: Issue Report
description: Create an issue report to help us improve
title: "[ISSUE] "
labels: bug
assignees: []

body:
  - type: markdown
    attributes:
      value: |
        **Please fill out this form to report a bug with the KidsChore Integration.**

  - type: input
    id: home_assistant_version
    attributes:
      label: Home Assistant Version
      description: "What version of Home Assistant are you using?"
      placeholder: "e.g., 2024.11.1"
    validations:
      required: true

  - type: input
    id: integration_version
    attributes:
      label: KidsChore Integration Version
      description: "What version of the integration are you using?"
      placeholder: "e.g., 0.4.8"
    validations:
      required: true

  - type: dropdown
    id: installation_method
    attributes:
      label: Installation Method
      description: "How did you install the integration?"
      options:
        - HACS
        - Manual
    validations:
      required: true

  - type: checkboxes
    id: prior_issue_check
    attributes:
      label: Did you check for existing issues?
      description: "You should check if there's a current or closed issue."
      options:
        - label: Yes, I have checked for existing issues
          required: true
        - label: No, I have not checked for existing issues

  - type: checkboxes
    id: debug_enabled
    attributes:
      label: Did you enable debug logging before and are ready to post logs?
      options:
        - label: Yes, I have enabled debug logging
          required: true
        - label: No, I have not enabled debug logging

  - type: textarea
    id: issue_description
    attributes:
      label: Describe the Issue
      description: "A clear and concise description of what the bug is."
      placeholder: "Provide a detailed description..."
    validations:
      required: true

  - type: markdown
    attributes:
      value: |
        ### **Logs**

        Please add the following to your `configuration.yaml` on your Home Assistant and restart:

        ```yaml
        logger:
          default: warning
          logs:
            custom_components.kidschores: debug
        ```

        See [Home Assistant Logger Documentation](https://www.home-assistant.io/integrations/logger) for more information.

  - type: textarea
    id: logs
    attributes:
      label: Logs
      description: "Paste your logs here."
      render: yaml

  - type: textarea
    id: additional_context
    attributes:
      label: Additional Context
      description: "Add any other context about the problem here."
      placeholder: "Any additional information..."
---


================================================
FILE: .github/ISSUE_TEMPLATE/02-feature_reques.yml
================================================
---
name: Feature Request
description: Suggest an idea for this project
title: "[REQ] "
labels: enhancement
assignees: []

body:
  - type: markdown
    attributes:
      value: |
        **Please describe the feature you would like to see added to the KidsChores Integration.**

  - type: checkboxes
    id: problem_exists
    attributes:
      label: Is your feature request related to a problem?
      options:
        - label: "Yes"

  - type: textarea
    id: problem
    attributes:
      label: Please describe the problem
      description: A clear and concise description of what the problem is.
      placeholder: Ex. I'm always frustrated when [...]
    validations:
      required: false
      
  - type: textarea
    id: solution
    attributes:
      label: Describe the solution you'd like
      description: A clear and concise description of what you want to happen.
    validations:
      required: true

  - type: textarea
    id: alternatives
    attributes:
      label: Describe alternatives you've considered
      description: A clear and concise description of any alternative solutions or features you've considered.

  - type: textarea
    id: additional_context
    attributes:
      label: Additional context
      description: Add any other context or screenshots about the feature request here.


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false


================================================
FILE: .github/workflows/hassfest.yaml
================================================
name: Validate with hassfest

on:
  push:
  pull_request:
  schedule:
    - cron: "0 0 * * *"

jobs:
  validate:
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@v3"
      - uses: home-assistant/actions/hassfest@master


================================================
FILE: .github/workflows/validate.yaml
================================================
name: HACS Action

on:
  push:
  pull_request:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:

jobs:
  validate-hacs:
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@v3"
      - name: HACS validation
        uses: "hacs/action@main"
        with:
          category: integration


================================================
FILE: .gitignore
================================================
__pycache__/
*.py[cod]


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.


================================================
FILE: README.md
================================================
> [!IMPORTANT]
> **⚠️ ACTIVE DEVELOPMENT HAS MOVED TO CHOREOPS**
>
> `KidsChores` has officially evolved into a new, expanded integration called **[ChoreOps](https://github.com/ccpk1/choreops)**.
> 
> 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.
> 
> **Please migrate to ChoreOps to access all new features and continued support.**
> 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.
> 
> 👉 **[Get started with ChoreOps here!](https://github.com/ccpk1/choreops)**

---

[![HACS Custom](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)
![GitHub Release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/ad-ha/kidschores-ha?include_prereleases)
![GitHub Downloads (all assets, latest release)](https://img.shields.io/github/downloads/ad-ha/kidschores-ha/latest/total)

[![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)
[![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)

<h1>KidsChores</h1>

<p align="center">
  <img src="https://github.com/user-attachments/assets/e95bdb54-2c4c-4a84-96b4-f47a46a1228a" alt="KidsChores logo" width="300">
</p>
<p align="center">
  <a href="https://buymeacoffee.com/varetas3d" target="_blank">
    <img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174">
  </a>
</p>
<p>
  <br>
  <br>
</p>

# 🏆 KidsChores: The Ultimate Home Assistant Chore & Reward System

**The easiest-to-use and most feature-rich chore management system for Home Assistant.**
Get up and running in **10 minutes or less**, with **unmatched capabilities** to gamify the process and keep kids engaged!

✅ **Track chores effortlessly** – Assign chores, set due dates, and track completions.

✅ **Gamify the experience** – **Badges, Achievements, and Challenges** keep kids motivated.

✅ **Bonuses & Penalties** – Reward extra effort and enforce accountability.

✅ **Customizable Rewards** – Give coins, stars, points, or any currency system you choose.

✅ **Built-in User Access Control** – Restricts actions based on roles (kids, parents, admins).

✅ **Smart Notifications** – Notify kids and parents; parents can approve chores & rewards from their phone or watch.

✅ **Calendar Integration & Custom Scheduling** – Automatically manage recurring chores and sync with Home Assistant’s calendar.

✅ **Works Offline & Keeps Data Local** – Everything is processed locally for **privacy & security**.
<br><br>
**"Designed for kids, but flexible for the whole family—assign chores to anyone, from toddlers to teens to adults!"**

📖 **[System Overviews, FAQ's, Tips & Tricks, and Usage Examples in the Wiki →](https://github.com/ad-ha/kidschores-ha/wiki)**

---

## ⚡ Quick Installation

📌 **Via HACS (Recommended)**

1. Ensure **HACS** is installed. ([HACS Setup Guide](https://hacs.xyz/docs/installation/manual))
2. In Home Assistant, go to **HACS > Custom Repositories**.
3. Add `https://github.com/ad-ha/kidschores-ha` as an **Integration**.
4. Search for **KidsChores**, install, and **restart Home Assistant**.

📖 **[Full Setup & Configuration Guide →](https://github.com/ad-ha/kidschores-ha/wiki/Installation-&-Setup)**

---

## 🌟 Key Features

### 👧👦 Multi-User Management

- **Profile Creation & Customization:**

  - Create and manage individual profiles for multiple kids and parents.
  - Track each child’s progress, achievements, and performance with ease.

- **Effortless Management:**
  - Handle multiple kids with a single integration while monitoring individual statistics and trends.
  - **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)**
 
  ---

### ⭐ **Customizable Points System**

- Personalize the points system by choosing your own name and icon (e.g., Stars, Bucks, Coins) to better resonate with your family.

  ---

### 🧹 **Chore Management**

- **Assign & Track Chores:**

  - 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)**
  - Supports **individual chores** (assigned to a single kid) and **shared chores** (requiring participation from multiple kids).
  - **Labels** can be used to **group chores** by type, location, or difficulty—or to **exclude specific chores** based on your family's needs.

- **Smart Notifications & Workflow Approvals:**

  - Parents and kids receive **dynamic notifications** for **chore claims, approvals, and overdue tasks**.
  - Notifications are **actionable** on **phones, tablets, and smartwatches**, allowing parents to **approve or reject** tasks with a single tap.
  - **Customizable reminders** help ensure chores stay on track and are completed on time.

- **Dynamic Chore States & Actions:**
  - Leverage dynamic buttons to claim, approve, or disapprove chores—completion with built-in authorization and contextual notifications.
  - Monitor progress with sensors that update on a per-kid and global level.
 
  ---

### 🎁 **Reward System**

- 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.

- **Customizable & Goal-Oriented:**

  - Add rewards tailored to your kid’s interests (e.g., extra screen time, a special outing).
  - Assign point values to **encourage saving** and **set goals**.

- **Seamless Claim & Workflow Approval Process:**
  - Kids can **claim rewards** when they meet the point requirement.
  - Parents receive an **approval notification**; once approved, **points are automatically deducted**, and the parent is responsible for delivering the reward.
 
  ---

### 🏅 **Badge System**

- 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)**

- **Earned Through Chores & Points:**

  - Kids can unlock badges by **completing chores** or **earning points** (e.g., 100 chores or 100 points).
  - Badge progress is **tracked from the start**, so kids receive credit for past achievements.

- **Multipliers & Tracking:**
  - Badges can apply a **points multiplier** to boost future earnings (e.g., 1.5x points per chore).
  - Tracks each kid’s **highest badge earned** and **full badge history**.
 
  ---

### ⚖️ **Bonuses & Penalties**

- 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)**

- **Bonuses: Reward Extra Effort**

  - Award **extra points** for exceptional behavior, teamwork, or going above expectations.
  - Can be applied manually or automatically through the system, with **custom labels and tracking**.

- **Penalties: Encourage Accountability**
  - Deduct points for missed chores or rule-breaking to **reinforce responsibility**.
  - Easily track applied penalties and ensure fair, transparent adjustments.
 
  ---

### 🏆 **Challenges & Achievements**

- 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)**

- **Achievements: Personal Milestones**

  - Earned by **completing a set number of chores** or **maintaining streaks** over time (e.g., 100 total chores, 30-day streak).
  - Tracks individual progress and provides **long-term motivation**.

- **Challenges: Time-Bound Goals**
  - Require kids to **complete specific tasks within a set timeframe** (e.g., 50 chores in a month).
  - Can be **individual or shared**, encouraging teamwork toward a common goal.

  ---

### 📅 **Calendar Integration**

- KidsChores integrates with **Home Assistant’s calendar**, allowing chores and challenges to displayed alongside other household events.

- **Sync Chores to Calendar:**

  - View **due dates** for individual and shared chores directly in the Home Assistant calendar.
  - Helps parents and kids **plan ahead** and stay organized.

- **Track Challenges & Time-Sensitive Goals:**
  - Challenges with set timeframes (e.g., "Complete 50 chores in a month") appear in the calendar for **easy progress tracking**.
  - Provides a **visual timeline** of ongoing and upcoming challenges.

  ---

### 📊 **Detailed Statistics & Advanced Controls**

- KidsChores provides **comprehensive tracking** through **real-time sensors and interactive buttons**, giving parents full insight into chore activity and progress.

- **Comprehensive Sensors & Data Tracking:**

  - Monitor **daily, weekly, and monthly stats** on chore completions, points earned, rewards redeemed, badges awarded, and penalties applied.
  - Analyze **historical trends** to celebrate progress, adjust incentives, and identify areas for improvement.

- **Interactive Controls & Automation:**

  - Use dynamic buttons for **claiming chores, approving rewards, and applying bonuses or penalties** directly from the UI.
  - Seamlessly integrate with Home Assistant automations for **custom alerts, reports, and dashboard insights**.

- 📖 **[View the Full List of Sensors & Actions →](https://github.com/ad-ha/kidschores-ha/wiki/Sensors-&-Buttons)**

  ---

### 🛠 Customization & User-Friendly Interface

- **🛠 Dynamic Buttons & Actions:**

  - Manage chores and points directly from the Home Assistant UI with buttons for claiming, approving, redeeming, and adjusting points.

- **🌐 Multilingual Support:**

  - Currently available in English and Spanish to cater to a diverse user base.

- **🔧 Easy Setup & Maintenance:**

  - 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:
    - **Points**
    - **Kids & Parents**
    - **Chores**
    - **Rewards**
    - **Badges**
    - **Penalties & Bonuses**
    - **Achievements & Challenges**

- **Organize with Home Assistant Labels:**
  - 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.
 
---

### ⚙️ Make KidsChores Your Own

---

- 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.  

  📅 **Want to set schedules from your Google Calendar?**  
  📲 **Want to claim chores using NFC tags?**  
  ✅ **Want to automatically approve specific chores?**  
  ⏳ **Want to automatically apply a penalty or a custom alert when a chore goes overdue?**  
  
  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.  

---

## 🔐 **Security & Privacy**

🔹 **100% Local & Private** – Your data stays on your Home Assistant instance, ensuring complete privacy.

🔹 **No External Data Sharing** – No cloud services, no third-party access—everything runs securely on your local network.

🔹 **Built-in User Access Control** – Restrict actions based on roles to prevent unauthorized changes.

With **KidsChores**, your family’s information remains private, secure, and fully under your control.

---

## 🤝 Join the Community & Contribute

🚀 **Get Help & Share Ideas**

- 💬 **Join Community Discussions** → [Home Assistant Forum](https://community.home-assistant.io/t/kidschores-family-chore-management-integration)
- 🛠️ **Report Issues & Request Features** → [GitHub Issues](https://github.com/ad-ha/kidschores-ha/issues)

👨‍💻 **Want to contribute?**

- Submit a **pull request**: [GitHub Contributions](https://github.com/ad-ha/kidschores-ha/pulls).
- Help with **translations** and **documentation updates**.

---

KidsChores 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.

Whether you want to **encourage responsibility**, **motivate with rewards**, or simply **streamline household tasks**, KidsChores has you covered.

**Get started today and transform how your family manages chores, rewards, and accountability!**

---

## LICENSE

This project is licensed under the [GPL-3.0 license](LICENSE). See the LICENSE file for details.

---

## DISCLAIMER

THIS 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.


================================================
FILE: custom_components/kidschores/__init__.py
================================================
# File: __init__.py
"""Initialization file for the KidsChores integration.

Handles setting up the integration, including loading configuration entries,
initializing data storage, and preparing the coordinator for data handling.

Key Features:
- Config entry setup and unload support.
- Coordinator initialization for data synchronization.
- Storage management for persistent data handling.
"""

from __future__ import annotations

import asyncio

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.exceptions import ConfigEntryNotReady

from .const import (
    DOMAIN,
    LOGGER,
    NOTIFICATION_EVENT,
    STORAGE_KEY,
    PLATFORMS,
)
from .coordinator import KidsChoresDataCoordinator
from .notification_action_handler import async_handle_notification_action
from .storage_manager import KidsChoresStorageManager
from .services import async_setup_services, async_unload_services


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up the integration from a config entry."""
    LOGGER.info("Starting setup for KidsChores entry: %s", entry.entry_id)

    # Initialize the storage manager to handle persistent data.
    storage_manager = KidsChoresStorageManager(hass, STORAGE_KEY)
    # Initialize new file.
    await storage_manager.async_initialize()

    # Create the data coordinator for managing updates and synchronization.
    coordinator = KidsChoresDataCoordinator(hass, entry, storage_manager)

    try:
        # Perform the first refresh to load data.
        await coordinator.async_config_entry_first_refresh()
    except ConfigEntryNotReady as e:
        LOGGER.error("Failed to refresh coordinator data: %s", e)
        raise ConfigEntryNotReady from e

    # Store the coordinator and data manager in hass.data.
    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
        "coordinator": coordinator,
        "storage_manager": storage_manager,
    }

    # Set up services required by the integration.
    async_setup_services(hass)

    # Forward the setup to supported platforms (sensors, buttons, etc.).
    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

    # Listen for notification actions from the companion app.
    hass.bus.async_listen(
        NOTIFICATION_EVENT,
        lambda event: asyncio.run_coroutine_threadsafe(
            async_handle_notification_action(hass, event), hass.loop
        ),
    )

    LOGGER.info("KidsChores setup complete for entry: %s", entry.entry_id)
    return True


async def async_unload_entry(hass, entry):
    """Unload a config entry.

    Args:
        hass: Home Assistant instance.
        entry: Config entry to unload.

    """
    LOGGER.info("Unloading KidsChores entry: %s", entry.entry_id)

    # Unload platforms
    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

    if unload_ok:
        hass.data[DOMAIN].pop(entry.entry_id)

        # Await service unloading
        await async_unload_services(hass)

    return unload_ok


async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
    """Handle removal of a config entry."""
    LOGGER.info("Removing KidsChores entry: %s", entry.entry_id)

    # Safely check if data exists before attempting to access it
    if DOMAIN in hass.data and entry.entry_id in hass.data[DOMAIN]:
        storage_manager: KidsChoresStorageManager = hass.data[DOMAIN][entry.entry_id][
            "storage_manager"
        ]
        await storage_manager.async_delete_storage()

    LOGGER.info("KidsChores entry data cleared: %s", entry.entry_id)


================================================
FILE: custom_components/kidschores/button.py
================================================
# File: button.py
"""Buttons for KidsChores integration.

Features:
1) Chore Buttons (Claim & Approve) with user-defined or default icons.
2) Reward Buttons using user-defined or default icons.
3) Penalty Buttons using user-defined or default icons.
4) Bonus Buttons using user-defined or default icons.
5) PointsAdjustButton: manually increments/decrements a kid's points (e.g., +1, -1, +2, -2, etc.).
6) ApproveRewardButton: allows parents to approve rewards claimed by kids.

"""

from homeassistant.auth.models import User
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.exceptions import HomeAssistantError

from .const import (
    ATTR_LABELS,
    BUTTON_BONUS_PREFIX,
    BUTTON_DISAPPROVE_CHORE_PREFIX,
    BUTTON_DISAPPROVE_REWARD_PREFIX,
    BUTTON_PENALTY_PREFIX,
    BUTTON_REWARD_PREFIX,
    CONF_POINTS_LABEL,
    DATA_PENDING_CHORE_APPROVALS,
    DATA_PENDING_REWARD_APPROVALS,
    DEFAULT_BONUS_ICON,
    DEFAULT_CHORE_APPROVE_ICON,
    DEFAULT_CHORE_CLAIM_ICON,
    DEFAULT_DISAPPROVE_ICON,
    DEFAULT_PENALTY_ICON,
    DEFAULT_POINTS_ADJUST_MINUS_ICON,
    DEFAULT_POINTS_ADJUST_MINUS_MULTIPLE_ICON,
    DEFAULT_POINTS_ADJUST_PLUS_ICON,
    DEFAULT_POINTS_ADJUST_PLUS_MULTIPLE_ICON,
    DEFAULT_POINTS_LABEL,
    DEFAULT_REWARD_ICON,
    DOMAIN,
    ERROR_NOT_AUTHORIZED_ACTION_FMT,
    LOGGER,
)
from .coordinator import KidsChoresDataCoordinator
from .kc_helpers import (
    is_user_authorized_for_global_action,
    is_user_authorized_for_kid,
    get_friendly_label,
)


async def async_setup_entry(
    hass: HomeAssistant,
    entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
):
    """Set up dynamic buttons.

    - Chores (Claim & Approve & Disapprove)
    - Rewards (Redeem & Approve & Disapprove)
    - Penalties
    - Kid points adjustments (e.g., +1, -1, +10, -10, etc.)
    - Approve Reward Workflow

    """
    data = hass.data[DOMAIN][entry.entry_id]
    coordinator: KidsChoresDataCoordinator = data["coordinator"]

    points_label = entry.options.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL)

    entities = []

    # Create buttons for chores (Claim, Approve & Disapprove)
    for chore_id, chore_info in coordinator.chores_data.items():
        chore_name = chore_info.get("name", f"Chore {chore_id}")
        assigned_kids_ids = chore_info.get("assigned_kids", [])

        # If user defined an icon, use it; else fallback to default for chore claim
        chore_claim_icon = chore_info.get("icon", DEFAULT_CHORE_CLAIM_ICON)
        # For "approve," use a distinct icon
        chore_approve_icon = chore_info.get("icon", DEFAULT_CHORE_APPROVE_ICON)

        for kid_id in assigned_kids_ids:
            kid_name = coordinator._get_kid_name_by_id(kid_id) or f"Kid {kid_id}"
            # Claim Button
            entities.append(
                ClaimChoreButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    chore_id=chore_id,
                    chore_name=chore_name,
                    icon=chore_claim_icon,
                )
            )
            # Approve Button
            entities.append(
                ApproveChoreButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    chore_id=chore_id,
                    chore_name=chore_name,
                    icon=chore_approve_icon,
                )
            )
            # Disapprove Button
            entities.append(
                DisapproveChoreButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    chore_id=chore_id,
                    chore_name=chore_name,
                )
            )

    # Create reward buttons (Redeem, Approve & Disapprove)
    for kid_id, kid_info in coordinator.kids_data.items():
        kid_name = kid_info.get("name", f"Kid {kid_id}")
        for reward_id, reward_info in coordinator.rewards_data.items():
            # If no user-defined icon, fallback to DEFAULT_REWARD_ICON
            reward_icon = reward_info.get("icon", DEFAULT_REWARD_ICON)
            # Redeem Reward Button
            entities.append(
                RewardButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    reward_id=reward_id,
                    reward_name=reward_info.get("name", f"Reward {reward_id}"),
                    icon=reward_icon,
                )
            )
            # Approve Reward Button
            entities.append(
                ApproveRewardButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    reward_id=reward_id,
                    reward_name=reward_info.get("name", f"Reward {reward_id}"),
                    icon=reward_info.get("icon", DEFAULT_REWARD_ICON),
                )
            )
            # Disapprove Reward Button
            entities.append(
                DisapproveRewardButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    reward_id=reward_id,
                    reward_name=reward_info.get("name", f"Reward {reward_id}"),
                )
            )

    # Create penalty buttons
    for kid_id, kid_info in coordinator.kids_data.items():
        kid_name = kid_info.get("name", f"Kid {kid_id}")
        for penalty_id, penalty_info in coordinator.penalties_data.items():
            # If no user-defined icon, fallback to DEFAULT_PENALTY_ICON
            penalty_icon = penalty_info.get("icon", DEFAULT_PENALTY_ICON)
            entities.append(
                PenaltyButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    penalty_id=penalty_id,
                    penalty_name=penalty_info.get("name", f"Penalty {penalty_id}"),
                    icon=penalty_icon,
                )
            )

    # Create bonus buttons
    for kid_id, kid_info in coordinator.kids_data.items():
        kid_name = kid_info.get("name", f"Kid {kid_id}")
        for bonus_id, bonus_info in coordinator.bonuses_data.items():
            # If no user-defined icon, fallback to DEFAULT_BONUS_ICON
            bonus_icon = bonus_info.get("icon", DEFAULT_BONUS_ICON)
            entities.append(
                BonusButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    bonus_id=bonus_id,
                    bonus_name=bonus_info.get("name", f"Bonus {bonus_id}"),
                    icon=bonus_icon,
                )
            )

    # Create "points adjustment" buttons for each kid (±1, ±2, ±10, etc.)
    POINT_DELTAS = [+1, -1, +2, -2, +10, -10]
    for kid_id, kid_info in coordinator.kids_data.items():
        kid_name = kid_info.get("name", f"Kid {kid_id}")
        for delta in POINT_DELTAS:
            entities.append(
                PointsAdjustButton(
                    coordinator=coordinator,
                    entry=entry,
                    kid_id=kid_id,
                    kid_name=kid_name,
                    delta=delta,
                    points_label=points_label,
                )
            )

    async_add_entities(entities)


# ------------------ Chore Buttons ------------------
class ClaimChoreButton(CoordinatorEntity, ButtonEntity):
    """Button to claim a chore as done (set chore state=claimed)."""

    _attr_has_entity_name = True
    _attr_translation_key = "claim_chore_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        chore_id: str,
        chore_name: str,
        icon: str,
    ):
        """Initialize the claim chore button."""

        super().__init__(coordinator)
        self._entry = entry
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._chore_id = chore_id
        self._chore_name = chore_name
        self._attr_unique_id = f"{entry.entry_id}_{kid_id}_{chore_id}_claim"
        self._attr_icon = icon
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "chore_name": chore_name,
        }
        self.entity_id = f"button.kc_{kid_name}_chore_claim_{chore_name}"

    async def async_press(self):
        """Handle the button press event."""
        try:
            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_kid(
                self.hass, user_id, self._kid_id
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("claim chores")
                )

            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None
            user_name = user_obj.name if user_obj else "Unknown"

            self.coordinator.claim_chore(
                kid_id=self._kid_id,
                chore_id=self._chore_id,
                user_name=user_name,
            )
            LOGGER.info(
                "Chore '%s' claimed by kid '%s' (user: %s)",
                self._chore_name,
                self._kid_name,
                user_name,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to claim chore '%s' for kid '%s': %s",
                self._chore_name,
                self._kid_name,
                e,
            )
        except Exception as e:
            LOGGER.error(
                "Failed to claim chore '%s' for kid '%s': %s",
                self._chore_name,
                self._kid_name,
                e,
            )

    @property
    def extra_state_attributes(self):
        """Include extra state attributes for the button."""
        chore_info = self.coordinator.chores_data.get(self._chore_id, {})
        stored_labels = chore_info.get("chore_labels", [])
        friendly_labels = [
            get_friendly_label(self.hass, label) for label in stored_labels
        ]

        attributes = {
            ATTR_LABELS: friendly_labels,
        }

        return attributes


class ApproveChoreButton(CoordinatorEntity, ButtonEntity):
    """Button to approve a claimed chore for a kid (set chore state=approved or partial)."""

    _attr_has_entity_name = True
    _attr_translation_key = "approve_chore_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        chore_id: str,
        chore_name: str,
        icon: str,
    ):
        """Initialize the approve chore button."""

        super().__init__(coordinator)
        self._entry = entry
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._chore_id = chore_id
        self._chore_name = chore_name
        self._attr_unique_id = f"{entry.entry_id}_{kid_id}_{chore_id}_approve"
        self._attr_icon = icon
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "chore_name": chore_name,
        }
        self.entity_id = f"button.kc_{kid_name}_chore_approval_{chore_name}"

    async def async_press(self):
        """Handle the button press event."""
        try:
            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_global_action(
                self.hass, user_id, "approve_chore"
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("approve chores")
                )

            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None
            parent_name = user_obj.name if user_obj else "ParentOrAdmin"

            self.coordinator.approve_chore(
                parent_name=parent_name,
                kid_id=self._kid_id,
                chore_id=self._chore_id,
            )
            LOGGER.info(
                "Chore '%s' approved for kid '%s'",
                self._chore_name,
                self._kid_name,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to approve chore '%s' for kid '%s': %s",
                self._chore_name,
                self._kid_name,
                e,
            )
        except Exception as e:
            LOGGER.error(
                "Failed to approve chore '%s' for kid '%s': %s",
                self._chore_name,
                self._kid_name,
                e,
            )

    @property
    def extra_state_attributes(self):
        """Include extra state attributes for the button."""
        chore_info = self.coordinator.chores_data.get(self._chore_id, {})
        stored_labels = chore_info.get("chore_labels", [])
        friendly_labels = [
            get_friendly_label(self.hass, label) for label in stored_labels
        ]

        attributes = {
            ATTR_LABELS: friendly_labels,
        }

        return attributes


class DisapproveChoreButton(CoordinatorEntity, ButtonEntity):
    """Button to disapprove a chore."""

    _attr_has_entity_name = True
    _attr_translation_key = "disapprove_chore_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        chore_id: str,
        chore_name: str,
        icon: str = DEFAULT_DISAPPROVE_ICON,
    ):
        """Initialize the disapprove chore button."""

        super().__init__(coordinator)
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._chore_id = chore_id
        self._chore_name = chore_name
        self._attr_unique_id = (
            f"{entry.entry_id}_{BUTTON_DISAPPROVE_CHORE_PREFIX}{kid_id}_{chore_id}"
        )
        self._attr_icon = icon
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "chore_name": chore_name,
        }
        self.entity_id = f"button.kc_{kid_name}_chore_disapproval_{chore_name}"

    async def async_press(self):
        """Handle the button press event."""
        try:
            # Check if there's a pending approval for this kid and chore.
            pending_approvals = self.coordinator._data.get(
                DATA_PENDING_CHORE_APPROVALS, []
            )
            if not any(
                approval["kid_id"] == self._kid_id
                and approval["chore_id"] == self._chore_id
                for approval in pending_approvals
            ):
                raise HomeAssistantError(
                    f"No pending approval found for chore '{self._chore_name}' for kid '{self._kid_name}'."
                )

            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_global_action(
                self.hass, user_id, "disapprove_chore"
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("disapprove chores")
                )

            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None
            parent_name = user_obj.name if user_obj else "ParentOrAdmin"

            self.coordinator.disapprove_chore(
                parent_name=parent_name,
                kid_id=self._kid_id,
                chore_id=self._chore_id,
            )
            LOGGER.info(
                "Chore '%s' disapproved for kid '%s' by parent '%s'",
                self._chore_name,
                self._kid_name,
                parent_name,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to disapprove chore '%s' for kid '%s': %s",
                self._chore_name,
                self._kid_name,
                e,
            )
        except Exception as e:
            LOGGER.error(
                "Failed to disapprove chore '%s' for kid '%s': %s",
                self._chore_name,
                self._kid_name,
                e,
            )

    @property
    def extra_state_attributes(self):
        """Include extra state attributes for the button."""
        chore_info = self.coordinator.chores_data.get(self._chore_id, {})
        stored_labels = chore_info.get("chore_labels", [])
        friendly_labels = [
            get_friendly_label(self.hass, label) for label in stored_labels
        ]

        attributes = {
            ATTR_LABELS: friendly_labels,
        }

        return attributes


# ------------------ Reward Buttons ------------------
class RewardButton(CoordinatorEntity, ButtonEntity):
    """Button to redeem a reward for a kid."""

    _attr_has_entity_name = True
    _attr_translation_key = "claim_reward_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        reward_id: str,
        reward_name: str,
        icon: str,
    ):
        """Initialize the reward button."""
        super().__init__(coordinator)
        self._entry = entry
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._reward_id = reward_id
        self._reward_name = reward_name
        self._attr_unique_id = (
            f"{entry.entry_id}_{BUTTON_REWARD_PREFIX}{kid_id}_{reward_id}"
        )
        self._attr_icon = icon
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "reward_name": reward_name,
        }
        self.entity_id = f"button.kc_{kid_name}_reward_claim_{reward_name}"

    async def async_press(self):
        """Handle the button press event."""
        try:
            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_kid(
                self.hass, user_id, self._kid_id
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("redeem rewards")
                )

            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None
            parent_name = user_obj.name if user_obj else "Unknown"

            self.coordinator.redeem_reward(
                parent_name=parent_name,
                kid_id=self._kid_id,
                reward_id=self._reward_id,
            )
            LOGGER.info(
                "Reward '%s' redeemed for kid '%s' by parent '%s'",
                self._reward_name,
                self._kid_name,
                parent_name,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to redeem reward '%s' for kid '%s': %s",
                self._reward_name,
                self._kid_name,
                e,
            )
        except Exception as e:
            LOGGER.error(
                "Failed to redeem reward '%s' for kid '%s': %s",
                self._reward_name,
                self._kid_name,
                e,
            )

    @property
    def extra_state_attributes(self):
        """Include extra state attributes for the button."""
        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})
        stored_labels = reward_info.get("reward_labels", [])
        friendly_labels = [
            get_friendly_label(self.hass, label) for label in stored_labels
        ]

        attributes = {
            ATTR_LABELS: friendly_labels,
        }

        return attributes


class ApproveRewardButton(CoordinatorEntity, ButtonEntity):
    """Button for parents to approve a reward claimed by a kid.

    Prevents unauthorized or premature reward approvals.
    """

    _attr_has_entity_name = True
    _attr_translation_key = "approve_reward_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        reward_id: str,
        reward_name: str,
        icon: str,
    ):
        """Initialize the approve reward button."""

        super().__init__(coordinator)
        self._entry = entry
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._reward_id = reward_id
        self._reward_name = reward_name
        self._attr_unique_id = f"{entry.entry_id}_{kid_id}_{reward_id}_approve_reward"
        self._attr_icon = icon
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "reward_name": reward_name,
        }
        self.entity_id = f"button.kc_{kid_name}_reward_approval_{reward_name}"

    async def async_press(self):
        """Handle the button press event."""
        try:
            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_global_action(
                self.hass, user_id, "approve_reward"
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("approve rewards")
                )

            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None
            parent_name = user_obj.name if user_obj else "ParentOrAdmin"

            # Approve the reward
            self.coordinator.approve_reward(
                parent_name=parent_name,
                kid_id=self._kid_id,
                reward_id=self._reward_id,
            )

            LOGGER.info(
                "Reward '%s' approved for kid '%s' by parent '%s'",
                self._reward_name,
                self._kid_name,
                parent_name,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to approve reward '%s' for kid '%s': %s",
                self._reward_name,
                self._kid_name,
                e,
            )
            # Send a persistent notification for the error
            if user_id:
                self.hass.components.persistent_notification.create(
                    f"Failed to approve reward '{self._reward_name}' for {self._kid_name}: {e}",
                    title="Reward Approval Failed",
                    notification_id=f"approve_reward_error_{self._reward_id}",
                )
        except Exception as e:
            LOGGER.error(
                "Failed to approve reward '%s' for kid '%s': %s",
                self._reward_name,
                self._kid_name,
                e,
            )
            # Send a persistent notification for the unexpected error
            if user_id:
                self.hass.components.persistent_notification.create(
                    f"An unexpected error occurred while approving reward '{self._reward_name}' for {self._kid_name}",
                    title="Reward Approval Error",
                    notification_id=f"approve_reward_unexpected_error_{self._reward_id}",
                )

    @property
    def extra_state_attributes(self):
        """Include extra state attributes for the button."""
        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})
        stored_labels = reward_info.get("reward_labels", [])
        friendly_labels = [
            get_friendly_label(self.hass, label) for label in stored_labels
        ]

        attributes = {
            ATTR_LABELS: friendly_labels,
        }

        return attributes


class DisapproveRewardButton(CoordinatorEntity, ButtonEntity):
    """Button to disapprove a reward."""

    _attr_has_entity_name = True
    _attr_translation_key = "disapprove_reward_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        reward_id: str,
        reward_name: str,
        icon: str = DEFAULT_DISAPPROVE_ICON,
    ):
        """Initialize the disapprove reward button."""

        super().__init__(coordinator)
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._reward_id = reward_id
        self._reward_name = reward_name
        self._attr_unique_id = (
            f"{entry.entry_id}_{BUTTON_DISAPPROVE_REWARD_PREFIX}{kid_id}_{reward_id}"
        )
        self._attr_icon = icon
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "reward_name": reward_name,
        }
        self.entity_id = f"button.kc_{kid_name}_reward_disapproval_{reward_name}"

    async def async_press(self):
        """Handle the button press event."""
        try:
            # Check if there's a pending approval for this kid and reward.
            pending_approvals = self.coordinator._data.get(
                DATA_PENDING_REWARD_APPROVALS, []
            )
            if not any(
                approval["kid_id"] == self._kid_id
                and approval["reward_id"] == self._reward_id
                for approval in pending_approvals
            ):
                raise HomeAssistantError(
                    f"No pending approval found for reward '{self._reward_name}' for kid '{self._kid_name}'."
                )

            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_global_action(
                self.hass, user_id, "disapprove_reward"
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("disapprove rewards")
                )

            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None
            parent_name = user_obj.name if user_obj else "ParentOrAdmin"

            self.coordinator.disapprove_reward(
                parent_name=parent_name,
                kid_id=self._kid_id,
                reward_id=self._reward_id,
            )
            LOGGER.info(
                "Reward '%s' disapproved for kid '%s' by parent '%s'",
                self._reward_name,
                self._kid_name,
                parent_name,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to disapprove reward '%s' for kid '%s': %s",
                self._reward_name,
                self._kid_name,
                e,
            )
        except Exception as e:
            LOGGER.error(
                "Failed to disapprove reward '%s' for kid '%s': %s",
                self._reward_name,
                self._kid_name,
                e,
            )

    @property
    def extra_state_attributes(self):
        """Include extra state attributes for the button."""
        reward_info = self.coordinator.rewards_data.get(self._reward_id, {})
        stored_labels = reward_info.get("reward_labels", [])
        friendly_labels = [
            get_friendly_label(self.hass, label) for label in stored_labels
        ]

        attributes = {
            ATTR_LABELS: friendly_labels,
        }

        return attributes


# ------------------ Penalty Button ------------------
class PenaltyButton(CoordinatorEntity, ButtonEntity):
    """Button to apply a penalty for a kid.

    Uses user-defined or default penalty icon.
    """

    _attr_has_entity_name = True
    _attr_translation_key = "penalty_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        penalty_id: str,
        penalty_name: str,
        icon: str,
    ):
        """Initialize the penalty button."""

        super().__init__(coordinator)
        self._entry = entry
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._penalty_id = penalty_id
        self._penalty_name = penalty_name
        self._attr_unique_id = (
            f"{entry.entry_id}_{BUTTON_PENALTY_PREFIX}{kid_id}_{penalty_id}"
        )
        self._attr_icon = icon
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "penalty_name": penalty_name,
        }
        self.entity_id = f"button.kc_{kid_name}_penalty_{penalty_name}"

    async def async_press(self):
        """Handle the button press event."""
        try:
            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_global_action(
                self.hass, user_id, "apply_penalty"
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("apply penalties")
                )

            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None
            parent_name = user_obj.name if user_obj else "Unknown"

            self.coordinator.apply_penalty(
                parent_name=parent_name,
                kid_id=self._kid_id,
                penalty_id=self._penalty_id,
            )
            LOGGER.info(
                "Penalty '%s' applied to kid '%s' by '%s'",
                self._penalty_name,
                self._kid_name,
                parent_name,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to apply penalty '%s' for kid '%s': %s",
                self._penalty_name,
                self._kid_name,
                e,
            )
        except Exception as e:
            LOGGER.error(
                "Failed to apply penalty '%s' for kid '%s': %s",
                self._penalty_name,
                self._kid_name,
                e,
            )

    @property
    def extra_state_attributes(self):
        """Include extra state attributes for the button."""
        penalty_info = self.coordinator.penalties_data.get(self._penalty_id, {})
        stored_labels = penalty_info.get("penalty_labels", [])
        friendly_labels = [
            get_friendly_label(self.hass, label) for label in stored_labels
        ]

        attributes = {
            ATTR_LABELS: friendly_labels,
        }

        return attributes


# ------------------ Points Adjust Button ------------------
class PointsAdjustButton(CoordinatorEntity, ButtonEntity):
    """Button that increments or decrements a kid's points by 'delta'.

    For example: +1, -1, +10, -10, etc.
    Uses icons from const.py for plus/minus, or fallback if desired.
    """

    _attr_has_entity_name = True
    _attr_translation_key = "manual_adjustment_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        delta: int,
        points_label: str,
    ):
        """Initialize the points adjust buttons."""

        super().__init__(coordinator)
        self._entry = entry
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._delta = delta
        self._points_label = str(points_label)

        sign_label = f"+{delta}" if delta >= 0 else f"-{delta}"
        sign_text = f"plus_{delta}" if delta >= 0 else f"minus_{delta}"
        self._attr_unique_id = f"{entry.entry_id}_{kid_id}_adjust_points_{delta}"
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "sign_label": sign_label,
            "points_label": points_label,
        }
        self.entity_id = f"button.kc_{kid_name}_{sign_text}_points"

        # Decide the icon based on whether delta is positive or negative
        if delta >= 2:
            self._attr_icon = DEFAULT_POINTS_ADJUST_PLUS_MULTIPLE_ICON
        elif delta > 0:
            self._attr_icon = DEFAULT_POINTS_ADJUST_PLUS_ICON
        elif delta <= -2:
            self._attr_icon = DEFAULT_POINTS_ADJUST_MINUS_MULTIPLE_ICON
        elif delta < 0:
            self._attr_icon = DEFAULT_POINTS_ADJUST_MINUS_ICON
        else:
            self._attr_icon = DEFAULT_POINTS_ADJUST_PLUS_ICON

    async def async_press(self):
        """Handle the button press event."""
        try:
            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_global_action(
                self.hass, user_id, "adjust_points"
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("adjust points")
                )

            current_points = self.coordinator.kids_data[self._kid_id]["points"]
            new_points = current_points + self._delta
            self.coordinator.update_kid_points(
                kid_id=self._kid_id,
                new_points=new_points,
            )
            LOGGER.info(
                "Adjusted points for kid '%s' by %d => total %d",
                self._kid_name,
                self._delta,
                new_points,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to adjust points for kid '%s' by %d: %s",
                self._kid_name,
                self._delta,
                e,
            )
        except Exception as e:
            LOGGER.error(
                "Failed to adjust points for kid '%s' by %d: %s",
                self._kid_name,
                self._delta,
                e,
            )


class BonusButton(CoordinatorEntity, ButtonEntity):
    """Button to apply a bonus for a kid.

    Uses user-defined or default bonus icon.
    """

    _attr_has_entity_name = True
    _attr_translation_key = "bonus_button"

    def __init__(
        self,
        coordinator: KidsChoresDataCoordinator,
        entry: ConfigEntry,
        kid_id: str,
        kid_name: str,
        bonus_id: str,
        bonus_name: str,
        icon: str,
    ):
        """Initialize the bonus button."""
        super().__init__(coordinator)
        self._entry = entry
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._bonus_id = bonus_id
        self._bonus_name = bonus_name
        self._attr_unique_id = (
            f"{entry.entry_id}_{BUTTON_BONUS_PREFIX}{kid_id}_{bonus_id}"
        )
        self._attr_icon = icon
        self._attr_translation_placeholders = {
            "kid_name": kid_name,
            "bonus_name": bonus_name,
        }
        self.entity_id = f"button.kc_{kid_name}_bonus_{bonus_name}"

    async def async_press(self):
        """Handle the button press event."""
        try:
            user_id = self._context.user_id if self._context else None
            if user_id and not await is_user_authorized_for_global_action(
                self.hass, user_id, "apply_bonus"
            ):
                raise HomeAssistantError(
                    ERROR_NOT_AUTHORIZED_ACTION_FMT.format("apply bonus")
                )

            user_obj = await self.hass.auth.async_get_user(user_id) if user_id else None
            parent_name = user_obj.name if user_obj else "Unknown"

            self.coordinator.apply_bonus(
                parent_name=parent_name,
                kid_id=self._kid_id,
                bonus_id=self._bonus_id,
            )
            LOGGER.info(
                "Bonus '%s' applied to kid '%s' by '%s'",
                self._bonus_name,
                self._kid_name,
                parent_name,
            )
            await self.coordinator.async_request_refresh()

        except HomeAssistantError as e:
            LOGGER.error(
                "Authorization failed to apply bonus '%s' for kid '%s': %s",
                self._bonus_name,
                self._kid_name,
                e,
            )
        except Exception as e:
            LOGGER.error(
                "Failed to apply bonus '%s' for kid '%s': %s",
                self._bonus_name,
                self._kid_name,
                e,
            )

    @property
    def extra_state_attributes(self):
        """Include extra state attributes for the button."""
        bonus_info = self.coordinator.bonuses_data.get(self._bonus_id, {})
        stored_labels = bonus_info.get("bonus_labels", [])
        friendly_labels = [
            get_friendly_label(self.hass, label) for label in stored_labels
        ]

        attributes = {
            ATTR_LABELS: friendly_labels,
        }

        return attributes


================================================
FILE: custom_components/kidschores/calendar.py
================================================
# File: calendar.py

import datetime
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.util import dt as dt_util

from .const import (
    DOMAIN,
    FREQUENCY_BIWEEKLY,
    FREQUENCY_CUSTOM,
    FREQUENCY_DAILY,
    FREQUENCY_MONTHLY,
    FREQUENCY_NONE,
    FREQUENCY_WEEKLY,
    LOGGER,
    WEEKDAY_OPTIONS,
    ATTR_KID_NAME,
)

# Map weekday integers (0=Monday, …) to e.g. "mon","tue","wed" in WEEKDAY_OPTIONS.
WEEKDAY_MAP = {i: key for i, key in enumerate(WEEKDAY_OPTIONS.keys())}

# For chores without a due_date, we generate up to 3 months
FOREVER_DURATION = datetime.timedelta(days=90)


async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities
):
    """Set up the KidsChores calendar platform."""
    try:
        coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
    except KeyError:
        LOGGER.error("Coordinator not found in hass.data for entry %s", entry.entry_id)
        return

    entities = []
    for kid_id, kid_info in coordinator.kids_data.items():
        kid_name = kid_info.get("name", f"Kid {kid_id}")
        entities.append(KidsChoresCalendarEntity(coordinator, kid_id, kid_name, entry))
    async_add_entities(entities)


class KidsChoresCalendarEntity(CalendarEntity):
    """Calendar entity representing a kid's combined chores + challenges."""

    def __init__(self, coordinator, kid_id: str, kid_name: str, config_entry):
        super().__init__()
        self.coordinator = coordinator
        self._kid_id = kid_id
        self._kid_name = kid_name
        self._config_entry = config_entry
        self._attr_name = f"KidsChores Calendar: {kid_name}"
        self._attr_unique_id = f"{config_entry.entry_id}_{kid_id}_calendar"
        self.entity_id = f"calendar.kc_{kid_name}"

    async def async_get_events(
        self, hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
    ) -> list[CalendarEvent]:
        """
        Return CalendarEvent objects for:
         - chores assigned to this kid
         - challenges assigned to this kid
        overlapping [start, end].
        """
        local_tz = dt_util.get_time_zone(self.hass.config.time_zone)
        if start.tzinfo is None:
            start = start.replace(tzinfo=local_tz)
        if end.tzinfo is None:
            end = end.replace(tzinfo=local_tz)

        events: list[CalendarEvent] = []

        # 1) Generate chore events
        for chore in self.coordinator.chores_data.values():
            if self._kid_id in chore.get("assigned_kids", []):
                events.extend(self._generate_events_for_chore(chore, start, end))

        # 2) Generate challenge events
        for challenge in self.coordinator.challenges_data.values():
            if self._kid_id in challenge.get("assigned_kids", []):
                evs = self._generate_events_for_challenge(challenge, start, end)
                events.extend(evs)

        return events

    def _generate_events_for_chore(
        self,
        chore: dict,
        window_start: datetime.datetime,
        window_end: datetime.datetime,
    ) -> list[CalendarEvent]:
        """Same recurring-chores logic from earlier solutions."""
        events: list[CalendarEvent] = []

        summary = chore.get("name", "Unnamed Chore")
        description = chore.get("description", "")
        recurring = chore.get("recurring_frequency", FREQUENCY_NONE)
        applicable_days = chore.get("applicable_days", [])

        # Parse chore due_date if any
        due_date_str = chore.get("due_date")
        due_dt: datetime.datetime | None = None
        if due_date_str:
            dt_parsed = dt_util.parse_datetime(due_date_str)
            if dt_parsed:
                due_dt = dt_util.as_local(dt_parsed)

        def is_midnight(dt_obj: datetime.datetime) -> bool:
            return (dt_obj.hour, dt_obj.minute, dt_obj.second) == (0, 0, 0)

        def overlaps(ev: CalendarEvent) -> bool:
            """Check if event overlaps [window_start, window_end]."""
            sdt = ev.start
            edt = ev.end
            if isinstance(sdt, datetime.date) and not isinstance(
                sdt, datetime.datetime
            ):
                tz = dt_util.get_time_zone(self.hass.config.time_zone)
                sdt = datetime.datetime.combine(sdt, datetime.time.min, tzinfo=tz)
            if isinstance(edt, datetime.date) and not isinstance(
                edt, datetime.datetime
            ):
                tz = dt_util.get_time_zone(self.hass.config.time_zone)
                edt = datetime.datetime.combine(edt, datetime.time.min, tzinfo=tz)
            if not sdt or not edt:
                return False
            return (edt > window_start) and (sdt < window_end)

        # --- Non-recurring chores ---
        if recurring == FREQUENCY_NONE:
            if due_dt:
                # single event if in window
                if window_start <= due_dt <= window_end:
                    if is_midnight(due_dt):
                        e = CalendarEvent(
                            summary=summary,
                            start=due_dt.date(),
                            end=due_dt.date() + datetime.timedelta(days=1),
                            description=description,
                        )
                    else:
                        e = CalendarEvent(
                            summary=summary,
                            start=due_dt,
                            end=due_dt + datetime.timedelta(hours=1),
                            description=description,
                        )
                    if overlaps(e):
                        events.append(e)
            else:
                # No due_date => possibly show on applicable_days for next 3 months
                if applicable_days:
                    gen_start = window_start
                    gen_end = min(
                        window_end,
                        dt_util.as_local(datetime.datetime.now() + FOREVER_DURATION),
                    )
                    current = gen_start
                    while current <= gen_end:
                        if WEEKDAY_MAP[current.weekday()] in applicable_days:
                            e = CalendarEvent(
                                summary=summary,
                                start=current.date(),
                                end=current.date() + datetime.timedelta(days=1),
                                description=description,
                            )
                            if overlaps(e):
                                events.append(e)
                        current += datetime.timedelta(days=1)

            return events

        # --- Recurring chores with a due_date ---
        if due_dt:
            cutoff = min(due_dt, window_end)
            if cutoff < window_start:
                return events

            if recurring == FREQUENCY_DAILY:
                if window_start <= due_dt <= window_end:
                    if is_midnight(due_dt):
                        e = CalendarEvent(
                            summary=summary,
                            start=due_dt.date(),
                            end=due_dt.date() + datetime.timedelta(days=1),
                            description=description,
                        )
                    else:
                        e = CalendarEvent(
                            summary=summary,
                            start=due_dt,
                            end=due_dt + datetime.timedelta(hours=1),
                            description=description,
                        )
                    if overlaps(e):
                        events.append(e)

            elif recurring == FREQUENCY_WEEKLY:
                start_event = due_dt - datetime.timedelta(weeks=1)
                end_event = due_dt
                if start_event < window_end and end_event > window_start:
                    e = CalendarEvent(
                        summary=summary,
                        start=start_event.date(),
                        end=(end_event.date() + datetime.timedelta(days=1)),
                        description=description,
                    )
                    if overlaps(e):
                        events.append(e)

            elif recurring == FREQUENCY_BIWEEKLY:
                start_event = due_dt - datetime.timedelta(weeks=2)
                end_event = due_dt
                if start_event < window_end and end_event > window_start:
                    e = CalendarEvent(
                        summary=summary,
                        start=start_event.date(),
                        end=(end_event.date() + datetime.timedelta(days=1)),
                        description=description,
                    )
                    if overlaps(e):
                        events.append(e)

            elif recurring == FREQUENCY_MONTHLY:
                first_day = due_dt.replace(day=1)
                if first_day < window_end and due_dt > window_start:
                    e = CalendarEvent(
                        summary=summary,
                        start=first_day.date(),
                        end=(due_dt.date() + datetime.timedelta(days=1)),
                        description=description,
                    )
                    if overlaps(e):
                        events.append(e)

            elif recurring == FREQUENCY_CUSTOM:
                interval = chore.get("custom_interval", 1)
                unit = chore.get("custom_interval_unit", "days")
                if unit == "days":
                    start_event = due_dt - datetime.timedelta(days=interval)
                elif unit == "weeks":
                    start_event = due_dt - datetime.timedelta(weeks=interval)
                elif unit == "months":
                    start_event = due_dt - datetime.timedelta(days=30 * interval)
                else:
                    start_event = due_dt

                if start_event < window_end and due_dt > window_start:
                    e = CalendarEvent(
                        summary=summary,
                        start=start_event.date(),
                        end=(due_dt.date() + datetime.timedelta(days=1)),
                        description=description,
                    )
                    if overlaps(e):
                        events.append(e)

            return events

        # --- Recurring chores without a due_date => next 3 months
        gen_start = window_start
        future_limit = dt_util.as_local(datetime.datetime.now() + FOREVER_DURATION)
        cutoff = min(window_end, future_limit)

        if recurring == FREQUENCY_DAILY:
            current = gen_start
            while current <= cutoff:
                if (
                    applicable_days
                    and WEEKDAY_MAP[current.weekday()] not in applicable_days
                ):
                    current += datetime.timedelta(days=1)
                    continue
                e = CalendarEvent(
                    summary=summary,
                    start=current.date(),
                    end=current.date() + datetime.timedelta(days=1),
                    description=description,
                )
                if overlaps(e):
                    events.append(e)
                current += datetime.timedelta(days=1)
            return events

        if recurring in (FREQUENCY_WEEKLY, FREQUENCY_BIWEEKLY):
            week_delta = 7 if recurring == FREQUENCY_WEEKLY else 14
            current = gen_start
            # align to Monday
            while current.weekday() != 0:
                current += datetime.timedelta(days=1)
            while current <= cutoff:
                # multi-day block from Monday..Sunday (or 2 weeks for biweekly)
                block_days = 6 if recurring == FREQUENCY_WEEKLY else 13
                start_block = current
                end_block = current + datetime.timedelta(days=block_days)
                e = CalendarEvent(
                    summary=summary,
                    start=start_block.date(),
                    end=end_block.date() + datetime.timedelta(days=1),
                    description=description,
                )
                if overlaps(e):
                    events.append(e)
                current += datetime.timedelta(days=week_delta)
            return events

        if recurring == FREQUENCY_MONTHLY:
            cur = gen_start
            while cur <= cutoff:
                first_day = cur.replace(day=1)
                next_month = first_day + datetime.timedelta(days=32)
                next_month = next_month.replace(day=1)
                last_day = next_month - datetime.timedelta(days=1)

                e = CalendarEvent(
                    summary=summary,
                    start=first_day.date(),
                    end=last_day.date() + datetime.timedelta(days=1),
                    description=description,
                )
                if overlaps(e):
                    events.append(e)
                cur = next_month
            return events

        if recurring == FREQUENCY_CUSTOM:
            interval = chore.get("custom_interval", 1)
            unit = chore.get("custom_interval_unit", "days")
            if unit == "days":
                step = datetime.timedelta(days=interval)
            elif unit == "weeks":
                step = datetime.timedelta(weeks=interval)
            elif unit == "months":
                step = datetime.timedelta(days=30 * interval)
            else:
                step = datetime.timedelta(days=interval)

            current = gen_start
            while current <= cutoff:
                # Check applicable days
                if (
                    applicable_days
                    and WEEKDAY_MAP[current.weekday()] not in applicable_days
                ):
                    current += step
                    continue
                e = CalendarEvent(
                    summary=summary,
                    start=current.date(),
                    end=current.date() + step,
                    description=description,
                )
                if overlaps(e):
                    events.append(e)
                current += step
            return events

        return events

    def _generate_events_for_challenge(
        self,
        challenge: dict,
        window_start: datetime.datetime,
        window_end: datetime.datetime,
    ) -> list[CalendarEvent]:
        """
        Produce a single multi-day event for each challenge that has valid start_date/end_date.
        Only if it overlaps the requested [window_start, window_end].
        """
        events: list[CalendarEvent] = []

        challenge_name = challenge.get("name", "Unnamed Challenge")
        description = challenge.get("description", "")
        start_str = challenge.get("start_date")
        end_str = challenge.get("end_date")
        if not start_str or not end_str:
            return events  # no valid date range => skip

        start_dt = dt_util.parse_datetime(start_str)
        end_dt = dt_util.parse_datetime(end_str)
        if not start_dt or not end_dt:
            return events  # parsing failed => skip

        # Convert to local
        local_start = dt_util.as_local(start_dt)
        local_end = dt_util.as_local(end_dt)

        # If the challenge times are midnight-based, we can treat them as all-day.
        # But let's keep it simpler => always treat as an all-day block from date(start) to date(end)+1
        # so the user sees a big multi-day block.
        if local_start > window_end or local_end < window_start:
            return events  # out of range

        # Build an all-day event from local_start.date() to local_end.date() + 1 day
        ev = CalendarEvent(
            summary=f"Challenge: {challenge_name}",
            start=local_start.date(),
            end=local_end.date() + datetime.timedelta(days=1),
            description=description,
        )

        # Overlap check (similar logic):
        def overlaps(e: CalendarEvent) -> bool:
            sdt = e.start
            edt = e.end
            # convert if needed
            tz = dt_util.get_time_zone(self.hass.config.time_zone)
            if isinstance(sdt, datetime.date) and not isinstance(
                sdt, datetime.datetime
            ):
                sdt = datetime.datetime.combine(sdt, datetime.time.min, tzinfo=tz)
            if isinstance(edt, datetime.date) and not isinstance(
                edt, datetime.datetime
            ):
                edt = datetime.datetime.combine(edt, datetime.time.min, tzinfo=tz)
            return bool(sdt and edt and (edt > window_start) and (sdt < window_end))

        if overlaps(ev):
            events.append(ev)

        return events

    @property
    def event(self) -> CalendarEvent | None:
        """
        Return a single "current" event (chore or challenge) if one is active now (±1h).
        Otherwise None.
        """
        now = dt_util.as_local(datetime.datetime.utcnow())
        window_start = now - datetime.timedelta(hours=1)
        window_end = now + datetime.timedelta(hours=1)
        all_events = self._generate_all_events(window_start, window_end)
        for e in all_events:
            # Convert date->datetime for comparison
            tz = dt_util.get_time_zone(self.hass.config.time_zone)
            sdt = e.start
            edt = e.end
            if isinstance(sdt, datetime.date) and not isinstance(
                sdt, datetime.datetime
            ):
                sdt = datetime.datetime.combine(sdt, datetime.time.min, tzinfo=tz)
            if isinstance(edt, datetime.date) and not isinstance(
                edt, datetime.datetime
            ):
                edt = datetime.datetime.combine(edt, datetime.time.min, tzinfo=tz)
            if sdt and edt and sdt <= now < edt:
                return e
        return None

    def _generate_all_events(
        self, window_start: datetime.datetime, window_end: datetime.datetime
    ) -> list[CalendarEvent]:
        """Generate chores + challenges for this kid in the given window."""
        events = []
        # chores
        for chore in self.coordinator.chores_data.values():
            if self._kid_id in chore.get("assigned_kids", []):
                events.extend(
                    self._generate_events_for_chore(chore, window_start, window_end)
                )
        # challenges
        for challenge in self.coordinator.challenges_data.values():
            if self._kid_id in challenge.get("assigned_kids", []):
                events.extend(
                    self._generate_events_for_challenge(
                        challenge, window_start, window_end
                    )
                )
        return events

    @property
    def extra_state_attributes(self):
        return {ATTR_KID_NAME: self._kid_name}


================================================
FILE: custom_components/kidschores/config_flow.py
================================================
# File: config_flow.py
"""Multi-step config flow for the KidsChores integration, storing entities by internal_id.

Ensures that all add/edit/delete operations reference entities via internal_id for consistency.
"""

import datetime
import uuid
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
from typing import Any, Optional

from .const import (
    ACHIEVEMENT_TYPE_STREAK,
    CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW,
    CONF_APPLICABLE_DAYS,
    CONF_ACHIEVEMENTS,
    CONF_BADGES,
    CONF_CHALLENGES,
    CONF_CHORES,
    CONF_KIDS,
    CONF_NOTIFY_ON_APPROVAL,
    CONF_NOTIFY_ON_CLAIM,
    CONF_NOTIFY_ON_DISAPPROVAL,
    CONF_PARENTS,
    CONF_PENALTIES,
    CONF_POINTS_ICON,
    CONF_POINTS_LABEL,
    CONF_REWARDS,
    CONF_BONUSES,
    DEFAULT_APPLICABLE_DAYS,
    DEFAULT_NOTIFY_ON_APPROVAL,
    DEFAULT_NOTIFY_ON_CLAIM,
    DEFAULT_NOTIFY_ON_DISAPPROVAL,
    DEFAULT_POINTS_ICON,
    DEFAULT_POINTS_LABEL,
    FREQUENCY_CUSTOM,
    DOMAIN,
    LOGGER,
)
from .flow_helpers import (
    build_points_schema,
    build_kid_schema,
    build_parent_schema,
    build_chore_schema,
    build_badge_schema,
    build_reward_schema,
    build_penalty_schema,
    build_achievement_schema,
    build_challenge_schema,
    ensure_utc_datetime,
    build_bonus_schema,
)


class KidsChoresConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    """Config Flow for KidsChores with internal_id-based entity management."""

    VERSION = 1

    def __init__(self) -> None:
        """Initialize the config flow."""
        self._data: dict[str, Any] = {}
        self._kids_temp: dict[str, dict[str, Any]] = {}
        self._parents_temp: dict[str, dict[str, Any]] = {}
        self._chores_temp: dict[str, dict[str, Any]] = {}
        self._badges_temp: dict[str, dict[str, Any]] = {}
        self._rewards_temp: dict[str, dict[str, Any]] = {}
        self._achievements_temp: dict[str, dict[str, Any]] = {}
        self._challenges_temp: dict[str, dict[str, Any]] = {}
        self._penalties_temp: dict[str, dict[str, Any]] = {}
        self._bonuses_temp: dict[str, dict[str, Any]] = {}

        self._kid_count: int = 0
        self._parents_count: int = 0
        self._chore_count: int = 0
        self._badge_count: int = 0
        self._reward_count: int = 0
        self._achievement_count: int = 0
        self._challenge_count: int = 0
        self._penalty_count: int = 0
        self._bonus_count: int = 0

        self._kid_index: int = 0
        self._parents_index: int = 0
        self._chore_index: int = 0
        self._badge_index: int = 0
        self._reward_index: int = 0
        self._achievement_index: int = 0
        self._challenge_index: int = 0
        self._penalty_index: int = 0
        self._bonus_index: int = 0

    async def async_step_user(self, user_input: Optional[dict[str, Any]] = None):
        """Start the config flow with an intro step."""

        # Check if there's an existing KidsChores entry
        if any(self._async_current_entries()):
            return self.async_abort(reason="single_instance_allowed")

        # Continue your normal flow
        return await self.async_step_intro()

    async def async_step_intro(self, user_input=None):
        """Intro / welcome step. Press Next to continue."""
        if user_input is not None:
            return await self.async_step_points_label()

        return self.async_show_form(step_id="intro", data_schema=vol.Schema({}))

    async def async_step_points_label(self, user_input=None):
        """Let the user define a custom label for points."""
        errors = {}

        if user_input is not None:
            points_label = user_input.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL)
            points_icon = user_input.get(CONF_POINTS_ICON, DEFAULT_POINTS_ICON)

            self._data[CONF_POINTS_LABEL] = points_label
            self._data[CONF_POINTS_ICON] = points_icon

            return await self.async_step_kid_count()

        points_schema = build_points_schema(
            default_label=DEFAULT_POINTS_LABEL, default_icon=DEFAULT_POINTS_ICON
        )

        return self.async_show_form(
            step_id="points_label", data_schema=points_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # KIDS
    # --------------------------------------------------------------------------
    async def async_step_kid_count(self, user_input=None):
        """Ask how many kids to define initially."""
        errors = {}
        if user_input is not None:
            try:
                self._kid_count = int(user_input["kid_count"])
                if self._kid_count < 0:
                    raise ValueError
                if self._kid_count == 0:
                    return await self.async_step_chore_count()
                self._kid_index = 0
                return await self.async_step_kids()
            except ValueError:
                errors["base"] = "invalid_kid_count"

        schema = vol.Schema({vol.Required("kid_count", default=1): vol.Coerce(int)})
        return self.async_show_form(
            step_id="kid_count", data_schema=schema, errors=errors
        )

    async def async_step_kids(self, user_input=None):
        """Collect each kid's info using internal_id as the primary key.

        Store in self._kids_temp as a dict keyed by internal_id.
        """
        errors = {}
        if user_input is not None:
            kid_name = user_input["kid_name"].strip()
            ha_user_id = user_input.get("ha_user") or ""
            enable_mobile_notifications = user_input.get(
                "enable_mobile_notifications", True
            )
            notify_service = user_input.get("mobile_notify_service") or ""
            enable_persist = user_input.get("enable_persistent_notifications", True)

            if not kid_name:
                errors["kid_name"] = "invalid_kid_name"
            elif any(
                kid_data["name"] == kid_name for kid_data in self._kids_temp.values()
            ):
                errors["kid_name"] = "duplicate_kid"
            else:
                internal_id = user_input.get("internal_id", str(uuid.uuid4()))
                self._kids_temp[internal_id] = {
                    "name": kid_name,
                    "ha_user_id": ha_user_id,
                    "enable_notifications": enable_mobile_notifications,
                    "mobile_notify_service": notify_service,
                    "use_persistent_notifications": enable_persist,
                    "internal_id": internal_id,
                }
                LOGGER.debug("Added kid: %s with ID: %s", kid_name, internal_id)

            self._kid_index += 1
            if self._kid_index >= self._kid_count:
                return await self.async_step_parent_count()
            return await self.async_step_kids()

        # Retrieve HA users for linking
        users = await self.hass.auth.async_get_users()
        kid_schema = build_kid_schema(
            self.hass,
            users=users,
            default_kid_name="",
            default_ha_user_id=None,
            default_enable_mobile_notifications=False,
            default_mobile_notify_service=None,
            default_enable_persistent_notifications=False,
        )
        return self.async_show_form(
            step_id="kids", data_schema=kid_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # PARENTS
    # --------------------------------------------------------------------------
    async def async_step_parent_count(self, user_input=None):
        """Ask how many parents to define initially."""
        errors = {}
        if user_input is not None:
            try:
                self._parents_count = int(user_input["parent_count"])
                if self._parents_count < 0:
                    raise ValueError
                if self._parents_count == 0:
                    return await self.async_step_chore_count()
                self._parents_index = 0
                return await self.async_step_parents()
            except ValueError:
                errors["base"] = "invalid_parent_count"

        schema = vol.Schema({vol.Required("parent_count", default=1): vol.Coerce(int)})
        return self.async_show_form(
            step_id="parent_count", data_schema=schema, errors=errors
        )

    async def async_step_parents(self, user_input=None):
        """Collect each parent's info using internal_id as the primary key.

        Store in self._parents_temp as a dict keyed by internal_id.
        """
        errors = {}
        if user_input is not None:
            parent_name = user_input["parent_name"].strip()
            ha_user_id = user_input.get("ha_user_id") or ""
            associated_kids = user_input.get("associated_kids", [])
            enable_mobile_notifications = user_input.get(
                "enable_mobile_notifications", True
            )
            notify_service = user_input.get("mobile_notify_service") or ""
            enable_persist = user_input.get("enable_persistent_notifications", True)

            if not parent_name:
                errors["parent_name"] = "invalid_parent_name"
            elif any(
                parent_data["name"] == parent_name
                for parent_data in self._parents_temp.values()
            ):
                errors["parent_name"] = "duplicate_parent"
            else:
                internal_id = user_input.get("internal_id", str(uuid.uuid4()))
                self._parents_temp[internal_id] = {
                    "name": parent_name,
                    "ha_user_id": ha_user_id,
                    "associated_kids": associated_kids,
                    "enable_notifications": enable_mobile_notifications,
                    "mobile_notify_service": notify_service,
                    "use_persistent_notifications": enable_persist,
                    "internal_id": internal_id,
                }
                LOGGER.debug("Added parent: %s with ID: %s", parent_name, internal_id)

            self._parents_index += 1
            if self._parents_index >= self._parents_count:
                return await self.async_step_chore_count()
            return await self.async_step_parents()

        # Retrieve kids for association from _kids_temp
        kids_dict = {
            kid_data["name"]: kid_id for kid_id, kid_data in self._kids_temp.items()
        }

        users = await self.hass.auth.async_get_users()

        parent_schema = build_parent_schema(
            self.hass,
            users=users,
            kids_dict=kids_dict,
            default_parent_name="",
            default_ha_user_id=None,
            default_associated_kids=[],
            default_enable_mobile_notifications=False,
            default_mobile_notify_service=None,
            default_enable_persistent_notifications=False,
            internal_id=None,
        )
        return self.async_show_form(
            step_id="parents", data_schema=parent_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # CHORES
    # --------------------------------------------------------------------------
    async def async_step_chore_count(self, user_input=None):
        """Ask how many chores to define."""
        errors = {}
        if user_input is not None:
            try:
                self._chore_count = int(user_input["chore_count"])
                if self._chore_count < 0:
                    raise ValueError
                if self._chore_count == 0:
                    return await self.async_step_badge_count()
                self._chore_index = 0
                return await self.async_step_chores()
            except ValueError:
                errors["base"] = "invalid_chore_count"

        schema = vol.Schema({vol.Required("chore_count", default=1): vol.Coerce(int)})
        return self.async_show_form(
            step_id="chore_count", data_schema=schema, errors=errors
        )

    async def async_step_chores(self, user_input=None):
        """Collect chore details using internal_id as the primary key.

        Store in self._chores_temp as a dict keyed by internal_id.
        """
        errors = {}

        if user_input is not None:
            chore_name = user_input["chore_name"].strip()
            internal_id = user_input.get("internal_id", str(uuid.uuid4()))

            if user_input.get("due_date"):
                raw_due = user_input["due_date"]
                try:
                    due_date_str = ensure_utc_datetime(self.hass, raw_due)
                    due_dt = dt_util.parse_datetime(due_date_str)
                    if due_dt and due_dt < dt_util.utcnow():
                        errors["due_date"] = "due_date_in_past"
                except ValueError:
                    errors["due_date"] = "invalid_due_date"
                    due_date_str = None
            else:
                due_date_str = None

            if not chore_name:
                errors["chore_name"] = "invalid_chore_name"
            elif any(
                chore_data["name"] == chore_name
                for chore_data in self._chores_temp.values()
            ):
                errors["chore_name"] = "duplicate_chore"

            if errors:
                kids_dict = {
                    kid_data["name"]: kid_id
                    for kid_id, kid_data in self._kids_temp.items()
                }
                # Re-show the form with the user's current input and errors:
                default_data = user_input.copy()
                return self.async_show_form(
                    step_id="chores",
                    data_schema=build_chore_schema(kids_dict, default_data),
                    errors=errors,
                )

            if user_input.get("recurring_frequency") != FREQUENCY_CUSTOM:
                user_input.pop("custom_interval", None)
                user_input.pop("custom_interval_unit", None)

            # If no errors, store the chore
            self._chores_temp[internal_id] = {
                "name": chore_name,
                "default_points": user_input["default_points"],
                "partial_allowed": user_input["partial_allowed"],
                "shared_chore": user_input["shared_chore"],
                "assigned_kids": user_input["assigned_kids"],
                "allow_multiple_claims_per_day": user_input[
                    "allow_multiple_claims_per_day"
                ],
                "description": user_input.get("chore_description", ""),
                "chore_labels": user_input.get("chore_labels", []),
                "icon": user_input.get("icon", ""),
                "recurring_frequency": user_input.get("recurring_frequency", "none"),
                "custom_interval": user_input.get("custom_interval"),
                "custom_interval_unit": user_input.get("custom_interval_unit"),
                "due_date": due_date_str,
                "applicable_days": user_input.get(
                    CONF_APPLICABLE_DAYS, DEFAULT_APPLICABLE_DAYS
                ),
                "notify_on_claim": user_input.get(
                    CONF_NOTIFY_ON_CLAIM, DEFAULT_NOTIFY_ON_CLAIM
                ),
                "notify_on_approval": user_input.get(
                    CONF_NOTIFY_ON_APPROVAL, DEFAULT_NOTIFY_ON_APPROVAL
                ),
                "notify_on_disapproval": user_input.get(
                    CONF_NOTIFY_ON_DISAPPROVAL, DEFAULT_NOTIFY_ON_DISAPPROVAL
                ),
                "internal_id": internal_id,
            }
            LOGGER.debug("Added chore: %s with ID: %s", chore_name, internal_id)

            self._chore_index += 1
            if self._chore_index >= self._chore_count:
                return await self.async_step_badge_count()
            return await self.async_step_chores()

        # Use flow_helpers.build_chore_schema, passing the current kids
        kids_dict = {
            kid_data["name"]: kid_id for kid_id, kid_data in self._kids_temp.items()
        }
        default_data = {}
        chore_schema = build_chore_schema(kids_dict, default_data)
        return self.async_show_form(
            step_id="chores", data_schema=chore_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # BADGES
    # --------------------------------------------------------------------------
    async def async_step_badge_count(self, user_input=None):
        """Ask how many badges to define."""
        errors = {}
        if user_input is not None:
            try:
                self._badge_count = int(user_input["badge_count"])
                if self._badge_count < 0:
                    raise ValueError
                if self._badge_count == 0:
                    return await self.async_step_reward_count()
                self._badge_index = 0
                return await self.async_step_badges()
            except ValueError:
                errors["base"] = "invalid_badge_count"

        schema = vol.Schema({vol.Required("badge_count", default=0): vol.Coerce(int)})
        return self.async_show_form(
            step_id="badge_count", data_schema=schema, errors=errors
        )

    async def async_step_badges(self, user_input=None):
        """Collect badge details using internal_id as the primary key.

        Store in self._badges_temp as a dict keyed by internal_id.
        """
        errors = {}
        if user_input is not None:
            badge_name = user_input["badge_name"].strip()
            internal_id = user_input.get("internal_id", str(uuid.uuid4()))

            if not badge_name:
                errors["badge_name"] = "invalid_badge_name"
            elif any(
                badge_data["name"] == badge_name
                for badge_data in self._badges_temp.values()
            ):
                errors["badge_name"] = "duplicate_badge"
            else:
                self._badges_temp[internal_id] = {
                    "name": badge_name,
                    "threshold_type": user_input["threshold_type"],
                    "threshold_value": user_input["threshold_value"],
                    "points_multiplier": user_input["points_multiplier"],
                    "icon": user_input.get("icon", ""),
                    "internal_id": internal_id,
                    "description": user_input.get("badge_description", ""),
                    "badge_labels": user_input.get("badge_labels", []),
                }
                LOGGER.debug("Added badge: %s with ID: %s", badge_name, internal_id)

            self._badge_index += 1
            if self._badge_index >= self._badge_count:
                return await self.async_step_reward_count()
            return await self.async_step_badges()

        badge_schema = build_badge_schema()
        return self.async_show_form(
            step_id="badges", data_schema=badge_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # REWARDS
    # --------------------------------------------------------------------------
    async def async_step_reward_count(self, user_input=None):
        """Ask how many rewards to define."""
        errors = {}
        if user_input is not None:
            try:
                self._reward_count = int(user_input["reward_count"])
                if self._reward_count < 0:
                    raise ValueError
                if self._reward_count == 0:
                    return await self.async_step_penalty_count()
                self._reward_index = 0
                return await self.async_step_rewards()
            except ValueError:
                errors["base"] = "invalid_reward_count"

        schema = vol.Schema({vol.Required("reward_count", default=0): vol.Coerce(int)})
        return self.async_show_form(
            step_id="reward_count", data_schema=schema, errors=errors
        )

    async def async_step_rewards(self, user_input=None):
        """Collect reward details using internal_id as the primary key.

        Store in self._rewards_temp as a dict keyed by internal_id.
        """
        errors = {}
        if user_input is not None:
            reward_name = user_input["reward_name"].strip()
            internal_id = user_input.get("internal_id", str(uuid.uuid4()))

            if not reward_name:
                errors["reward_name"] = "invalid_reward_name"
            elif any(
                reward_data["name"] == reward_name
                for reward_data in self._rewards_temp.values()
            ):
                errors["reward_name"] = "duplicate_reward"
            else:
                self._rewards_temp[internal_id] = {
                    "name": reward_name,
                    "cost": user_input["reward_cost"],
                    "description": user_input.get("reward_description", ""),
                    "reward_labels": user_input.get("reward_labels", []),
                    "icon": user_input.get("icon", ""),
                    "internal_id": internal_id,
                }
                LOGGER.debug("Added reward: %s with ID: %s", reward_name, internal_id)

            self._reward_index += 1
            if self._reward_index >= self._reward_count:
                return await self.async_step_penalty_count()
            return await self.async_step_rewards()

        reward_schema = build_reward_schema()
        return self.async_show_form(
            step_id="rewards", data_schema=reward_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # PENALTIES
    # --------------------------------------------------------------------------
    async def async_step_penalty_count(self, user_input=None):
        """Ask how many penalties to define."""
        errors = {}
        if user_input is not None:
            try:
                self._penalty_count = int(user_input["penalty_count"])
                if self._penalty_count < 0:
                    raise ValueError
                if self._penalty_count == 0:
                    return await self.async_step_bonus_count()
                self._penalty_index = 0
                return await self.async_step_penalties()
            except ValueError:
                errors["base"] = "invalid_penalty_count"

        schema = vol.Schema({vol.Required("penalty_count", default=0): vol.Coerce(int)})
        return self.async_show_form(
            step_id="penalty_count", data_schema=schema, errors=errors
        )

    async def async_step_penalties(self, user_input=None):
        """Collect penalty details using internal_id as the primary key.

        Store in self._penalties_temp as a dict keyed by internal_id.
        """
        errors = {}
        if user_input is not None:
            penalty_name = user_input["penalty_name"].strip()
            penalty_points = user_input["penalty_points"]
            internal_id = user_input.get("internal_id", str(uuid.uuid4()))

            if not penalty_name:
                errors["penalty_name"] = "invalid_penalty_name"
            elif any(
                penalty_data["name"] == penalty_name
                for penalty_data in self._penalties_temp.values()
            ):
                errors["penalty_name"] = "duplicate_penalty"
            else:
                self._penalties_temp[internal_id] = {
                    "name": penalty_name,
                    "description": user_input.get("penalty_description", ""),
                    "penalty_labels": user_input.get("penalty_labels", []),
                    "points": -abs(penalty_points),  # Ensure points are negative
                    "icon": user_input.get("icon", ""),
                    "internal_id": internal_id,
                }
                LOGGER.debug("Added penalty: %s with ID: %s", penalty_name, internal_id)

            self._penalty_index += 1
            if self._penalty_index >= self._penalty_count:
                return await self.async_step_bonus_count()
            return await self.async_step_penalties()

        penalty_schema = build_penalty_schema()
        return self.async_show_form(
            step_id="penalties", data_schema=penalty_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # BONUSES
    # --------------------------------------------------------------------------
    async def async_step_bonus_count(self, user_input=None):
        """Ask how many bonuses to define."""
        errors = {}
        if user_input is not None:
            try:
                self._bonus_count = int(user_input["bonus_count"])
                if self._bonus_count < 0:
                    raise ValueError
                if self._bonus_count == 0:
                    return await self.async_step_achievement_count()
                self._bonus_index = 0
                return await self.async_step_bonuses()
            except ValueError:
                errors["base"] = "invalid_bonus_count"

        schema = vol.Schema({vol.Required("bonus_count", default=0): vol.Coerce(int)})
        return self.async_show_form(
            step_id="bonus_count", data_schema=schema, errors=errors
        )

    async def async_step_bonuses(self, user_input=None):
        """Collect bonus details using internal_id as the primary key.

        Store in self._bonuses_temp as a dict keyed by internal_id.
        """
        errors = {}
        if user_input is not None:
            bonus_name = user_input["bonus_name"].strip()
            bonus_points = user_input["bonus_points"]
            internal_id = user_input.get("internal_id", str(uuid.uuid4()))

            if not bonus_name:
                errors["bonus_name"] = "invalid_bonus_name"
            elif any(
                bonus_data["name"] == bonus_name
                for bonus_data in self._bonuses_temp.values()
            ):
                errors["bonus_name"] = "duplicate_bonus"
            else:
                self._bonuses_temp[internal_id] = {
                    "name": bonus_name,
                    "description": user_input.get("bonus_description", ""),
                    "bonus_labels": user_input.get("bonus_labels", []),
                    "points": abs(bonus_points),  # Ensure points are positive
                    "icon": user_input.get("icon", ""),
                    "internal_id": internal_id,
                }
                LOGGER.debug("Added bonus '%s' with ID: %s", bonus_name, internal_id)

            self._bonus_index += 1
            if self._bonus_index >= self._bonus_count:
                return await self.async_step_achievement_count()
            return await self.async_step_bonuses()

        schema = build_bonus_schema()
        return self.async_show_form(
            step_id="bonuses", data_schema=schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # ACHIEVEMENTS
    # --------------------------------------------------------------------------
    async def async_step_achievement_count(self, user_input=None):
        """Ask how many achievements to define initially."""
        errors = {}
        if user_input is not None:
            try:
                self._achievement_count = int(user_input["achievement_count"])
                if self._achievement_count < 0:
                    raise ValueError
                if self._achievement_count == 0:
                    return await self.async_step_challenge_count()
                self._achievement_index = 0
                return await self.async_step_achievements()
            except ValueError:
                errors["base"] = "invalid_achievement_count"
        schema = vol.Schema(
            {vol.Required("achievement_count", default=0): vol.Coerce(int)}
        )
        return self.async_show_form(
            step_id="achievement_count", data_schema=schema, errors=errors
        )

    async def async_step_achievements(self, user_input=None):
        """Collect each achievement's details using internal_id as the key."""
        errors = {}

        if user_input is not None:
            achievement_name = user_input["name"].strip()
            if not achievement_name:
                errors["name"] = "invalid_achievement_name"
            elif any(
                achievement_data["name"] == achievement_name
                for achievement_data in self._achievements_temp.values()
            ):
                errors["name"] = "duplicate_achievement"
            else:
                _type = user_input["type"]

                if _type == ACHIEVEMENT_TYPE_STREAK:
                    chore_id = user_input.get("selected_chore_id")
                    if not chore_id or chore_id == "None":
                        errors["selected_chore_id"] = "a_chore_must_be_selected"

                    final_chore_id = chore_id
                else:
                    # Discard chore if not streak
                    final_chore_id = ""

                if not errors:
                    internal_id = user_input.get("internal_id", str(uuid.uuid4()))
                    self._achievements_temp[internal_id] = {
                        "name": achievement_name,
                        "description": user_input.get("description", ""),
                        "achievement_labels": user_input.get("achievement_labels", []),
                        "icon": user_input.get("icon", ""),
                        "assigned_kids": user_input["assigned_kids"],
                        "type": _type,
                        "selected_chore_id": final_chore_id,
                        "criteria": user_input.get("criteria", "").strip(),
                        "target_value": user_input["target_value"],
                        "reward_points": user_input["reward_points"],
                        "internal_id": internal_id,
                        "progress": {},
                    }

                    self._achievement_index += 1
                    if self._achievement_index >= self._achievement_count:
                        return await self.async_step_challenge_count()
                    return await self.async_step_achievements()

        kids_dict = {
            kid_data["name"]: kid_id for kid_id, kid_data in self._kids_temp.items()
        }
        all_chores = self._chores_temp
        achievement_schema = build_achievement_schema(
            kids_dict=kids_dict, chores_dict=all_chores, default=None
        )
        return self.async_show_form(
            step_id="achievements", data_schema=achievement_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # CHALLENGES
    # --------------------------------------------------------------------------
    async def async_step_challenge_count(self, user_input=None):
        """Ask how many challenges to define initially."""
        errors = {}
        if user_input is not None:
            try:
                self._challenge_count = int(user_input["challenge_count"])
                if self._challenge_count < 0:
                    raise ValueError
                if self._challenge_count == 0:
                    return await self.async_step_finish()
                self._challenge_index = 0
                return await self.async_step_challenges()
            except ValueError:
                errors["base"] = "invalid_challenge_count"
        schema = vol.Schema(
            {vol.Required("challenge_count", default=0): vol.Coerce(int)}
        )
        return self.async_show_form(
            step_id="challenge_count", data_schema=schema, errors=errors
        )

    async def async_step_challenges(self, user_input=None):
        """Collect each challenge's details using internal_id as the key."""
        errors = {}
        if user_input is not None:
            challenge_name = user_input["name"].strip()
            if not challenge_name:
                errors["name"] = "invalid_challenge_name"
            elif any(
                challenge_data["name"] == challenge_name
                for challenge_data in self._challenges_temp.values()
            ):
                errors["name"] = "duplicate_challenge"
            else:
                _type = user_input["type"]

                if _type == CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW:
                    chosen_chore_id = user_input.get("selected_chore_id")
                    if not chosen_chore_id or chosen_chore_id == "None":
                        errors["selected_chore_id"] = "a_chore_must_be_selected"
                    final_chore_id = chosen_chore_id
                else:
                    # Discard chore if not "CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW"
                    final_chore_id = ""

                # Process start_date and end_date using the helper:
                start_date_input = user_input.get("start_date")
                end_date_input = user_input.get("end_date")

                if start_date_input:
                    try:
                        start_date = ensure_utc_datetime(self.hass, start_date_input)
                        start_dt = dt_util.parse_datetime(start_date)
                        if start_dt and start_dt < dt_util.utcnow():
                            errors["start_date"] = "start_date_in_past"
                    except Exception:
                        errors["start_date"] = "invalid_start_date"
                        start_date = None
                else:
                    start_date = None

                if end_date_input:
                    try:
                        end_date = ensure_utc_datetime(self.hass, end_date_input)
                        end_dt = dt_util.parse_datetime(end_date)
                        if end_dt and end_dt <= dt_util.utcnow():
                            errors["end_date"] = "end_date_in_past"
                        if start_date:
                            # Compare start_dt and end_dt if both are valid
                            if end_dt and start_dt and end_dt <= start_dt:
                                errors["end_date"] = "end_date_not_after_start_date"
                    except Exception:
                        errors["end_date"] = "invalid_end_date"
                        end_date = None
                else:
                    end_date = None

                if not errors:
                    internal_id = user_input.get("internal_id", str(uuid.uuid4()))
                    self._challenges_temp[internal_id] = {
                        "name": challenge_name,
                        "description": user_input.get("description", ""),
                        "challenge_labels": user_input.get("challenge_labels", []),
                        "icon": user_input.get("icon", ""),
                        "assigned_kids": user_input["assigned_kids"],
                        "type": _type,
                        "selected_chore_id": final_chore_id,
                        "criteria": user_input.get("criteria", "").strip(),
                        "target_value": user_input["target_value"],
                        "reward_points": user_input["reward_points"],
                        "start_date": start_date,
                        "end_date": end_date,
                        "internal_id": internal_id,
                        "progress": {},
                    }
                    self._challenge_index += 1
                    if self._challenge_index >= self._challenge_count:
                        return await self.async_step_finish()
                    return await self.async_step_challenges()

        kids_dict = {
            kid_data["name"]: kid_id for kid_id, kid_data in self._kids_temp.items()
        }
        all_chores = self._chores_temp
        default_data = user_input if user_input else None
        challenge_schema = build_challenge_schema(
            kids_dict=kids_dict,
            chores_dict=all_chores,
            default=default_data,
        )
        return self.async_show_form(
            step_id="challenges", data_schema=challenge_schema, errors=errors
        )

    # --------------------------------------------------------------------------
    # FINISH
    # --------------------------------------------------------------------------
    async def async_step_finish(self, user_input=None):
        """Finalize summary and create the config entry."""
        if user_input is not None:
            return self._create_entry()

        # Create a mapping from kid_id to kid_name for easy lookup
        kid_id_to_name = {
            kid_id: data["name"] for kid_id, data in self._kids_temp.items()
        }

        # Enhance parents summary to include associated kids by name
        parents_summary = []
        for parent in self._parents_temp.values():
            associated_kids_names = [
                kid_id_to_name.get(kid_id, "Unknown")
                for kid_id in parent.get("associated_kids", [])
            ]
            if associated_kids_names:
                kids_str = ", ".join(associated_kids_names)
                parents_summary.append(f"{parent['name']} (Kids: {kids_str})")
            else:
                parents_summary.append(parent["name"])

        summary = (
            f"\nKids: {', '.join(kid_data['name'] for kid_data in self._kids_temp.values()) or 'None'}\n\n"
            f"Parents: {', '.join(parents_summary) or 'None'}\n\n"
            f"Chores: {', '.join(chore_data['name'] for chore_data in self._chores_temp.values()) or 'None'}\n\n"
            f"Badges: {', '.join(badge_data['name'] for badge_data in self._badges_temp.values()) or 'None'}\n\n"
            f"Rewards: {', '.join(reward_data['name'] for reward_data in self._rewards_temp.values()) or 'None'}\n\n"
            f"Penalties: {', '.join(penalty_data['name'] for penalty_data in self._penalties_temp.values()) or 'None'}\n\n"
            f"Bonuses: {', '.join(bonus_data['name'] for bonus_data in self._bonuses_temp.values()) or 'None'}\n\n"
            f"Achievements: {', '.join(achievement_data['name'] for achievement_data in self._achievements_temp.values()) or 'None'}\n\n"
            f"Challenges: {', '.join(challenge_data['name'] for challenge_data in self._challenges_temp.values()) or 'None'}\n\n"
        )
        return self.async_show_form(
            step_id="finish",
            data_schema=vol.Schema({}),
            description_placeholders={"summary": summary},
        )

    def _create_entry(self):
        """Finalize config entry with data and options using internal_id as keys."""
        entry_data = {}
        entry_options = {
            CONF_POINTS_LABEL: self._data.get(CONF_POINTS_LABEL, DEFAULT_POINTS_LABEL),
            CONF_POINTS_ICON: self._data.get(CONF_POINTS_ICON, DEFAULT_POINTS_ICON),
            CONF_KIDS: self._kids_temp,
            CONF_PARENTS: self._parents_temp,
            CONF_CHORES: self._chores_temp,
            CONF_BADGES: self._badges_temp,
            CONF_REWARDS: self._rewards_temp,
            CONF_PENALTIES: self._penalties_temp,
            CONF_BONUSES: self._bonuses_temp,
            CONF_ACHIEVEMENTS: self._achievements_temp,
            CONF_CHALLENGES: self._challenges_temp,
        }

        LOGGER.debug(
            "Creating entry with data=%s, options=%s", entry_data, entry_options
        )
        return self.async_create_entry(
            title="KidsChores", data=entry_data, options=entry_options
        )

    @staticmethod
    @callback
    def async_get_options_flow(config_entry):
        """Return the Options Flow."""
        from .options_flow import KidsChoresOptionsFlowHandler

        return KidsChoresOptionsFlowHandler(config_entry)


================================================
FILE: custom_components/kidschores/const.py
================================================
# File: const.py
"""Constants for the KidsChores integration.

This file centralizes configuration keys, defaults, labels, domain names,
event names, and platform identifiers for consistency across the integration.
It also supports localization by defining all labels and UI texts used in sensors,
services, and options flow.
"""

import logging

from homeassistant.const import Platform

# -------------------- General --------------------
# Integration Domain and Logging
DOMAIN = "kidschores"
LOGGER = logging.getLogger(__package__)

# Supported Platforms
PLATFORMS = [
    Platform.BUTTON,
    Platform.CALENDAR,
    Platform.SELECT,
    Platform.SENSOR,
]

# Storage and Versioning
STORAGE_KEY = "kidschores_data"  # Persistent storage key
STORAGE_VERSION = 1  # Storage version

# Update Interval
UPDATE_INTERVAL = 5  # Update interval for coordinator (in minutes)

# -------------------- Configuration --------------------
# Configuration Keys
CONF_ACHIEVEMENTS = "achievements"
CONF_APPLICABLE_DAYS = "applicable_days"
CONF_BADGES = "badges"  # Key for badges configuration
CONF_CHALLENGES = "challenges"
CONF_CHORES = "chores"  # Key for chores configuration
CONF_GLOBAL = "global"
CONF_KIDS = "kids"  # Key for kids configuration
CONF_PARENTS = "parents"  # Key for parents configuration
CONF_PENALTIES = "penalties"  # Key for penalties configuration
CONF_POINTS_ICON = "points_icon"
CONF_POINTS_LABEL = "points_label"  # Custom label for points
CONF_REWARDS = "rewards"  # Key for rewards configuration
CONF_BONUSES = "bonuses"

# Options Flow Management
OPTIONS_FLOW_ACHIEVEMENTS = "manage_achievements"  # Edit achivements step
OPTIONS_FLOW_BADGES = "manage_badges"  # Edit badges step
OPTIONS_FLOW_CHALLENGES = "manage_challenges"  # Edit challenges step
OPTIONS_FLOW_CHORES = "manage_chores"  # Edit chores step
OPTIONS_FLOW_KIDS = "manage_kids"  # Edit kids step
OPTIONS_FLOW_PARENTS = "manage_parents"  # Edit parents step
OPTIONS_FLOW_PENALTIES = "manage_penalties"  # Edit penalties step
OPTIONS_FLOW_REWARDS = "manage_rewards"  # Edit rewards step
OPTIONS_FLOW_BONUSES = "manage_bonuses"  # Edit bonuses step

# Validation Keys
VALIDATION_DUE_DATE = "due_date"  # Optional due date for chores
VALIDATION_PARTIAL_ALLOWED = "partial_allowed"  # Allow partial points in chores
VALIDATION_THRESHOLD_TYPE = "threshold_type"  # Badge criteria type
VALIDATION_THRESHOLD_VALUE = "threshold_value"  # Badge criteria value

# Notification configuration keys
CONF_ENABLE_MOBILE_NOTIFICATIONS = "enable_mobile_notifications"
CONF_MOBILE_NOTIFY_SERVICE = "mobile_notify_service"
CONF_ENABLE_PERSISTENT_NOTIFICATIONS = "enable_persistent_notifications"
CONF_NOTIFY_ON_CLAIM = "notify_on_claim"
CONF_NOTIFY_ON_APPROVAL = "notify_on_approval"
CONF_NOTIFY_ON_DISAPPROVAL = "notify_on_disapproval"
CONF_CHORE_NOTIFY_SERVICE = "chore_notify_service"

NOTIFICATION_EVENT = "mobile_app_notification_action"

# Achievement types
ACHIEVEMENT_TYPE_STREAK = "chore_streak"  # e.g., "Make bed 20 days in a row"
ACHIEVEMENT_TYPE_TOTAL = "chore_total"  # e.g., "Complete 100 chores overall"
ACHIEVEMENT_TYPE_DAILY_MIN = (
    "daily_minimum"  # e.g., "Complete minimum 5 chores in one day"
)

# Challenge types
CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW = (
    "total_within_window"  # e.g., "Complete 50 chores in 30 days"
)
CHALLENGE_TYPE_DAILY_MIN = "daily_minimum"  # e.g., "Do 2 chores each day for 14 days"


# -------------------- Defaults --------------------
# Default Icons
DEFAULT_ACHIEVEMENTS_ICON = "mdi:trophy-award"  # Default icon for achievements
DEFAULT_BADGE_ICON = "mdi:shield-star-outline"  # Default icon for badges
DEFAULT_CALENDAR_ICON = "mdi:calendar"  # Default icon for calendar sensors
DEFAULT_CHALLENGES_ICON = "mdi:trophy"  # Default icon for achievements
DEFAULT_CHORE_APPROVE_ICON = "mdi:checkbox-marked-circle-outline"
DEFAULT_CHORE_BINARY_ICON = (
    "mdi:checkbox-blank-circle-outline"  # For chore status binary sensor
)
DEFAULT_CHORE_CLAIM_ICON = "mdi:clipboard-check-outline"
DEFAULT_CHORE_SENSOR_ICON = (
    "mdi:checkbox-blank-circle-outline"  # For chore status sensor
)
DEFAULT_DISAPPROVE_ICON = (
    "mdi:close-circle-outline"  # Default icon for disapprove buttons
)
DEFAULT_ICON = "mdi:star-outline"  # Default icon for general points display
DEFAULT_PENALTY_ICON = "mdi:alert-outline"  # Default icon for penalties
DEFAULT_POINTS_ADJUST_MINUS_ICON = "mdi:minus-circle-outline"
DEFAULT_POINTS_ADJUST_PLUS_ICON = "mdi:plus-circle-outline"
DEFAULT_POINTS_ADJUST_MINUS_MULTIPLE_ICON = "mdi:minus-circle-multiple-outline"
DEFAULT_POINTS_ADJUST_PLUS_MULTIPLE_ICON = "mdi:plus-circle-multiple-outline"
DEFAULT_POINTS_ICON = "mdi:star-outline"  # Default icon for points
DEFAULT_STREAK_ICON = "mdi:blur-linear"  # Default icon for streaks
DEFAULT_BONUS_ICON = "mdi:seal"  # Default icon for bonuses
DEFAULT_REWARD_ICON = "mdi:gift-outline"  # Default icon for rewards
DEFAULT_TROPHY_ICON = "mdi:trophy"  # For highest-badge sensor fallback
DEFAULT_TROPHY_OUTLINE = "mdi:trophy-outline"

# Default Values
DEFAULT_APPLICABLE_DAYS = []  # Empty means the chore applies every day.
DEFAULT_BADGE_THRESHOLD = 50  # Default points threshold for badges
DEFAULT_MULTIPLE_CLAIMS_PER_DAY = False  # Allow only one chore claim per day
DEFAULT_PARTIAL_ALLOWED = False  # Partial points not allowed by default
DEFAULT_POINTS = 5  # Default points awarded for each chore
DEFAULT_POINTS_MULTIPLIER = 1  # Default points multiplier for badges
DEFAULT_POINTS_LABEL = "Points"  # Default label for points displayed in UI
DEFAULT_PENALTY_POINTS = 2  # Default points deducted for each penalty
DEFAULT_BONUS_POINTS = 2  # Default points added for each bonus
DEFAULT_REMINDER_DELAY = 30  # Default reminder delay in minutes
DEFAULT_REWARD_COST = 10  # Default cost for each reward
DEFAULT_DAILY_RESET_TIME = {
    "hour": 0,
    "minute": 0,
    "second": 0,
}  # Daily reset at midnight
DEFAULT_MONTHLY_RESET_DAY = 1  # Monthly reset on the 1st day
DEFAULT_WEEKLY_RESET_DAY = 0  # Weekly reset on Monday (0 = Monday, 6 = Sunday)
DEFAULT_NOTIFY_ON_CLAIM = True
DEFAULT_NOTIFY_ON_APPROVAL = True
DEFAULT_NOTIFY_ON_DISAPPROVAL = True

# -------------------- Recurring Frequencies --------------------
FREQUENCY_BIWEEKLY = "biweekly"
FREQUENCY_CUSTOM = "custom"
FREQUENCY_DAILY = "daily"
FREQUENCY_MONTHLY = "monthly"
FREQUENCY_NONE = "none"
FREQUENCY_WEEKLY = "weekly"

# -------------------- Data Keys --------------------
# Data Keys for Coordinator and Storage
DATA_ACHIEVEMENTS = "achievements"  # Key for storing achievements data
DATA_BADGES = "badges"  # Key for storing badges data
DATA_CHALLENGES = "challenges"  # Key for storing challenges data
DATA_CHORES = "chores"  # Key for storing chores data
DATA_KIDS = "kids"  # Key for storing kids data in storage
DATA_PARENTS = "parents"  # Key for storing parent data
DATA_PENDING_CHORE_APPROVALS = "pending_chore_approvals"  # Pending chore approvals
DATA_PENDING_REWARD_APPROVALS = "pending_reward_approvals"  # Pending reward approvals
DATA_PENALTIES = "penalties"  # Key for storing penalties data
DATA_REWARDS = "rewards"  # Key for storing rewards data
DATA_BONUSES = "bonuses"  # Key for storing bonuses data

# -------------------- States --------------------
# Badge Threshold Types
BADGE_THRESHOLD_TYPE_CHORE_COUNT = (
    "chore_count"  # Badges for completing a number of chores
)
BADGE_THRESHOLD_TYPE_POINTS = "points"  # Badges awarded for reaching points

# Chore States
CHORE_STATE_APPROVED = "approved"  # Chore fully approved
CHORE_STATE_APPROVED_IN_PART = "approved_in_part"  # Chore approved for some kids
CHORE_STATE_CLAIMED = "claimed"  # Chore claimed by a kid
CHORE_STATE_CLAIMED_IN_PART = "claimed_in_part"  # Chore claimed by some kids
CHORE_STATE_INDEPENDENT = "independent"  # Chore is not shared
CHORE_STATE_OVERDUE = "overdue"  # Chore not completed before the due date
CHORE_STATE_PARTIAL = "partial"  # Chore approved with partial points
CHORE_STATE_PENDING = "pending"  # Default state: chore pending approval
CHORE_STATE_UNKNOWN = "unknown"  # Unknown chore state


# Reward States
REWARD_STATE_APPROVED = "approved"  # Reward fully approved
REWARD_STATE_CLAIMED = "claimed"  # Reward claimed by a kid
REWARD_STATE_NOT_CLAIMED = "not_claimed"  # Default state: reward not claimed
REWARD_STATE_UNKNOWN = "unknown"  # Unknown reward state

# -------------------- Events --------------------
# Event Names
EVENT_CHORE_COMPLETED = "kidschores_chore_completed"  # Event for chore completion
EVENT_REWARD_REDEEMED = "kidschores_reward_redeemed"  # Event for redeeming a reward

# -------------------- Actions --------------------
# Action titles for notifications
ACTION_TITLE_APPROVE = "Approve"
ACTION_TITLE_DISAPPROVE = "Disapprove"
ACTION_TITLE_REMIND_30 = "Remind in 30 mins"

# Action identifiers
ACTION_APPROVE_CHORE = "APPROVE_CHORE"
ACTION_DISAPPROVE_CHORE = "DISAPPROVE_CHORE"
ACTION_APPROVE_REWARD = "APPROVE_REWARD"
ACTION_DISAPPROVE_REWARD = "DISAPPROVE_REWARD"
ACTION_REMIND_30 = "REMIND_30"

# -------------------- Sensors --------------------
# Sensor Attributes
ATTR_ACHIEVEMENT_NAME = "achievement_name"
ATTR_ALL_EARNED_BADGES = "all_earned_badges"
ATTR_ALLOW_MULTIPLE_CLAIMS_PER_DAY = "allow_multiple_claims_per_day"
ATTR_APPLICABLE_DAYS = "applicable_days"
ATTR_AWARDED = "awarded"
ATTR_ASSIGNED_KIDS = "assigned_kids"
ATTR_ASSOCIATED_CHORE = "associated_chore"
ATTR_BADGES = "badges"
ATTR_CHALLENGE_NAME = "challenge_name"
ATTR_CHALLENGE_TYPE = "challenge_type"
ATTR_CHORE_APPROVALS_COUNT = "chore_approvals_count"
ATTR_CHORE_APPROVALS_TODAY = "chore_approvals_today"
ATTR_CHORE_CLAIMS_COUNT = "chore_claims_count"
ATTR_CHORE_CURRENT_STREAK = "chore_current_streak"
ATTR_CHORE_HIGHEST_STREAK = "chore_highest_streak"
ATTR_CHORE_NAME = "chore_name"
ATTR_CLAIMED_ON = "Claimed on"
ATTR_COST = "cost"
ATTR_CRITERIA = "criteria"
ATTR_CUSTOM_FREQUENCY_INTERVAL = "custom_frequency_interval"
ATTR_CUSTOM_FREQUENCY_UNIT = "custom_frequency_unit"
ATTR_DEFAULT_POINTS = "default_points"
ATTR_DESCRIPTION = "description"
ATTR_DUE_DATE = "due_date"
ATTR_END_DATE = "end_date"
ATTR_GLOBAL_STATE = "global_state"
ATTR_HIGHEST_BADGE_THRESHOLD_VALUE = "highest_badge_threshold_value"
ATTR_KID_NAME = "kid_name"
ATTR_KID_STATE = "kid_state"
ATTR_LABELS = "labels"
ATTR_KIDS_EARNED = "kids_earned"
ATTR_LAST_DATE = "last_date"
ATTR_PARTIAL_ALLOWED = "partial_allowed"
ATTR_PENALTY_NAME = "penalty_name"
ATTR_PENALTY_POINTS = "penalty_points"
ATTR_POINTS_MULTIPLIER = "points_multiplier"
ATTR_POINTS_TO_NEXT_BADGE = "points_to_next_badge"
ATTR_RAW_PROGRESS = "raw_progress"
ATTR_RAW_STREAK = "raw_streak"
ATTR_RECURRING_FREQUENCY = "recurring_frequency"
ATTR_REDEEMED_ON = "Redeemed on"
ATTR_REWARD_APPROVALS_COUNT = "reward_approvals_count"
ATTR_REWARD_CLAIMS_COUNT = "reward_claims_count"
ATTR_REWARD_NAME = "reward_name"
ATTR_REWARD_POINTS = "reward_points"
ATTR_BONUS_NAME = "bonus_name"
ATTR_BONUS_POINTS = "bonus_points"
ATTR_START_DATE = "start_date"
ATTR_SHARED_CHORE = "shared_chore"
ATTR_TARGET_VALUE = "target_value"
ATTR_THRESHOLD_TYPE = "threshold_type"
ATTR_TYPE = "type"

# Calendar Attributes
ATTR_CAL_SUMMARY = "summary"
ATTR_CAL_START = "start"
ATTR_CAL_END = "end"
ATTR_CAL_ALL_DAY = "all_day"
ATTR_CAL_DESCRIPTION = "description"
ATTR_CAL_MANUFACTURER = "manufacturer"

# Sensor Types
SENSOR_TYPE_BADGES = "badges"  # Sensor tracking earned badges
SENSOR_TYPE_CHORE_APPROVALS = "chore_approvals"  # Chore approvals sensor
SENSOR_TYPE_CHORE_CLAIMS = "chore_claims"  # Chore claims sensor
SENSOR_TYPE_COMPLETED_DAILY = (
    "completed_daily"  # Sensor tracking daily chores completed
)
SENSOR_TYPE_COMPLETED_MONTHLY = (
    "completed_monthly"  # Sensor tracking monthly chores completed
)
SENSOR_TYPE_COMPLETED_WEEKLY = (
    "completed_weekly"  # Sensor tracking weekly chores completed
)
SENSOR_TYPE_PENALTY_APPLIES = "penalty_applies"  # Penalty applies sensor
SENSOR_TYPE_POINTS = "points"  # Sensor tracking total points
SENSOR_TYPE_PENDING_CHORE_APPROVALS = (
    "pending_chore_approvals"  # Pending chore approvals
)
SENSOR_TYPE_PENDING_REWARD_APPROVALS = (
    "pending_reward_approvals"  # Pending reward approvals
)
SENSOR_TYPE_REWARD_APPROVALS = "reward_approvals"  # Reward approvals sensor
SENSOR_TYPE_REWARD_CLAIMS = "reward_claims"  # Reward claims sensor
SENSOR_TYPE_BONUS_APPLIES = "bonus_applies"  # Bonus applies sensor


# -------------------- Services --------------------
# Custom Services
SERVICE_APPLY_PENALTY = "apply_penalty"  # Apply penalty service
SERVICE_APPROVE_CHORE = "approve_chore"  # Approve chore service
SERVICE_APPROVE_REWARD = "approve_reward"  # Approve reward service
SERVICE_CLAIM_CHORE = "claim_chore"  # Claim chore service
SERVICE_DISAPPROVE_CHORE = "disapprove_chore"  # Disapprove chore service
SERVICE_DISAPPROVE_REWARD = "disapprove_reward"  # Disapprove reward service
SERVICE_REDEEM_REWARD = "redeem_reward"  # Redeem reward service
SERVICE_RESET_ALL_CHORES = "reset_all_chores"  # Reset all chores service
SERVICE_RESET_ALL_DATA = "reset_all_data"  # Reset all data service
SERVICE_RESET_OVERDUE_CHORES = "reset_overdue_chores"  # Reset overdue chores
SERVICE_SET_CHORE_DUE_DATE = "set_chore_due_date"  # Set or reset chores due date
SERVICE_SKIP_CHORE_DUE_DATE = (
    "skip_chore_due_date"  # Skip chores due date and reschedule
)
SERVICE_APPLY_BONUS = "apply_bonus"  # Apply bonus service
SERVICE_RESET_PENALTIES = "reset_penalties"  # Reset penalties service
SERVICE_RESET_BONUSES = "reset_bonuses"  # Reset bonuses service
SERVICE_RESET_REWARDS = "reset_rewards"  # Reset rewards service

# Field Names (for consistency across services)
FIELD_CHORE_ID = "chore_id"
FIELD_CHORE_NAME = "chore_name"
FIELD_DUE_DATE = "due_date"
FIELD_KID_NAME = "kid_name"
FIELD_PARENT_NAME = "parent_name"
FIELD_PENALTY_NAME = "penalty_name"
FIELD_POINTS_AWARDED = "points_awarded"
FIELD_REWARD_NAME = "reward_name"
FIELD_BONUS_NAME = "bonus_name"

# -------------------- Labels --------------------
# Labels for Sensors and UI
LABEL_BADGES = "Badges"
LABEL_COMPLETED_DAILY = "Daily Completed Chores"
LABEL_COMPLETED_MONTHLY = "Monthly Completed Chores"
LABEL_COMPLETED_WEEKLY = "Weekly Completed Chores"
LABEL_POINTS = "Points"

# -------------------- Buttons --------------------
# Button Prefixes for Dynamic Creation
BUTTON_DISAPPROVE_CHORE_PREFIX = "disapprove_chore_button_"  # Disapprove chore button
BUTTON_DISAPPROVE_REWARD_PREFIX = (
    "disapprove_reward_button_"  # Disapprove reward button
)
BUTTON_PENALTY_PREFIX = (
    "penalty_button_"  # Prefix for dynamically created penalty buttons
)
BUTTON_REWARD_PREFIX = "reward_button_"  # Prefix for dynamically created reward buttons
BUTTON_BONUS_PREFIX = "bonus_button_"  # Prefix for dynamically created bonus buttons

# -------------------- Errors and Warnings --------------------
DUE_DATE_NOT_SET = "Not Set"
ERROR_CHORE_NOT_FOUND = "Chore not found."  # Error for missing chore
ERROR_CHORE_NOT_FOUND_FMT = "Chore '{}' not found"  # Error format for missing chore
ERROR_INVALID_POINTS = "Invalid points."  # Error for invalid points input
ERROR_KID_NOT_FOUND = "Kid not found."  # Error for non-existent kid
ERROR_KID_NOT_FOUND_FMT = "Kid '{}' not found"  # Error format for missing kid
ERROR_NOT_AUTHORIZED_ACTION_FMT = "Not authorized to {}."  # Auth error format
ERROR_NOT_AUTHORIZED_FMT = (
    "User not authorized to {} for this kid."  # Auth error format
)
ERROR_PENALTY_NOT_FOUND = "Penalty not found."  # Error for missing penalty
ERROR_PENALTY_NOT_FOUND_FMT = (
    "Penalty '{}' not found"  # Error format for missing penalty
)
ERROR_REWARD_NOT_FOUND = "Reward not found."  # Error for missing reward
ERROR_REWARD_NOT_FOUND_FMT = "Reward '{}' not found"  # Error format for missing reward
ERROR_BONUS_NOT_FOUND = "Bonus not found."  # Error for missing bonus
ERROR_BONUS_NOT_FOUND_FMT = "Bonus '{}' not found"  # Error format for missing bonus
ERROR_USER_NOT_AUTHORIZED = (
    "User is not authorized to perform this action."  # Auth error
)
MSG_NO_ENTRY_FOUND = "No KidsChores entry found"

# Unknown States
UNKNOWN_CHORE = "Unknown Chore"  # Error for unknown chore
UNKNOWN_KID = "Unknown Kid"  # Error for unknown kid
UNKNOWN_REWARD = "Unknown Reward"  # Error for unknown reward

# -------------------- Parent Approval Workflow --------------------
PARENT_APPROVAL_REQUIRED = True  # Enable parent approval for certain actions
HA_USERNAME_LINK_ENABLED = True  # Enable linking kids to HA usernames


# ---------------------------- Weekdays -----------------------------
WEEKDAY_OPTIONS = {
    "mon": "Monday",
    "tue": "Tuesday",
    "wed": "Wednesday",
    "thu": "Thursday",
    "fri": "Friday",
    "sat": "Saturday",
    "sun": "Sunday",
}


================================================
FILE: custom_components/kidschores/coordinator.py
================================================
# File: coordinator.py
"""Coordinator for the KidsChores integration.

Handles data synchronization, chore claiming and approval, badge tracking,
reward redemption, penalty application, and recurring chore handling.
Manages entities primarily using internal_id for consistency.
"""

import asyncio
import uuid
from calendar import monthrange
from datetime import datetime, timedelta
from typing import Any, Optional

from homeassistant.auth.models import User
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.event import async_track_time_change
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util


from .const import (
    ACHIEVEMENT_TYPE_DAILY_MIN,
    ACHIEVEMENT_TYPE_STREAK,
    ACHIEVEMENT_TYPE_TOTAL,
    ACTION_APPROVE_CHORE,
    ACTION_APPROVE_REWARD,
    ACTION_DISAPPROVE_CHORE,
    ACTION_DISAPPROVE_REWARD,
    ACTION_REMIND_30,
    ACTION_TITLE_APPROVE,
    ACTION_TITLE_DISAPPROVE,
    ACTION_TITLE_REMIND_30,
    BADGE_THRESHOLD_TYPE_CHORE_COUNT,
    BADGE_THRESHOLD_TYPE_POINTS,
    CHALLENGE_TYPE_DAILY_MIN,
    CHALLENGE_TYPE_TOTAL_WITHIN_WINDOW,
    CHORE_STATE_APPROVED,
    CHORE_STATE_APPROVED_IN_PART,
    CHORE_STATE_CLAIMED,
    CHORE_STATE_CLAIMED_IN_PART,
    CHORE_STATE_INDEPENDENT,
    CHORE_STATE_OVERDUE,
    CHORE_STATE_PARTIAL,
    CHORE_STATE_PENDING,
    CHORE_STATE_UNKNOWN,
    CONF_ACHIEVEMENTS,
    CONF_APPLICABLE_DAYS,
    CONF_BADGES,
    CONF_CHALLENGES,
    CONF_CHORES,
    CONF_ENABLE_MOBILE_NOTIFICATIONS,
    CONF_ENABLE_PERSISTENT_NOTIFICATIONS,
    CONF_KIDS,
    CONF_MOBILE_NOTIFY_SERVICE,
    CONF_NOTIFY_ON_APPROVAL,
    CONF_NOTIFY_ON_CLAIM,
    CONF_NOTIFY_ON_DISAPPROVAL,
    CONF_PARENTS,
    CONF_PENALTIES,
    CONF_REWARDS,
    CONF_BONUSES,
    DATA_ACHIEVEMENTS,
    DATA_BADGES,
    DATA_CHALLENGES,
    DATA_CHORES,
    DATA_KIDS,
    DATA_PARENTS,
    DATA_PENDING_CHORE_APPROVALS,
    DATA_PENDING_REWARD_APPROVALS,
    DATA_PENALTIES,
    DATA_REWARDS,
    DATA_BONUSES,
    DEFAULT_APPLICABLE_DAYS,
    DEFAULT_BADGE_THRESHOLD,
    DEFAULT_DAILY_RESET_TIME,
    DEFAULT_ICON,
    DEFAULT_MONTHLY_RESET_DAY,
    DEFAULT_MULTIPLE_CLAIMS_PER_DAY,
    DEFAULT_NOTIFY_ON_APPROVAL,
    DEFAULT_NOTIFY_ON_CLAIM,
    DEFAULT_NOTIFY_ON_DISAPPROVAL,
    DEFAULT_PARTIAL_ALLOWED,
    DEFAULT_PENALTY_ICON,
    DEFAULT_PENALTY_POINTS,
    DEFAULT_POINTS,
    DEFAULT_POINTS_MULTIPLIER,
    DEFAULT_REWARD_COST,
    DEFAULT_REWARD_ICON,
    DEFAULT_BONUS_ICON,
    DEFAULT_BONUS_POINTS,
    DEFAULT_WEEKLY_RESET_DAY,
    DOMAIN,
    FREQUENCY_BIWEEKLY,
    FREQUENCY_CUSTOM,
    FREQUENCY_DAILY,
    FREQUENCY_MONTHLY,
    FREQUENCY_NONE,
    FREQUENCY_WEEKLY,
    LOGGER,
    UPDATE_INTERVAL,
    WEEKDAY_OPTIONS,
)

from .storage_manager import KidsChoresStorageManager
from .notification_helper import async_send_notification


class KidsChoresDataCoordinator(DataUpdateCoordinator):
    """Coordinator for KidsChores integration.

    Manages data primarily using internal_id for entities.
    """

    def __init__(
        self,
        hass: HomeAssistant,
        config_entry: ConfigEntry,
        storage_manager: KidsChoresStorageManager,
    ):
        """Initialize the KidsChoresDataCoordinator."""
        super().__init__(
            hass,
            LOGGER,
            name=f"{DOMAIN}_coordinator",
            update_interval=timedelta(minutes=UPDATE_INTERVAL),
        )
        self.config_entry = config_entry
        self.storage_manager = storage_manager
        self._data: dict[str, Any] = {}

    # -------------------------------------------------------------------------------------
    # Migrate Data and Converters
    # -------------------------------------------------------------------------------------

    def _migrate_datetime(self, dt_str: str) -> str:
        """Convert a datetime string to a UTC-aware ISO string."""
        if not isinstance(dt_str, str):
            return dt_str

        try:
            # Try to parse using Home Assistant’s utility first:
            dt_obj = dt_util.parse_datetime(dt_str)
            if dt_obj is None:
                # Fallback using fromisoformat
                dt_obj = datetime.fromisoformat(dt_str)
            # If naive, assume local time and make it aware:
            if dt_obj.tzinfo is None:
                dt_obj = dt_obj.replace(
                    tzinfo=dt_util.get_time_zone(self.hass.config.time_zone)
                )
            # Convert to UTC
            dt_obj_utc = dt_util.as_utc(dt_obj)
            return dt_obj_utc.isoformat()
        except Exception as err:
            LOGGER.warning("Error migrating datetime '%s': %s", dt_str, err)
            return dt_str

    def _migrate_stored_datetimes(self):
        """Walk through stored data and convert known datetime fields to UTC-aware ISO strings."""
        # For each chore, migrate due_date, last_completed, and last_claimed
        for chore in self._data.get(DATA_CHORES, {}).values():
            if chore.get("due_date"):
                chore["due_date"] = self._migrate_datetime(chore["due_date"])
            if chore.get("last_completed"):
                chore["last_completed"] = self._migrate_datetime(
                    chore["last_completed"]
                )
            if chore.get("last_claimed"):
                chore["last_claimed"] = self._migrate_datetime(chore["last_claimed"])
        # Also, migrate timestamps in pending approvals
        for approval in self._data.get(DATA_PENDING_CHORE_APPROVALS, []):
            if approval.get("timestamp"):
                approval["timestamp"] = self._migrate_datetime(approval["timestamp"])
        for approval in self._data.get(DATA_PENDING_REWARD_APPROVALS, []):
            if approval.get("timestamp"):
                approval["timestamp"] = self._migrate_datetime(approval["timestamp"])

        # Migrate datetime on Challenges
        for challenge in self._data.get(DATA_CHALLENGES, {}).values():
            start_date = challenge.get("start_date")
            if not isinstance(start_date, str) or not start_date.strip():
                challenge["start_date"] = None
            else:
                challenge["start_date"] = self._migrate_datetime(start_date)

            end_date = challenge.get("end_date")
            if not isinstance(end_date, str) or not end_date.strip():
                challenge["end_date"] = None
            else:
                challenge["end_date"] = self._migrate_datetime(end_date)

    def _migrate_chore_data(self):
        """Migrate each chore's data to include new fields if missing.

        This method iterates over each chore entry in the stored data and ensures
        that the following keys are present:
        - CONF_APPLICABLE_DAYS (defaults to DEFAULT_APPLICABLE_DAYS)
        - CONF_NOTIFY_ON_CLAIM (defaults to DEFAULT_NOTIFY_ON_CLAIM)
        - CONF_NOTIFY_ON_APPROVAL (defaults to DEFAULT_NOTIFY_ON_APPROVAL)
        - CONF_NOTIFY_ON_DISAPPROVAL (defaults to DEFAULT_NOTIFY_ON_DISAPPROVAL)
        """
        chores = self._data.get(DATA_CHORES, {})
        for chore in chores.values():
            chore.setdefault(CONF_APPLICABLE_DAYS, DEFAULT_APPLICABLE_DAYS)
            chore.setdefault(CONF_NOTIFY_ON_CLAIM, DEFAULT_NOTIFY_ON_CLAIM)
            chore.setdefault(CONF_NOTIFY_ON_APPROVAL, DEFAULT_NOTIFY_ON_APPROVAL)
            chore.setdefault(CONF_NOTIFY_ON_DISAPPROVAL, DEFAULT_NOTIFY_ON_DISAPPROVAL)
        LOGGER.info("Chore data migration complete.")

    # -------------------------------------------------------------------------------------
    # Normalize Lists
    # -------------------------------------------------------------------------------------

    def _normalize_kid_lists(self, kid_info: dict[str, Any]) -> None:
        "Normalize lists and ensuring they are not dict"
        for key in [
            "claimed_chores",
            "approved_chores",
            "pending_rewards",
            "redeemed_rewards",
        ]:
            if not isinstance(kid_info.get(key), list):
                kid_info[key] = []

    # -------------------------------------------------------------------------------------
    # Periodic + First Refresh
    # -------------------------------------------------------------------------------------

    async def _async_update_data(self):
        """Periodic update."""
        try:
            # Check overdue chores
            await self._check_overdue_chores()

            # Notify entities of changes
            self.async_update_listeners()

            return self._data
        except Exception as err:
            raise UpdateFailed(f"Error updating KidsChores data: {err}") from err

    async def async_config_entry_first_refresh(self):
        """Load from storage and merge config options."""
        stored_data = self.storage_manager.get_data()
        if stored_data:
            self._data = stored_data

            # Migrate any datetime fields in stored data to UTC-aware strings
            self._migrate_stored_datetimes()

            # Migrate chore data and add new fields
            self._migrate_chore_data()

        else:
            self._data = {
                DATA_KIDS: {},
                DATA_CHORES: {},
                DATA_BADGES: {},
                DATA_REWARDS: {},
                DATA_PARENTS: {},
                DATA_PENALTIES: {},
                DATA_BONUSES: {},
                DATA_ACHIEVEMENTS: {},
                DATA_CHALLENGES: {},
                DATA_PENDING_CHORE_APPROVALS: [],
                DATA_PENDING_REWARD_APPROVALS: [],
            }

        if not isinstance(self._data.get(DATA_PENDING_CHORE_APPROVALS), list):
            self._data[DATA_PENDING_CHORE_APPROVALS] = []
        if not isinstance(self._data.get(DATA_PENDING_REWARD_APPROVALS), list):
            self._data[DATA_PENDING_REWARD_APPROVALS] = []

        # Register daily/weekly/monthly resets
        async_track_time_change(
            self.hass, self._reset_all_chore_counts, **DEFAULT_DAILY_RESET_TIME
        )

        # Merge config entry data (options) into the stored data
        self._initialize_data_from_config()

        # Normalize all kids list fields
        for kid in self._data.get(DATA_KIDS, {}).values():
            self._normalize_kid_lists(kid)

        self._persist()
        await super().async_config_entry_first_refresh()

    # -------------------------------------------------------------------------------------
    # Data Initialization from Config
    # -------------------------------------------------------------------------------------

    def _initialize_data_from_config(self):
        """Merge config_entry options with stored data structures using internal_id."""
        options = self.config_entry.options

        # Retrieve configuration dictionaries from config entry options
        config_sections = {
            DATA_KIDS: options.get(CONF_KIDS, {}),
            DATA_PARENTS: options.get(CONF_PARENTS, {}),
            DATA_CHORES: options.get(CONF_CHORES, {}),
            DATA_BADGES: options.get(CONF_BADGES, {}),
            DATA_REWARDS: options.get(CONF_REWARDS, {}),
            DATA_PENALTIES: options.get(CONF_PENALTIES, {}),
            DATA_BONUSES: options.get(CONF_BONUSES, {}),
            DATA_ACHIEVEMENTS: options.get(CONF_ACHIEVEMENTS, {}),
            DATA_CHALLENGES: options.get(CONF_CHALLENGES, {}),
        }

        # Ensure minimal structure
        self._ensure_minimal_structure()

        # Initialize each section using private helper
        for section_key, data_dict in config_sections.items():
            init_func = getattr(self, f"_initialize_{section_key}", None)
            if init_func:
                init_func(data_dict)
            else:
                self._data.setdefault(section_key, data_dict)
                LOGGER.warning("No initializer found for section '%s'", section_key)

        # Recalculate Badges on reload
        self._recalculate_all_badges()

    def _ensure_minimal_structure(self):
        """Ensure that all necessary data sections are present."""
        for key in [
            DATA_KIDS,
            DATA_PARENTS,
            DATA_CHORES,
            DATA_BADGES,
            DATA_REWARDS,
            DATA_PENALTIES,
            DATA_BONUSES,
            DATA_ACHIEVEMENTS,
            DATA_CHALLENGES,
        ]:
            self._data.setdefault(key, {})

        for key in [DATA_PENDING_CHORE_APPROVALS, DATA_PENDING_REWARD_APPROVALS]:
            if not isinstance(self._data.get(key), list):
                self._data[key] = []

    # -------------------------------------------------------------------------------------
    # Helpers to Sync Entities from config
    # -------------------------------------------------------------------------------------

    def _initialize_kids(self, kids_dict: dict[str, Any]):
        self._sync_entities(DATA_KIDS, kids_dict, self._create_kid, self._update_kid)

    def _initialize_parents(self, parents_dict: dict[str, Any]):
        self._sync_entities(
            DATA_PARENTS, parents_dict, self._create_parent, self._update_parent
        )

    def _initialize_chores(self, chores_dict: dict[str, Any]):
        self._sync_entities(
            DATA_CHORES, chores_dict, self._create_chore, self._update_chore
        )

    def _initialize_badges(self, badges_dict: dict[str, Any]):
        self._sync_entities(
            DATA_BADGES, badges_dict, self._create_badge, self._update_badge
        )

    def _initialize_rewards(self, rewards_dict: dict[str, Any]):
        self._sync_entities(
            DATA_REWARDS, rewards_dict, self._create_reward, self._update_reward
        )

    def _initialize_penalties(self, penalties_dict: dict[str, Any]):
        self._sync_entities(
            DATA_PENALTIES, penalties_dict, self._create_penalty, self._update_penalty
        )

    def _initialize_achievements(self, achievements_dict: dict[str, Any]):
        self._sync_entities(
            DATA_ACHIEVEMENTS,
            achievements_dict,
            self._create_achievement,
            self._update_achievement,
        )

    def _initialize_challenges(self, challenges_dict: dict[str, Any]):
        self._sync_entities(
            DATA_CHALLENGES,
            challenges_dict,
            self._create_challenge,
            self._update_challenge,
        )

    def _initialize_bonuses(self, bonuses_dict: dict[str, Any]):
        self._sync_entities(
            DATA_BONUSES, bonuses_dict, self._create_bonus, self._update_bonus
        )

    def _sync_entities(
        self,
        section: str,
        config_data: dict[str, Any],
        create_method,
        update_method,
    ):
        """Synchronize entities in a given data section based on config_data."""
        existing_ids = set(self._data[section].keys())
        config_ids = set(config_data.keys())

        # Identify entities to remove
        entities_to_remove = existing_ids - config_ids
        for entity_id in entities_to_remove:
            # Remove entity from data
            del self._data[section][entity_id]

            # Remove entity from HA registry
            self._remove_entities_in_ha(section, entity_id)
            if section == DATA_CHORES:
                for kid_id in self.kids_data.keys():
                    self._remove_kid_chore_entities(kid_id, entity_id)

            # Perform general clean-up
            self._cleanup_all_links()

            # Remove deleted kids from parents list
            self._cleanup_parent_assignments()

            # Remove chore approvals on chore delete
            self._cleanup_pending_chore_approvals()

            # Remove reward approvals on reward delete
            if section == DATA_REWARDS:
                self._cleanup_pending_reward_approvals()

        # Add or update entities
        for entity_id, entity_body in config_data.items():
            if entity_id not in self._data[section]:
                create_method(entity_id, entity_body)
            else:
                update_method(entity_id, entity_body)

        # Remove orphaned shared chore sensors.
        if section == DATA_CHORES:
            self.hass.async_create_task(self._remove_orphaned_shared_chore_sensors())

        # Remove orphaned achievement and challenges sensors
        self.hass.async_create_task(self._remove_orphaned_achievement_entities())
        self.hass.async_create_task(self._remove_orphaned_challenge_entities())

    def _cleanup_all_links(self) -> None:
        """Run all cross-entity cleanup routines."""
        self._cleanup_deleted_kid_references()
        self._cleanup_deleted_chore_references()
        self._cleanup_deleted_chore_in_achievements()
        self._cleanup_deleted_chore_in_challenges()

    def _remove_entities_in_ha(self, section: str, item_id: str):
        """Remove all platform entities whose unique_id references the given item_id."""
        ent_reg = er.async_get(self.hass)
        for entity_entry in list(ent_reg.entities.values()):
            if str(item_id) in str(entity_entry.unique_id):
                ent_reg.async_remove(entity_entry.entity_id)
                LOGGER.debug(
                    "Auto-removed entity '%s' with unique_id '%s' from registry",
                    entity_entry.entity_id,
                    entity_entry.unique_id,
                )

    async def _remove_orphaned_shared_chore_sensors(self):
        """Remove SharedChoreGlobalStateSensor entities for chores no longer marked as shared."""
        ent_reg = er.async_get(self.hass)
        prefix = f"{self.config_entry.entry_id}_"
        suffix = "_global_state"
        for entity_entry in list(ent_reg.entities.values()):
            if (
                entity_entry.domain == "sensor"
                and entity_entry.unique_id.startswith(prefix)
                and entity_entry.unique_id.endswith(suffix)
            ):
                chore_id = entity_entry.unique_id[len(prefix) : -len(suffix)]
                chore_info = self.chores_data.get(chore_id)
                if not chore_info or not chore_info.get("shared_chore", False):
                    ent_reg.async_remove(entity_entry.entity_id)
                    LOGGER.debug(
                        "Removed orphaned SharedChoreGlobalStateSensor: %s",
                        entity_entry.entity_id,
                    )

    async def _remove_orphaned_achievement_entities(self) -> None:
        """Remove achievement progress entities for kids that are no longer assigned."""
        ent_reg = er.async_get(self.hass)
        prefix = f"{self.config_entry.entry_id}_"
        suffix = "_achievement_progress"
        for entity_entry in list(ent_reg.entities.values()):
            if (
                entity_entry.domain == "sensor"
                and entity_entry.unique_id.startswith(prefix)
                and entity_entry.unique_id.endswith(suffix)
            ):
                core_id = entity_entry.unique_id[len(prefix) : -len(suffix)]
                parts = core_id.split("_", 1)
                if len(parts) != 2:
                    continue

                kid_id, achievement_id = parts
                achievement = self._data.get(DATA_ACHIEVEMENTS, {}).get(achievement_id)
                if not achievement or kid_id not in achievement.get(
                    "assigned_kids", []
                ):
                    ent_reg.async_remove(entity_entry.entity_id)
                    LOGGER.debug(
                        "Removed orphaned achievement progress sensor '%s' because kid '%s' is not assigned to achievement '%s'",
                        entity_entry.entity_id,
                        kid_id,
                        achievement_id,
                    )

    async def _remove_orphaned_challenge_entities(self) -> None:
        """Remove challenge progress sensor entities for kids no longer assigned."""
        ent_reg = er.async_get(self.hass)
        prefix = f"{self.config_entry.entry_id}_"
        suffix = "_challenge_progress"
        for entity_entry in list(ent_reg.entities.values()):
            if (
                entity_entry.domain == "sensor"
                and entity_entry.unique_id.startswith(prefix)
                and entity_entry.unique_id.endswith(suffix)
            ):
                core_id = entity_entry.unique_id[len(prefix) : -len(suffix)]
                parts = core_id.split("_", 1)
                if len(parts) != 2:
                    continue

                kid_id, challenge_id = parts
                challenge = self._data.get(DATA_CHALLENGES, {}).get(challenge_id)
                if not challenge or kid_id not in challenge.get("assigned_kids", []):
                    ent_reg.async_remove(entity_entry.entity_id)
                    LOGGER.debug(
                        "Removed orphaned challenge progress sensor '%s' because kid '%s' is not assigned to challenge '%s'",
                        entity_entry.entity_id,
                        kid_id,
                        challenge_id,
                    )

    def _remove_kid_chore_entities(self, kid_id: str, chore_id: str) -> None:
        """Remove all kid-specific chore entities for a given kid and chore."""
        ent_reg = er.async_get(self.hass)
        for entity_entry in list(ent_reg.entities.values()):
            if (kid_id in entity_entry.unique_id) and (
                chore_id in entity_entry.unique_id
            ):
                ent_reg.async_remove(entity_entry.entity_id)
                LOGGER.debug(
                    "Removed kid-specific entity '%s' for kid '%s' and chore '%s'",
                    entity_entry.entity_id,
                    kid_id,
                    chore_id,
                )

    def _cleanup_chore_from_kid(self, kid_id: str, chore_id: str) -> None:
        """Remove references to a specific chore from a kid's data."""
        kid = self.kids_data.get(kid_id)
        if not kid:
            return

        # Remove from lists if present
        for key in ["claimed_chores", "approved_chores"]:
            if chore_id in kid.get(key, []):
                kid[key] = [c for c in kid[key] if c != chore_id]
                LOGGER.debug(
                    "Removed chore '%s' from kid '%s' list '%s'", chore_id, kid_id, key
                )

        # Remove from dictionary fields if present
        for dict_key in ["chore_claims", "chore_approvals"]:
            if chore_id in kid.get(dict_key, {}):
                kid[dict_key].pop(chore_id)
                LOGGER.debug(
                    "Removed chore '%s' from kid '%s' dict '%s'",
                    chore_id,
                    kid_id,
                    dict_key,
                )

        # Remove from chore streaks if present
        if "chore_streaks" in kid and chore_id in kid["chore_streaks"]:
            kid["chore_streaks"].pop(chore_id)
            LOGGER.debug(
                "Removed chore streak for chore '%s' from kid '%s'", chore_id, kid_id
            )

        # Remove any pending chore approvals for this kid and chore
        self._data[DATA_PENDING_CHORE_APPROVALS] = [
            ap
            for ap in self._data.get(DATA_PENDING_CHORE_APPROVALS, [])
            if not (ap.get("kid_id") == kid_id and ap.get("chore_id") == chore_id)
        ]

    def _cleanup_pending_chore_approvals(self) -> None:
        """Remove any pending chore approvals for chore IDs that no longer exist."""
        valid_chore_ids = set(self._data.get(DATA_CHORES, {}).keys())
        self._data[DATA_PENDING_CHORE_APPROVALS] = [
            ap
            for ap in self._data.get(DATA_PENDING_CHORE_APPROVALS, [])
            if ap.get("chore_id") in valid_chore_ids
        ]

    def _cleanup_pending_reward_approvals(self) -> None:
        """Remove any pending reward approvals for reward IDs that no longer exist."""
        valid_reward_ids = set(self._data.get(DATA_REWARDS, {}).keys())
        self._data[DATA_PENDING_REWARD_APPROVALS] = [
            approval
            for approval in self._data.get(DATA_PENDING_REWARD_APPROVALS, [])
            if approval.get("reward_id") in valid_reward_ids
        ]

    def _cleanup_deleted_kid_references(self) -> None:
        """Remove references to kids that no longer exist from other sections."""
        valid_kid_ids = set(self.kids_data.keys())

        # Remove deleted kid IDs from all chore assignments
        for chore in self._data.get(DATA_CHORES, {}).values():
            if "assigned_kids" in chore:
                original = chore["assigned_kids"]
                filtered = [kid for kid in original if kid in valid_kid_ids]
                if filtered != original:
                    chore["assigned_kids"] = filtered
                    LOGGER.debug(
                        "Cleaned up assigned_kids in chore '%s'", chore.get("name")
                    )

        # Remove progress in achievements and challenges
        for section in [DATA_ACHIEVEMENTS, DATA_CHALLENGES]:
            for entity in self._data.get(section, {}).values():
                progress = entity.get("progress", {})
                keys_to_remove = [kid for kid in progress if kid not in valid_kid_ids]
                for kid in keys_to_remove:
                    del progress[kid]
                    LOGGER.debug(
                        "Removed progress for deleted kid '%s' in section '%s'",
                        kid,
                        section,
                    )
                if "assigned_kids" in entity:
                    original_assigned = entity["assigned_kids"]
                    filtered_assigned = [
                        kid for kid in original_assigned if kid in valid_kid_ids
                    ]
                    if filtered_assigned != original_assigned:
                        entity["assigned_kids"] = filtered_assigned
                        LOGGER.debug(
                            "Cleaned up assigned_kids in %s '%s'",
                            section,
                            entity.get("name"),
                        )
Download .txt
gitextract_644k2xfj/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── 01-issue_report.yml
│   │   ├── 02-feature_reques.yml
│   │   └── config.yml
│   └── workflows/
│       ├── hassfest.yaml
│       └── validate.yaml
├── .gitignore
├── LICENSE
├── README.md
├── custom_components/
│   └── kidschores/
│       ├── __init__.py
│       ├── button.py
│       ├── calendar.py
│       ├── config_flow.py
│       ├── const.py
│       ├── coordinator.py
│       ├── flow_helpers.py
│       ├── kc_helpers.py
│       ├── manifest.json
│       ├── notification_action_handler.py
│       ├── notification_helper.py
│       ├── options_flow.py
│       ├── select.py
│       ├── sensor.py
│       ├── services.py
│       ├── services.yaml
│       ├── storage_manager.py
│       └── translations/
│           ├── en.json
│           └── es.json
└── hacs.json
Download .txt
SYMBOL INDEX (427 symbols across 14 files)

FILE: custom_components/kidschores/__init__.py
  function async_setup_entry (line 35) | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> ...
  function async_unload_entry (line 78) | async def async_unload_entry(hass, entry):
  function async_remove_entry (line 100) | async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) ->...

FILE: custom_components/kidschores/button.py
  function async_setup_entry (line 55) | async def async_setup_entry(
  class ClaimChoreButton (line 222) | class ClaimChoreButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 228) | def __init__(
    method async_press (line 254) | async def async_press(self):
    method extra_state_attributes (line 297) | def extra_state_attributes(self):
  class ApproveChoreButton (line 312) | class ApproveChoreButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 318) | def __init__(
    method async_press (line 344) | async def async_press(self):
    method extra_state_attributes (line 386) | def extra_state_attributes(self):
  class DisapproveChoreButton (line 401) | class DisapproveChoreButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 407) | def __init__(
    method async_press (line 434) | async def async_press(self):
    method extra_state_attributes (line 490) | def extra_state_attributes(self):
  class RewardButton (line 506) | class RewardButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 512) | def __init__(
    method async_press (line 539) | async def async_press(self):
    method extra_state_attributes (line 582) | def extra_state_attributes(self):
  class ApproveRewardButton (line 597) | class ApproveRewardButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 606) | def __init__(
    method async_press (line 632) | async def async_press(self):
    method extra_state_attributes (line 691) | def extra_state_attributes(self):
  class DisapproveRewardButton (line 706) | class DisapproveRewardButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 712) | def __init__(
    method async_press (line 739) | async def async_press(self):
    method extra_state_attributes (line 795) | def extra_state_attributes(self):
  class PenaltyButton (line 811) | class PenaltyButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 820) | def __init__(
    method async_press (line 848) | async def async_press(self):
    method extra_state_attributes (line 891) | def extra_state_attributes(self):
  class PointsAdjustButton (line 907) | class PointsAdjustButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 917) | def __init__(
    method async_press (line 957) | async def async_press(self):
  class BonusButton (line 998) | class BonusButton(CoordinatorEntity, ButtonEntity):
    method __init__ (line 1007) | def __init__(
    method async_press (line 1034) | async def async_press(self):
    method extra_state_attributes (line 1077) | def extra_state_attributes(self):

FILE: custom_components/kidschores/calendar.py
  function async_setup_entry (line 29) | async def async_setup_entry(
  class KidsChoresCalendarEntity (line 46) | class KidsChoresCalendarEntity(CalendarEntity):
    method __init__ (line 49) | def __init__(self, coordinator, kid_id: str, kid_name: str, config_ent...
    method async_get_events (line 59) | async def async_get_events(
    method _generate_events_for_chore (line 89) | def _generate_events_for_chore(
    method _generate_events_for_challenge (line 363) | def _generate_events_for_challenge(
    method event (line 427) | def event(self) -> CalendarEvent | None:
    method _generate_all_events (line 453) | def _generate_all_events(
    method extra_state_attributes (line 475) | def extra_state_attributes(self):

FILE: custom_components/kidschores/config_flow.py
  class KidsChoresConfigFlow (line 60) | class KidsChoresConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    method __init__ (line 65) | def __init__(self) -> None:
    method async_step_user (line 98) | async def async_step_user(self, user_input: Optional[dict[str, Any]] =...
    method async_step_intro (line 108) | async def async_step_intro(self, user_input=None):
    method async_step_points_label (line 115) | async def async_step_points_label(self, user_input=None):
    method async_step_kid_count (line 139) | async def async_step_kid_count(self, user_input=None):
    method async_step_kids (line 159) | async def async_step_kids(self, user_input=None):
    method async_step_parent_count (line 215) | async def async_step_parent_count(self, user_input=None):
    method async_step_parents (line 235) | async def async_step_parents(self, user_input=None):
    method async_step_chore_count (line 302) | async def async_step_chore_count(self, user_input=None):
    method async_step_chores (line 322) | async def async_step_chores(self, user_input=None):
    method async_step_badge_count (line 422) | async def async_step_badge_count(self, user_input=None):
    method async_step_badges (line 442) | async def async_step_badges(self, user_input=None):
    method async_step_reward_count (line 485) | async def async_step_reward_count(self, user_input=None):
    method async_step_rewards (line 505) | async def async_step_rewards(self, user_input=None):
    method async_step_penalty_count (line 546) | async def async_step_penalty_count(self, user_input=None):
    method async_step_penalties (line 566) | async def async_step_penalties(self, user_input=None):
    method async_step_bonus_count (line 608) | async def async_step_bonus_count(self, user_input=None):
    method async_step_bonuses (line 628) | async def async_step_bonuses(self, user_input=None):
    method async_step_achievement_count (line 670) | async def async_step_achievement_count(self, user_input=None):
    method async_step_achievements (line 691) | async def async_step_achievements(self, user_input=None):
    method async_step_challenge_count (line 753) | async def async_step_challenge_count(self, user_input=None):
    method async_step_challenges (line 774) | async def async_step_challenges(self, user_input=None):
    method async_step_finish (line 870) | async def async_step_finish(self, user_input=None):
    method _create_entry (line 910) | def _create_entry(self):
    method async_get_options_flow (line 936) | def async_get_options_flow(config_entry):

FILE: custom_components/kidschores/coordinator.py
  class KidsChoresDataCoordinator (line 112) | class KidsChoresDataCoordinator(DataUpdateCoordinator):
    method __init__ (line 118) | def __init__(
    method _migrate_datetime (line 139) | def _migrate_datetime(self, dt_str: str) -> str:
    method _migrate_stored_datetimes (line 162) | def _migrate_stored_datetimes(self):
    method _migrate_chore_data (line 196) | def _migrate_chore_data(self):
    method _normalize_kid_lists (line 218) | def _normalize_kid_lists(self, kid_info: dict[str, Any]) -> None:
    method _async_update_data (line 233) | async def _async_update_data(self):
    method async_config_entry_first_refresh (line 246) | async def async_config_entry_first_refresh(self):
    method _initialize_data_from_config (line 297) | def _initialize_data_from_config(self):
    method _ensure_minimal_structure (line 329) | def _ensure_minimal_structure(self):
    method _initialize_kids (line 352) | def _initialize_kids(self, kids_dict: dict[str, Any]):
    method _initialize_parents (line 355) | def _initialize_parents(self, parents_dict: dict[str, Any]):
    method _initialize_chores (line 360) | def _initialize_chores(self, chores_dict: dict[str, Any]):
    method _initialize_badges (line 365) | def _initialize_badges(self, badges_dict: dict[str, Any]):
    method _initialize_rewards (line 370) | def _initialize_rewards(self, rewards_dict: dict[str, Any]):
    method _initialize_penalties (line 375) | def _initialize_penalties(self, penalties_dict: dict[str, Any]):
    method _initialize_achievements (line 380) | def _initialize_achievements(self, achievements_dict: dict[str, Any]):
    method _initialize_challenges (line 388) | def _initialize_challenges(self, challenges_dict: dict[str, Any]):
    method _initialize_bonuses (line 396) | def _initialize_bonuses(self, bonuses_dict: dict[str, Any]):
    method _sync_entities (line 401) | def _sync_entities(
    method _cleanup_all_links (line 452) | def _cleanup_all_links(self) -> None:
    method _remove_entities_in_ha (line 459) | def _remove_entities_in_ha(self, section: str, item_id: str):
    method _remove_orphaned_shared_chore_sensors (line 471) | async def _remove_orphaned_shared_chore_sensors(self):
    method _remove_orphaned_achievement_entities (line 491) | async def _remove_orphaned_achievement_entities(self) -> None:
    method _remove_orphaned_challenge_entities (line 520) | async def _remove_orphaned_challenge_entities(self) -> None:
    method _remove_kid_chore_entities (line 547) | def _remove_kid_chore_entities(self, kid_id: str, chore_id: str) -> None:
    method _cleanup_chore_from_kid (line 562) | def _cleanup_chore_from_kid(self, kid_id: str, chore_id: str) -> None:
    method _cleanup_pending_chore_approvals (line 601) | def _cleanup_pending_chore_approvals(self) -> None:
    method _cleanup_pending_reward_approvals (line 610) | def _cleanup_pending_reward_approvals(self) -> None:
    method _cleanup_deleted_kid_references (line 619) | def _cleanup_deleted_kid_references(self) -> None:
    method _cleanup_deleted_chore_references (line 659) | def _cleanup_deleted_chore_references(self) -> None:
    method _cleanup_parent_assignments (line 689) | def _cleanup_parent_assignments(self) -> None:
    method _cleanup_deleted_chore_in_achievements (line 703) | def _cleanup_deleted_chore_in_achievements(self) -> None:
    method _cleanup_deleted_chore_in_challenges (line 715) | def _cleanup_deleted_chore_in_challenges(self) -> None:
    method _create_kid (line 732) | def _create_kid(self, kid_id: str, kid_data: dict[str, Any]):
    method _update_kid (line 778) | def _update_kid(self, kid_id: str, kid_data: dict[str, Any]):
    method _create_parent (line 823) | def _create_parent(self, parent_id: str, parent_data: dict[str, Any]):
    method _update_parent (line 852) | def _update_parent(self, parent_id: str, parent_data: dict[str, Any]):
    method _create_chore (line 885) | def _create_chore(self, chore_id: str, chore_data: dict[str, Any]):
    method _update_chore (line 974) | def _update_chore(self, chore_id: str, chore_data: dict[str, Any]):
    method _create_badge (line 1056) | def _create_badge(self, badge_id: str, badge_data: dict[str, Any]):
    method _update_badge (line 1081) | def _update_badge(self, badge_id: str, badge_data: dict[str, Any]):
    method _create_reward (line 1112) | def _create_reward(self, reward_id: str, reward_data: dict[str, Any]):
    method _update_reward (line 1127) | def _update_reward(self, reward_id: str, reward_data: dict[str, Any]):
    method _create_penalty (line 1141) | def _create_penalty(self, penalty_id: str, penalty_data: dict[str, Any]):
    method _update_penalty (line 1156) | def _update_penalty(self, penalty_id: str, penalty_data: dict[str, Any]):
    method _create_bonus (line 1172) | def _create_bonus(self, bonus_id: str, bonus_data: dict[str, Any]):
    method _update_bonus (line 1187) | def _update_bonus(self, bonus_id: str, bonus_data: dict[str, Any]):
    method _create_achievement (line 1201) | def _create_achievement(
    method _update_achievement (line 1224) | def _update_achievement(
    method _create_challenge (line 1266) | def _create_challenge(self, challenge_id: str, challenge_data: dict[st...
    method _update_challenge (line 1293) | def _update_challenge(self, challenge_id: str, challenge_data: dict[st...
    method kids_data (line 1338) | def kids_data(self) -> dict[str, Any]:
    method parents_data (line 1343) | def parents_data(self) -> dict[str, Any]:
    method chores_data (line 1348) | def chores_data(self) -> dict[str, Any]:
    method badges_data (line 1353) | def badges_data(self) -> dict[str, Any]:
    method rewards_data (line 1358) | def rewards_data(self) -> dict[str, Any]:
    method penalties_data (line 1363) | def penalties_data(self) -> dict[str, Any]:
    method achievements_data (line 1368) | def achievements_data(self) -> dict[str, Any]:
    method challenges_data (line 1373) | def challenges_data(self) -> dict[str, Any]:
    method bonuses_data (line 1378) | def bonuses_data(self) -> dict[str, Any]:
    method add_parent (line 1386) | def add_parent(self, parent_def: dict[str, Any]):
    method remove_parent (line 1424) | def remove_parent(self, parent_id: str):
    method claim_chore (line 1439) | def claim_chore(self, kid_id: str, chore_id: str, user_name: str):
    method approve_chore (line 1515) | def approve_chore(
    method disapprove_chore (line 1669) | def disapprove_chore(self, parent_name: str, kid_id: str, chore_id: str):
    method update_chore_state (line 1696) | def update_chore_state(self, chore_id: str, state: str):
    method _process_chore_state (line 1721) | def _process_chore_state(
    method update_kid_points (line 1893) | def update_kid_points(self, kid_id: str, new_points: float):
    method redeem_reward (line 1935) | def redeem_reward(self, parent_name: str, kid_id: str, reward_id: str):
    method approve_reward (line 1998) | def approve_reward(self, parent_name: str, kid_id: str, reward_id: str):
    method disapprove_reward (line 2059) | def disapprove_reward(self, parent_name: str, kid_id: str, reward_id: ...
    method add_badge (line 2095) | def add_badge(self, badge_def: dict[str, Any]):
    method _check_badges_for_kid (line 2126) | def _check_badges_for_kid(self, kid_id: str):
    method _award_badge (line 2149) | def _award_badge(self, kid_id: str, badge_id: str):
    method _update_kid_multiplier (line 2194) | def _update_kid_multiplier(self, kid_id: str):
    method _recalculate_all_badges (line 2208) | def _recalculate_all_badges(self):
    method apply_penalty (line 2243) | def apply_penalty(self, parent_name: str, kid_id: str, penalty_id: str):
    method add_penalty (line 2277) | def add_penalty(self, penalty_def: dict[str, Any]):
    method apply_bonus (line 2302) | def apply_bonus(self, parent_name: str, kid_id: str, bonus_id: str):
    method add_bonus (line 2336) | def add_bonus(self, bonus_def: dict[str, Any]):
    method _check_achievements_for_kid (line 2360) | def _check_achievements_for_kid(self, kid_id: str):
    method _award_achievement (line 2427) | def _award_achievement(self, kid_id: str, achievement_id: str):
    method _check_challenges_for_kid (line 2491) | def _check_challenges_for_kid(self, kid_id: str):
    method _award_challenge (line 2554) | def _award_challenge(self, kid_id: str, challenge_id: str):
    method _update_streak_progress (line 2602) | def _update_streak_progress(self, progress: dict, today: datetime.date):
    method _update_chore_streak_for_kid (line 2624) | def _update_chore_streak_for_kid(
    method _update_overall_chore_streak (line 2660) | def _update_overall_chore_streak(self, kid_id: str, completion_date: d...
    method _check_overdue_chores (line 2684) | async def _check_overdue_chores(self):
    method _reset_all_chore_counts (line 2851) | async def _reset_all_chore_counts(self, now: datetime):
    method _handle_recurring_chore_resets (line 2860) | async def _handle_recurring_chore_resets(self, now: datetime):
    method _reset_chore_counts (line 2879) | async def _reset_chore_counts(self, frequency: str, now: datetime):
    method _reschedule_recurring_chores (line 2901) | async def _reschedule_recurring_chores(self, now: datetime):
    method _reset_daily_chore_statuses (line 2940) | async def _reset_daily_chore_statuses(self, target_freqs: list[str]):
    method _reset_daily_reward_statuses (line 2997) | async def _reset_daily_reward_statuses(self):
    method _reschedule_next_due_date (line 3018) | def _reschedule_next_due_date(self, chore_info: dict[str, Any]):
    method _add_months (line 3132) | def _add_months(self, dt_in: datetime, months: int) -> datetime:
    method set_chore_due_date (line 3144) | def set_chore_due_date(self, chore_id: str, due_date: Optional[datetim...
    method skip_chore_due_date (line 3197) | def skip_chore_due_date(self, chore_id: str) -> None:
    method reset_overdue_chores (line 3219) | def reset_overdue_chores(
    method reset_penalties (line 3262) | def reset_penalties(
    method reset_bonuses (line 3325) | def reset_bonuses(
    method reset_rewards (line 3390) | def reset_rewards(
    method _update_all_chore_due_dates_in_config (line 3492) | async def _update_all_chore_due_dates_in_config(self) -> None:
    method _update_chore_due_date_in_config (line 3512) | async def _update_chore_due_date_in_config(
    method send_kc_notification (line 3573) | async def send_kc_notification(
    method _notify_kid (line 3645) | async def _notify_kid(
    method _notify_parents (line 3687) | async def _notify_parents(
    method remind_in_minutes (line 3733) | async def remind_in_minutes(
    method _persist (line 3846) | def _persist(self):
    method _get_kid_id_by_name (line 3855) | def _get_kid_id_by_name(self, kid_name: str) -> Optional[str]:
    method _get_kid_name_by_id (line 3862) | def _get_kid_name_by_id(self, kid_id: str) -> Optional[str]:

FILE: custom_components/kidschores/flow_helpers.py
  function build_points_schema (line 47) | def build_points_schema(
  function build_kid_schema (line 61) | def build_kid_schema(
  function build_parent_schema (line 111) | def build_parent_schema(
  function build_chore_schema (line 175) | def build_chore_schema(kids_dict, default=None):
  function build_badge_schema (line 297) | def build_badge_schema(default=None):
  function build_reward_schema (line 349) | def build_reward_schema(default=None):
  function build_achievement_schema (line 381) | def build_achievement_schema(kids_dict, chores_dict, default=None):
  function build_challenge_schema (line 476) | def build_challenge_schema(kids_dict, chores_dict, default=None):
  function build_penalty_schema (line 578) | def build_penalty_schema(default=None):
  function build_bonus_schema (line 616) | def build_bonus_schema(default=None):
  function process_penalty_form_input (line 658) | def process_penalty_form_input(user_input: dict) -> dict:
  function _get_notify_services (line 666) | def _get_notify_services(hass: HomeAssistant) -> list[dict[str, str]]:
  function ensure_utc_datetime (line 678) | def ensure_utc_datetime(hass: HomeAssistant, dt_value: any) -> str:

FILE: custom_components/kidschores/kc_helpers.py
  function _get_kidschores_coordinator (line 14) | def _get_kidschores_coordinator(
  function is_user_authorized_for_global_action (line 34) | async def is_user_authorized_for_global_action(
  function is_user_authorized_for_kid (line 71) | async def is_user_authorized_for_kid(
  function _get_kid_id_by_name (line 127) | def _get_kid_id_by_name(self, kid_name: str) -> Optional[str]:
  function _get_kid_name_by_id (line 135) | def _get_kid_name_by_id(self, kid_id: str) -> Optional[str]:
  function get_friendly_label (line 143) | def get_friendly_label(hass, label_name: str) -> str:

FILE: custom_components/kidschores/notification_action_handler.py
  function async_handle_notification_action (line 20) | async def async_handle_notification_action(hass: HomeAssistant, event: E...

FILE: custom_components/kidschores/notification_helper.py
  function async_send_notification (line 20) | async def async_send_notification(

FILE: custom_components/kidschores/options_flow.py
  function _ensure_str (line 58) | def _ensure_str(value):
  class KidsChoresOptionsFlowHandler (line 66) | class KidsChoresOptionsFlowHandler(config_entries.OptionsFlow):
    method __init__ (line 72) | def __init__(self, config_entry: config_entries.ConfigEntry):
    method async_step_init (line 78) | async def async_step_init(self, user_input=None):
    method async_step_manage_points (line 126) | async def async_step_manage_points(self, user_input=None):
    method async_step_manage_entity (line 157) | async def async_step_manage_entity(self, user_input=None):
    method async_step_select_entity (line 196) | async def async_step_select_entity(self, user_input=None):
    method _get_entity_dict (line 249) | def _get_entity_dict(self):
    method async_step_add_kid (line 272) | async def async_step_add_kid(self, user_input=None):
    method async_step_add_parent (line 321) | async def async_step_add_parent(self, user_input=None):
    method async_step_add_chore (line 383) | async def async_step_add_chore(self, user_input=None):
    method async_step_add_badge (line 477) | async def async_step_add_badge(self, user_input=None):
    method async_step_add_reward (line 514) | async def async_step_add_reward(self, user_input=None):
    method async_step_add_penalty (line 550) | async def async_step_add_penalty(self, user_input=None):
    method async_step_add_bonus (line 589) | async def async_step_add_bonus(self, user_input=None):
    method async_step_add_achievement (line 625) | async def async_step_add_achievement(self, user_input=None):
    method async_step_add_challenge (line 687) | async def async_step_add_challenge(self, user_input=None):
    method async_step_edit_kid (line 779) | async def async_step_edit_kid(self, user_input=None):
    method async_step_edit_parent (line 839) | async def async_step_edit_parent(self, user_input=None):
    method async_step_edit_chore (line 908) | async def async_step_edit_chore(self, user_input=None):
    method async_step_edit_badge (line 1039) | async def async_step_edit_badge(self, user_input=None):
    method async_step_edit_reward (line 1082) | async def async_step_edit_reward(self, user_input=None):
    method async_step_edit_penalty (line 1123) | async def async_step_edit_penalty(self, user_input=None):
    method async_step_edit_bonus (line 1170) | async def async_step_edit_bonus(self, user_input=None):
    method async_step_edit_achievement (line 1215) | async def async_step_edit_achievement(self, user_input=None):
    method async_step_edit_challenge (line 1282) | async def async_step_edit_challenge(self, user_input=None):
    method async_step_delete_kid (line 1407) | async def async_step_delete_kid(self, user_input=None):
    method async_step_delete_parent (line 1435) | async def async_step_delete_parent(self, user_input=None):
    method async_step_delete_chore (line 1463) | async def async_step_delete_chore(self, user_input=None):
    method async_step_delete_badge (line 1491) | async def async_step_delete_badge(self, user_input=None):
    method async_step_delete_reward (line 1519) | async def async_step_delete_reward(self, user_input=None):
    method async_step_delete_penalty (line 1547) | async def async_step_delete_penalty(self, user_input=None):
    method async_step_delete_achievement (line 1575) | async def async_step_delete_achievement(self, user_input=None):
    method async_step_delete_challenge (line 1603) | async def async_step_delete_challenge(self, user_input=None):
    method async_step_delete_bonus (line 1631) | async def async_step_delete_bonus(self, user_input=None):
    method _update_and_reload (line 1660) | async def _update_and_reload(self):

FILE: custom_components/kidschores/select.py
  function async_setup_entry (line 22) | async def async_setup_entry(
  class KidsChoresSelectBase (line 51) | class KidsChoresSelectBase(CoordinatorEntity, SelectEntity):
    method __init__ (line 57) | def __init__(self, coordinator: KidsChoresDataCoordinator, entry: Conf...
    method current_option (line 64) | def current_option(self) -> Optional[str]:
    method async_select_option (line 71) | async def async_select_option(self, option: str) -> None:
  class ChoresSelect (line 85) | class ChoresSelect(KidsChoresSelectBase):
    method __init__ (line 91) | def __init__(self, coordinator: KidsChoresDataCoordinator, entry: Conf...
    method options (line 99) | def options(self) -> list[str]:
  class RewardsSelect (line 110) | class RewardsSelect(KidsChoresSelectBase):
    method __init__ (line 116) | def __init__(self, coordinator: KidsChoresDataCoordinator, entry: Conf...
    method options (line 124) | def options(self) -> list[str]:
  class PenaltiesSelect (line 135) | class PenaltiesSelect(KidsChoresSelectBase):
    method __init__ (line 141) | def __init__(self, coordinator: KidsChoresDataCoordinator, entry: Conf...
    method options (line 149) | def options(self) -> list[str]:
  class ChoresKidSelect (line 160) | class ChoresKidSelect(KidsChoresSelectBase):
    method __init__ (line 166) | def __init__(
    method options (line 178) | def options(self) -> list[str]:
  class BonusesSelect (line 188) | class BonusesSelect(KidsChoresSelectBase):
    method __init__ (line 194) | def __init__(self, coordinator: KidsChoresDataCoordinator, entry: Conf...
    method options (line 202) | def options(self) -> list[str]:

FILE: custom_components/kidschores/sensor.py
  function async_setup_entry (line 141) | async def async_setup_entry(
  class ChoreStatusSensor (line 371) | class ChoreStatusSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 377) | def __init__(self, coordinator, entry, kid_id, kid_name, chore_id, cho...
    method native_value (line 394) | def native_value(self):
    method extra_state_attributes (line 410) | def extra_state_attributes(self):
    method icon (line 475) | def icon(self):
  class KidPointsSensor (line 482) | class KidPointsSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 488) | def __init__(self, coordinator, entry, kid_id, kid_name, points_label,...
    method native_value (line 505) | def native_value(self):
    method native_unit_of_measurement (line 511) | def native_unit_of_measurement(self):
    method icon (line 516) | def icon(self):
  class KidMaxPointsEverSensor (line 522) | class KidMaxPointsEverSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 528) | def __init__(self, coordinator, entry, kid_id, kid_name, points_label,...
    method native_value (line 542) | def native_value(self):
    method icon (line 548) | def icon(self):
    method native_unit_of_measurement (line 553) | def native_unit_of_measurement(self):
  class CompletedChoresTotalSensor (line 559) | class CompletedChoresTotalSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 565) | def __init__(self, coordinator, entry, kid_id, kid_name):
    method native_value (line 578) | def native_value(self):
  class CompletedChoresDailySensor (line 585) | class CompletedChoresDailySensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 591) | def __init__(self, coordinator, entry, kid_id, kid_name):
    method native_value (line 603) | def native_value(self):
  class CompletedChoresWeeklySensor (line 610) | class CompletedChoresWeeklySensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 616) | def __init__(self, coordinator, entry, kid_id, kid_name):
    method native_value (line 628) | def native_value(self):
  class CompletedChoresMonthlySensor (line 635) | class CompletedChoresMonthlySensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 641) | def __init__(self, coordinator, entry, kid_id, kid_name):
    method native_value (line 653) | def native_value(self):
  class KidBadgesSensor (line 660) | class KidBadgesSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 666) | def __init__(self, coordinator, entry, kid_id, kid_name):
    method native_value (line 677) | def native_value(self):
    method extra_state_attributes (line 683) | def extra_state_attributes(self):
  class KidHighestBadgeSensor (line 690) | class KidHighestBadgeSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 696) | def __init__(self, coordinator, entry, kid_id, kid_name):
    method _find_highest_badge (line 707) | def _find_highest_badge(self):
    method native_value (line 737) | def native_value(self) -> str:
    method icon (line 746) | def icon(self):
    method extra_state_attributes (line 762) | def extra_state_attributes(self):
  class BadgeSensor (line 810) | class BadgeSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 816) | def __init__(
    method native_value (line 834) | def native_value(self) -> float:
    method extra_state_attributes (line 840) | def extra_state_attributes(self):
    method icon (line 872) | def icon(self) -> str:
  class PendingChoreApprovalsSensor (line 879) | class PendingChoreApprovalsSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 885) | def __init__(self, coordinator, entry):
    method native_value (line 894) | def native_value(self):
    method extra_state_attributes (line 900) | def extra_state_attributes(self):
  class PendingRewardApprovalsSensor (line 928) | class PendingRewardApprovalsSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 934) | def __init__(self, coordinator, entry):
    method native_value (line 943) | def native_value(self):
    method extra_state_attributes (line 949) | def extra_state_attributes(self):
  class RewardClaimsSensor (line 977) | class RewardClaimsSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 983) | def __init__(self, coordinator, entry, kid_id, kid_name, reward_id, re...
    method native_value (line 999) | def native_value(self):
    method icon (line 1005) | def icon(self):
  class RewardApprovalsSensor (line 1012) | class RewardApprovalsSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1018) | def __init__(self, coordinator, entry, kid_id, kid_name, reward_id, re...
    method native_value (line 1034) | def native_value(self):
    method icon (line 1040) | def icon(self):
  class SharedChoreGlobalStateSensor (line 1047) | class SharedChoreGlobalStateSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1053) | def __init__(
    method native_value (line 1072) | def native_value(self) -> str:
    method extra_state_attributes (line 1078) | def extra_state_attributes(self) -> dict:
    method icon (line 1126) | def icon(self) -> str:
  class RewardStatusSensor (line 1133) | class RewardStatusSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1139) | def __init__(
    method native_value (line 1164) | def native_value(self) -> str:
    method extra_state_attributes (line 1174) | def extra_state_attributes(self) -> dict:
    method icon (line 1201) | def icon(self) -> str:
  class ChoreClaimsSensor (line 1208) | class ChoreClaimsSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1214) | def __init__(self, coordinator, entry, kid_id, kid_name, chore_id, cho...
    method native_value (line 1229) | def native_value(self):
    method icon (line 1235) | def icon(self):
  class ChoreApprovalsSensor (line 1242) | class ChoreApprovalsSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1248) | def __init__(self, coordinator, entry, kid_id, kid_name, chore_id, cho...
    method native_value (line 1263) | def native_value(self):
    method icon (line 1269) | def icon(self):
  class PenaltyAppliesSensor (line 1276) | class PenaltyAppliesSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1282) | def __init__(self, coordinator, entry, kid_id, kid_name, penalty_id, p...
    method native_value (line 1297) | def native_value(self):
    method extra_state_attributes (line 1303) | def extra_state_attributes(self):
    method icon (line 1321) | def icon(self):
  class KidPointsEarnedDailySensor (line 1328) | class KidPointsEarnedDailySensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1334) | def __init__(self, coordinator, entry, kid_id, kid_name, points_label,...
    method native_value (line 1348) | def native_value(self):
    method native_unit_of_measurement (line 1354) | def native_unit_of_measurement(self):
    method icon (line 1359) | def icon(self):
  class KidPointsEarnedWeeklySensor (line 1365) | class KidPointsEarnedWeeklySensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1371) | def __init__(self, coordinator, entry, kid_id, kid_name, points_label,...
    method native_value (line 1386) | def native_value(self):
    method native_unit_of_measurement (line 1392) | def native_unit_of_measurement(self):
    method icon (line 1397) | def icon(self):
  class KidPointsEarnedMonthlySensor (line 1403) | class KidPointsEarnedMonthlySensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1409) | def __init__(self, coordinator, entry, kid_id, kid_name, points_label,...
    method native_value (line 1424) | def native_value(self):
    method native_unit_of_measurement (line 1430) | def native_unit_of_measurement(self):
    method icon (line 1435) | def icon(self):
  class AchievementSensor (line 1441) | class AchievementSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1447) | def __init__(self, coordinator, entry, achievement_id, achievement_name):
    method native_value (line 1461) | def native_value(self):
    method extra_state_attributes (line 1534) | def extra_state_attributes(self):
    method icon (line 1592) | def icon(self):
  class ChallengeSensor (line 1601) | class ChallengeSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1607) | def __init__(self, coordinator, entry, challenge_id, challenge_name):
    method native_value (line 1621) | def native_value(self):
    method extra_state_attributes (line 1658) | def extra_state_attributes(self):
    method icon (line 1720) | def icon(self):
  class AchievementProgressSensor (line 1727) | class AchievementProgressSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1733) | def __init__(
    method native_value (line 1760) | def native_value(self) -> float:
    method extra_state_attributes (line 1809) | def extra_state_attributes(self) -> dict:
    method icon (line 1872) | def icon(self) -> str:
  class ChallengeProgressSensor (line 1882) | class ChallengeProgressSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 1888) | def __init__(
    method native_value (line 1915) | def native_value(self) -> float:
    method extra_state_attributes (line 1954) | def extra_state_attributes(self) -> dict:
    method icon (line 2014) | def icon(self) -> str:
  class KidHighestStreakSensor (line 2024) | class KidHighestStreakSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 2030) | def __init__(
    method native_value (line 2050) | def native_value(self) -> int:
    method extra_state_attributes (line 2056) | def extra_state_attributes(self) -> dict:
    method icon (line 2075) | def icon(self) -> str:
  class ChoreStreakSensor (line 2081) | class ChoreStreakSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 2087) | def __init__(
    method native_value (line 2112) | def native_value(self) -> int:
    method extra_state_attributes (line 2120) | def extra_state_attributes(self) -> dict:
    method icon (line 2143) | def icon(self) -> str:
  class BonusAppliesSensor (line 2150) | class BonusAppliesSensor(CoordinatorEntity, SensorEntity):
    method __init__ (line 2156) | def __init__(self, coordinator, entry, kid_id, kid_name, bonus_id, bon...
    method native_value (line 2171) | def native_value(self):
    method extra_state_attributes (line 2177) | def extra_state_attributes(self):
    method icon (line 2195) | def icon(self):

FILE: custom_components/kidschores/services.py
  function async_setup_services (line 172) | def async_setup_services(hass: HomeAssistant):
  function async_unload_services (line 1061) | async def async_unload_services(hass: HomeAssistant):
  function _get_first_kidschores_entry (line 1089) | def _get_first_kidschores_entry(hass: HomeAssistant) -> Optional[str]:
  function _get_kid_id_by_name (line 1097) | def _get_kid_id_by_name(
  function _get_chore_id_by_name (line 1107) | def _get_chore_id_by_name(
  function _get_reward_id_by_name (line 1117) | def _get_reward_id_by_name(
  function _get_penalty_id_by_name (line 1127) | def _get_penalty_id_by_name(
  function _get_bonus_id_by_name (line 1137) | def _get_bonus_id_by_name(

FILE: custom_components/kidschores/storage_manager.py
  class KidsChoresStorageManager (line 30) | class KidsChoresStorageManager:
    method __init__ (line 36) | def __init__(self, hass, storage_key=STORAGE_KEY):
    method async_initialize (line 49) | async def async_initialize(self):
    method data (line 79) | def data(self):
    method get_data (line 83) | def get_data(self):
    method set_data (line 87) | def set_data(self, new_data: dict):
    method get_kids (line 91) | def get_kids(self):
    method get_parents (line 95) | def get_parents(self):
    method get_chores (line 99) | def get_chores(self):
    method get_badges (line 103) | def get_badges(self):
    method get_rewards (line 107) | def get_rewards(self):
    method get_penalties (line 111) | def get_penalties(self):
    method get_bonuses (line 115) | def get_bonuses(self):
    method get_achievements (line 119) | def get_achievements(self):
    method get_challenges (line 123) | def get_challenges(self):
    method get_pending_chore_approvals (line 127) | def get_pending_chore_approvals(self):
    method get_pending_reward_aprovals (line 131) | def get_pending_reward_aprovals(self):
    method link_user_to_kid (line 135) | async def link_user_to_kid(self, user_id, kid_id):
    method unlink_user (line 143) | async def unlink_user(self, user_id):
    method get_linked_kids (line 150) | async def get_linked_kids(self):
    method async_save (line 155) | async def async_save(self):
    method async_clear_data (line 163) | async def async_clear_data(self):
    method async_delete_storage (line 182) | async def async_delete_storage(self) -> None:
    method async_update_data (line 198) | async def async_update_data(self, key, value):
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (748K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 882,
    "preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/01-issue_report.yml",
    "chars": 2654,
    "preview": "---\nname: Issue Report\ndescription: Create an issue report to help us improve\ntitle: \"[ISSUE] \"\nlabels: bug\nassignees: ["
  },
  {
    "path": ".github/ISSUE_TEMPLATE/02-feature_reques.yml",
    "chars": 1325,
    "preview": "---\nname: Feature Request\ndescription: Suggest an idea for this project\ntitle: \"[REQ] \"\nlabels: enhancement\nassignees: ["
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 28,
    "preview": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/workflows/hassfest.yaml",
    "chars": 242,
    "preview": "name: Validate with hassfest\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  validate:\n    run"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "chars": 313,
    "preview": "name: HACS Action\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n\njobs:\n  validat"
  },
  {
    "path": ".gitignore",
    "chars": 23,
    "preview": "__pycache__/\n*.py[cod]\n"
  },
  {
    "path": "LICENSE",
    "chars": 35149,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.md",
    "chars": 13720,
    "preview": "> [!IMPORTANT]\n> **⚠️ ACTIVE DEVELOPMENT HAS MOVED TO CHOREOPS**\n>\n> `KidsChores` has officially evolved into a new, exp"
  },
  {
    "path": "custom_components/kidschores/__init__.py",
    "chars": 3703,
    "preview": "# File: __init__.py\n\"\"\"Initialization file for the KidsChores integration.\n\nHandles setting up the integration, includin"
  },
  {
    "path": "custom_components/kidschores/button.py",
    "chars": 37607,
    "preview": "# File: button.py\n\"\"\"Buttons for KidsChores integration.\n\nFeatures:\n1) Chore Buttons (Claim & Approve) with user-defined"
  },
  {
    "path": "custom_components/kidschores/calendar.py",
    "chars": 19094,
    "preview": "# File: calendar.py\n\nimport datetime\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import"
  },
  {
    "path": "custom_components/kidschores/config_flow.py",
    "chars": 40162,
    "preview": "# File: config_flow.py\n\"\"\"Multi-step config flow for the KidsChores integration, storing entities by internal_id.\n\nEnsur"
  },
  {
    "path": "custom_components/kidschores/const.py",
    "chars": 16680,
    "preview": "# File: const.py\n\"\"\"Constants for the KidsChores integration.\n\nThis file centralizes configuration keys, defaults, label"
  },
  {
    "path": "custom_components/kidschores/coordinator.py",
    "chars": 165110,
    "preview": "# File: coordinator.py\n\"\"\"Coordinator for the KidsChores integration.\n\nHandles data synchronization, chore claiming and "
  },
  {
    "path": "custom_components/kidschores/flow_helpers.py",
    "chars": 26759,
    "preview": "# File: flow_helpers.py\n\"\"\"Helpers for the KidsChores integration's Config and Options flow.\n\nProvides schema builders a"
  },
  {
    "path": "custom_components/kidschores/kc_helpers.py",
    "chars": 4424,
    "preview": "# File: kc_helpers.py\n\"\"\"KidsChores helper functions and shared logic.\"\"\"\n\nfrom homeassistant.core import HomeAssistant\n"
  },
  {
    "path": "custom_components/kidschores/manifest.json",
    "chars": 331,
    "preview": "{\n  \"domain\": \"kidschores\",\n  \"name\": \"KidsChores\",\n  \"codeowners\": [\"@ad-ha\"],\n  \"config_flow\": true,\n  \"dependencies\":"
  },
  {
    "path": "custom_components/kidschores/notification_action_handler.py",
    "chars": 3700,
    "preview": "# File: notification_action_handler.py\n\"\"\"Handle notification actions from HA companion notifications.\"\"\"\n\nfrom homeassi"
  },
  {
    "path": "custom_components/kidschores/notification_helper.py",
    "chars": 1917,
    "preview": "# File: notification_helper.py\n\"\"\"Sends notifications using Home Assistant's notify services.\n\nThis module implements a "
  },
  {
    "path": "custom_components/kidschores/options_flow.py",
    "chars": 69448,
    "preview": "# File: options_flow.py\n\"\"\"Options Flow for the KidsChores integration, managing entities by internal_id.\n\nHandles add/e"
  },
  {
    "path": "custom_components/kidschores/select.py",
    "chars": 7356,
    "preview": "# File: select.py\n\"\"\"Select entities for the KidsChores integration.\n\nAllows the user to pick from all chores, all rewar"
  },
  {
    "path": "custom_components/kidschores/sensor.py",
    "chars": 85147,
    "preview": "# File: sensor.py\n\"\"\"Sensors for the KidsChores integration.\n\nThis file defines all sensor entities for each Kid, Chore,"
  },
  {
    "path": "custom_components/kidschores/services.py",
    "chars": 40902,
    "preview": "# File: services.py\n\"\"\"Defines custom services for the KidsChores integration.\n\nThese services allow direct actions thro"
  },
  {
    "path": "custom_components/kidschores/services.yaml",
    "chars": 10324,
    "preview": "# File: services.yaml\n#\n# Custom services documentation for the KidsChores integration.\n# These services allow direct ac"
  },
  {
    "path": "custom_components/kidschores/storage_manager.py",
    "chars": 7336,
    "preview": "# File: storage_manager.py\n\"\"\"Handles persistent data storage for the KidsChores integration.\n\nUses Home Assistant's Sto"
  },
  {
    "path": "custom_components/kidschores/translations/en.json",
    "chars": 54856,
    "preview": "{\n  \"title\": \"KidsChores\",\n  \"config\": {\n    \"step\": {\n      \"intro\": {\n        \"title\": \"Welcome to KidsChores\",\n      "
  },
  {
    "path": "custom_components/kidschores/translations/es.json",
    "chars": 59076,
    "preview": "{\n  \"title\": \"KidsChores\",\n  \"config\": {\n    \"step\": {\n      \"intro\": {\n        \"title\": \"Bienvenido a KidsChores\",\n    "
  },
  {
    "path": "hacs.json",
    "chars": 102,
    "preview": "{\n  \"name\": \"KidsChores\",\n  \"homeassistant\": \"2024.12\",\n  \"hacs\": \"1.33.0\",\n  \"render_readme\": true\n}\n"
  }
]

About this extraction

This page contains the full source code of the ad-ha/kidschores-ha GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (691.8 KB), approximately 152.3k tokens, and a symbol index with 427 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!