pax_global_header00006660000000000000000000000064150024431020014501gustar00rootroot0000000000000052 comment=136c0cdf73c91fb230eb8cfbb4eea0abd615e25d python-supervisor-client-0.3.1/000077500000000000000000000000001500244310200165165ustar00rootroot00000000000000python-supervisor-client-0.3.1/.devcontainer/000077500000000000000000000000001500244310200212555ustar00rootroot00000000000000python-supervisor-client-0.3.1/.devcontainer/devcontainer.json000066400000000000000000000027061500244310200246360ustar00rootroot00000000000000{ "name": "Supervisor dev", "context": "..", "dockerFile": "../Dockerfile.dev", "containerEnv": { "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" }, "remoteEnv": { "PATH": "${containerEnv:VIRTUAL_ENV}/bin:${containerEnv:PATH}" }, "postCreateCommand": "script/devcontainer_setup", "postStartCommand": "bash devcontainer_bootstrap", "runArgs": ["-e", "GIT_EDITOR=code --wait"], "customizations": { "vscode": { "extensions": [ "charliermarsh.ruff", "ms-python.vscode-pylance", "visualstudioexptteam.vscodeintellicode", "redhat.vscode-yaml", "esbenp.prettier-vscode", "GitHub.vscode-pull-request-github", "tamasfe.even-better-toml" ], "settings": { "python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python", "python.pythonPath": "/home/vscode/.local/ha-venv/bin/python", "python.terminal.activateEnvInCurrentTerminal": true, "python.testing.pytestArgs": ["--no-cov"], "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, "files.trimTrailingWhitespace": true, "terminal.integrated.profiles.linux": { "zsh": { "path": "/usr/bin/zsh" } }, "terminal.integrated.defaultProfile.linux": "zsh", "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" } } } } } python-supervisor-client-0.3.1/.gitattributes000066400000000000000000000000501500244310200214040ustar00rootroot00000000000000* text eol=lf *.py whitespace=errorpython-supervisor-client-0.3.1/.github/000077500000000000000000000000001500244310200200565ustar00rootroot00000000000000python-supervisor-client-0.3.1/.github/CODEOWNERS000066400000000000000000000000241500244310200214450ustar00rootroot00000000000000.github/* @mdegat01 python-supervisor-client-0.3.1/.github/CODE_OF_CONDUCT.md000066400000000000000000000062141500244310200226600ustar00rootroot00000000000000# Code of conduct ## Our pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project lead at frenck@addons.community. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project lead is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ python-supervisor-client-0.3.1/.github/CONTRIBUTING.md000066400000000000000000000022571500244310200223150ustar00rootroot00000000000000# Contributing When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. Please note we have a code of conduct, please follow it in all your interactions with the project. ## Issues and feature requests You've found a bug in the source code, a mistake in the documentation or maybe you'd like a new feature? You can help us by submitting an issue to our [GitHub Repository][github]. Before you create an issue, make sure you search the archive, maybe your question was already answered. Even better: You could submit a pull request with a fix / new feature! ## Pull request process 1. Search our repository for open or closed [pull requests][prs] that relates to your submission. You don't want to duplicate effort. 2. You may merge the pull request in once you have the sign-off of another developer, or if you do not have permission to do that, you may request the reviewer to merge it for you. [github]: https://github.com/home-assistant-libs/python-supervisor-client/issues [prs]: https://github.com/home-assistant-libs/python-supervisor-client/pulls python-supervisor-client-0.3.1/.github/FUNDING.yaml000066400000000000000000000000311500244310200220260ustar00rootroot00000000000000--- github: - mdegat01 python-supervisor-client-0.3.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000005141500244310200225630ustar00rootroot00000000000000# Problem/Motivation > (Why the issue was filed) ## Expected behavior > (What you expected to happen) ## Actual behavior > (What actually happened) ## Steps to reproduce > (How can someone else make/see it happen) ## Proposed changes > (If you have a proposed change, workaround or fix, > describe the rationale behind it) python-supervisor-client-0.3.1/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000003761500244310200236650ustar00rootroot00000000000000# Proposed Changes > (Describe the changes and rationale behind them) ## Related Issues > ([Github link][autolink-references] to related issues or pull requests) [autolink-references]: https://help.github.com/articles/autolinked-references-and-urls/ python-supervisor-client-0.3.1/.github/dependabot.yml000066400000000000000000000004631500244310200227110ustar00rootroot00000000000000--- version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily time: "06:00" open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily time: "06:00" open-pull-requests-limit: 10 python-supervisor-client-0.3.1/.github/labels.yml000066400000000000000000000055051500244310200220500ustar00rootroot00000000000000--- - name: "breaking-change" color: ee0701 description: "A breaking change for existing users." - name: "bugfix" color: ee0701 description: >- Inconsistencies or issues which will cause a problem for users or implementers. - name: "documentation" color: 0052cc description: "Solely about the documentation of the project." - name: "enhancement" color: 1d76db description: "Enhancement of the code, not introducing new features." - name: "refactor" color: 1d76db description: "Improvement of existing code, not introducing new features." - name: "performance" color: 1d76db description: "Improving performance, not introducing new features." - name: "new-feature" color: 0e8a16 description: "New features or options." - name: "maintenance" color: 2af79e description: "Generic maintenance tasks." - name: "ci" color: 1d76db description: "Work that improves the continue integration." - name: "dependencies" color: 1d76db description: "Upgrade or downgrade of project dependencies." - name: "in-progress" color: fbca04 description: "Issue is currently being resolved by a developer." - name: "stale" color: fef2c0 description: >- There has not been activity on this issue or PR for quite some time. - name: "no-stale" color: fef2c0 description: "This issue or PR is exempted from the stable bot." - name: "security" color: ee0701 description: "Marks a security issue that needs to be resolved asap." - name: "incomplete" color: fef2c0 description: "Marks a PR or issue that is missing information." - name: "invalid" color: fef2c0 description: "Marks a PR or issue that is missing information." - name: "beginner-friendly" color: 0e8a16 description: >- Good first issue for people wanting to contribute to the project. - name: "help-wanted" color: 0e8a16 description: >- We need some extra helping hands or expertise in order to resolve this. - name: "hacktoberfest" description: "Issues/PRs are participating in the Hacktoberfest." color: fbca04 - name: "hacktoberfest-accepted" description: "Issues/PRs are participating in the Hacktoberfest." color: fbca04 - name: "priority-critical" color: ee0701 description: >- This should be dealt with ASAP. Not fixing this issue would be a serious error. - name: "priority-high" color: b60205 description: >- After critical issues are fixed, these should be dealt with before any further issues. - name: "priority-medium" color: 0e8a16 description: "This issue may be useful, and needs some attention." - name: "priority-low" color: e4ea8a description: "Nice addition, maybe... someday..." - name: "major" color: b60205 description: "This PR causes a major version bump in the version number." - name: "minor" color: 0e8a16 description: "This PR causes a minor version bump in the version number." python-supervisor-client-0.3.1/.github/release-drafter.yml000066400000000000000000000016571500244310200236570ustar00rootroot00000000000000--- change-template: "- #$NUMBER $TITLE @$AUTHOR" sort-direction: ascending categories: - title: ":boom: Breaking Changes" label: "breaking-change" - title: ":wrench: Build" label: "build" - title: ":boar: Chore" label: "chore" - title: ":books: Documentation" label: "documentation" - title: ":sparkles: New Features" label: "new-feature" - title: ":zap: Performance" label: "performance" - title: ":recycle: Refactor" label: "refactor" - title: ":green_heart: CI" label: "ci" - title: ":bug: Bug Fixes" label: "bugfix" - title: ":white_check_mark: Test" label: "test" - title: ":arrow_up: Dependency Updates" label: "dependencies" collapse-after: 1 include-labels: - "breaking-change" - "build" - "chore" - "documentation" - "performance" - "refactor" - "new-feature" - "bugfix" - "dependencies" - "test" - "ci" template: | $CHANGES python-supervisor-client-0.3.1/.github/renovate.json000066400000000000000000000016111500244310200225730ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "schedule": ["before 2am"], "rebaseWhen": "behind-base-branch", "dependencyDashboard": true, "labels": ["dependencies", "no-stale"], "commitMessagePrefix": "⬆️", "dockerfile": { "fileMatch": ["^Dockerfile\\.dev$"] }, "packageRules": [ { "matchManagers": ["pep621"], "addLabels": ["python"] }, { "matchManagers": ["pep621"], "matchDepTypes": ["dev"], "rangeStrategy": "pin" }, { "matchManagers": ["github-actions"], "addLabels": ["github_actions"], "rangeStrategy": "pin" }, { "matchManagers": ["dockerfile"], "addLabels": ["docker"], "rangeStrategy": "pin" }, { "matchManagers": ["dockerfile", "github-actions", "pep621"], "matchUpdateTypes": ["minor", "patch"], "automerge": true } ] } python-supervisor-client-0.3.1/.github/workflows/000077500000000000000000000000001500244310200221135ustar00rootroot00000000000000python-supervisor-client-0.3.1/.github/workflows/ci.yaml000066400000000000000000000262431500244310200234010ustar00rootroot00000000000000--- name: CI # yamllint disable-line rule:truthy on: push: branches: - main pull_request: ~ workflow_dispatch: env: DEFAULT_PYTHON: "3.12" PRE_COMMIT_CACHE: ~/.cache/pre-commit concurrency: group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: # Separate job to pre-populate the base dependency cache # This prevent upcoming jobs to do the same individually prepare: runs-on: ubuntu-latest outputs: python-version: ${{ steps.python.outputs.python-version }} name: Prepare Python dependencies steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python id: python uses: actions/setup-python@v5.6.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v4.2.3 with: path: venv key: | ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate pip install -U pip setuptools pip install -e ".[dev]" - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v4.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true key: | ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} restore-keys: | ${{ runner.os }}-pre-commit- - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | . venv/bin/activate pre-commit install-hooks lint-ruff-format: name: Check ruff-format runs-on: ubuntu-latest needs: prepare steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python ${{ needs.prepare.outputs.python-version }} uses: actions/setup-python@v5.6.0 id: python with: python-version: ${{ needs.prepare.outputs.python-version }} - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v4.2.3 with: path: venv key: | ${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | echo "Failed to restore Python virtual environment from cache" exit 1 - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v4.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: | ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | echo "Failed to restore Python virtual environment from cache" exit 1 - name: Run ruff-format run: | . venv/bin/activate pre-commit run \ --hook-stage manual ruff-format --all-files --show-diff-on-failure env: RUFF_OUTPUT_FORMAT: github lint-ruff: name: Check ruff runs-on: ubuntu-latest needs: prepare steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python ${{ needs.prepare.outputs.python-version }} uses: actions/setup-python@v5.6.0 id: python with: python-version: ${{ needs.prepare.outputs.python-version }} - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v4.2.3 with: path: venv key: | ${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | echo "Failed to restore Python virtual environment from cache" exit 1 - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v4.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: | ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | echo "Failed to restore Python virtual environment from cache" exit 1 - name: Run ruff run: | . venv/bin/activate pre-commit run \ --hook-stage manual ruff --all-files --show-diff-on-failure env: RUFF_OUTPUT_FORMAT: github lint-other: name: Check other linters runs-on: ubuntu-24.04 needs: prepare steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python ${{ needs.prepare.outputs.python-version }} uses: actions/setup-python@v5.6.0 id: python with: python-version: ${{ needs.prepare.outputs.python-version }} - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v4.2.3 with: path: venv key: | ${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | echo "Failed to restore Python virtual environment from cache" exit 1 - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v4.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: | ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | echo "Failed to restore Python virtual environment from cache" exit 1 - name: Register yamllint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/yamllint.json" - name: Run yamllint run: | . venv/bin/activate pre-commit run \ --hook-stage manual yamllint --all-files --show-diff-on-failure - name: Register check-json problem matcher run: | echo "::add-matcher::.github/workflows/matchers/check-json.json" - name: Run check-json run: | . venv/bin/activate pre-commit run --hook-stage manual check-json --all-files - name: Run prettier run: | . venv/bin/activate pre-commit run --hook-stage manual prettier --all-files - name: Register check executables problem matcher # yamllint disable rule:line-length run: | echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json" # yamllint enable rule:line-length - name: Run executables check run: | . venv/bin/activate pre-commit run \ --hook-stage manual check-executables-have-shebangs --all-files - name: Register codespell problem matcher run: | echo "::add-matcher::.github/workflows/matchers/codespell.json" - name: Run codespell run: | . venv/bin/activate pre-commit run \ --show-diff-on-failure --hook-stage manual codespell --all-files - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" - name: Check Dockerfile.dev uses: docker://hadolint/hadolint:v1.18.2 with: args: hadolint Dockerfile.dev pytest: runs-on: ubuntu-latest needs: prepare name: Run tests Python ${{ needs.prepare.outputs.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python ${{ needs.prepare.outputs.python-version }} uses: actions/setup-python@v5.6.0 id: python with: python-version: ${{ needs.prepare.outputs.python-version }} - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v4.2.3 with: path: venv key: | ${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | echo "Failed to restore Python virtual environment from cache" exit 1 - name: Register Python problem matcher run: | echo "::add-matcher::.github/workflows/matchers/python.json" - name: Install Pytest Annotation plugin run: | . venv/bin/activate # Ideally this should be part of our dependencies # However this plugin is fairly new and doesn't run correctly # on a non-GitHub environment. pip install pytest-github-actions-annotate-failures - name: Run pytest run: | . venv/bin/activate pytest \ -qq \ --timeout=10 \ --durations=10 \ --cov aiohasupervisor \ -o console_output_style=count \ tests - name: Upload coverage artifact uses: actions/upload-artifact@v4.3.6 with: name: coverage-${{ matrix.python-version }} path: .coverage coverage: name: Process test coverage runs-on: ubuntu-latest needs: ["pytest", "prepare"] steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python ${{ needs.prepare.outputs.python-version }} uses: actions/setup-python@v5.6.0 id: python with: python-version: ${{ needs.prepare.outputs.python-version }} - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v4.2.3 with: path: venv key: | ${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | echo "Failed to restore Python virtual environment from cache" exit 1 - name: Download all coverage artifacts uses: actions/download-artifact@v4.1.8 - name: Combine coverage results run: | . venv/bin/activate coverage combine coverage*/.coverage* coverage report coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v5.4.2 python-supervisor-client-0.3.1/.github/workflows/labels.yaml000066400000000000000000000007361500244310200242470ustar00rootroot00000000000000--- name: Sync labels # yamllint disable-line rule:truthy on: push: branches: - main paths: - .github/labels.yml workflow_dispatch: jobs: labels: name: ♻️ Sync labels runs-on: ubuntu-latest steps: - name: ⤵️ Check out code from GitHub uses: actions/checkout@v4.2.2 - name: 🚀 Run Label Syncer uses: micnncim/action-label-syncer@v1.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} python-supervisor-client-0.3.1/.github/workflows/lock.yaml000066400000000000000000000006651500244310200237360ustar00rootroot00000000000000--- name: Lock # yamllint disable-line rule:truthy on: schedule: - cron: "0 9 * * *" workflow_dispatch: jobs: lock: name: 🔒 Lock closed issues and PRs runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v5.0.1 with: github-token: ${{ github.token }} issue-inactive-days: "30" issue-lock-reason: "" pr-inactive-days: "1" pr-lock-reason: "" python-supervisor-client-0.3.1/.github/workflows/matchers/000077500000000000000000000000001500244310200237215ustar00rootroot00000000000000python-supervisor-client-0.3.1/.github/workflows/matchers/check-executables-have-shebangs.json000066400000000000000000000003351500244310200327050ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "check-executables-have-shebangs", "pattern": [ { "regexp": "^(.+):\\s(.+)$", "file": 1, "message": 2 } ] } ] } python-supervisor-client-0.3.1/.github/workflows/matchers/check-json.json000066400000000000000000000004301500244310200266350ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "check-json", "pattern": [ { "regexp": "^(.+):\\s(.+\\sline\\s(\\d+)\\scolumn\\s(\\d+).+)$", "file": 1, "message": 2, "line": 3, "column": 4 } ] } ] } python-supervisor-client-0.3.1/.github/workflows/matchers/codespell.json000066400000000000000000000004001500244310200265600ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "codespell", "severity": "warning", "pattern": [ { "regexp": "^(.+):(\\d+):\\s(.+)$", "file": 1, "line": 2, "message": 3 } ] } ] } python-supervisor-client-0.3.1/.github/workflows/matchers/hadolint.json000066400000000000000000000004011500244310200264110ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "hadolint", "pattern": [ { "regexp": "^(.+):(\\d+)\\s+((DL\\d{4}).+)$", "file": 1, "line": 2, "message": 3, "code": 4 } ] } ] } python-supervisor-client-0.3.1/.github/workflows/matchers/mypy.json000066400000000000000000000004121500244310200256070ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "mypy", "pattern": [ { "regexp": "^(.+):(\\d+):\\s(error|warning):\\s(.+)$", "file": 1, "line": 2, "severity": 3, "message": 4 } ] } ] } python-supervisor-client-0.3.1/.github/workflows/matchers/python.json000066400000000000000000000005201500244310200261320ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "python", "pattern": [ { "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", "file": 1, "line": 2 }, { "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", "message": 2 } ] } ] } python-supervisor-client-0.3.1/.github/workflows/matchers/yamllint.json000066400000000000000000000006431500244310200264500ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "yamllint", "pattern": [ { "regexp": "^(.*\\.ya?ml)$", "file": 1 }, { "regexp": "^\\s{2}(\\d+):(\\d+)\\s+(error|warning)\\s+(.*?)\\s+\\((.*)\\)$", "line": 1, "column": 2, "severity": 3, "message": 4, "code": 5, "loop": true } ] } ] } python-supervisor-client-0.3.1/.github/workflows/pr-labels.yaml000066400000000000000000000013021500244310200246540ustar00rootroot00000000000000--- name: PR Labels # yamllint disable-line rule:truthy on: pull_request_target: types: - opened - labeled - unlabeled - synchronize workflow_call: jobs: pr_labels: name: Verify runs-on: ubuntu-latest steps: - name: 🏷 Verify PR has a valid label uses: jesusvasquez333/verify-pr-label-action@v1.4.0 with: pull-request-number: "${{ github.event.pull_request.number }}" github-token: "${{ secrets.GITHUB_TOKEN }}" valid-labels: >- breaking-change, bugfix, documentation, enhancement, refactor, performance, new-feature, maintenance, ci, dependencies disable-reviews: true python-supervisor-client-0.3.1/.github/workflows/release-drafter.yaml000066400000000000000000000005741500244310200260520ustar00rootroot00000000000000--- name: Release Drafter # yamllint disable-line rule:truthy on: push: branches: - main workflow_dispatch: jobs: update_release_draft: name: ✏️ Draft release runs-on: ubuntu-latest steps: - name: 🚀 Run Release Drafter uses: release-drafter/release-drafter@v6.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} python-supervisor-client-0.3.1/.github/workflows/release.yaml000066400000000000000000000062471500244310200244300ustar00rootroot00000000000000--- name: Publish releases # yamllint disable-line rule:truthy on: release: types: [published] workflow_dispatch: inputs: version: description: "Version for release on TestPyPI" required: true env: PYTHON_VERSION: "3.12" jobs: build: name: Builds and publishes releases to PyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 - name: Store version from inputs if: github.event_name == 'workflow_dispatch' run: echo "tag=${{ inputs.version }}" >> $GITHUB_ENV - name: Get version from tag if: github.event_name == 'release' run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Validate version number if: github.event_name == 'release' run: >- if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then if ! [[ "${tag}" =~ "b" ]]; then echo "Pre-release: Tag is missing beta suffix (${tag})" exit 1 fi else if [[ "${tag}" =~ "b" ]]; then echo "Release: Tag must not have a beta suffix (${tag})" exit 1 fi fi - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install build run: >- pip install build tomli tomli-w - name: Set Python project version from tag shell: python run: |- import tomli import tomli_w with open("pyproject.toml", "rb") as f: pyproject = tomli.load(f) pyproject["project"]["version"] = "${{ env.tag }}" with open("pyproject.toml", "wb") as f: tomli_w.dump(pyproject, f) - name: Build python package run: >- python3 -m build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ publish-pypi: name: Publishes releases to PyPI runs-on: ubuntu-latest if: github.event_name == 'release' needs: - build environment: name: pypi url: https://pypi.org/p/aiohasupervisor permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@release/v1 publish-test-pypi: name: Publishes releases to Test-PyPI runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' needs: - build environment: name: testpypi url: https://test.pypi.org/p/aiohasupervisor permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution 📦 to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ python-supervisor-client-0.3.1/.github/workflows/stale.yaml000066400000000000000000000025601500244310200241120ustar00rootroot00000000000000--- name: Stale # yamllint disable-line rule:truthy on: schedule: - cron: "0 8 * * *" workflow_dispatch: jobs: stale: name: 🧹 Clean up stale issues and PRs runs-on: ubuntu-latest steps: - name: 🚀 Run stale uses: actions/stale@v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 days-before-close: 7 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale,help-wanted" stale-issue-message: > There hasn't been any activity on this issue recently, so we clean up some of the older and inactive issues. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by leaving a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thanks! stale-pr-label: "stale" exempt-pr-labels: "no-stale" stale-pr-message: > There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. Thank you for your contributions. python-supervisor-client-0.3.1/.gitignore000066400000000000000000000022131500244310200205040ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # VS Code .vscode/* !.vscode/cSpell.json !.vscode/tasks.json !.vscode/launch.json # mypy /.mypy_cache/* /.dmypy.jsonpython-supervisor-client-0.3.1/.pre-commit-config.yaml000066400000000000000000000040151500244310200227770ustar00rootroot00000000000000--- repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.2 hooks: - id: ruff args: - --fix - id: ruff-format files: ^((aiohasupervisor|script|tests)/.+)?[^/]+\.(py|pyi)$ - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell args: - --ignore-words-list=hass - --skip="./.*,*.csv,*.json,*.ambr" - --quiet-level=2 exclude_types: [csv, json, html] exclude: ^tests/fixtures/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-executables-have-shebangs stages: [manual] - id: check-json exclude: (.vscode|.devcontainer) - id: no-commit-to-branch args: - --branch=main - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.0.3 hooks: - id: prettier - repo: https://github.com/cdce8p/python-typing-update rev: v0.6.0 hooks: # Run `python-typing-update` hook manually from time to time # to update python typing syntax. # Will require manual work, before submitting changes! # pre-commit run --hook-stage manual python-typing-update --all-files - id: python-typing-update stages: [manual] args: - --py312-plus - --force - --keep-updates files: ^(aiohasupervisor|script|tests)/.+\.py$ - repo: local hooks: # Run mypy through our wrapper script in order to get the possible # pyenv and/or virtualenv activated; it may not have been e.g. if # committing from a GUI tool that was not launched from an activated # shell. - id: mypy name: mypy entry: script/run-in-env.sh mypy language: script types_or: [python, pyi] require_serial: true files: ^aiohasupervisor/.+\.(py|pyi)$ stages: [manual] python-supervisor-client-0.3.1/.prettierignore000066400000000000000000000000121500244310200215520ustar00rootroot00000000000000.gitignorepython-supervisor-client-0.3.1/.vscode/000077500000000000000000000000001500244310200200575ustar00rootroot00000000000000python-supervisor-client-0.3.1/.vscode/launch.json000066400000000000000000000003171500244310200222250ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "Debug Tests", "type": "debugpy", "request": "launch", "console": "internalConsole", "justMyCode": false } ] } python-supervisor-client-0.3.1/.vscode/tasks.json000066400000000000000000000016601500244310200221020ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "label": "Pytest", "type": "shell", "command": "pytest --timeout=10 tests", "group": { "kind": "test", "isDefault": true }, "presentation": { "reveal": "always", "panel": "new" }, "problemMatcher": [] }, { "label": "Ruff Check", "type": "shell", "command": "ruff check --fix supervisor tests", "group": { "kind": "test", "isDefault": true }, "presentation": { "reveal": "always", "panel": "new" }, "problemMatcher": [] }, { "label": "Ruff Format", "type": "shell", "command": "ruff format supervisor tests", "group": { "kind": "test", "isDefault": true }, "presentation": { "reveal": "always", "panel": "new" }, "problemMatcher": [] } ] } python-supervisor-client-0.3.1/.yamlint000066400000000000000000000022001500244310200201660ustar00rootroot00000000000000rules: braces: level: error min-spaces-inside: 0 max-spaces-inside: 1 min-spaces-inside-empty: -1 max-spaces-inside-empty: -1 brackets: level: error min-spaces-inside: 0 max-spaces-inside: 0 min-spaces-inside-empty: -1 max-spaces-inside-empty: -1 colons: level: error max-spaces-before: 0 max-spaces-after: 1 commas: level: error max-spaces-before: 0 min-spaces-after: 1 max-spaces-after: 1 comments: level: error require-starting-space: true min-spaces-from-content: 1 comments-indentation: level: error document-end: level: error present: false document-start: level: error present: false empty-lines: level: error max: 1 max-start: 0 max-end: 1 hyphens: level: error max-spaces-after: 1 indentation: level: error spaces: 2 indent-sequences: true check-multi-line-strings: false key-duplicates: level: error line-length: disable new-line-at-end-of-file: level: error new-lines: level: error type: unix trailing-spaces: level: error truthy: level: errorpython-supervisor-client-0.3.1/Dockerfile.dev000066400000000000000000000011211500244310200212600ustar00rootroot00000000000000FROM mcr.microsoft.com/devcontainers/python:1-3.12 SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Uninstall pre-installed formatting and linting tools to prevent version conflicts # Install uv RUN \ pipx uninstall mypy \ && pipx uninstall pylint \ && pip3 install --no-cache-dir uv==0.2.26 # Make venv USER vscode ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv" RUN uv venv $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" # Copy in bootstrap script COPY script/devcontainer_bootstrap /usr/bin WORKDIR /workspaces # Set the default shell to bash instead of sh ENV SHELL /bin/bash python-supervisor-client-0.3.1/LICENSE000066400000000000000000000261351500244310200175320ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. python-supervisor-client-0.3.1/README.md000066400000000000000000000064401500244310200200010ustar00rootroot00000000000000# Home Assistant Supervisor Client ## Client Library for Home Assistant Supervisor Python client for interfacing with the [Home Assistant Supervisor](https://github.com/home-assistant/supervisor) via its [REST API](https://developers.home-assistant.io/docs/api/supervisor/endpoints). Currently used in the [Home Assistant Supervisor integration](https://www.home-assistant.io/integrations/hassio/) in Home Assistant. Add-ons which interface with Supervisor can also leverage it. The library expects to find the access token in the `SUPERVISOR_TOKEN` env which is set automatically by Supervisor for add-ons. Currently there is no way to get a long-lived access token for Supervisor outside these use cases so the library's usage is limited to these. ## Installation The library is published on `pip` and can be installed that way: ```sh pip install aiohasupervisor ``` And then used via import ```py import asyncio import supervisor_client asyncio.run(supervisor_client.info()) ``` Output would look like the example response in `/info` from [here](https://developers.home-assistant.io/docs/api/supervisor/endpoints#root) ## Developing & contributing ### Prerequisites The client can interact remotely with the Home Assistant Supervisor using the `remote_api` add-on from the [developer add-ons repository](https://github.com/home-assistant/addons-development). After installing and starting the add-on, a token is shown in the `remote_api` add-on log, which is needed for further development. It is also recommended you use Visual Studio Code for development with the devcontainer extension. This will read the published devcontainer configuration and setup the development environment for you. ### Get the source code Fork ([https://github.com/home-assistant-libs/python-supervisor-client/fork](https://github.com/home-assistant/python-supervisor-client/fork)) or clone this repository. ### Using it in development From within the devcontainer, open terminal and do the following: ```shell uv pip install -e . export SUPERVISOR_API_URL=http://192.168.1.2 export SUPERVISOR_TOKEN=replace_this_with_remote_api_token python examples/connectivity_test.py ``` Output should match the example response in `/info` as shown/linked above in [Installation](#installation). **Note**: Replace the `192.168.1.2` with the IP address of your Home Assistant instance running the `remote_api` add-on and use the token provided. ### Contributing a change 1. Create a feature branch on your fork/clone of the git repository. 2. Commit your changes. 3. Rebase your local changes against the `main` branch. 4. Run test suite with the `pytest tests` command or use Test Explorer and confirm that it passes. 5. Use `ruff` to format your code with the rules configured in this project 6. Create a new Pull Request ## Release Any time the API changes for Home Assistant Supervisor a corresponding release should be published here. Once that release is available on pip a PR should be created to [Home Assistant Core](https://github.com/home-assistant/core) updating its [Home Assistant Supervisor integration](https://github.com/home-assistant/core/blob/dev/homeassistant/components/hassio). Follow the directions for updating [Requirements](https://developers.home-assistant.io/docs/creating_integration_manifest#requirements) in the Integration manifest. python-supervisor-client-0.3.1/aiohasupervisor/000077500000000000000000000000001500244310200217415ustar00rootroot00000000000000python-supervisor-client-0.3.1/aiohasupervisor/__init__.py000066400000000000000000000013361500244310200240550ustar00rootroot00000000000000"""Init file for aiohasupervisor.""" from aiohasupervisor.exceptions import ( SupervisorAuthenticationError, SupervisorBadRequestError, SupervisorConnectionError, SupervisorError, SupervisorForbiddenError, SupervisorNotFoundError, SupervisorResponseError, SupervisorServiceUnavailableError, SupervisorTimeoutError, ) from aiohasupervisor.root import SupervisorClient __all__ = [ "SupervisorError", "SupervisorConnectionError", "SupervisorAuthenticationError", "SupervisorBadRequestError", "SupervisorForbiddenError", "SupervisorNotFoundError", "SupervisorResponseError", "SupervisorServiceUnavailableError", "SupervisorTimeoutError", "SupervisorClient", ] python-supervisor-client-0.3.1/aiohasupervisor/addons.py000066400000000000000000000065671500244310200236010ustar00rootroot00000000000000"""Addons client for Supervisor.""" from typing import Any from .client import _SupervisorComponentClient from .const import TIMEOUT_60_SECONDS, ResponseType from .models.addons import ( AddonsConfigValidate, AddonsList, AddonsOptions, AddonsSecurityOptions, AddonsStats, AddonsUninstall, InstalledAddon, InstalledAddonComplete, ) class AddonsClient(_SupervisorComponentClient): """Handles installed addon access in Supervisor.""" async def list(self) -> list[InstalledAddon]: """Get installed addons.""" result = await self._client.get("addons") return AddonsList.from_dict(result.data).addons async def addon_info(self, addon: str) -> InstalledAddonComplete: """Get all info for addon.""" result = await self._client.get(f"addons/{addon}/info") return InstalledAddonComplete.from_dict(result.data) async def uninstall_addon( self, addon: str, options: AddonsUninstall | None = None, ) -> None: """Uninstall an addon.""" await self._client.post( f"addons/{addon}/uninstall", json=options.to_dict() if options else None, timeout=TIMEOUT_60_SECONDS, ) async def start_addon(self, addon: str) -> None: """Start an addon.""" await self._client.post(f"addons/{addon}/start", timeout=TIMEOUT_60_SECONDS) async def stop_addon(self, addon: str) -> None: """Stop an addon.""" await self._client.post(f"addons/{addon}/stop", timeout=TIMEOUT_60_SECONDS) async def restart_addon(self, addon: str) -> None: """Restart an addon.""" await self._client.post(f"addons/{addon}/restart", timeout=None) async def set_addon_options(self, addon: str, options: AddonsOptions) -> None: """Set options for addon.""" await self._client.post(f"addons/{addon}/options", json=options.to_dict()) async def addon_config_validate( self, addon: str, config: dict[str, Any], ) -> AddonsConfigValidate: """Validate config for an addon.""" result = await self._client.post( f"addons/{addon}/options/validate", response_type=ResponseType.JSON, json=config, ) return AddonsConfigValidate.from_dict(result.data) async def addon_config(self, addon: str) -> dict[str, Any]: """Get config for addon.""" result = await self._client.get(f"addons/{addon}/options/config") return result.data async def rebuild_addon(self, addon: str) -> None: """Rebuild an addon (only available for local addons built from source).""" await self._client.post(f"addons/{addon}/rebuild") async def write_addon_stdin(self, addon: str, stdin: bytes) -> None: """Write to stdin of an addon (if supported by addon).""" await self._client.post(f"addons/{addon}/stdin", data=stdin) async def set_addon_security( self, addon: str, options: AddonsSecurityOptions ) -> None: """Set security options for addon.""" await self._client.post(f"addons/{addon}/security", json=options.to_dict()) async def addon_stats(self, addon: str) -> AddonsStats: """Get stats for addon.""" result = await self._client.get(f"addons/{addon}/stats") return AddonsStats.from_dict(result.data) # Omitted for now - Log endpoints python-supervisor-client-0.3.1/aiohasupervisor/backups.py000066400000000000000000000121541500244310200237460ustar00rootroot00000000000000"""Backups client for supervisor.""" from collections.abc import AsyncIterator from aiohttp import MultipartWriter from multidict import MultiDict from .client import _SupervisorComponentClient from .const import ResponseType from .models.backups import ( Backup, BackupComplete, BackupJob, BackupList, BackupsInfo, BackupsOptions, DownloadBackupOptions, FreezeOptions, FullBackupOptions, FullRestoreOptions, NewBackup, PartialBackupOptions, PartialRestoreOptions, RemoveBackupOptions, UploadBackupOptions, UploadedBackup, ) class BackupsClient(_SupervisorComponentClient): """Handles backups access in Supervisor.""" async def list(self) -> list[Backup]: """List backups.""" result = await self._client.get("backups") return BackupList.from_dict(result.data).backups async def info(self) -> BackupsInfo: """Get backups info.""" result = await self._client.get("backups/info") return BackupsInfo.from_dict(result.data) async def set_options(self, options: BackupsOptions) -> None: """Set options for backups.""" await self._client.post("backups/options", json=options.to_dict()) async def reload(self) -> None: """Reload backups cache.""" await self._client.post("backups/reload") async def freeze(self, options: FreezeOptions | None = None) -> None: """Start a freeze for external snapshot process.""" await self._client.post( "backups/freeze", json=options.to_dict() if options else None ) async def thaw(self) -> None: """Thaw an active freeze when external snapshot process ends.""" await self._client.post("backups/thaw") async def full_backup(self, options: FullBackupOptions | None = None) -> NewBackup: """Create a new full backup.""" result = await self._client.post( "backups/new/full", json=options.to_dict() if options else None, response_type=ResponseType.JSON, timeout=None, ) return NewBackup.from_dict(result.data) async def partial_backup(self, options: PartialBackupOptions) -> NewBackup: """Create a new partial backup.""" result = await self._client.post( "backups/new/partial", json=options.to_dict(), response_type=ResponseType.JSON, timeout=None, ) return NewBackup.from_dict(result.data) async def backup_info(self, backup: str) -> BackupComplete: """Get backup details.""" result = await self._client.get(f"backups/{backup}/info") return BackupComplete.from_dict(result.data) async def remove_backup( self, backup: str, options: RemoveBackupOptions | None = None ) -> None: """Remove a backup.""" await self._client.delete( f"backups/{backup}", json=options.to_dict() if options else None ) async def full_restore( self, backup: str, options: FullRestoreOptions | None = None ) -> BackupJob: """Start full restore from backup.""" result = await self._client.post( f"backups/{backup}/restore/full", json=options.to_dict() if options else None, response_type=ResponseType.JSON, timeout=None, ) return BackupJob.from_dict(result.data) async def partial_restore( self, backup: str, options: PartialRestoreOptions ) -> BackupJob: """Start partial restore from backup.""" result = await self._client.post( f"backups/{backup}/restore/partial", json=options.to_dict(), response_type=ResponseType.JSON, timeout=None, ) return BackupJob.from_dict(result.data) async def upload_backup( self, stream: AsyncIterator[bytes], options: UploadBackupOptions | None = None ) -> str: """Upload backup by stream and return slug.""" params = MultiDict() if options: if options.location: for location in options.location: params.add("location", location) if options.filename: params.add("filename", options.filename.as_posix()) with MultipartWriter("form-data") as mp: mp.append(stream) result = await self._client.post( "backups/new/upload", params=params, data=mp, response_type=ResponseType.JSON, timeout=None, ) return UploadedBackup.from_dict(result.data).slug async def download_backup( self, backup: str, options: DownloadBackupOptions | None = None ) -> AsyncIterator[bytes]: """Download backup and return stream.""" params = MultiDict() if options and options.location: params.add("location", options.location) result = await self._client.get( f"backups/{backup}/download", params=params, response_type=ResponseType.STREAM, timeout=None, ) return result.data python-supervisor-client-0.3.1/aiohasupervisor/client.py000066400000000000000000000175031500244310200235770ustar00rootroot00000000000000"""Internal client for making requests and managing session with Supervisor.""" from dataclasses import dataclass, field from http import HTTPMethod, HTTPStatus from importlib import metadata from typing import Any from aiohttp import ( ClientError, ClientResponse, ClientResponseError, ClientSession, ClientTimeout, ) from multidict import MultiDict from yarl import URL from .const import DEFAULT_TIMEOUT, ResponseType from .exceptions import ( SupervisorAuthenticationError, SupervisorBadRequestError, SupervisorConnectionError, SupervisorError, SupervisorForbiddenError, SupervisorNotFoundError, SupervisorResponseError, SupervisorServiceUnavailableError, SupervisorTimeoutError, ) from .models.base import Response, ResultType from .utils.aiohttp import ChunkAsyncStreamIterator VERSION = metadata.version(__package__) def is_json(response: ClientResponse, *, raise_on_fail: bool = False) -> bool: """Check if response is json according to Content-Type.""" content_type = response.headers.get("Content-Type", "") if "application/json" not in content_type: if raise_on_fail: raise SupervisorResponseError( "Unexpected response received from supervisor when expecting" f"JSON. Status: {response.status}, content type: {content_type}", ) return False return True @dataclass(slots=True) class _SupervisorClient: """Main class for handling connections with Supervisor.""" api_host: str token: str session: ClientSession | None = None _close_session: bool = field(default=False, init=False) async def _raise_on_status(self, response: ClientResponse) -> None: """Raise appropriate exception on status.""" if response.status >= HTTPStatus.BAD_REQUEST.value: exc_type: type[SupervisorError] = SupervisorError match response.status: case HTTPStatus.BAD_REQUEST: exc_type = SupervisorBadRequestError case HTTPStatus.UNAUTHORIZED: exc_type = SupervisorAuthenticationError case HTTPStatus.FORBIDDEN: exc_type = SupervisorForbiddenError case HTTPStatus.NOT_FOUND: exc_type = SupervisorNotFoundError case HTTPStatus.SERVICE_UNAVAILABLE: exc_type = SupervisorServiceUnavailableError if is_json(response): result = Response.from_json(await response.text()) raise exc_type(result.message, result.job_id) raise exc_type() async def _request( self, method: HTTPMethod, uri: str, *, params: dict[str, str] | MultiDict[str] | None, response_type: ResponseType, json: dict[str, Any] | None = None, data: Any = None, timeout: ClientTimeout | None = DEFAULT_TIMEOUT, ) -> Response: """Handle a request to Supervisor.""" try: url = URL(self.api_host).joinpath(uri) except ValueError as err: raise SupervisorError from err # This check is to make sure the normalized URL string is the same as the URL # string that was passed in. If they are different, then the passed in uri # contained characters that were removed by the normalization # such as ../../../../etc/passwd if not url.raw_path.endswith(uri): raise SupervisorError(f"Invalid request {uri}") match response_type: case ResponseType.TEXT: accept = "text/plain, */*" case _: accept = "application/json, text/plain, */*" headers = { "User-Agent": f"AioHASupervisor/{VERSION}", "Accept": accept, "Authorization": f"Bearer {self.token}", } if self.session is None: self.session = ClientSession() self._close_session = True try: response = await self.session.request( method.value, url, timeout=timeout, headers=headers, params=params, json=json, data=data, ) await self._raise_on_status(response) match response_type: case ResponseType.JSON: is_json(response, raise_on_fail=True) return Response.from_json(await response.text()) case ResponseType.TEXT: return Response(ResultType.OK, await response.text()) case ResponseType.STREAM: return Response( ResultType.OK, ChunkAsyncStreamIterator(response.content) ) case _: return Response(ResultType.OK) except (UnicodeDecodeError, ClientResponseError) as err: raise SupervisorResponseError( "Unusable response received from Supervisor, check logs", ) from err except TimeoutError as err: raise SupervisorTimeoutError("Timeout connecting to Supervisor") from err except ClientError as err: raise SupervisorConnectionError( "Error occurred connecting to supervisor", ) from err async def get( self, uri: str, *, params: dict[str, str] | MultiDict[str] | None = None, response_type: ResponseType = ResponseType.JSON, timeout: ClientTimeout | None = DEFAULT_TIMEOUT, ) -> Response: """Handle a GET request to Supervisor.""" return await self._request( HTTPMethod.GET, uri, params=params, response_type=response_type, timeout=timeout, ) async def post( self, uri: str, *, params: dict[str, str] | MultiDict[str] | None = None, response_type: ResponseType = ResponseType.NONE, json: dict[str, Any] | None = None, data: Any = None, timeout: ClientTimeout | None = DEFAULT_TIMEOUT, ) -> Response: """Handle a POST request to Supervisor.""" return await self._request( HTTPMethod.POST, uri, params=params, response_type=response_type, json=json, data=data, timeout=timeout, ) async def put( self, uri: str, *, params: dict[str, str] | MultiDict[str] | None = None, json: dict[str, Any] | None = None, timeout: ClientTimeout | None = DEFAULT_TIMEOUT, ) -> Response: """Handle a PUT request to Supervisor.""" return await self._request( HTTPMethod.PUT, uri, params=params, response_type=ResponseType.NONE, json=json, timeout=timeout, ) async def delete( self, uri: str, *, params: dict[str, str] | MultiDict[str] | None = None, json: dict[str, Any] | None = None, timeout: ClientTimeout | None = DEFAULT_TIMEOUT, ) -> Response: """Handle a DELETE request to Supervisor.""" return await self._request( HTTPMethod.DELETE, uri, params=params, response_type=ResponseType.NONE, json=json, timeout=timeout, ) async def close(self) -> None: """Close open client session.""" if self.session and self._close_session: await self.session.close() class _SupervisorComponentClient: """Common ancestor for all component clients of supervisor.""" def __init__(self, client: _SupervisorClient) -> None: """Initialize sub module with client for API calls.""" self._client = client python-supervisor-client-0.3.1/aiohasupervisor/const.py000066400000000000000000000005101500244310200234350ustar00rootroot00000000000000"""Constants for aiohasupervisor.""" from enum import StrEnum from aiohttp import ClientTimeout DEFAULT_TIMEOUT = ClientTimeout(total=10) TIMEOUT_60_SECONDS = ClientTimeout(total=60) class ResponseType(StrEnum): """Expected response type.""" NONE = "none" JSON = "json" STREAM = "stream" TEXT = "text" python-supervisor-client-0.3.1/aiohasupervisor/discovery.py000066400000000000000000000023541500244310200243260ustar00rootroot00000000000000"""Discovery client for supervisor.""" from uuid import UUID from .client import _SupervisorComponentClient from .const import TIMEOUT_60_SECONDS, ResponseType from .models.discovery import Discovery, DiscoveryConfig, DiscoveryList, SetDiscovery class DiscoveryClient(_SupervisorComponentClient): """Handles discovery access in supervisor.""" async def list(self) -> list[Discovery]: """List discovered active services.""" result = await self._client.get("discovery", timeout=TIMEOUT_60_SECONDS) return DiscoveryList.from_dict(result.data).discovery async def get(self, uuid: UUID) -> Discovery: """Get discovery details for a service.""" result = await self._client.get(f"discovery/{uuid.hex}") return Discovery.from_dict(result.data) async def delete(self, uuid: UUID) -> None: """Remove discovery for a service.""" await self._client.delete(f"discovery/{uuid.hex}") async def set(self, config: DiscoveryConfig) -> UUID: """Inform supervisor of an available service.""" result = await self._client.post( "discovery", json=config.to_dict(), response_type=ResponseType.JSON ) return SetDiscovery.from_dict(result.data).uuid python-supervisor-client-0.3.1/aiohasupervisor/exceptions.py000066400000000000000000000024251500244310200244770ustar00rootroot00000000000000"""Exceptions from supervisor client.""" class SupervisorError(Exception): """Generic exception.""" def __init__(self, message: str | None = None, job_id: str | None = None) -> None: """Initialize exception.""" if message is not None: super().__init__(message) else: super().__init__() self.job_id: str | None = job_id class SupervisorConnectionError(SupervisorError, ConnectionError): """Unknown error connecting to supervisor.""" class SupervisorTimeoutError(SupervisorError, TimeoutError): """Timeout connecting to supervisor.""" class SupervisorBadRequestError(SupervisorError): """Invalid request made to supervisor.""" class SupervisorAuthenticationError(SupervisorError): """Invalid authentication sent to supervisor.""" class SupervisorForbiddenError(SupervisorError): """Client is not allowed to take the action requested.""" class SupervisorNotFoundError(SupervisorError): """Requested resource does not exist.""" class SupervisorServiceUnavailableError(SupervisorError): """Cannot complete request because a required service is unavailable.""" class SupervisorResponseError(SupervisorError): """Unusable response received from Supervisor with the wrong type or encoding.""" python-supervisor-client-0.3.1/aiohasupervisor/homeassistant.py000066400000000000000000000043071500244310200252010ustar00rootroot00000000000000"""Home Assistant client for supervisor.""" from .client import _SupervisorComponentClient from .models.homeassistant import ( HomeAssistantInfo, HomeAssistantOptions, HomeAssistantRebuildOptions, HomeAssistantRestartOptions, HomeAssistantStats, HomeAssistantStopOptions, HomeAssistantUpdateOptions, ) class HomeAssistantClient(_SupervisorComponentClient): """Handles Home Assistant access in supervisor.""" async def info(self) -> HomeAssistantInfo: """Get Home Assistant info.""" result = await self._client.get("core/info") return HomeAssistantInfo.from_dict(result.data) async def stats(self) -> HomeAssistantStats: """Get Home Assistant stats.""" result = await self._client.get("core/stats") return HomeAssistantStats.from_dict(result.data) async def set_options(self, options: HomeAssistantOptions) -> None: """Set Home Assistant options.""" await self._client.post("core/options", json=options.to_dict()) async def update(self, options: HomeAssistantUpdateOptions | None = None) -> None: """Update Home Assistant.""" await self._client.post( "core/update", json=options.to_dict() if options else None, timeout=None ) async def restart(self, options: HomeAssistantRestartOptions | None = None) -> None: """Restart Home Assistant.""" await self._client.post( "core/restart", json=options.to_dict() if options else None ) async def stop(self, options: HomeAssistantStopOptions | None = None) -> None: """Stop Home Assistant.""" await self._client.post( "core/stop", json=options.to_dict() if options else None ) async def start(self) -> None: """Start Home Assistant.""" await self._client.post("core/start") async def check_config(self) -> None: """Check Home Assistant config.""" await self._client.post("core/check") async def rebuild(self, options: HomeAssistantRebuildOptions | None = None) -> None: """Rebuild Home Assistant.""" await self._client.post( "core/rebuild", json=options.to_dict() if options else None ) python-supervisor-client-0.3.1/aiohasupervisor/host.py000066400000000000000000000030171500244310200232710ustar00rootroot00000000000000"""Host client for supervisor.""" from .client import _SupervisorComponentClient from .const import TIMEOUT_60_SECONDS from .models.host import ( HostInfo, HostOptions, RebootOptions, Service, ServiceList, ShutdownOptions, ) class HostClient(_SupervisorComponentClient): """Handles host access in supervisor.""" async def info(self) -> HostInfo: """Get host info.""" result = await self._client.get("host/info") return HostInfo.from_dict(result.data) async def reboot(self, options: RebootOptions | None = None) -> None: """Reboot host.""" await self._client.post( "host/reboot", json=options.to_dict() if options else None, timeout=TIMEOUT_60_SECONDS, ) async def shutdown(self, options: ShutdownOptions | None = None) -> None: """Shutdown host.""" await self._client.post( "host/shutdown", json=options.to_dict() if options else None ) async def reload(self) -> None: """Reload host info cache.""" await self._client.post("host/reload") async def set_options(self, options: HostOptions) -> None: """Set host options.""" await self._client.post("host/options", json=options.to_dict()) async def services(self) -> list[Service]: """Get list of available services on host.""" result = await self._client.get("host/services") return ServiceList.from_dict(result.data).services # Omitted for now - Log endpoints python-supervisor-client-0.3.1/aiohasupervisor/jobs.py000066400000000000000000000021161500244310200232500ustar00rootroot00000000000000"""Jobs client for supervisor.""" from uuid import UUID from .client import _SupervisorComponentClient from .models.jobs import Job, JobsInfo, JobsOptions class JobsClient(_SupervisorComponentClient): """Handles Jobs access in Supervisor.""" async def info(self) -> JobsInfo: """Get Jobs info.""" result = await self._client.get("jobs/info") return JobsInfo.from_dict(result.data) async def set_options(self, options: JobsOptions) -> None: """Set Jobs options.""" await self._client.post("jobs/options", json=options.to_dict()) async def reset(self) -> None: """Reset Jobs options (primarily clears previously ignored job conditions).""" await self._client.post("jobs/reset") async def get_job(self, job: UUID) -> Job: """Get details of a job.""" result = await self._client.get(f"jobs/{job.hex}") return Job.from_dict(result.data) async def delete_job(self, job: UUID) -> None: """Remove a done job from Supervisor's cache.""" await self._client.delete(f"jobs/{job.hex}") python-supervisor-client-0.3.1/aiohasupervisor/models/000077500000000000000000000000001500244310200232245ustar00rootroot00000000000000python-supervisor-client-0.3.1/aiohasupervisor/models/__init__.py000066400000000000000000000127361500244310200253460ustar00rootroot00000000000000"""Models for supervisor client.""" from aiohasupervisor.models.addons import ( AddonBoot, AddonBootConfig, AddonsConfigValidate, AddonsOptions, AddonsSecurityOptions, AddonsStats, AddonStage, AddonStartup, AddonState, AddonsUninstall, AppArmor, Capability, CpuArch, InstalledAddon, InstalledAddonComplete, Repository, StoreAddon, StoreAddonComplete, StoreAddonUpdate, StoreAddRepository, StoreInfo, SupervisorRole, ) from aiohasupervisor.models.backups import ( LOCATION_CLOUD_BACKUP, LOCATION_LOCAL_STORAGE, AddonSet, Backup, BackupAddon, BackupComplete, BackupContent, BackupJob, BackupLocationAttributes, BackupsInfo, BackupsOptions, BackupType, DownloadBackupOptions, Folder, FreezeOptions, FullBackupOptions, FullRestoreOptions, NewBackup, PartialBackupOptions, PartialRestoreOptions, RemoveBackupOptions, UploadBackupOptions, ) from aiohasupervisor.models.discovery import ( Discovery, DiscoveryConfig, ) from aiohasupervisor.models.homeassistant import ( HomeAssistantInfo, HomeAssistantOptions, HomeAssistantRebuildOptions, HomeAssistantRestartOptions, HomeAssistantStats, HomeAssistantStopOptions, HomeAssistantUpdateOptions, ) from aiohasupervisor.models.host import ( HostInfo, HostOptions, RebootOptions, Service, ServiceState, ShutdownOptions, ) from aiohasupervisor.models.jobs import ( Job, JobCondition, JobError, JobsInfo, JobsOptions, ) from aiohasupervisor.models.mounts import ( CIFSMountRequest, CIFSMountResponse, MountCifsVersion, MountsInfo, MountsOptions, MountState, MountType, MountUsage, NFSMountRequest, NFSMountResponse, ) from aiohasupervisor.models.network import ( AccessPoint, AuthMethod, DockerNetwork, InterfaceMethod, InterfaceType, IPv4, IPv4Config, IPv6, IPv6Config, NetworkInfo, NetworkInterface, NetworkInterfaceConfig, Vlan, VlanConfig, Wifi, WifiConfig, WifiMode, ) from aiohasupervisor.models.os import ( BootSlot, BootSlotName, DataDisk, GreenInfo, GreenOptions, MigrateDataOptions, OSInfo, OSUpdate, RaucState, SetBootSlotOptions, YellowInfo, YellowOptions, ) from aiohasupervisor.models.resolution import ( Check, CheckOptions, CheckType, ContextType, Issue, IssueType, ResolutionInfo, Suggestion, SuggestionType, UnhealthyReason, UnsupportedReason, ) from aiohasupervisor.models.root import ( AvailableUpdate, HostFeature, LogLevel, RootInfo, SupervisorState, UpdateChannel, UpdateType, ) from aiohasupervisor.models.supervisor import ( SupervisorInfo, SupervisorOptions, SupervisorStats, SupervisorUpdateOptions, ) __all__ = [ "HostFeature", "SupervisorState", "UpdateChannel", "LogLevel", "UpdateType", "RootInfo", "AvailableUpdate", "AddonStage", "AddonStartup", "AddonBoot", "AddonBootConfig", "CpuArch", "Capability", "AppArmor", "SupervisorRole", "AddonState", "StoreAddon", "StoreAddonComplete", "InstalledAddon", "InstalledAddonComplete", "AddonsOptions", "AddonsConfigValidate", "AddonsSecurityOptions", "AddonsStats", "AddonsUninstall", "Repository", "StoreInfo", "StoreAddonUpdate", "StoreAddRepository", "Check", "CheckOptions", "CheckType", "ContextType", "Issue", "IssueType", "ResolutionInfo", "Suggestion", "SuggestionType", "UnhealthyReason", "UnsupportedReason", "SupervisorInfo", "SupervisorOptions", "SupervisorStats", "SupervisorUpdateOptions", "HomeAssistantInfo", "HomeAssistantOptions", "HomeAssistantRebuildOptions", "HomeAssistantRestartOptions", "HomeAssistantStats", "HomeAssistantStopOptions", "HomeAssistantUpdateOptions", "RaucState", "BootSlotName", "BootSlot", "OSInfo", "OSUpdate", "MigrateDataOptions", "DataDisk", "SetBootSlotOptions", "GreenInfo", "GreenOptions", "YellowInfo", "YellowOptions", "LOCATION_CLOUD_BACKUP", "LOCATION_LOCAL_STORAGE", "AddonSet", "Backup", "BackupAddon", "BackupComplete", "BackupContent", "BackupJob", "BackupLocationAttributes", "BackupsInfo", "BackupsOptions", "BackupType", "DownloadBackupOptions", "Folder", "FreezeOptions", "FullBackupOptions", "FullRestoreOptions", "NewBackup", "PartialBackupOptions", "PartialRestoreOptions", "RemoveBackupOptions", "UploadBackupOptions", "Discovery", "DiscoveryConfig", "AccessPoint", "AuthMethod", "DockerNetwork", "InterfaceMethod", "InterfaceType", "IPv4", "IPv4Config", "IPv6", "IPv6Config", "NetworkInfo", "NetworkInterface", "NetworkInterfaceConfig", "Vlan", "VlanConfig", "Wifi", "WifiConfig", "WifiMode", "HostInfo", "HostOptions", "RebootOptions", "Service", "ServiceState", "ShutdownOptions", "Job", "JobCondition", "JobError", "JobsInfo", "JobsOptions", "CIFSMountRequest", "CIFSMountResponse", "MountCifsVersion", "MountsInfo", "MountsOptions", "MountState", "MountType", "MountUsage", "NFSMountRequest", "NFSMountResponse", ] python-supervisor-client-0.3.1/aiohasupervisor/models/addons.py000066400000000000000000000175161500244310200250600ustar00rootroot00000000000000"""Models for Supervisor addons.""" from abc import ABC from dataclasses import dataclass, field from enum import StrEnum from ipaddress import IPv4Address from typing import Any from mashumaro import field_options from mashumaro.config import TO_DICT_ADD_BY_ALIAS_FLAG, BaseConfig from .base import DEFAULT, ContainerStats, Options, Request, RequestConfig, ResponseData # --- ENUMS ---- class AddonStage(StrEnum): """AddonStage type.""" STABLE = "stable" EXPERIMENTAL = "experimental" DEPRECATED = "deprecated" class AddonBootConfig(StrEnum): """AddonBootConfig type.""" AUTO = "auto" MANUAL = "manual" MANUAL_ONLY = "manual_only" class AddonBoot(StrEnum): """AddonBoot type.""" AUTO = "auto" MANUAL = "manual" class CpuArch(StrEnum): """CpuArch type.""" ARMV7 = "armv7" ARMHF = "armhf" AARCH64 = "aarch64" I386 = "i386" AMD64 = "amd64" class Capability(StrEnum): """Capability type. This is an incomplete list. Supervisor regularly adds support for new privileged capabilities as addon developers request them. Therefore when returning a list of capabilities, there may be some which are not in this list parsed as strings on older versions of the client. """ BPF = "BPF" DAC_READ_SEARCH = "DAC_READ_SEARCH" IPC_LOCK = "IPC_LOCK" NET_ADMIN = "NET_ADMIN" NET_RAW = "NET_RAW" PERFMON = "PERFMON" SYS_ADMIN = "SYS_ADMIN" SYS_MODULE = "SYS_MODULE" SYS_NICE = "SYS_NICE" SYS_PTRACE = "SYS_PTRACE" SYS_RAWIO = "SYS_RAWIO" SYS_RESOURCE = "SYS_RESOURCE" SYS_TIME = "SYS_TIME" class AppArmor(StrEnum): """AppArmor type.""" DEFAULT = "default" DISABLE = "disable" PROFILE = "profile" class SupervisorRole(StrEnum): """SupervisorRole type.""" ADMIN = "admin" BACKUP = "backup" DEFAULT = "default" HOMEASSISTANT = "homeassistant" MANAGER = "manager" class AddonState(StrEnum): """AddonState type.""" STARTUP = "startup" STARTED = "started" STOPPED = "stopped" UNKNOWN = "unknown" ERROR = "error" class AddonStartup(StrEnum): """AddonStartup type.""" APPLICATION = "application" INITIALIZE = "initialize" ONCE = "once" SERVICES = "services" SYSTEM = "system" # --- OBJECTS ---- @dataclass(frozen=True) class AddonInfoBaseFields(ABC): """AddonInfoBaseFields ABC type.""" advanced: bool available: bool build: bool description: str homeassistant: str | None icon: bool logo: bool name: str repository: str slug: str stage: AddonStage update_available: bool url: str | None version_latest: str version: str | None @dataclass(frozen=True) class AddonInfoStoreBaseFields(ABC): """AddonInfoAllStoreFields ABC type.""" arch: list[CpuArch] documentation: bool @dataclass(frozen=True) class AddonInfoStoreExtFields(ABC): """AddonInfoStoreExtFields ABC type.""" apparmor: AppArmor auth_api: bool docker_api: bool full_access: bool homeassistant_api: bool host_network: bool host_pid: bool ingress: bool long_description: str | None rating: int signed: bool # Hassio is deprecated name for supervisor supervisor_api: bool = field(metadata=field_options(alias="hassio_api")) supervisor_role: SupervisorRole = field( metadata=field_options(alias="hassio_role"), ) class Config(BaseConfig): """Mashumaro config options.""" code_generation_options = [TO_DICT_ADD_BY_ALIAS_FLAG] # noqa: RUF012 @dataclass(frozen=True) class AddonInfoStoreExtInstalledBaseFields(ABC): """AddonInfoStoreExtInstalledBaseFields ABC type.""" detached: bool @dataclass(frozen=True, slots=True) class StoreAddon(AddonInfoBaseFields, AddonInfoStoreBaseFields, ResponseData): """StoreAddon type.""" installed: bool @dataclass(frozen=True, slots=True) class StoreAddonComplete( AddonInfoBaseFields, AddonInfoStoreBaseFields, AddonInfoStoreExtFields, AddonInfoStoreExtInstalledBaseFields, ResponseData, ): """StoreAddonComplete type.""" installed: bool @dataclass(frozen=True, slots=True) class InstalledAddon( AddonInfoBaseFields, AddonInfoStoreExtInstalledBaseFields, ResponseData, ): """InstalledAddon type.""" state: AddonState @dataclass(frozen=True, slots=True) class InstalledAddonComplete( AddonInfoBaseFields, AddonInfoStoreExtInstalledBaseFields, AddonInfoStoreBaseFields, AddonInfoStoreExtFields, ResponseData, ): """InstalledAddonComplete model.""" state: AddonState hostname: str dns: list[str] protected: bool boot: AddonBoot boot_config: AddonBootConfig options: dict[str, Any] schema: list[dict[str, Any]] | None machine: list[str] network: dict[str, int | None] | None network_description: dict[str, str] | None host_ipc: bool host_uts: bool host_dbus: bool privileged: list[Capability | str] changelog: bool stdin: bool gpio: bool usb: bool uart: bool kernel_modules: bool devicetree: bool udev: bool video: bool audio: bool startup: AddonStartup services: list[str] discovery: list[str] translations: dict[str, Any] webui: str | None ingress_entry: str | None ingress_url: str | None ingress_port: int | None ingress_panel: bool | None audio_input: str | None audio_output: str | None auto_update: bool ip_address: IPv4Address watchdog: bool devices: list[str] system_managed: bool system_managed_config_entry: str | None @dataclass(frozen=True, slots=True) class AddonsList(ResponseData): """AddonsList model.""" addons: list[InstalledAddon] @dataclass(frozen=True, slots=True) class AddonsOptions(Options): """AddonsOptions model.""" # Options term is used to reference both general options and addon-specific config # Therefore this field is config to match UI rather then Supervisor's API config: dict[str, Any] | None = field( # type: ignore[assignment] default=DEFAULT, metadata=field_options(alias="options"), ) boot: AddonBoot | None = None auto_update: bool | None = None network: dict[str, int | None] | None = DEFAULT # type: ignore[assignment] audio_input: str | None = DEFAULT # type: ignore[assignment] audio_output: str | None = DEFAULT # type: ignore[assignment] ingress_panel: bool | None = None watchdog: bool | None = None class Config(RequestConfig): """Mashumaro config options.""" serialize_by_alias = True @dataclass(frozen=True, slots=True) class AddonsConfigValidate(ResponseData): """AddonsConfigValidate model.""" message: str valid: bool pwned: bool | None @dataclass(frozen=True, slots=True) class AddonsSecurityOptions(Options): """AddonsSecurityOptions model.""" protected: bool | None = None @dataclass(frozen=True, slots=True) class AddonsStats(ContainerStats): """AddonsStats model.""" @dataclass(frozen=True, slots=True) class AddonsUninstall(Request): """AddonsUninstall model.""" remove_config: bool | None = None @dataclass(frozen=True, slots=True) class Repository(ResponseData): """Repository model.""" slug: str name: str source: str url: str maintainer: str @dataclass(frozen=True, slots=True) class StoreAddonsList(ResponseData): """StoreAddonsList model.""" addons: list[StoreAddon] @dataclass(frozen=True, slots=True) class StoreInfo(StoreAddonsList, ResponseData): """StoreInfo model.""" repositories: list[Repository] @dataclass(frozen=True, slots=True) class StoreAddonUpdate(Request): """StoreAddonUpdate model.""" backup: bool | None = None @dataclass(frozen=True, slots=True) class StoreAddRepository(Request): """StoreAddRepository model.""" repository: str python-supervisor-client-0.3.1/aiohasupervisor/models/backups.py000066400000000000000000000112641500244310200252320ustar00rootroot00000000000000"""Models for Supervisor backups.""" from abc import ABC from dataclasses import dataclass from datetime import datetime from enum import StrEnum from pathlib import PurePath from uuid import UUID from .base import Options, Request, ResponseData LOCATION_LOCAL_STORAGE = ".local" LOCATION_CLOUD_BACKUP = ".cloud_backup" # --- ENUMS ---- class BackupType(StrEnum): """BackupType type.""" FULL = "full" PARTIAL = "partial" class Folder(StrEnum): """Folder type.""" SHARE = "share" ADDONS = "addons/local" SSL = "ssl" MEDIA = "media" class AddonSet(StrEnum): """AddonSet type.""" ALL = "ALL" # --- OBJECTS ---- @dataclass(frozen=True, slots=True) class BackupContent(ResponseData): """BackupContent model.""" homeassistant: bool addons: list[str] folders: list[Folder] @dataclass(frozen=True, slots=True) class BackupLocationAttributes(ResponseData): """BackupLocationAttributes model.""" protected: bool size_bytes: int @dataclass(frozen=True) class BackupBaseFields(ABC): """BackupBaseFields ABC type.""" slug: str name: str date: datetime type: BackupType location_attributes: dict[str, BackupLocationAttributes] compressed: bool @dataclass(frozen=True, slots=True) class Backup(BackupBaseFields, ResponseData): """Backup model.""" content: BackupContent @dataclass(frozen=True, slots=True) class BackupAddon(ResponseData): """BackupAddon model.""" slug: str name: str version: str size: float @dataclass(frozen=True, slots=True) class BackupComplete(BackupBaseFields, ResponseData): """BackupComplete model.""" supervisor_version: str homeassistant: str | None addons: list[BackupAddon] repositories: list[str] folders: list[Folder] homeassistant_exclude_database: bool | None extra: dict | None @dataclass(frozen=True, slots=True) class BackupList(ResponseData): """BackupList model.""" backups: list[Backup] @dataclass(frozen=True, slots=True) class BackupsInfo(BackupList): """BackupsInfo model.""" days_until_stale: int @dataclass(frozen=True, slots=True) class BackupsOptions(Request): """BackupsOptions model.""" days_until_stale: int @dataclass(frozen=True, slots=True) class FreezeOptions(Request): """FreezeOptions model.""" timeout: int @dataclass(frozen=True) class PartialBackupRestoreOptions(ABC): # noqa: B024 """PartialBackupRestoreOptions ABC type.""" addons: set[str] | None = None folders: set[Folder] | None = None homeassistant: bool | None = None def __post_init__(self) -> None: """Validate at least one thing to backup/restore is included.""" if not any((self.addons, self.folders, self.homeassistant)): raise ValueError( "At least one of addons, folders, or homeassistant must have a value" ) @dataclass(frozen=True, slots=True) class FullBackupOptions(Request): """FullBackupOptions model.""" name: str | None = None password: str | None = None compressed: bool | None = None location: str | list[str] = None # type: ignore[assignment] homeassistant_exclude_database: bool | None = None background: bool | None = None extra: dict | None = None filename: PurePath | None = None @dataclass(frozen=True, slots=True) class PartialBackupOptions(FullBackupOptions, PartialBackupRestoreOptions): """PartialBackupOptions model.""" addons: AddonSet | set[str] | None = None @dataclass(frozen=True, slots=True) class BackupJob(ResponseData): """BackupJob model.""" job_id: UUID @dataclass(frozen=True, slots=True) class NewBackup(BackupJob): """NewBackup model.""" slug: str | None = None @dataclass(frozen=True, slots=True) class FullRestoreOptions(Request): """FullRestoreOptions model.""" password: str | None = None background: bool | None = None location: str = None # type: ignore[assignment] @dataclass(frozen=True, slots=True) class PartialRestoreOptions(FullRestoreOptions, PartialBackupRestoreOptions): """PartialRestoreOptions model.""" @dataclass(frozen=True, slots=True) class UploadBackupOptions(Options): """UploadBackupOptions model.""" location: set[str] = None filename: PurePath | None = None @dataclass(frozen=True, slots=True) class UploadedBackup(ResponseData): """UploadedBackup model.""" slug: str @dataclass(frozen=True, slots=True) class RemoveBackupOptions(Request): """RemoveBackupOptions model.""" location: set[str] = None @dataclass(frozen=True, slots=True) class DownloadBackupOptions(Request): """DownloadBackupOptions model.""" location: str = None # type: ignore[assignment] python-supervisor-client-0.3.1/aiohasupervisor/models/base.py000066400000000000000000000047131500244310200245150ustar00rootroot00000000000000"""Base types and internal models.""" from abc import ABC from dataclasses import dataclass from enum import StrEnum from typing import Any, Literal from mashumaro import DataClassDictMixin from mashumaro.config import BaseConfig from mashumaro.mixins.orjson import DataClassORJSONMixin class SentinelMeta(type): """Metaclass for sentinel to improve representation and make falsy. Credit to https://stackoverflow.com/a/69243488 . """ def __repr__(cls) -> str: """Represent class more like an enum.""" return f"<{cls.__name__}>" def __bool__(cls) -> Literal[False]: """Return false as a sentinel is akin to an empty value.""" return False class DEFAULT(metaclass=SentinelMeta): """Sentinel for default value when None is valid.""" class RequestConfig(BaseConfig): """Default Mashumaro config for all request models.""" omit_default = True @dataclass(frozen=True) class Request(ABC, DataClassDictMixin): """Omit default in requests to allow Supervisor to set default. If None is a valid value, the default value should be the sentinel DEFAULT for optional fields. """ class Config(RequestConfig): """Mashumaro config.""" @dataclass(frozen=True) class Options(ABC, DataClassDictMixin): """Superclass for Options models to ensure a field is present. All fields should be optional. If None is a valid value, use the DEFAULT sentinel. Client should only pass changed fields to Supervisor. """ def __post_init__(self) -> None: """Validate at least one field is present.""" if not self.to_dict(): raise ValueError("At least one field must have a value") class Config(RequestConfig): """Mashumaro config.""" @dataclass(frozen=True) class ResponseData(ABC, DataClassDictMixin): """Superclass for all response data objects.""" class ResultType(StrEnum): """ResultType type.""" OK = "ok" ERROR = "error" @dataclass(frozen=True, slots=True) class Response(DataClassORJSONMixin): """Response model for all JSON based endpoints.""" result: ResultType data: Any | None = None message: str | None = None job_id: str | None = None @dataclass(frozen=True, slots=True) class ContainerStats(ResponseData): """ContainerStats model.""" cpu_percent: float memory_usage: int memory_limit: int memory_percent: float network_rx: int network_tx: int blk_read: int blk_write: int python-supervisor-client-0.3.1/aiohasupervisor/models/discovery.py000066400000000000000000000013341500244310200256060ustar00rootroot00000000000000"""Models for discovery component.""" from dataclasses import dataclass from typing import Any from uuid import UUID from .base import Request, ResponseData @dataclass(frozen=True, slots=True) class DiscoveryConfig(Request): """DiscoveryConfig model.""" service: str config: dict[str, Any] @dataclass(frozen=True, slots=True) class Discovery(ResponseData): """Discovery model.""" addon: str service: str uuid: UUID config: dict[str, Any] @dataclass(frozen=True, slots=True) class DiscoveryList(ResponseData): """DiscoveryList model.""" discovery: list[Discovery] @dataclass(frozen=True, slots=True) class SetDiscovery(ResponseData): """SetDiscovery model.""" uuid: UUID python-supervisor-client-0.3.1/aiohasupervisor/models/homeassistant.py000066400000000000000000000037221500244310200264640ustar00rootroot00000000000000"""Models for Home Assistant.""" from dataclasses import dataclass from ipaddress import IPv4Address from .base import DEFAULT, ContainerStats, Options, Request, ResponseData # --- OBJECTS ---- @dataclass(frozen=True, slots=True) class HomeAssistantInfo(ResponseData): """HomeAssistantInfo model.""" version: str | None version_latest: str | None update_available: bool machine: str | None ip_address: IPv4Address arch: str | None image: str boot: bool port: int ssl: bool watchdog: bool audio_input: str | None audio_output: str | None backups_exclude_database: bool @dataclass(frozen=True, slots=True) class HomeAssistantStats(ContainerStats): """HomeAssistantStats model.""" @dataclass(frozen=True, slots=True) class HomeAssistantOptions(Options): """HomeAssistantOptions model.""" boot: bool | None = None image: str | None = DEFAULT # type: ignore[assignment] port: int | None = None ssl: bool | None = None watchdog: bool | None = None refresh_token: str | None = DEFAULT # type: ignore[assignment] audio_input: str | None = DEFAULT # type: ignore[assignment] audio_output: str | None = DEFAULT # type: ignore[assignment] backups_exclude_database: bool | None = None @dataclass(frozen=True, slots=True) class HomeAssistantUpdateOptions(Options): """HomeAssistantUpdateOptions model.""" version: str | None = None backup: bool | None = None @dataclass(frozen=True, slots=True) class HomeAssistantRestartOptions(Options): """HomeAssistantRestartOptions model.""" safe_mode: bool | None = None force: bool | None = None @dataclass(frozen=True, slots=True) class HomeAssistantRebuildOptions(Options): """HomeAssistantRebuildOptions model.""" safe_mode: bool | None = None force: bool | None = None @dataclass(frozen=True, slots=True) class HomeAssistantStopOptions(Request): """HomeAssistantStopOptions model.""" force: bool python-supervisor-client-0.3.1/aiohasupervisor/models/host.py000066400000000000000000000042671500244310200245640ustar00rootroot00000000000000"""Models for host APIs.""" from dataclasses import dataclass from datetime import datetime from enum import StrEnum from .base import Request, ResponseData from .root import HostFeature # --- ENUMS ---- class ServiceState(StrEnum): """ServiceState type. The service state is determined by systemd, not supervisor. The list below is pulled from `systemctl --state=help`. It may be incomplete and it may change based on the host. Therefore within a list of services there may be some with a state not in this list parsed as string. If you find this please create an issue or pr to get the state added. """ ACTIVE = "active" RELOADING = "reloading" INACTIVE = "inactive" FAILED = "failed" ACTIVATING = "activating" DEACTIVATING = "deactivating" MAINTENANCE = "maintenance" # --- OBJECTS ---- @dataclass(frozen=True, slots=True) class HostInfo(ResponseData): """HostInfo model.""" agent_version: str | None apparmor_version: str | None chassis: str | None virtualization: str | None cpe: str | None deployment: str | None disk_free: float disk_total: float disk_used: float disk_life_time: float | None features: list[HostFeature] hostname: str | None llmnr_hostname: str | None kernel: str | None operating_system: str | None timezone: str | None dt_utc: datetime | None dt_synchronized: bool | None use_ntp: bool | None startup_time: float | None boot_timestamp: int | None broadcast_llmnr: bool | None broadcast_mdns: bool | None @dataclass(frozen=True, slots=True) class ShutdownOptions(Request): """ShutdownOptions model.""" force: bool @dataclass(frozen=True, slots=True) class RebootOptions(Request): """RebootOptions model.""" force: bool @dataclass(frozen=True, slots=True) class HostOptions(Request): """HostOptions model.""" hostname: str @dataclass(frozen=True, slots=True) class Service(ResponseData): """Service model.""" name: str description: str state: ServiceState | str @dataclass(frozen=True, slots=True) class ServiceList(ResponseData): """ServiceList model.""" services: list[Service] python-supervisor-client-0.3.1/aiohasupervisor/models/jobs.py000066400000000000000000000036201500244310200245340ustar00rootroot00000000000000"""Models for Supervisor jobs.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime # noqa: TCH003 from enum import StrEnum from uuid import UUID # noqa: TCH003 from .base import Request, ResponseData # --- ENUMS ---- class JobCondition(StrEnum): """JobCondition type. This is an incomplete list. Supervisor regularly adds support for new job conditions as they are found to be needed. Therefore when returning a list of job conditions, there may be some which are not in this list parsed as strings on older versions of the client. """ AUTO_UPDATE = "auto_update" FREE_SPACE = "free_space" FROZEN = "frozen" HAOS = "haos" HEALTHY = "healthy" HOST_NETWORK = "host_network" INTERNET_HOST = "internet_host" INTERNET_SYSTEM = "internet_system" MOUNT_AVAILABLE = "mount_available" OS_AGENT = "os_agent" PLUGINS_UPDATED = "plugins_updated" RUNNING = "running" SUPERVISOR_UPDATED = "supervisor_updated" # --- OBJECTS ---- @dataclass(slots=True, frozen=True) class JobError(ResponseData): """JobError model.""" type: str message: str @dataclass(slots=True, frozen=True) class Job(ResponseData): """Job model.""" name: str | None reference: str | None uuid: UUID progress: float stage: str | None done: bool | None errors: list[JobError] created: datetime child_jobs: list[Job] @dataclass(slots=True, frozen=True) class JobsInfo(ResponseData): """JobsInfo model.""" ignore_conditions: list[JobCondition | str] jobs: list[Job] @dataclass(slots=True, frozen=True) class JobsOptions(Request): """JobsOptions model.""" # We only do `| str` in responses since we can't control what supervisor returns # Support for ignoring new job conditions will wait for a new version of library ignore_conditions: list[JobCondition] python-supervisor-client-0.3.1/aiohasupervisor/models/mounts.py000066400000000000000000000054151500244310200251300ustar00rootroot00000000000000"""Models for Supervisor mounts.""" from abc import ABC from dataclasses import dataclass, field from enum import StrEnum from pathlib import PurePath from typing import Literal from .base import Request, ResponseData # --- ENUMS ---- class MountType(StrEnum): """MountType type.""" CIFS = "cifs" NFS = "nfs" class MountUsage(StrEnum): """MountUsage type.""" BACKUP = "backup" MEDIA = "media" SHARE = "share" class MountState(StrEnum): """MountState type.""" ACTIVE = "active" ACTIVATING = "activating" DEACTIVATING = "deactivating" FAILED = "failed" INACTIVE = "inactive" MAINTENANCE = "maintenance" RELOADING = "reloading" class MountCifsVersion(StrEnum): """Mount CIFS version.""" LEGACY_1_0 = "1.0" LEGACY_2_0 = "2.0" # --- OBJECTS ---- @dataclass(frozen=True) class Mount(ABC): """Mount ABC type.""" usage: MountUsage server: str port: int | None = field(kw_only=True, default=None) @dataclass(frozen=True) class CIFSMount(ABC): """CIFSMount ABC type.""" share: str version: MountCifsVersion | None = field(kw_only=True, default=None) @dataclass(frozen=True) class NFSMount(ABC): """NFSMount ABC type.""" path: PurePath @dataclass(frozen=True) class MountResponse(ABC): """MountResponse model.""" name: str read_only: bool state: MountState | None user_path: PurePath | None @dataclass(frozen=True) class MountRequest(ABC): # noqa: B024 """MountRequest model.""" read_only: bool | None = field(kw_only=True, default=None) @dataclass(frozen=True, slots=True) class CIFSMountResponse(Mount, MountResponse, CIFSMount, ResponseData): """CIFSMountResponse model.""" type: Literal[MountType.CIFS] @dataclass(frozen=True, slots=True) class NFSMountResponse(Mount, MountResponse, NFSMount, ResponseData): """NFSMountResponse model.""" type: Literal[MountType.NFS] @dataclass(frozen=True, slots=True) class CIFSMountRequest(Mount, MountRequest, CIFSMount, Request): """CIFSMountRequest model.""" type: Literal[MountType.CIFS] = field(init=False, default=MountType.CIFS) username: str | None = field(kw_only=True, default=None) password: str | None = field(kw_only=True, default=None) @dataclass(frozen=True, slots=True) class NFSMountRequest(Mount, MountRequest, NFSMount, Request): """NFSMountRequest model.""" type: Literal[MountType.NFS] = field(init=False, default=MountType.NFS) @dataclass(frozen=True, slots=True) class MountsInfo(ResponseData): """MountsInfo model.""" default_backup_mount: str | None mounts: list[CIFSMountResponse | NFSMountResponse] @dataclass(frozen=True, slots=True) class MountsOptions(Request): """MountsOptions model.""" default_backup_mount: str | None python-supervisor-client-0.3.1/aiohasupervisor/models/network.py000066400000000000000000000074421500244310200252760ustar00rootroot00000000000000"""Models for supervisor network.""" from abc import ABC from dataclasses import dataclass from enum import StrEnum from ipaddress import ( IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, ) from .base import Options, Request, ResponseData # --- ENUMS ---- class InterfaceType(StrEnum): """InterfaceType type.""" ETHERNET = "ethernet" WIRELESS = "wireless" VLAN = "vlan" class InterfaceMethod(StrEnum): """InterfaceMethod type.""" DISABLED = "disabled" STATIC = "static" AUTO = "auto" class WifiMode(StrEnum): """WifiMode type.""" INFRASTRUCTURE = "infrastructure" MESH = "mesh" ADHOC = "adhoc" AP = "ap" class AuthMethod(StrEnum): """AuthMethod type.""" OPEN = "open" WEP = "wep" WPA_PSK = "wpa-psk" # --- OBJECTS ---- @dataclass(frozen=True) class IpBase(ABC): """IpBase ABC type.""" method: InterfaceMethod ready: bool | None @dataclass(frozen=True, slots=True) class IPv4(IpBase, ResponseData): """IPv4 model.""" address: list[IPv4Interface] nameservers: list[IPv4Address] gateway: IPv4Address | None @dataclass(frozen=True, slots=True) class IPv6(IpBase, ResponseData): """IPv6 model.""" address: list[IPv6Interface] nameservers: list[IPv6Address] gateway: IPv6Address | None @dataclass(frozen=True, slots=True) class Wifi(ResponseData): """Wifi model.""" mode: WifiMode auth: AuthMethod ssid: str signal: int | None @dataclass(frozen=True, slots=True) class Vlan(ResponseData): """Vlan model.""" id: int interface: str | None @dataclass(frozen=True, slots=True) class NetworkInterface(ResponseData): """NetworkInterface model.""" interface: str type: InterfaceType enabled: bool connected: bool primary: bool mac: str ipv4: IPv4 ipv6: IPv6 wifi: Wifi | None vlan: Vlan | None @dataclass(frozen=True, slots=True) class DockerNetwork(ResponseData): """DockerNetwork model.""" interface: str address: IPv4Network gateway: IPv4Address dns: IPv4Address @dataclass(frozen=True, slots=True) class NetworkInfo(ResponseData): """NetworkInfo model.""" interfaces: list[NetworkInterface] docker: DockerNetwork host_internet: bool | None supervisor_internet: bool @dataclass(frozen=True, slots=True) class IPv4Config(Request): """IPv4Config model.""" address: list[IPv4Interface] | None = None method: InterfaceMethod | None = None gateway: IPv4Address | None = None nameservers: list[IPv4Address] | None = None @dataclass(frozen=True, slots=True) class IPv6Config(Request): """IPv6Config model.""" address: list[IPv6Interface] | None = None method: InterfaceMethod | None = None gateway: IPv6Address | None = None nameservers: list[IPv6Address] | None = None @dataclass(frozen=True, slots=True) class WifiConfig(Request): """WifiConfig model.""" mode: WifiMode | None = None method: AuthMethod | None = None ssid: str | None = None psk: str | None = None @dataclass(frozen=True, slots=True) class NetworkInterfaceConfig(Options): """NetworkInterfaceConfig model.""" ipv4: IPv4Config | None = None ipv6: IPv6Config | None = None wifi: WifiConfig | None = None enabled: bool | None = None @dataclass(frozen=True, slots=True) class AccessPoint(ResponseData): """AccessPoint model.""" mode: WifiMode ssid: str frequency: int signal: int mac: str @dataclass(frozen=True, slots=True) class AccessPointList(ResponseData): """AccessPointList model.""" accesspoints: list[AccessPoint] @dataclass(frozen=True, slots=True) class VlanConfig(Options): """VlanConfig model.""" ipv4: IPv4Config | None = None ipv6: IPv6Config | None = None python-supervisor-client-0.3.1/aiohasupervisor/models/os.py000066400000000000000000000050711500244310200242220ustar00rootroot00000000000000"""Models for OS APIs.""" from dataclasses import dataclass from enum import StrEnum from pathlib import PurePath from .base import Options, Request, ResponseData # --- ENUMS ---- class RaucState(StrEnum): """RaucState type.""" GOOD = "good" BAD = "bad" ACTIVE = "active" class BootSlotName(StrEnum): """BootSlotName type.""" A = "A" B = "B" # --- OBJECTS ---- @dataclass(frozen=True, slots=True) class BootSlot(ResponseData): """BootSlot model.""" state: str status: RaucState | None version: str | None @dataclass(frozen=True, slots=True) class OSInfo(ResponseData): """OSInfo model.""" version: str | None version_latest: str | None update_available: bool board: str | None boot: str | None data_disk: str | None boot_slots: dict[str, BootSlot] @dataclass(frozen=True, slots=True) class OSUpdate(Request): """OSUpdate model.""" version: str | None = None @dataclass(frozen=True, slots=True) class MigrateDataOptions(Request): """MigrateDataOptions model.""" device: str @dataclass(frozen=True, slots=True) class DataDisk(ResponseData): """DataDisk model.""" name: str vendor: str model: str serial: str size: int id: str dev_path: PurePath @dataclass(frozen=True, slots=True) class DataDiskList(ResponseData): """DataDiskList model.""" disks: list[DataDisk] @dataclass(frozen=True, slots=True) class SetBootSlotOptions(Request): """SetBootSlotOptions model.""" boot_slot: BootSlotName @dataclass(frozen=True, slots=True) class GreenInfo(ResponseData): """GreenInfo model.""" activity_led: bool power_led: bool system_health_led: bool @dataclass(frozen=True, slots=True) class GreenOptions(Options): """GreenOptions model.""" activity_led: bool | None = None power_led: bool | None = None system_health_led: bool | None = None @dataclass(frozen=True, slots=True) class YellowInfo(ResponseData): """YellowInfo model.""" disk_led: bool heartbeat_led: bool power_led: bool @dataclass(frozen=True, slots=True) class YellowOptions(Options): """YellowOptions model.""" disk_led: bool | None = None heartbeat_led: bool | None = None power_led: bool | None = None @dataclass(frozen=True, slots=True) class SwapInfo(ResponseData): """SwapInfo model.""" swap_size: str | None swappiness: int | None @dataclass(frozen=True, slots=True) class SwapOptions(Options): """SwapOptions model.""" swap_size: str | None = None swappiness: int | None = None python-supervisor-client-0.3.1/aiohasupervisor/models/resolution.py000066400000000000000000000141311500244310200260010ustar00rootroot00000000000000"""Models for resolution center APIs.""" from dataclasses import dataclass from enum import StrEnum from uuid import UUID from .base import Options, ResponseData # --- ENUMS ---- class SuggestionType(StrEnum): """SuggestionType type. This is an incomplete list. Supervisor regularly adds new types of suggestions as they are discovered. Therefore when returning a suggestion, it may have a type that is not in this list parsed as strings on older versions of the client. """ ADOPT_DATA_DISK = "adopt_data_disk" CLEAR_FULL_BACKUP = "clear_full_backup" CREATE_FULL_BACKUP = "create_full_backup" DISABLE_BOOT = "disable_boot" EXECUTE_INTEGRITY = "execute_integrity" EXECUTE_REBOOT = "execute_reboot" EXECUTE_REBUILD = "execute_rebuild" EXECUTE_RELOAD = "execute_reload" EXECUTE_REMOVE = "execute_remove" EXECUTE_REPAIR = "execute_repair" EXECUTE_RESET = "execute_reset" EXECUTE_RESTART = "execute_restart" EXECUTE_START = "execute_start" EXECUTE_STOP = "execute_stop" EXECUTE_UPDATE = "execute_update" REGISTRY_LOGIN = "registry_login" RENAME_DATA_DISK = "rename_data_disk" class IssueType(StrEnum): """IssueType type. This is an incomplete list. Supervisor regularly adds new types of issues as they are discovered. Therefore when returning an issue, it may have a type that is not in this list parsed as strings on older versions of the client. """ BOOT_FAIL = "boot_fail" CORRUPT_DOCKER = "corrupt_docker" CORRUPT_REPOSITORY = "corrupt_repository" CORRUPT_FILESYSTEM = "corrupt_filesystem" DETACHED_ADDON_MISSING = "detached_addon_missing" DETACHED_ADDON_REMOVED = "detached_addon_removed" DEVICE_ACCESS_MISSING = "device_access_missing" DISABLED_DATA_DISK = "disabled_data_disk" DNS_LOOP = "dns_loop" DNS_SERVER_FAILED = "dns_server_failed" DNS_SERVER_IPV6_ERROR = "dns_server_ipv6_error" DOCKER_CONFIG = "docker_config" DOCKER_RATELIMIT = "docker_ratelimit" FATAL_ERROR = "fatal_error" FREE_SPACE = "free_space" IPV4_CONNECTION_PROBLEM = "ipv4_connection_problem" MISSING_IMAGE = "missing_image" MOUNT_FAILED = "mount_failed" MULTIPLE_DATA_DISKS = "multiple_data_disks" NO_CURRENT_BACKUP = "no_current_backup" PWNED = "pwned" REBOOT_REQUIRED = "reboot_required" SECURITY = "security" TRUST = "trust" UPDATE_FAILED = "update_failed" UPDATE_ROLLBACK = "update_rollback" class UnsupportedReason(StrEnum): """UnsupportedReason type. This is an incomplete list. Supervisor regularly adds new unsupported reasons as they are discovered. Therefore when returning a list of unsupported reasons, some may not be in this list parsed as strings on older versions of the client. """ APPARMOR = "apparmor" CGROUP_VERSION = "cgroup_version" CONNECTIVITY_CHECK = "connectivity_check" CONTENT_TRUST = "content_trust" DBUS = "dbus" DNS_SERVER = "dns_server" DOCKER_CONFIGURATION = "docker_configuration" DOCKER_VERSION = "docker_version" JOB_CONDITIONS = "job_conditions" LXC = "lxc" NETWORK_MANAGER = "network_manager" OS = "os" OS_AGENT = "os_agent" PRIVILEGED = "privileged" RESTART_POLICY = "restart_policy" SOFTWARE = "software" SOURCE_MODS = "source_mods" SUPERVISOR_VERSION = "supervisor_version" SYSTEMD = "systemd" SYSTEMD_JOURNAL = "systemd_journal" SYSTEMD_RESOLVED = "systemd_resolved" VIRTUALIZATION_IMAGE = "virtualization_image" class UnhealthyReason(StrEnum): """UnhealthyReason type. This is an incomplete list. Supervisor regularly adds new unhealthy reasons as they are discovered. Therefore when returning a list of unhealthy reasons, some may not be in this list parsed as strings on older versions of the client. """ DOCKER = "docker" OSERROR_BAD_MESSAGE = "oserror_bad_message" PRIVILEGED = "privileged" SUPERVISOR = "supervisor" SETUP = "setup" UNTRUSTED = "untrusted" class ContextType(StrEnum): """ContextType type.""" ADDON = "addon" CORE = "core" DNS_SERVER = "dns_server" MOUNT = "mount" OS = "os" PLUGIN = "plugin" SUPERVISOR = "supervisor" STORE = "store" SYSTEM = "system" class CheckType(StrEnum): """CheckType type. This is an incomplete list. Supervisor regularly adds new checks as they are discovered. Therefore when returning a list of checks, some may have a type that is not in this list parsed as strings on older versions of the client. """ ADDON_PWNED = "addon_pwned" BACKUPS = "backups" CORE_SECURITY = "core_security" DETACHED_ADDON_MISSING = "detached_addon_missing" DETACHED_ADDON_REMOVED = "detached_addon_removed" DISABLED_DATA_DISK = "disabled_data_disk" DNS_SERVER_IPV6 = "dns_server_ipv6" DNS_SERVER = "dns_server" DOCKER_CONFIG = "docker_config" FREE_SPACE = "free_space" MULTIPLE_DATA_DISKS = "multiple_data_disks" NETWORK_INTERFACE_IPV4 = "network_interface_ipv4" SUPERVISOR_TRUST = "supervisor_trust" # --- OBJECTS ---- @dataclass(frozen=True, slots=True) class Suggestion(ResponseData): """Suggestion model.""" type: SuggestionType | str context: ContextType reference: str | None uuid: UUID auto: bool @dataclass(frozen=True, slots=True) class Issue(ResponseData): """Issue model.""" type: IssueType | str context: ContextType reference: str | None uuid: UUID @dataclass(frozen=True, slots=True) class Check(ResponseData): """Check model.""" enabled: bool slug: CheckType | str @dataclass(frozen=True, slots=True) class SuggestionsList(ResponseData): """SuggestionsList model.""" suggestions: list[Suggestion] @dataclass(frozen=True, slots=True) class ResolutionInfo(SuggestionsList, ResponseData): """ResolutionInfo model.""" unsupported: list[UnsupportedReason | str] unhealthy: list[UnhealthyReason | str] issues: list[Issue] checks: list[Check] @dataclass(frozen=True, slots=True) class CheckOptions(Options): """CheckOptions model.""" enabled: bool | None = None python-supervisor-client-0.3.1/aiohasupervisor/models/root.py000066400000000000000000000044041500244310200245630ustar00rootroot00000000000000"""Models for root APIs.""" from dataclasses import dataclass from enum import StrEnum from .base import ResponseData # --- ENUMS ---- class HostFeature(StrEnum): """HostFeature type. This is an incomplete list. Supervisor regularly adds support for new host features as users request them. Therefore when returning a list of host features, there may be some which are not in this list parsed as strings on older versions of the client. """ DISK = "disk" HAOS = "haos" HOSTNAME = "hostname" JOURNAL = "journal" MOUNT = "mount" NETWORK = "network" OS_AGENT = "os_agent" REBOOT = "reboot" RESOLVED = "resolved" SERVICES = "services" SHUTDOWN = "shutdown" TIMEDATE = "timedate" class SupervisorState(StrEnum): """SupervisorState type.""" INITIALIZE = "initialize" SETUP = "setup" STARTUP = "startup" RUNNING = "running" FREEZE = "freeze" SHUTDOWN = "shutdown" STOPPING = "stopping" CLOSE = "close" class UpdateChannel(StrEnum): """UpdateChannel type.""" STABLE = "stable" BETA = "beta" DEV = "dev" class LogLevel(StrEnum): """LogLevel type.""" DEBUG = "debug" INFO = "info" WARNING = "warning" ERROR = "error" CRITICAL = "critical" class UpdateType(StrEnum): """UpdateType type.""" ADDON = "addon" CORE = "core" OS = "os" SUPERVISOR = "supervisor" # --- OBJECTS ---- @dataclass(frozen=True, slots=True) class RootInfo(ResponseData): """Root info object.""" supervisor: str homeassistant: str | None hassos: str | None docker: str hostname: str | None operating_system: str | None features: list[HostFeature | str] machine: str | None arch: str state: SupervisorState supported_arch: list[str] supported: bool channel: UpdateChannel logging: LogLevel timezone: str @dataclass(frozen=True, slots=True) class AvailableUpdate(ResponseData): """AvailableUpdate type.""" update_type: UpdateType panel_path: str version_latest: str name: str | None = None icon: str | None = None @dataclass(frozen=True, slots=True) class AvailableUpdates(ResponseData): """AvailableUpdates type.""" available_updates: list[AvailableUpdate] python-supervisor-client-0.3.1/aiohasupervisor/models/supervisor.py000066400000000000000000000024711500244310200260230ustar00rootroot00000000000000"""Models for supervisor component.""" from dataclasses import dataclass from ipaddress import IPv4Address from .base import ContainerStats, Options, Request, ResponseData from .root import LogLevel, UpdateChannel @dataclass(frozen=True, slots=True) class SupervisorInfo(ResponseData): """SupervisorInfo model.""" version: str version_latest: str update_available: bool channel: UpdateChannel arch: str supported: bool healthy: bool ip_address: IPv4Address timezone: str | None logging: LogLevel debug: bool debug_block: bool diagnostics: bool | None auto_update: bool country: str | None @dataclass(frozen=True, slots=True) class SupervisorStats(ContainerStats): """SupervisorStats model.""" @dataclass(frozen=True, slots=True) class SupervisorUpdateOptions(Request): """SupervisorUpdateOptions model.""" version: str @dataclass(frozen=True, slots=True) class SupervisorOptions(Options): """SupervisorOptions model.""" channel: UpdateChannel | None = None timezone: str | None = None logging: LogLevel | None = None debug: bool | None = None debug_block: bool | None = None diagnostics: bool | None = None content_trust: bool | None = None force_security: bool | None = None auto_update: bool | None = None python-supervisor-client-0.3.1/aiohasupervisor/mounts.py000066400000000000000000000025321500244310200236420ustar00rootroot00000000000000"""Mounts client for Supervisor.""" from .client import _SupervisorComponentClient from .models.mounts import CIFSMountRequest, MountsInfo, MountsOptions, NFSMountRequest class MountsClient(_SupervisorComponentClient): """Handle mounts access in supervisor.""" async def info(self) -> MountsInfo: """Get mounts info.""" result = await self._client.get("mounts") return MountsInfo.from_dict(result.data) async def options(self, options: MountsOptions) -> None: """Set mounts options.""" await self._client.post("mounts/options", json=options.to_dict()) async def create_mount( self, name: str, config: CIFSMountRequest | NFSMountRequest ) -> None: """Create a new mount.""" await self._client.post("mounts", json={"name": name, **config.to_dict()}) async def update_mount( self, name: str, config: CIFSMountRequest | NFSMountRequest ) -> None: """Update an existing mount.""" await self._client.put(f"mounts/{name}", json=config.to_dict()) async def delete_mount(self, name: str) -> None: """Delete an existing mount.""" await self._client.delete(f"mounts/{name}") async def reload_mount(self, name: str) -> None: """Reload details of an existing mount.""" await self._client.post(f"mounts/{name}/reload") python-supervisor-client-0.3.1/aiohasupervisor/network.py000066400000000000000000000034001500244310200240010ustar00rootroot00000000000000"""Network client for supervisor.""" from .client import _SupervisorComponentClient from .models.network import ( AccessPoint, AccessPointList, NetworkInfo, NetworkInterface, NetworkInterfaceConfig, VlanConfig, ) class NetworkClient(_SupervisorComponentClient): """Handles network access in supervisor.""" async def info(self) -> NetworkInfo: """Get network info.""" result = await self._client.get("network/info") return NetworkInfo.from_dict(result.data) async def reload(self) -> None: """Reload network info caches.""" await self._client.post("network/reload") async def interface_info(self, interface: str) -> NetworkInterface: """Get network interface info.""" result = await self._client.get(f"network/interface/{interface}/info") return NetworkInterface.from_dict(result.data) async def update_interface( self, interface: str, config: NetworkInterfaceConfig ) -> None: """Update a network interface.""" await self._client.post( f"network/interface/{interface}/update", json=config.to_dict() ) async def access_points(self, interface: str) -> list[AccessPoint]: """Get access points visible to a wireless interface.""" result = await self._client.get(f"network/interface/{interface}/accesspoints") return AccessPointList.from_dict(result.data).accesspoints async def save_vlan( self, interface: str, vlan: int, config: VlanConfig | None = None ) -> None: """Create or update a vlan for an ethernet interface.""" await self._client.post( f"network/interface/{interface}/vlan/{vlan}", json=config.to_dict() if config else None, ) python-supervisor-client-0.3.1/aiohasupervisor/os.py000066400000000000000000000054531500244310200227430ustar00rootroot00000000000000"""OS client for supervisor.""" from .client import _SupervisorComponentClient from .models.os import ( DataDisk, DataDiskList, GreenInfo, GreenOptions, MigrateDataOptions, OSInfo, OSUpdate, SetBootSlotOptions, SwapInfo, SwapOptions, YellowInfo, YellowOptions, ) class OSClient(_SupervisorComponentClient): """Handles OS access in supervisor.""" async def info(self) -> OSInfo: """Get OS info.""" result = await self._client.get("os/info") return OSInfo.from_dict(result.data) async def update(self, options: OSUpdate | None = None) -> None: """Update OS.""" await self._client.post( "os/update", json=options.to_dict() if options else None, timeout=None ) async def swap_info(self) -> SwapInfo: """Get swap settings.""" result = await self._client.get("os/config/swap") return SwapInfo.from_dict(result.data) async def set_swap_options(self, options: SwapOptions) -> None: """Set swap settings.""" await self._client.post("os/config/swap", json=options.to_dict()) async def config_sync(self) -> None: """Trigger config reload on OS.""" await self._client.post("os/config/sync") async def migrate_data(self, options: MigrateDataOptions) -> None: """Migrate data to new data disk and reboot.""" await self._client.post("os/datadisk/move", json=options.to_dict()) async def list_data_disks(self) -> list[DataDisk]: """Get all data disks.""" result = await self._client.get("os/datadisk/list") return DataDiskList.from_dict(result.data).disks async def wipe_data(self) -> None: """Trigger data disk wipe on host and reboot.""" await self._client.post("os/datadisk/wipe") async def set_boot_slot(self, options: SetBootSlotOptions) -> None: """Change active boot slot on host and reboot.""" await self._client.post("os/boot-slot", json=options.to_dict()) async def green_info(self) -> GreenInfo: """Get info for green board (if in use).""" result = await self._client.get("os/boards/green") return GreenInfo.from_dict(result.data) async def set_green_options(self, options: GreenOptions) -> None: """Set options for green board (if in use).""" await self._client.post("os/boards/green", json=options.to_dict()) async def yellow_info(self) -> YellowInfo: """Get info for yellow board (if in use).""" result = await self._client.get("os/boards/yellow") return YellowInfo.from_dict(result.data) async def set_yellow_options(self, options: YellowOptions) -> None: """Set options for yellow board (if in use).""" await self._client.post("os/boards/yellow", json=options.to_dict()) python-supervisor-client-0.3.1/aiohasupervisor/py.typed000066400000000000000000000000001500244310200234260ustar00rootroot00000000000000python-supervisor-client-0.3.1/aiohasupervisor/resolution.py000066400000000000000000000035471500244310200245270ustar00rootroot00000000000000"""Resolution center client for supervisor.""" from uuid import UUID from .client import _SupervisorComponentClient from .models.resolution import ( CheckOptions, CheckType, ResolutionInfo, Suggestion, SuggestionsList, ) class ResolutionClient(_SupervisorComponentClient): """Handles resolution center access in supervisor.""" async def info(self) -> ResolutionInfo: """Get resolution center info.""" result = await self._client.get("resolution/info") return ResolutionInfo.from_dict(result.data) async def check_options( self, check: CheckType | str, options: CheckOptions ) -> None: """Set options for a check.""" await self._client.post( f"resolution/check/{check}/options", json=options.to_dict() ) async def run_check(self, check: CheckType | str) -> None: """Run a check.""" await self._client.post(f"resolution/check/{check}/run") async def apply_suggestion(self, suggestion: UUID) -> None: """Apply a suggestion.""" await self._client.post(f"resolution/suggestion/{suggestion.hex}", timeout=None) async def dismiss_suggestion(self, suggestion: UUID) -> None: """Dismiss a suggestion.""" await self._client.delete(f"resolution/suggestion/{suggestion.hex}") async def dismiss_issue(self, issue: UUID) -> None: """Dismiss an issue.""" await self._client.delete(f"resolution/issue/{issue.hex}") async def suggestions_for_issue(self, issue: UUID) -> list[Suggestion]: """Get suggestions for issue.""" result = await self._client.get(f"resolution/issue/{issue.hex}/suggestions") return SuggestionsList.from_dict(result.data).suggestions async def healthcheck(self) -> None: """Run a healthcheck.""" await self._client.post("resolution/healthcheck") python-supervisor-client-0.3.1/aiohasupervisor/root.py000066400000000000000000000105341500244310200233010ustar00rootroot00000000000000"""Main client for supervisor.""" from typing import Self from aiohttp import ClientSession, ClientTimeout from .addons import AddonsClient from .backups import BackupsClient from .client import _SupervisorClient from .discovery import DiscoveryClient from .homeassistant import HomeAssistantClient from .host import HostClient from .jobs import JobsClient from .models.root import AvailableUpdate, AvailableUpdates, RootInfo from .mounts import MountsClient from .network import NetworkClient from .os import OSClient from .resolution import ResolutionClient from .store import StoreClient from .supervisor import SupervisorManagementClient class SupervisorClient: """Main supervisor client for all Supervisor access.""" def __init__( self, api_host: str, token: str, session: ClientSession | None = None, ) -> None: """Initialize client.""" self._client = _SupervisorClient(api_host, token, session) self._addons = AddonsClient(self._client) self._os = OSClient(self._client) self._backups = BackupsClient(self._client) self._discovery = DiscoveryClient(self._client) self._jobs = JobsClient(self._client) self._mounts = MountsClient(self._client) self._network = NetworkClient(self._client) self._host = HostClient(self._client) self._resolution = ResolutionClient(self._client) self._store = StoreClient(self._client) self._supervisor = SupervisorManagementClient(self._client) self._homeassistant = HomeAssistantClient(self._client) @property def addons(self) -> AddonsClient: """Get addons component client.""" return self._addons @property def homeassistant(self) -> HomeAssistantClient: """Get Home Assistant component client.""" return self._homeassistant @property def os(self) -> OSClient: """Get OS component client.""" return self._os @property def backups(self) -> BackupsClient: """Get backups component client.""" return self._backups @property def discovery(self) -> DiscoveryClient: """Get discovery component client.""" return self._discovery @property def jobs(self) -> JobsClient: """Get jobs component client.""" return self._jobs @property def mounts(self) -> MountsClient: """Get mounts component client.""" return self._mounts @property def network(self) -> NetworkClient: """Get network component client.""" return self._network @property def host(self) -> HostClient: """Get host component client.""" return self._host @property def resolution(self) -> ResolutionClient: """Get resolution center component client.""" return self._resolution @property def store(self) -> StoreClient: """Get store component client.""" return self._store @property def supervisor(self) -> SupervisorManagementClient: """Get supervisor component client.""" return self._supervisor async def info(self) -> RootInfo: """Get root info.""" result = await self._client.get("info") return RootInfo.from_dict(result.data) async def reload_updates(self) -> None: """Reload updates. Reload main components update information (OS, Supervisor, Core and plug-ins). """ await self._client.post("reload_updates", timeout=ClientTimeout(total=300)) async def refresh_updates(self) -> None: """Refresh updates. Discouraged as this endpoint does two things at once. Use the `reload_updates()` and `store.reload()` instead. """ await self._client.post("refresh_updates", timeout=ClientTimeout(total=300)) async def available_updates(self) -> list[AvailableUpdate]: """Get available updates.""" result = await self._client.get("available_updates") return AvailableUpdates.from_dict(result.data).available_updates async def close(self) -> None: """Close open client session.""" await self._client.close() async def __aenter__(self) -> Self: """Async enter, closes session on exit.""" return self async def __aexit__(self, *_exc_info: object) -> None: """Close session.""" await self.close() python-supervisor-client-0.3.1/aiohasupervisor/store.py000066400000000000000000000062161500244310200234540ustar00rootroot00000000000000"""Store client for supervisor.""" from .client import _SupervisorComponentClient from .const import ResponseType from .models.addons import ( Repository, StoreAddon, StoreAddonComplete, StoreAddonsList, StoreAddonUpdate, StoreAddRepository, StoreInfo, ) class StoreClient(_SupervisorComponentClient): """Handles store access in Supervisor.""" async def info(self) -> StoreInfo: """Get store info.""" result = await self._client.get("store") return StoreInfo.from_dict(result.data) async def addons_list(self) -> list[StoreAddon]: """Get list of store addons.""" result = await self._client.get("store/addons") return StoreAddonsList.from_dict(result.data).addons async def addon_info(self, addon: str) -> StoreAddonComplete: """Get store addon info.""" result = await self._client.get(f"store/addons/{addon}") return StoreAddonComplete.from_dict(result.data) async def addon_changelog(self, addon: str) -> str: """Get addon changelog.""" result = await self._client.get( f"store/addons/{addon}/changelog", response_type=ResponseType.TEXT ) return result.data async def addon_documentation(self, addon: str) -> str: """Get addon documentation.""" result = await self._client.get( f"store/addons/{addon}/documentation", response_type=ResponseType.TEXT ) return result.data async def install_addon(self, addon: str) -> None: """Install an addon.""" await self._client.post(f"store/addons/{addon}/install", timeout=None) async def update_addon( self, addon: str, options: StoreAddonUpdate | None = None ) -> None: """Update an addon to latest version.""" await self._client.post( f"store/addons/{addon}/update", json=options.to_dict() if options else None, timeout=None, ) async def reload(self) -> None: """Reload the store.""" await self._client.post("store/reload") async def repositories_list(self) -> list[Repository]: """Get list of repositories.""" result = await self._client.get("store/repositories") # This API is inconsistent with Supervisor's API model, data should be # a dictionary with a "repositories" field. It would break the CLI like # this but the CLI doesn't use it so it went unnoticed. return [Repository.from_dict(repo) for repo in result.data] async def repository_info(self, repository: str) -> Repository: """Get repository info.""" result = await self._client.get(f"store/repositories/{repository}") return Repository.from_dict(result.data) async def add_repository(self, options: StoreAddRepository) -> None: """Add a repository to the store.""" await self._client.post("store/repositories", json=options.to_dict()) async def remove_repository(self, repository: str) -> None: """Remove a repository from the store.""" await self._client.delete(f"store/repositories/{repository}") # Omitted for now - Icon/Logo endpoints python-supervisor-client-0.3.1/aiohasupervisor/supervisor.py000066400000000000000000000041311500244310200245330ustar00rootroot00000000000000"""Supervisor client for supervisor.""" from aiohttp import ClientTimeout from .client import _SupervisorComponentClient from .const import ResponseType from .models.supervisor import ( SupervisorInfo, SupervisorOptions, SupervisorStats, SupervisorUpdateOptions, ) class SupervisorManagementClient(_SupervisorComponentClient): """Handles supervisor access in supervisor.""" async def ping(self) -> None: """Check connection to supervisor.""" await self._client.get( "supervisor/ping", response_type=ResponseType.NONE, timeout=ClientTimeout(total=15), ) async def info(self) -> SupervisorInfo: """Get supervisor info.""" result = await self._client.get("supervisor/info") return SupervisorInfo.from_dict(result.data) async def stats(self) -> SupervisorStats: """Get supervisor stats.""" result = await self._client.get("supervisor/stats") return SupervisorStats.from_dict(result.data) async def update(self, options: SupervisorUpdateOptions | None = None) -> None: """Update supervisor. Providing a target version in options only works on development systems. On non-development systems this API will always update supervisor to the latest version and ignore that field. """ await self._client.post( "supervisor/update", json=options.to_dict() if options else None, timeout=None, ) async def reload(self) -> None: """Reload supervisor (add-ons, configuration, etc).""" await self._client.post("supervisor/reload") async def restart(self) -> None: """Restart supervisor.""" await self._client.post("supervisor/restart") async def set_options(self, options: SupervisorOptions) -> None: """Set supervisor options.""" await self._client.post("supervisor/options", json=options.to_dict()) async def repair(self) -> None: """Repair local supervisor and docker setup.""" await self._client.post("supervisor/repair") python-supervisor-client-0.3.1/aiohasupervisor/utils/000077500000000000000000000000001500244310200231015ustar00rootroot00000000000000python-supervisor-client-0.3.1/aiohasupervisor/utils/__init__.py000066400000000000000000000000541500244310200252110ustar00rootroot00000000000000"""Utilities used internally in library.""" python-supervisor-client-0.3.1/aiohasupervisor/utils/aiohttp.py000066400000000000000000000013761500244310200251320ustar00rootroot00000000000000"""Utilities for interacting with aiohttp.""" from typing import Self from aiohttp import StreamReader class ChunkAsyncStreamIterator: """Async iterator for chunked streams. Based on aiohttp.streams.ChunkTupleAsyncStreamIterator, but yields bytes instead of tuple[bytes, bool]. Borrowed from home-assistant/core. """ __slots__ = ("_stream",) def __init__(self, stream: StreamReader) -> None: """Initialize.""" self._stream = stream def __aiter__(self) -> Self: """Iterate.""" return self async def __anext__(self) -> bytes: """Yield next chunk.""" rv = await self._stream.readchunk() if rv == (b"", False): raise StopAsyncIteration return rv[0] python-supervisor-client-0.3.1/examples/000077500000000000000000000000001500244310200203345ustar00rootroot00000000000000python-supervisor-client-0.3.1/examples/__init__.py000066400000000000000000000000401500244310200224370ustar00rootroot00000000000000"""Aiohasupervisor examples.""" python-supervisor-client-0.3.1/examples/connectivity_test.py000066400000000000000000000013701500244310200244640ustar00rootroot00000000000000"""Simple connectivity test.""" import asyncio import json import os from typing import final from aiohasupervisor import SupervisorClient from aiohasupervisor.models import RootInfo SUPERVISOR_API_URL: final = os.environ.get("SUPERVISOR_API_URL") SUPERVISOR_TOKEN: final = os.environ.get("SUPERVISOR_TOKEN") if not SUPERVISOR_API_URL: raise RuntimeError("SUPERVISOR_API_URL env must be set") if not SUPERVISOR_TOKEN: raise RuntimeError("SUPERVISOR_TOKEN env must be set") client = SupervisorClient(SUPERVISOR_API_URL, SUPERVISOR_TOKEN) async def get_info() -> RootInfo: """Get root info.""" async with client: return await client.info() info = asyncio.run(get_info()) print(json.dumps(info.to_dict(), indent=4)) # noqa: T201 python-supervisor-client-0.3.1/pyproject.toml000066400000000000000000000066721500244310200214450ustar00rootroot00000000000000[build-system] requires = ["setuptools>=68.0,<79.1", "wheel>=0.40,<0.47"] build-backend = "setuptools.build_meta" [project] name = "aiohasupervisor" license = { text = "Apache-2.0" } description = "Asynchronous python client for Home Assistant Supervisor." readme = "README.md" authors = [ { name = "The Home Assistant Authors", email = "hello@home-assistant.io" }, ] keywords = ["docker", "home-assistant", "api", "client-library"] requires-python = ">=3.12.0" dependencies = [ "aiohttp>=3.3.0,<4.0.0", "mashumaro>=3.11,<4.0", "orjson>=3.6.1,<4.0.0", ] # The version is set by GH action on release! version = "0.0.0" [project.optional-dependencies] dev = [ # Production requirements "aiohttp==3.11.18", "mashumaro==3.15", "orjson==3.10.16", # Test requirements "aioresponses==0.7.8", "codespell==2.4.1", "coverage==7.8.0", "mypy==1.15.0", "pre-commit==4.2.0", "pytest-aiohttp==1.1.0", "pytest-cov==6.1.1", "pytest-timeout==2.3.1", "pytest==8.3.5", "ruff==0.11.6", "yamllint==1.37.0", ] [project.urls] "Homepage" = "https://www.home-assistant.io/" "Source Code" = "https://github.com/home-assistant-libs/python-supervisor-client" "Bug Reports" = "https://github.com/home-assistant-libs/python-supervisor-client/issues" "Docs: Dev" = "https://developers.home-assistant.io/" "Discord" = "https://www.home-assistant.io/join-chat/" "Forum" = "https://community.home-assistant.io/" [tool.setuptools] platforms = ["any"] zip-safe = false include-package-data = true [tool.setuptools.package-data] "aiohasupervisor" = ["py.typed"] [tool.setuptools.packages.find] include = ["aiohasupervisor*"] [tool.pytest.ini_options] testpaths = ["tests"] norecursedirs = [".git"] log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" filterwarnings = [ "error", "ignore:pkg_resources is deprecated as an API:DeprecationWarning:dirhash", "ignore::pytest.PytestUnraisableExceptionWarning", ] [tool.ruff] lint.select = ["ALL"] lint.ignore = [ "ANN401", # Opinionated warning on disallowing dynamically typed expressions "D203", # Conflicts with other rules "D213", # Conflicts with other rules "EM", # flake8-errmsg, more frustration then value "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "TRY003", # Avoid specifying long messages outside the exception class # Recommended to disable due to conflicts with formatter # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "COM812", "COM819", "D206", "D300", "E111", "E114", "E117", "ISC001", "ISC002", "Q000", "Q001", "Q002", "Q003", "W191", ] [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false [tool.ruff.lint.flake8-tidy-imports.banned-api] "pytz".msg = "use zoneinfo instead" [tool.ruff.lint.isort] force-sort-within-sections = true section-order = [ "future", "standard-library", "third-party", "first-party", "local-folder", ] forced-separate = ["tests"] known-first-party = ["aiohasupervisor", "tests"] combine-as-imports = true [tool.ruff.lint.mccabe] max-complexity = 25 python-supervisor-client-0.3.1/script/000077500000000000000000000000001500244310200200225ustar00rootroot00000000000000python-supervisor-client-0.3.1/script/devcontainer_bootstrap000077500000000000000000000001551500244310200245270ustar00rootroot00000000000000#!/bin/bash set -e uv pip install -U setuptools uv uv pip install --compile -e ".[dev]" uv pip install tox python-supervisor-client-0.3.1/script/devcontainer_setup000077500000000000000000000005701500244310200236530ustar00rootroot00000000000000#!/usr/bin/env bash set -e if [ ! -n "$VIRTUAL_ENV" ]; then if [ -x "$(command -v uv)" ]; then uv venv venv else python3 -m venv venv fi source venv/bin/activate fi if ! [ -x "$(command -v uv)" ]; then python3 -m pip install uv fi bash /usr/bin/devcontainer_bootstrap git config --global --add safe.directory "${WORKSPACE_DIRECTORY}" pre-commit install python-supervisor-client-0.3.1/script/run-in-env.sh000077500000000000000000000012451500244310200223610ustar00rootroot00000000000000#!/usr/bin/env sh set -eu # Used in venv activate script. # Would be an error if undefined. OSTYPE="${OSTYPE-}" # Activate pyenv and virtualenv if present, then run the specified command # pyenv, pyenv-virtualenv if [ -s .python-version ]; then PYENV_VERSION=$(head -n 1 .python-version) export PYENV_VERSION fi if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then . "${VIRTUAL_ENV}/bin/activate" else # other common virtualenvs my_path=$(git rev-parse --show-toplevel) for venv in venv .venv .; do if [ -f "${my_path}/${venv}/bin/activate" ]; then . "${my_path}/${venv}/bin/activate" break fi done fi exec "$@" python-supervisor-client-0.3.1/tests/000077500000000000000000000000001500244310200176605ustar00rootroot00000000000000python-supervisor-client-0.3.1/tests/__init__.py000066400000000000000000000005271500244310200217750ustar00rootroot00000000000000"""Tests for aiohasupervisor.""" from pathlib import Path def get_fixture_path(filename: str) -> Path: """Get fixture path.""" return Path(__package__) / "fixtures" / filename def load_fixture(filename: str) -> str: """Load a fixture.""" fixture = get_fixture_path(filename) return fixture.read_text(encoding="utf-8") python-supervisor-client-0.3.1/tests/conftest.py000066400000000000000000000013371500244310200220630ustar00rootroot00000000000000"""Shared fixtures for aiohasupervisor tests.""" from collections.abc import AsyncGenerator, Generator from aioresponses import aioresponses import pytest from aiohasupervisor import SupervisorClient from .const import SUPERVISOR_URL @pytest.fixture(name="supervisor_client") async def client() -> AsyncGenerator[SupervisorClient, None]: """Return a Supervisor client.""" async with SupervisorClient( SUPERVISOR_URL, "abc123", ) as supervisor_client: yield supervisor_client @pytest.fixture(name="responses") def aioresponses_fixture() -> Generator[aioresponses, None, None]: """Return aioresponses fixture.""" with aioresponses() as mocked_responses: yield mocked_responses python-supervisor-client-0.3.1/tests/const.py000066400000000000000000000001121500244310200213520ustar00rootroot00000000000000"""Constants for tests.""" SUPERVISOR_URL = "http://homeassistant.local" python-supervisor-client-0.3.1/tests/fixtures/000077500000000000000000000000001500244310200215315ustar00rootroot00000000000000python-supervisor-client-0.3.1/tests/fixtures/addon_stats.json000066400000000000000000000003671500244310200247350ustar00rootroot00000000000000{ "result": "ok", "data": { "cpu_percent": 0.0, "memory_usage": 24588288, "memory_limit": 3899138048, "memory_percent": 0.63, "network_rx": 1717120021, "network_tx": 1036769, "blk_read": 0, "blk_write": 0 } } python-supervisor-client-0.3.1/tests/fixtures/addons_config_validate.json000066400000000000000000000002631500244310200270730ustar00rootroot00000000000000{ "result": "ok", "data": { "message": "Missing option 'server' in root in Terminal & SSH (core_ssh). Got {'bad': 'config'}", "valid": false, "pwned": false } } python-supervisor-client-0.3.1/tests/fixtures/addons_info.json000066400000000000000000000104261500244310200247120ustar00rootroot00000000000000{ "result": "ok", "data": { "name": "Terminal & SSH", "slug": "core_ssh", "hostname": "core-ssh", "dns": ["core-ssh.local.hass.io"], "description": "Allow logging in remotely to Home Assistant using SSH", "long_description": "# Home Assistant Add-on: SSH server\n\nAllow logging in remotely to Home Assistant using SSH or just the web terminal with Ingress.\n\n![Supports aarch64 Architecture][aarch64-shield] ![Supports amd64 Architecture][amd64-shield] ![Supports armhf Architecture][armhf-shield] ![Supports armv7 Architecture][armv7-shield] ![Supports i386 Architecture][i386-shield]\n\n## About\n\nSetting up an SSH server allows access to your Home Assistant folders with any SSH\nclient. It also includes a command-line tool to access the Home Assistant API.\n\n\n[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg\n[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg\n[armhf-shield]: https://img.shields.io/badge/armhf-yes-green.svg\n[armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg\n[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg\n", "advanced": true, "stage": "stable", "repository": "core", "version_latest": "9.14.0", "protected": true, "rating": 7, "boot": "auto", "boot_config": "auto", "options": { "authorized_keys": [], "password": "", "apks": [], "server": { "tcp_forwarding": false } }, "schema": [ { "name": "authorized_keys", "multiple": true, "required": true, "type": "string" }, { "name": "password", "required": true, "type": "string", "format": "password" }, { "name": "apks", "multiple": true, "required": true, "type": "string" }, { "name": "server", "type": "schema", "optional": true, "multiple": false, "schema": [ { "name": "tcp_forwarding", "required": true, "type": "boolean" } ] } ], "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "machine": [], "homeassistant": null, "url": "https://github.com/home-assistant/addons/tree/master/ssh", "detached": false, "available": true, "build": false, "network": { "22/tcp": null }, "network_description": null, "host_network": false, "host_pid": false, "host_ipc": false, "host_uts": false, "host_dbus": true, "privileged": ["NET_RAW", "not_real"], "full_access": false, "apparmor": "default", "icon": true, "logo": true, "changelog": true, "documentation": true, "stdin": false, "hassio_api": true, "hassio_role": "manager", "auth_api": false, "homeassistant_api": false, "gpio": false, "usb": false, "uart": true, "kernel_modules": false, "devicetree": false, "udev": false, "docker_api": false, "video": false, "audio": true, "startup": "services", "services": [], "discovery": [], "translations": { "en": { "configuration": { "authorized_keys": { "name": "Authorized Keys", "description": "Your public keys that you wish to accept for login." }, "password": { "name": "Password", "description": "Set a password for login. We do NOT recommend this variant." }, "apks": { "name": "Packages", "description": "Additional software packages to install in the add-on container." }, "server": { "name": "Server", "description": "SSH Server configuration" } }, "network": { "22/tcp": "SSH Port" } } }, "ingress": true, "signed": true, "state": "started", "webui": null, "ingress_entry": "/api/hassio_ingress/rltExSetBjBZ-WFm4ch5YVdUNmSRIQtnKrYANqpEnms", "ingress_url": "/api/hassio_ingress/rltExSetBjBZ-WFm4ch5YVdUNmSRIQtnKrYANqpEnms/", "ingress_port": 8099, "ingress_panel": true, "audio_input": null, "audio_output": null, "auto_update": false, "ip_address": "172.30.33.0", "version": "9.14.0", "update_available": false, "watchdog": false, "devices": [], "system_managed": false, "system_managed_config_entry": null } } python-supervisor-client-0.3.1/tests/fixtures/addons_list.json000066400000000000000000000024251500244310200247320ustar00rootroot00000000000000{ "result": "ok", "data": { "addons": [ { "name": "Terminal & SSH", "slug": "core_ssh", "description": "Allow logging in remotely to Home Assistant using SSH", "advanced": true, "stage": "stable", "version": "9.14.0", "version_latest": "9.14.0", "update_available": false, "available": true, "detached": false, "homeassistant": null, "state": "started", "repository": "core", "build": false, "url": "https://github.com/home-assistant/addons/tree/master/ssh", "icon": true, "logo": true }, { "name": "Studio Code Server", "slug": "a0d7b954_vscode", "description": "Fully featured Visual Studio Code (VSCode) experience integrated in the Home Assistant frontend.", "advanced": false, "stage": "stable", "version": "5.15.0", "version_latest": "5.15.0", "update_available": false, "available": true, "detached": false, "homeassistant": null, "state": "started", "repository": "a0d7b954", "build": false, "url": "https://github.com/hassio-addons/addon-vscode", "icon": true, "logo": true } ] } } python-supervisor-client-0.3.1/tests/fixtures/addons_options_config.json000066400000000000000000000002171500244310200267740ustar00rootroot00000000000000{ "result": "ok", "data": { "authorized_keys": [], "password": "", "apks": [], "server": { "tcp_forwarding": false } } } python-supervisor-client-0.3.1/tests/fixtures/backup_background.json000066400000000000000000000001211500244310200260620ustar00rootroot00000000000000{ "result": "ok", "data": { "job_id": "dc9dbc16f6ad4de592ffa72c807ca2bf" } } python-supervisor-client-0.3.1/tests/fixtures/backup_foreground.json000066400000000000000000000001451500244310200261230ustar00rootroot00000000000000{ "result": "ok", "data": { "job_id": "dc9dbc16f6ad4de592ffa72c807ca2bf", "slug": "9ecf0028" } } python-supervisor-client-0.3.1/tests/fixtures/backup_info.json000066400000000000000000000017071500244310200247110ustar00rootroot00000000000000{ "result": "ok", "data": { "slug": "69558789", "type": "partial", "name": "addon_core_mosquitto_6.4.0", "date": "2024-05-31T00:00:00.000000+00:00", "size": 0.01, "size_bytes": 10123, "compressed": true, "protected": false, "location_attributes": { ".local": { "protected": false, "size_bytes": 10123 } }, "supervisor_version": "2024.05.0", "homeassistant": null, "location": null, "locations": [null], "addons": [ { "slug": "core_mosquitto", "name": "Mosquitto broker", "version": "6.4.0", "size": 0.0 } ], "repositories": [ "core", "local", "https://github.com/music-assistant/home-assistant-addon", "https://github.com/esphome/home-assistant-addon", "https://github.com/hassio-addons/repository" ], "folders": [], "homeassistant_exclude_database": null, "extra": null } } python-supervisor-client-0.3.1/tests/fixtures/backup_info_location_attributes.json000066400000000000000000000016231500244310200310440ustar00rootroot00000000000000{ "result": "ok", "data": { "slug": "d9c48f8b", "type": "partial", "name": "test_consolidate", "date": "2025-01-22T18:09:28.196333+00:00", "size": 0.01, "size_bytes": 10240, "compressed": true, "protected": true, "location_attributes": { ".local": { "protected": true, "size_bytes": 10240 }, "test": { "protected": true, "size_bytes": 10240 } }, "supervisor_version": "2025.01.1.dev2104", "homeassistant": null, "location": null, "locations": [null, "test"], "addons": [], "repositories": [ "https://github.com/esphome/home-assistant-addon", "https://github.com/hassio-addons/repository", "https://github.com/music-assistant/home-assistant-addon", "local", "core" ], "folders": ["ssl"], "homeassistant_exclude_database": null, "extra": {} } } python-supervisor-client-0.3.1/tests/fixtures/backup_info_no_homeassistant.json000066400000000000000000000017201500244310200303420ustar00rootroot00000000000000{ "result": "ok", "data": { "slug": "d13dedd0", "type": "partial", "name": "Studio Code Server", "date": "2023-08-10T19:37:01.084215+00:00", "size": 0.12, "size_bytes": 120123, "compressed": true, "protected": false, "location_attributes": { ".local": { "protected": false, "size_bytes": 120123 } }, "supervisor_version": "2023.08.2.dev1002", "homeassistant": null, "location": "Test", "locations": ["Test"], "addons": [ { "slug": "a0d7b954_vscode", "name": "Studio Code Server", "version": "5.6.1", "size": 0.1 } ], "repositories": [ "local", "https://github.com/hassio-addons/repository", "core", "https://github.com/music-assistant/home-assistant-addon", "https://github.com/esphome/home-assistant-addon" ], "folders": [], "homeassistant_exclude_database": null, "extra": null } } python-supervisor-client-0.3.1/tests/fixtures/backup_info_with_extra.json000066400000000000000000000017701500244310200271470ustar00rootroot00000000000000{ "result": "ok", "data": { "slug": "69558789", "type": "partial", "name": "addon_core_mosquitto_6.4.0", "date": "2024-05-31T00:00:00.000000+00:00", "size": 0.01, "size_bytes": 10123, "compressed": true, "protected": false, "location_attributes": { ".local": { "protected": false, "size_bytes": 10123 } }, "supervisor_version": "2024.05.0", "homeassistant": null, "location": null, "locations": [null], "addons": [ { "slug": "core_mosquitto", "name": "Mosquitto broker", "version": "6.4.0", "size": 0.0 } ], "repositories": [ "core", "local", "https://github.com/music-assistant/home-assistant-addon", "https://github.com/esphome/home-assistant-addon", "https://github.com/hassio-addons/repository" ], "folders": [], "homeassistant_exclude_database": null, "extra": { "user": "test", "scheduled": true } } } python-supervisor-client-0.3.1/tests/fixtures/backup_info_with_locations.json000066400000000000000000000020401500244310200300060ustar00rootroot00000000000000{ "result": "ok", "data": { "slug": "69558789", "type": "partial", "name": "addon_core_mosquitto_6.4.0", "date": "2024-05-31T00:00:00.000000+00:00", "size": 0.01, "size_bytes": 10123, "compressed": true, "protected": false, "location_attributes": { ".local": { "protected": false, "size_bytes": 10123 }, "Test": { "protected": false, "size_bytes": 10123 } }, "supervisor_version": "2024.05.0", "homeassistant": null, "location": null, "locations": [null, "Test"], "addons": [ { "slug": "core_mosquitto", "name": "Mosquitto broker", "version": "6.4.0", "size": 0.0 } ], "repositories": [ "core", "local", "https://github.com/music-assistant/home-assistant-addon", "https://github.com/esphome/home-assistant-addon", "https://github.com/hassio-addons/repository" ], "folders": [], "homeassistant_exclude_database": null, "extra": null } } python-supervisor-client-0.3.1/tests/fixtures/backup_restore.json000066400000000000000000000001211500244310200254260ustar00rootroot00000000000000{ "result": "ok", "data": { "job_id": "dc9dbc16f6ad4de592ffa72c807ca2bf" } } python-supervisor-client-0.3.1/tests/fixtures/backup_uploaded.json000066400000000000000000000000671500244310200255510ustar00rootroot00000000000000{ "result": "ok", "data": { "slug": "7fed74c8" } } python-supervisor-client-0.3.1/tests/fixtures/backups_info.json000066400000000000000000000031151500244310200250670ustar00rootroot00000000000000{ "result": "ok", "data": { "backups": [ { "slug": "58bc7491", "name": "Full Backup 2024-04-06 00:05:39", "date": "2024-04-06T07:05:40.000000+00:00", "type": "full", "size": 828.81, "size_bytes": 828810000, "location": null, "locations": [null], "protected": false, "location_attributes": { ".local": { "protected": false, "size_bytes": 828810000 } }, "compressed": true, "content": { "homeassistant": true, "addons": [ "core_matter_server", "core_samba", "core_ssh", "a0d7b954_vscode", "core_configurator", "core_mosquitto", "d5369777_music_assistant_beta", "cebe7a76_hassio_google_drive_backup" ], "folders": ["share", "addons/local", "ssl", "media"] } }, { "slug": "69558789", "name": "addon_core_mosquitto_6.4.0", "date": "2024-05-31T20:48:03.838030+00:00", "type": "partial", "size": 0.01, "size_bytes": 10123, "location": null, "locations": [null], "protected": false, "location_attributes": { ".local": { "protected": false, "size_bytes": 120123 } }, "compressed": true, "content": { "homeassistant": false, "addons": ["core_mosquitto"], "folders": [] } } ], "days_until_stale": 30 } } python-supervisor-client-0.3.1/tests/fixtures/backups_list.json000066400000000000000000000030601500244310200251060ustar00rootroot00000000000000{ "result": "ok", "data": { "backups": [ { "slug": "58bc7491", "name": "Full Backup 2024-04-06 00:05:39", "date": "2024-04-06T07:05:40.000000+00:00", "type": "full", "size": 828.81, "size_bytes": 828810000, "location": null, "locations": [null], "protected": false, "location_attributes": { ".local": { "protected": false, "size_bytes": 828810000 } }, "compressed": true, "content": { "homeassistant": true, "addons": [ "core_matter_server", "core_samba", "core_ssh", "a0d7b954_vscode", "core_configurator", "core_mosquitto", "d5369777_music_assistant_beta", "cebe7a76_hassio_google_drive_backup" ], "folders": ["share", "addons/local", "ssl", "media"] } }, { "slug": "69558789", "name": "addon_core_mosquitto_6.4.0", "date": "2024-05-31T20:48:03.838030+00:00", "type": "partial", "size": 0.01, "size_bytes": 10123, "location": null, "locations": [null], "protected": false, "location_attributes": { ".local": { "protected": false, "size_bytes": 10123 } }, "compressed": true, "content": { "homeassistant": false, "addons": ["core_mosquitto"], "folders": [] } } ] } } python-supervisor-client-0.3.1/tests/fixtures/backups_list_location_attributes.json000066400000000000000000000013501500244310200312440ustar00rootroot00000000000000{ "result": "ok", "data": { "backups": [ { "slug": "d9c48f8b", "name": "test_consolidate", "date": "2025-01-22T18:09:28.196333+00:00", "type": "partial", "size": 0.01, "size_bytes": 10240, "location": null, "locations": [null, "test"], "protected": true, "location_attributes": { ".local": { "protected": true, "size_bytes": 10240 }, "test": { "protected": true, "size_bytes": 10240 } }, "compressed": true, "content": { "homeassistant": false, "addons": [], "folders": ["ssl"] } } ] } } python-supervisor-client-0.3.1/tests/fixtures/discovery_get.json000066400000000000000000000005031500244310200252700ustar00rootroot00000000000000{ "result": "ok", "data": { "addon": "core_mosquitto", "service": "mqtt", "uuid": "889ca604cff84004894e53d181655b3a", "config": { "host": "core-mosquitto", "port": 1883, "ssl": false, "protocol": "3.1.1", "username": "homeassistant", "password": "abc123" } } } python-supervisor-client-0.3.1/tests/fixtures/discovery_list.json000066400000000000000000000021311500244310200254630ustar00rootroot00000000000000{ "result": "ok", "data": { "discovery": [ { "addon": "core_mosquitto", "service": "mqtt", "uuid": "889ca604cff84004894e53d181655b3a", "config": { "host": "core-mosquitto", "port": 1883, "ssl": false, "protocol": "3.1.1", "username": "homeassistant", "password": "abc123" } } ], "services": { "vlc_telnet": ["core_vlc"], "esphome": [ "5c53de3b_esphome-beta", "5c53de3b_esphome-dev", "5c53de3b_esphome" ], "zwave_js": [ "core_zwave_js", "a0d7b954_zwavejs2mqtt", "77f1785d_zwave_mock_server" ], "matter": ["core_matter_server"], "wyoming": [ "core_whisper", "core_assist_microphone", "core_piper", "core_openwakeword" ], "otbr": ["core_openthread_border_router", "core_silabs_multiprotocol"], "mqtt": ["core_mosquitto"], "motioneye": ["a0d7b954_motioneye"], "deconz": ["core_deconz"], "adguard": ["a0d7b954_adguard"] } } } python-supervisor-client-0.3.1/tests/fixtures/discovery_set.json000066400000000000000000000001251500244310200253040ustar00rootroot00000000000000{ "result": "ok", "data": { "uuid": "889ca604cff84004894e53d181655b3a" } } python-supervisor-client-0.3.1/tests/fixtures/hadolint.json000066400000000000000000000004011500244310200242210ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "hadolint", "pattern": [ { "regexp": "^(.+):(\\d+)\\s+((DL\\d{4}).+)$", "file": 1, "line": 2, "message": 3, "code": 4 } ] } ] } python-supervisor-client-0.3.1/tests/fixtures/homeassistant_info.json000066400000000000000000000006721500244310200263260ustar00rootroot00000000000000{ "result": "ok", "data": { "version": "2024.9.0", "version_latest": "2024.9.0", "update_available": false, "machine": "odroid-n2", "ip_address": "172.30.32.1", "arch": "aarch64", "image": "ghcr.io/home-assistant/odroid-n2-homeassistant", "boot": true, "port": 8123, "ssl": false, "watchdog": true, "audio_input": null, "audio_output": null, "backups_exclude_database": false } } python-supervisor-client-0.3.1/tests/fixtures/homeassistant_stats.json000066400000000000000000000003531500244310200265250ustar00rootroot00000000000000{ "result": "ok", "data": { "cpu_percent": 0.01, "memory_usage": 678883328, "memory_limit": 3899138048, "memory_percent": 17.41, "network_rx": 0, "network_tx": 0, "blk_read": 0, "blk_write": 0 } } python-supervisor-client-0.3.1/tests/fixtures/host_info.json000066400000000000000000000017731500244310200244240ustar00rootroot00000000000000{ "result": "ok", "data": { "agent_version": "1.6.0", "apparmor_version": "3.1.2", "chassis": "embedded", "virtualization": "", "cpe": "cpe:2.3:o:home-assistant:haos:12.4.dev20240527:*:development:*:*:*:odroid-n2:*", "deployment": "development", "disk_free": 20.1, "disk_total": 27.9, "disk_used": 6.7, "disk_life_time": 10.0, "features": [ "reboot", "shutdown", "services", "network", "hostname", "timedate", "os_agent", "haos", "resolved", "journal", "disk", "mount" ], "hostname": "homeassistant", "llmnr_hostname": "homeassistant3", "kernel": "6.6.32-haos", "operating_system": "Home Assistant OS 12.4.dev20240527", "timezone": "Etc/UTC", "dt_utc": "2024-10-03T00:00:00.000000+00:00", "dt_synchronized": true, "use_ntp": true, "startup_time": 1.966311, "boot_timestamp": 1716927644219811, "broadcast_llmnr": true, "broadcast_mdns": true } } python-supervisor-client-0.3.1/tests/fixtures/host_services.json000066400000000000000000000021401500244310200253010ustar00rootroot00000000000000{ "result": "ok", "data": { "services": [ { "name": "emergency.service", "description": "Emergency Shell", "state": "inactive" }, { "name": "bluetooth.service", "description": "Bluetooth service", "state": "inactive" }, { "name": "haos-swapfile.service", "description": "HAOS swap", "state": "inactive" }, { "name": "hassos-config.service", "description": "HassOS Configuration Manager", "state": "inactive" }, { "name": "dropbear.service", "description": "Dropbear SSH daemon", "state": "active" }, { "name": "systemd-time-wait-sync.service", "description": "Wait Until Kernel Time Synchronized", "state": "active" }, { "name": "systemd-journald.service", "description": "Journal Service", "state": "active" }, { "name": "systemd-resolved.service", "description": "Network Name Resolution", "state": "active" } ] } } python-supervisor-client-0.3.1/tests/fixtures/jobs_get_job.json000066400000000000000000000017371500244310200250620ustar00rootroot00000000000000{ "result": "ok", "data": { "name": "backup_manager_partial_backup", "reference": "89cafa67", "uuid": "2febe59311f94d6ba36f6f9f73357ca8", "progress": 0, "stage": "finishing_file", "done": true, "errors": [], "created": "2025-01-30T20:55:12.859349+00:00", "child_jobs": [ { "name": "backup_store_folders", "reference": "89cafa67", "uuid": "f4bac7d9240f434a9f6a21aff7f7ade3", "progress": 0, "stage": null, "done": true, "errors": [], "created": "2025-01-30T20:55:12.892008+00:00", "child_jobs": [ { "name": "backup_folder_save", "reference": "ssl", "uuid": "fb328a1a048d4b9b982bf6b9070c03a6", "progress": 0, "stage": null, "done": true, "errors": [], "created": "2025-01-30T20:55:12.892699+00:00", "child_jobs": [] } ] } ] } } python-supervisor-client-0.3.1/tests/fixtures/jobs_info.json000066400000000000000000000031731500244310200244000ustar00rootroot00000000000000{ "result": "ok", "data": { "ignore_conditions": ["free_space"], "jobs": [ { "name": "backup_manager_partial_backup", "reference": "89cafa67", "uuid": "2febe59311f94d6ba36f6f9f73357ca8", "progress": 0, "stage": "finishing_file", "done": true, "errors": [], "created": "2025-01-30T20:55:12.859349+00:00", "child_jobs": [ { "name": "backup_store_folders", "reference": "89cafa67", "uuid": "f4bac7d9240f434a9f6a21aff7f7ade3", "progress": 0, "stage": null, "done": true, "errors": [], "created": "2025-01-30T20:55:12.892008+00:00", "child_jobs": [ { "name": "backup_folder_save", "reference": "ssl", "uuid": "fb328a1a048d4b9b982bf6b9070c03a6", "progress": 0, "stage": null, "done": true, "errors": [], "created": "2025-01-30T20:55:12.892699+00:00", "child_jobs": [] } ] } ] }, { "name": "backup_manager_partial_restore", "reference": "cfddca18", "uuid": "6e61e9629669499981780b700ec730ec", "progress": 0, "stage": null, "done": true, "errors": [ { "type": "BackupInvalidError", "message": "Invalid password for backup cfddca18" } ], "created": "2025-01-30T20:55:15.000000+00:00", "child_jobs": [] } ] } } python-supervisor-client-0.3.1/tests/fixtures/mounts_info.json000066400000000000000000000015621500244310200247700ustar00rootroot00000000000000{ "result": "ok", "data": { "default_backup_mount": "Test", "mounts": [ { "share": "backup", "server": "test.local", "name": "Test", "type": "cifs", "usage": "backup", "read_only": false, "version": null, "state": "active", "user_path": null }, { "share": "share", "server": "test2.local", "name": "Test2", "type": "cifs", "usage": "share", "read_only": true, "version": "2.0", "port": 12345, "state": "active", "user_path": "/share/Test2" }, { "server": "test3.local", "name": "Test3", "type": "nfs", "usage": "media", "read_only": false, "path": "media", "state": "active", "user_path": "/media/Test3" } ] } } python-supervisor-client-0.3.1/tests/fixtures/network_access_points.json000066400000000000000000000006141500244310200270330ustar00rootroot00000000000000{ "result": "ok", "data": { "accesspoints": [ { "mode": "infrastructure", "ssid": "UPC4814466", "frequency": 2462, "signal": 47, "mac": "AA:BB:CC:DD:EE:FF" }, { "mode": "infrastructure", "ssid": "VQ@35(55720", "frequency": 5660, "signal": 63, "mac": "FF:EE:DD:CC:BB:AA" } ] } } python-supervisor-client-0.3.1/tests/fixtures/network_info.json000066400000000000000000000016011500244310200251260ustar00rootroot00000000000000{ "result": "ok", "data": { "interfaces": [ { "interface": "end0", "type": "ethernet", "enabled": true, "connected": true, "primary": true, "mac": "00:11:22:33:44:55", "ipv4": { "method": "static", "address": ["192.168.1.2/24"], "nameservers": ["192.168.1.1"], "gateway": "192.168.1.1", "ready": true }, "ipv6": { "method": "disabled", "address": ["fe80::819d:c479:d712:7a77/64"], "nameservers": [], "gateway": null, "ready": true }, "wifi": null, "vlan": null } ], "docker": { "interface": "hassio", "address": "172.30.32.0/23", "gateway": "172.30.32.1", "dns": "172.30.32.3" }, "host_internet": true, "supervisor_internet": true } } python-supervisor-client-0.3.1/tests/fixtures/network_interface_info.json000066400000000000000000000010611500244310200271460ustar00rootroot00000000000000{ "result": "ok", "data": { "interface": "end0", "type": "ethernet", "enabled": true, "connected": true, "primary": true, "mac": "00:11:22:33:44:55", "ipv4": { "method": "static", "address": ["192.168.1.2/24"], "nameservers": ["192.168.1.1"], "gateway": "192.168.1.1", "ready": true }, "ipv6": { "method": "disabled", "address": ["fe80::819d:c479:d712:7a77/64"], "nameservers": [], "gateway": null, "ready": true }, "wifi": null, "vlan": null } } python-supervisor-client-0.3.1/tests/fixtures/os_config_swap.json000066400000000000000000000001211500244310200254160ustar00rootroot00000000000000{ "result": "ok", "data": { "swappiness": 1, "swap_size": "1G" } } python-supervisor-client-0.3.1/tests/fixtures/os_datadisk_list.json000066400000000000000000000005301500244310200257420ustar00rootroot00000000000000{ "result": "ok", "data": { "devices": ["SSK-SSK-Storage-DF123"], "disks": [ { "name": "SSK SSK Storage (DF123)", "vendor": "SSK", "model": "SSK Storage", "serial": "DF123", "size": 250059350016, "id": "SSK-SSK-Storage-DF123", "dev_path": "/dev/sda" } ] } } python-supervisor-client-0.3.1/tests/fixtures/os_green_info.json000066400000000000000000000001471500244310200252420ustar00rootroot00000000000000{ "result": "ok", "data": { "activity_led": true, "power_led": true, "system_health_led": true } } python-supervisor-client-0.3.1/tests/fixtures/os_info.json000066400000000000000000000006171500244310200240640ustar00rootroot00000000000000{ "result": "ok", "data": { "version": "13.0", "version_latest": "13.1", "update_available": true, "board": "odroid-n2", "boot": "B", "data_disk": "BJTD4R-0xaabbccdd", "boot_slots": { "A": { "state": "inactive", "status": "good", "version": null }, "B": { "state": "booted", "status": "good", "version": "13.0" } } } } python-supervisor-client-0.3.1/tests/fixtures/os_yellow_info.json000066400000000000000000000001371500244310200254540ustar00rootroot00000000000000{ "result": "ok", "data": { "disk_led": true, "heartbeat_led": true, "power_led": true } } python-supervisor-client-0.3.1/tests/fixtures/resolution_info.json000066400000000000000000000022771500244310200256520ustar00rootroot00000000000000{ "result": "ok", "data": { "unsupported": [], "unhealthy": ["supervisor"], "suggestions": [ { "type": "create_full_backup", "context": "system", "reference": null, "uuid": "f87d3556f02c4004a47111c072c76fac", "auto": false } ], "issues": [ { "type": "no_current_backup", "context": "system", "reference": null, "uuid": "7f0eac2b61c9456dab6970507a276c36" } ], "checks": [ { "enabled": true, "slug": "dns_server_ipv6" }, { "enabled": true, "slug": "disabled_data_disk" }, { "enabled": true, "slug": "detached_addon_missing" }, { "enabled": true, "slug": "multiple_data_disks" }, { "enabled": true, "slug": "backups" }, { "enabled": true, "slug": "supervisor_trust" }, { "enabled": true, "slug": "network_interface_ipv4" }, { "enabled": true, "slug": "dns_server" }, { "enabled": true, "slug": "free_space" }, { "enabled": true, "slug": "detached_addon_removed" }, { "enabled": true, "slug": "docker_config" }, { "enabled": true, "slug": "addon_pwned" }, { "enabled": true, "slug": "core_security" } ] } } python-supervisor-client-0.3.1/tests/fixtures/resolution_suggestions_for_issue.json000066400000000000000000000003711500244310200313400ustar00rootroot00000000000000{ "result": "ok", "data": { "suggestions": [ { "type": "create_full_backup", "context": "system", "reference": null, "uuid": "f87d3556f02c4004a47111c072c76fac", "auto": false } ] } } python-supervisor-client-0.3.1/tests/fixtures/root_available_updates.json000066400000000000000000000005441500244310200271370ustar00rootroot00000000000000{ "result": "ok", "data": { "available_updates": [ { "update_type": "core", "panel_path": "/update-available/core", "version_latest": "2024.9.0.dev202408010224" }, { "update_type": "os", "panel_path": "/update-available/os", "version_latest": "13.0.dev20240802" } ] } } python-supervisor-client-0.3.1/tests/fixtures/root_info.json000066400000000000000000000013611500244310200244230ustar00rootroot00000000000000{ "result": "ok", "data": { "supervisor": "2024.07.1.dev3001", "homeassistant": "2024.6.0.dev202405280218", "hassos": "12.4.dev20240527", "docker": "25.0.5", "hostname": "homeassistant", "operating_system": "Home Assistant OS 12.4.dev20240527", "features": [ "reboot", "shutdown", "services", "network", "hostname", "timedate", "os_agent", "haos", "resolved", "journal", "disk", "mount", "not_real" ], "machine": "odroid-n2", "arch": "aarch64", "state": "running", "supported_arch": ["aarch64", "armv7", "armhf"], "supported": true, "channel": "dev", "logging": "info", "timezone": "America/New_York" } } python-supervisor-client-0.3.1/tests/fixtures/store_addon_changelog.txt000066400000000000000000000007531500244310200266070ustar00rootroot00000000000000# Changelog ## 6.4.1 - Increase default max_queued_messages to 8192 to fix dropped messages during Home Assistant startup ## 6.4.0 - Update mosquitto to 2.0.18 ## 6.3.1 - Add ability to use a pre-hashed password for custom logins ## 6.3.0 - Update mosquitto to 2.0.17 ## 6.2.1 - Add explicit dependencies for dynamic security plugin and asynchronous name resolver ## 6.2.0 - Update mosquitto to 2.0.15 - Update libwebsockets to 4.3.2 (fixes Unable to create websockets listener) python-supervisor-client-0.3.1/tests/fixtures/store_addon_documentation.txt000066400000000000000000000025441500244310200275310ustar00rootroot00000000000000# Home Assistant Add-on: Mosquitto broker ## Installation Follow these steps to get the add-on installed on your system: 1. Navigate in your Home Assistant frontend to **Settings** -> **Add-ons** -> **Add-on store**. 2. Find the "Mosquitto broker" add-on and click it. 3. Click on the "INSTALL" button. ## How to use The add-on has a couple of options available. To get the add-on running: 1. Start the add-on. 2. Have some patience and wait a couple of minutes. 3. Check the add-on log output to see the result. Create a new user for MQTT via your Home Assistant's frontend **Settings** -> **People** -> **Users** , (i.e. not on Mosquitto's **Configuration** tab). Notes: 1. This name cannot be `homeassistant` or `addons`, those are reserved usernames. 2. If you do not see the option to create a new user, ensure that **Advanced Mode** is enabled in your Home Assistant profile. To use the Mosquitto as a broker, go to the integration page and install the configuration with one click: 1. Navigate in your Home Assistant frontend to **Settings** -> **Devices & Services** -> **Integrations**. 2. MQTT should appear as a discovered integration at the top of the page 3. Select it and check the box to enable MQTT discovery if desired, and hit submit. If you have old MQTT settings available, remove this old integration and restart Home Assistant to see the new one. python-supervisor-client-0.3.1/tests/fixtures/store_addon_info.json000066400000000000000000000036371500244310200257510ustar00rootroot00000000000000{ "result": "ok", "data": { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "An Open Source MQTT broker", "documentation": true, "homeassistant": null, "icon": true, "installed": true, "logo": true, "name": "Mosquitto broker", "repository": "core", "slug": "core_mosquitto", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/mosquitto", "version_latest": "6.4.1", "version": "6.4.1", "apparmor": "default", "auth_api": true, "detached": false, "docker_api": false, "full_access": false, "hassio_api": false, "hassio_role": "default", "homeassistant_api": false, "host_network": false, "host_pid": false, "ingress": false, "long_description": "# Home Assistant Add-on: Mosquitto broker\n\nMQTT broker for Home Assistant.\n\n![Supports aarch64 Architecture][aarch64-shield] ![Supports amd64 Architecture][amd64-shield] ![Supports armhf Architecture][armhf-shield] ![Supports armv7 Architecture][armv7-shield] ![Supports i386 Architecture][i386-shield]\n\n## About\n\nYou can use this add-on to install Eclipse Mosquitto, which is an open-source (EPL/EDL licensed) message broker that implements the MQTT protocol. Mosquitto is lightweight and is suitable for use on all devices from low power single board computers to full servers. For more information, please see [mosquitto].\n\n[mosquitto]: https://mosquitto.org\n[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg\n[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg\n[armhf-shield]: https://img.shields.io/badge/armhf-yes-green.svg\n[armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg\n[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg\n", "rating": 7, "signed": true } } python-supervisor-client-0.3.1/tests/fixtures/store_addons_list.json000066400000000000000000001616001500244310200261470ustar00rootroot00000000000000{ "result": "ok", "data": { "addons": [ { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "The Music Assistant server is a free, opensource Media library manager that connects to your streaming services and a wide range of connected speakers.", "documentation": false, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Music Assistant Server", "repository": "d5369777", "slug": "d5369777_music_assistant", "stage": "stable", "update_available": false, "url": "https://music-assistant.io", "version_latest": "2.2.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "The open platform for beautiful analytics and monitoring", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Grafana", "repository": "a0d7b954", "slug": "a0d7b954_grafana", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-grafana", "version_latest": "10.0.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Scan for HDMI CEC devices", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "CEC Scanner", "repository": "core", "slug": "core_cec_scan", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/cec_scan", "version_latest": "3.0", "version": null }, { "advanced": false, "arch": ["amd64", "armhf", "aarch64"], "available": true, "build": false, "description": "Control a Zigbee network with ConBee or RaspBee by Dresden Elektronik.", "documentation": true, "homeassistant": "0.91.2", "icon": true, "installed": false, "logo": true, "name": "deCONZ", "repository": "core", "slug": "core_deconz", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/deconz", "version_latest": "7.0.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Connected Home over IP (Matter) Python Controller REPL", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "CHIP Controller REPL", "repository": "77f1785d", "slug": "77f1785d_chip_controller_repl", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/chip_controller_repl", "version_latest": "0.4.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "AirPlay capabilities for your Sonos (and UPnP) devices.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "AirSonos", "repository": "a0d7b954", "slug": "a0d7b954_airsonos", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-airsonos", "version_latest": "4.2.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Whisparr is an adult video collection manager for Usenet and BitTorrent users", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Whisparr", "repository": "a0d7b954", "slug": "a0d7b954_whisparr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-whisparr/tree/main/README.md", "version_latest": "0.2.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Allow logging in remotely to Home Assistant using SSH", "documentation": true, "homeassistant": null, "icon": true, "installed": true, "logo": true, "name": "Terminal & SSH", "repository": "core", "slug": "core_ssh", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/ssh", "version_latest": "9.14.0", "version": "9.14.0" }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "The most scalable open-source MQTT broker for IoT. An alternative for the Mosquitto add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "EMQX", "repository": "a0d7b954", "slug": "a0d7b954_emqx", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-emqx", "version_latest": "0.7.0", "version": null }, { "advanced": false, "arch": ["armv7", "aarch64", "amd64"], "available": true, "build": false, "description": "Zigbee and OpenThread multiprotocol add-on", "documentation": true, "homeassistant": "2024.1.0", "icon": true, "installed": false, "logo": true, "name": "Silicon Labs Multiprotocol", "repository": "core", "slug": "core_silabs_multiprotocol", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/silabs-multiprotocol\n", "version_latest": "2.4.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Book Manager and Automation (Sonarr for Ebooks)", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Readarr", "repository": "a0d7b954", "slug": "a0d7b954_readarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-readarr/tree/main/README.md", "version_latest": "0.2.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Manage custom Python modules in Home Assistant deps", "documentation": false, "homeassistant": "2021.7.0", "icon": false, "installed": false, "logo": true, "name": "Custom deps deployment", "repository": "77f1785d", "slug": "77f1785d_custom_deps", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development", "version_latest": "1.3.4", "version": null }, { "advanced": true, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Explore your SQLite database", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "SQLite Web", "repository": "a0d7b954", "slug": "a0d7b954_sqlite-web", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-sqlite-web/tree/main/README.md", "version_latest": "4.2.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Play Spotify music on your Home Assistant device", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Spotify Connect", "repository": "a0d7b954", "slug": "a0d7b954_spotify", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-spotify-connect", "version_latest": "0.13.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "A simple DNS server", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Dnsmasq", "repository": "core", "slug": "core_dnsmasq", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/dnsmasq", "version_latest": "1.8.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Manage battery backup (UPS) devices", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Network UPS Tools", "repository": "a0d7b954", "slug": "a0d7b954_nut", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-nut", "version_latest": "0.13.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Manage Nginx proxy hosts with a simple, powerful interface", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Nginx Proxy Manager", "repository": "a0d7b954", "slug": "a0d7b954_nginxproxymanager", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-nginx-proxy-manager", "version_latest": "1.0.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Browser-based log utility for Home Assistant", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "Log Viewer", "repository": "a0d7b954", "slug": "a0d7b954_logviewer", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-log-viewer", "version_latest": "0.17.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Protect your privacy and access Home Assistant via Tor", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Tor", "repository": "a0d7b954", "slug": "a0d7b954_tor", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-tor", "version_latest": "5.0.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Fast, modern, secure VPN tunnel", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "WireGuard", "repository": "a0d7b954", "slug": "a0d7b954_wireguard", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-wireguard", "version_latest": "0.10.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Recorded media, live TV, online news, and podcasts ready to stream.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Plex Media Server", "repository": "a0d7b954", "slug": "a0d7b954_plex", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-plex", "version_latest": "3.5.2", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "Text-to-speech with Piper", "documentation": true, "homeassistant": "2023.8.0.dev20230718", "icon": true, "installed": false, "logo": true, "name": "Piper", "repository": "core", "slug": "core_piper", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/blob/master/piper", "version_latest": "1.5.2", "version": null }, { "advanced": true, "arch": ["amd64", "armv7", "aarch64"], "available": true, "build": false, "description": "Beta version of ESPHome add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "ESPHome (beta)", "repository": "5c53de3b", "slug": "5c53de3b_esphome-beta", "stage": "experimental", "update_available": false, "url": "https://beta.esphome.io/", "version_latest": "2024.9.0b3", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": false, "build": false, "description": "Matter WebSocket Server for Home Assistant Matter support.", "documentation": true, "homeassistant": "2024.6.0", "icon": true, "installed": false, "logo": true, "name": "Matter Server", "repository": "core", "slug": "core_matter_server", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/matter_server", "version_latest": "6.5.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Movie organizer/manager for usenet and torrent users", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Radarr", "repository": "a0d7b954", "slug": "a0d7b954_radarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-radarr/tree/main/README.md", "version_latest": "0.8.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Fully configurable Z-Wave JS gateway and control panel", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Z-Wave JS UI", "repository": "a0d7b954", "slug": "a0d7b954_zwavejs2mqtt", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-zwave-js-ui", "version_latest": "3.11.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Monitoring and tracking tool for Plex Media Server", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Tautulli", "repository": "a0d7b954", "slug": "a0d7b954_tautulli", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-tautulli/tree/main/README.md", "version_latest": "4.0.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Flow-based programming for the Internet of Things", "documentation": true, "homeassistant": "2023.3.0", "icon": true, "installed": false, "logo": true, "name": "Node-RED", "repository": "a0d7b954", "slug": "a0d7b954_nodered", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-node-red", "version_latest": "18.0.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Create documents containing live code, equations, visualizations, and explanatory text", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "JupyterLab", "repository": "a0d7b954", "slug": "a0d7b954_jupyterlablite", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-jupyterlab", "version_latest": "0.15.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Request management and media discovery tool for the Plex ecosystem", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Overseerr", "repository": "a0d7b954", "slug": "a0d7b954_overseerr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-overseerr/tree/main/README.md", "version_latest": "0.1.0", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64", "armv7"], "available": true, "build": false, "description": "openWakeWord using the Wyoming protocol", "documentation": true, "homeassistant": "2023.9.0.dev20230809", "icon": false, "installed": false, "logo": false, "name": "openWakeWord", "repository": "core", "slug": "core_openwakeword", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/blob/master/openwakeword", "version_latest": "1.10.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Simple & Free Wiki Software", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Bookstack", "repository": "a0d7b954", "slug": "a0d7b954_bookstack", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-bookstack", "version_latest": "2.0.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Connected Home over IP (Matter) Tool example application.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "CHIP Tool", "repository": "77f1785d", "slug": "77f1785d_chip_tool", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/chip_tool", "version_latest": "0.4.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "A simple DHCP server", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "DHCP server", "repository": "core", "slug": "core_dhcp_server", "stage": "stable", "update_available": false, "url": "https://home-assistant.io/addons/dhcp_server/", "version_latest": "1.4.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "An Open Source MQTT broker", "documentation": true, "homeassistant": null, "icon": true, "installed": true, "logo": true, "name": "Mosquitto broker", "repository": "core", "slug": "core_mosquitto", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/mosquitto", "version_latest": "6.4.1", "version": "6.4.1" }, { "advanced": false, "arch": ["armv7", "aarch64", "amd64"], "available": true, "build": false, "description": "Zigbee NCP and OpenThread multiprotocol add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Silicon Labs Concurrent Multiprotocol", "repository": "77f1785d", "slug": "77f1785d_silabs_concurrent_multiprotocol", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/silabs-concurrent-multiprotocol", "version_latest": "0.1.5", "version": null }, { "advanced": false, "arch": ["amd64"], "available": false, "build": false, "description": "Fighting disease with a world wide distributed super computer", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Folding@home", "repository": "a0d7b954", "slug": "a0d7b954_foldingathome", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-foldingathome", "version_latest": "0.7.2", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Shutdown Windows machines remotely", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "RPC Shutdown", "repository": "core", "slug": "core_rpc_shutdown", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/rpc_shutdown", "version_latest": "2.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "ERP beyond your fridge! A groceries & household management solution for your home", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Grocy", "repository": "a0d7b954", "slug": "a0d7b954_grocy", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-grocy", "version_latest": "0.21.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Network-wide ads & trackers blocking DNS server", "documentation": true, "homeassistant": "0.113.2", "icon": true, "installed": false, "logo": true, "name": "AdGuard Home", "repository": "a0d7b954", "slug": "a0d7b954_adguard", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-adguard-home", "version_latest": "5.1.2", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Expose Home Assistant folders with SMB/CIFS", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Samba share", "repository": "core", "slug": "core_samba", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/samba", "version_latest": "12.3.2", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "amd64"], "available": true, "build": false, "description": "A virtual personal assistant developed by Google", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Google Assistant SDK", "repository": "core", "slug": "core_google_assistant", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/google_assistant", "version_latest": "2.5.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armhf", "armv7", "i386"], "available": true, "build": false, "description": "Simple, elegant and feature-rich CCTV/NVR for your cameras", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "motionEye", "repository": "a0d7b954", "slug": "a0d7b954_motioneye", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-motioneye", "version_latest": "0.20.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Free and easy binary newsreader", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "SABnzbd", "repository": "a0d7b954", "slug": "a0d7b954_sabnzbd", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-sabnzbd/tree/main/README.md", "version_latest": "0.2.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "It is a self-hosted monitoring tool like \"Uptime Robot\"", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Uptime Kuma", "repository": "a0d7b954", "slug": "a0d7b954_uptime-kuma", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-uptime-kuma", "version_latest": "0.12.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Radically simplify your network with a virtual networking layer that works the same everywhere", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "ZeroTier One", "repository": "a0d7b954", "slug": "a0d7b954_zerotier", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-zerotier", "version_latest": "0.18.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Open source password management solution", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Vaultwarden (Bitwarden)", "repository": "a0d7b954", "slug": "a0d7b954_bitwarden", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-bitwarden", "version_latest": "0.22.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Manage certificate from Let's Encrypt", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Let's Encrypt", "repository": "core", "slug": "core_letsencrypt", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/letsencrypt", "version_latest": "5.1.4", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A web interface for the official MariaDB add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "phpMyAdmin", "repository": "a0d7b954", "slug": "a0d7b954_phpmyadmin", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-phpmyadmin", "version_latest": "0.9.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A cross-platform system monitoring tool", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "Glances", "repository": "a0d7b954", "slug": "a0d7b954_glances", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-glances", "version_latest": "0.21.1", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Program the manufacturer and vendor strings to a SkyConnect's CP2102N chip", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "SkyConnect CP2102N Programmer", "repository": "77f1785d", "slug": "77f1785d_skyconnect_cp2102n_programmer", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/skyconnect_cp2102n_programmer", "version_latest": "1.0.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Python Apps and Dashboard using AppDaemon 4.x for Home Assistant", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "AppDaemon", "repository": "a0d7b954", "slug": "a0d7b954_appdaemon", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-appdaemon", "version_latest": "0.16.6", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Simple browser-based file editor for Home Assistant", "documentation": true, "homeassistant": "0.91.1", "icon": true, "installed": false, "logo": true, "name": "File editor", "repository": "core", "slug": "core_configurator", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/configurator", "version_latest": "5.8.0", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": true, "description": "Development (nightly) version of Music Assistant. Do not install this add-on unless you're specifically asked or a developer! This add-on installs the latest version from the main branch directly from git at startup.", "documentation": false, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Music Assistant Server (dev)", "repository": "d5369777", "slug": "d5369777_music_assistant_dev", "stage": "experimental", "update_available": false, "url": "https://music-assistant.io", "version_latest": "1.0.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "An SSL/TLS proxy", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "NGINX Home Assistant SSL proxy", "repository": "core", "slug": "core_nginx_proxy", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/nginx_proxy", "version_latest": "3.10.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Centrally manage all your Sonoff-Tasmota devices", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "TasmoAdmin", "repository": "a0d7b954", "slug": "a0d7b954_sonweb", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-tasmoadmin", "version_latest": "0.30.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Scalable datastore for metrics, events, and real-time analytics", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "InfluxDB", "repository": "a0d7b954", "slug": "a0d7b954_influxdb", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-influxdb", "version_latest": "5.0.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Remote Debug proxy for Supervisor/ptvsd", "documentation": false, "homeassistant": null, "icon": false, "installed": true, "logo": false, "name": "Remote ptvsd debugger", "repository": "77f1785d", "slug": "77f1785d_remote_debug", "stage": "experimental", "update_available": false, "url": "https://developers.home-assistant.io/docs/supervisor/debugging", "version_latest": "1.2.0", "version": "1.2.0" }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Indexer manager/proxy built on the popular arr stack to integrate with your various PVR apps", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Prowlarr", "repository": "a0d7b954", "slug": "a0d7b954_prowlarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-prowlarr/tree/main/README.md", "version_latest": "0.11.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A local NTP (Network Time Protocol) server for cameras etc.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "chrony", "repository": "a0d7b954", "slug": "a0d7b954_chrony", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-chrony", "version_latest": "4.0.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "AirPlay capabilities for your Chromecast devices.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "AirCast", "repository": "a0d7b954", "slug": "a0d7b954_aircast", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-aircast", "version_latest": "4.2.2", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "Use Assist with local microphone", "documentation": true, "homeassistant": "2023.12.1", "icon": false, "installed": false, "logo": false, "name": "Assist Microphone", "repository": "core", "slug": "core_assist_microphone", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/blob/master/assist_microphone", "version_latest": "1.2.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "OpenThread Border Router add-on", "documentation": true, "homeassistant": "2023.6.0.dev20230531", "icon": true, "installed": false, "logo": true, "name": "OpenThread Border Router", "repository": "core", "slug": "core_openthread_border_router", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/openthread_border_router", "version_latest": "2.10.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Modern GPS Tracking Platform", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Traccar", "repository": "a0d7b954", "slug": "a0d7b954_traccar", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-traccar", "version_latest": "0.25.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "TellStick and TellStick Duo service", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "TellStick", "repository": "core", "slug": "core_tellstick", "stage": "deprecated", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/tellstick", "version_latest": "2.2.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Zero config VPN for building secure networks", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Tailscale", "repository": "a0d7b954", "slug": "a0d7b954_tailscale", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-tailscale", "version_latest": "0.21.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Free Dynamic DNS (DynDNS or DDNS) service with Let's Encrypt support", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Duck DNS", "repository": "core", "slug": "core_duckdns", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/duckdns", "version_latest": "1.18.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Remote API proxy for Home Assistant", "documentation": false, "homeassistant": null, "icon": false, "installed": true, "logo": false, "name": "Remote API proxy", "repository": "77f1785d", "slug": "77f1785d_remote_api", "stage": "experimental", "update_available": false, "url": "https://developers.home-assistant.io/docs/supervisor/development/#supervisor-api-access", "version_latest": "1.3.0", "version": "1.3.0" }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A secure and fast FTP server for Home Assistant", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "FTP", "repository": "a0d7b954", "slug": "a0d7b954_ftp", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-ftp", "version_latest": "5.1.0", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "BETA version of Music Assistant. Install this version if you want to keep up with the latest and greatest and don't mind some beta testing. Follow us on discord to report and discuss feedback!", "documentation": false, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Music Assistant Server (beta)", "repository": "d5369777", "slug": "d5369777_music_assistant_beta", "stage": "stable", "update_available": false, "url": "https://music-assistant.io", "version_latest": "2.3.0b24", "version": null }, { "advanced": false, "arch": ["amd64", "i386", "armv7", "aarch64"], "available": true, "build": false, "description": "Turn your device into a Media Player with VLC", "documentation": true, "homeassistant": "2021.3.0.dev20210216", "icon": true, "installed": false, "logo": true, "name": "VLC", "repository": "core", "slug": "core_vlc", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/vlc", "version_latest": "0.3.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Example add-on by Community Home Assistant Add-ons", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Example", "repository": "a0d7b954", "slug": "a0d7b954_example", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-example", "version_latest": "9.0.0", "version": null }, { "advanced": true, "arch": ["amd64", "armv7", "aarch64"], "available": true, "build": false, "description": "Development version of ESPHome add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "ESPHome (dev)", "repository": "5c53de3b", "slug": "5c53de3b_esphome-dev", "stage": "experimental", "update_available": false, "url": "https://next.esphome.io/", "version_latest": "2024.10.0-dev20240917", "version": null }, { "advanced": false, "arch": ["amd64", "i386", "armhf", "armv7", "aarch64"], "available": true, "build": false, "description": "Control a Z-Wave network with Home Assistant Z-Wave JS", "documentation": true, "homeassistant": "2021.2.0b0", "icon": true, "installed": false, "logo": true, "name": "Z-Wave JS", "repository": "core", "slug": "core_zwave_js", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/zwave_js", "version_latest": "0.7.2", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "Speech-to-text with Whisper", "documentation": true, "homeassistant": "2023.8.0.dev20230728", "icon": true, "installed": false, "logo": true, "name": "Whisper", "repository": "core", "slug": "core_whisper", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/blob/master/whisper", "version_latest": "2.1.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Fully featured Visual Studio Code (VSCode) experience integrated in the Home Assistant frontend.", "documentation": true, "homeassistant": null, "icon": true, "installed": true, "logo": true, "name": "Studio Code Server", "repository": "a0d7b954", "slug": "a0d7b954_vscode", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-vscode", "version_latest": "5.15.0", "version": "5.15.0" }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "A SQL database server", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "MariaDB", "repository": "core", "slug": "core_mariadb", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/mariadb", "version_latest": "2.7.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A supercharged SSH & Web Terminal access to your Home Assistant instance", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "Advanced SSH & Web Terminal", "repository": "a0d7b954", "slug": "a0d7b954_ssh", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-ssh", "version_latest": "19.0.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Download and manage subtitles for Sonarr and Radarr", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Bazarr", "repository": "a0d7b954", "slug": "a0d7b954_bazarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-bazarr/tree/main/README.md", "version_latest": "0.2.0", "version": null }, { "advanced": false, "arch": ["amd64", "armv7", "aarch64"], "available": true, "build": false, "description": "ESPHome add-on for intelligently managing all your ESP8266/ESP32 devices", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "ESPHome", "repository": "5c53de3b", "slug": "5c53de3b_esphome", "stage": "stable", "update_available": false, "url": "https://esphome.io/", "version_latest": "2024.8.3", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Expose GPIO modules and digital sensors via MQTT for remote control and monitoring.", "documentation": true, "homeassistant": null, "icon": false, "installed": false, "logo": false, "name": "MQTT IO", "repository": "a0d7b954", "slug": "a0d7b954_mqtt-io", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-mqtt-io", "version_latest": "0.4.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Manage your UniFi network using a web browser", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "UniFi Network Application", "repository": "a0d7b954", "slug": "a0d7b954_unifi", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-unifi", "version_latest": "3.2.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Smart PVR for newsgroup and bittorrent users", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Sonarr", "repository": "a0d7b954", "slug": "a0d7b954_sonarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-sonarr/tree/main/README.md", "version_latest": "0.2.4", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Testing Home Assistant latency using MQTT.", "documentation": true, "homeassistant": null, "icon": false, "installed": false, "logo": false, "name": "Real-Time latency test", "repository": "77f1785d", "slug": "77f1785d_rt_test", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/rt_test", "version_latest": "0.1.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Simple git pull to update the local configuration", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Git pull", "repository": "core", "slug": "core_git_pull", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/git_pull", "version_latest": "7.14.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Looks and smells like Sonarr but made for music", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Lidarr", "repository": "a0d7b954", "slug": "a0d7b954_lidarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-lidarr/tree/main/README.md", "version_latest": "0.5.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A self-hosted web IRC client", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "The Lounge", "repository": "a0d7b954", "slug": "a0d7b954_thelounge", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-thelounge", "version_latest": "0.19.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armhf", "armv7", "i386"], "available": true, "build": false, "description": "Silicon Labs firmware flasher add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Silicon Labs Flasher", "repository": "core", "slug": "core_silabs_flasher", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/silabs_flasher\n", "version_latest": "0.3.0", "version": null } ] } } python-supervisor-client-0.3.1/tests/fixtures/store_info.json000066400000000000000000001645261500244310200246110ustar00rootroot00000000000000{ "result": "ok", "data": { "addons": [ { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "The Music Assistant server is a free, opensource Media library manager that connects to your streaming services and a wide range of connected speakers.", "documentation": false, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Music Assistant Server", "repository": "d5369777", "slug": "d5369777_music_assistant", "stage": "stable", "update_available": false, "url": "https://music-assistant.io", "version_latest": "2.2.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "The open platform for beautiful analytics and monitoring", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Grafana", "repository": "a0d7b954", "slug": "a0d7b954_grafana", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-grafana", "version_latest": "10.0.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Scan for HDMI CEC devices", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "CEC Scanner", "repository": "core", "slug": "core_cec_scan", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/cec_scan", "version_latest": "3.0", "version": null }, { "advanced": false, "arch": ["amd64", "armhf", "aarch64"], "available": true, "build": false, "description": "Control a Zigbee network with ConBee or RaspBee by Dresden Elektronik.", "documentation": true, "homeassistant": "0.91.2", "icon": true, "installed": false, "logo": true, "name": "deCONZ", "repository": "core", "slug": "core_deconz", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/deconz", "version_latest": "7.0.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Connected Home over IP (Matter) Python Controller REPL", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "CHIP Controller REPL", "repository": "77f1785d", "slug": "77f1785d_chip_controller_repl", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/chip_controller_repl", "version_latest": "0.4.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "AirPlay capabilities for your Sonos (and UPnP) devices.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "AirSonos", "repository": "a0d7b954", "slug": "a0d7b954_airsonos", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-airsonos", "version_latest": "4.2.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Whisparr is an adult video collection manager for Usenet and BitTorrent users", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Whisparr", "repository": "a0d7b954", "slug": "a0d7b954_whisparr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-whisparr/tree/main/README.md", "version_latest": "0.2.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Allow logging in remotely to Home Assistant using SSH", "documentation": true, "homeassistant": null, "icon": true, "installed": true, "logo": true, "name": "Terminal & SSH", "repository": "core", "slug": "core_ssh", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/ssh", "version_latest": "9.14.0", "version": "9.14.0" }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "The most scalable open-source MQTT broker for IoT. An alternative for the Mosquitto add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "EMQX", "repository": "a0d7b954", "slug": "a0d7b954_emqx", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-emqx", "version_latest": "0.7.0", "version": null }, { "advanced": false, "arch": ["armv7", "aarch64", "amd64"], "available": true, "build": false, "description": "Zigbee and OpenThread multiprotocol add-on", "documentation": true, "homeassistant": "2024.1.0", "icon": true, "installed": false, "logo": true, "name": "Silicon Labs Multiprotocol", "repository": "core", "slug": "core_silabs_multiprotocol", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/silabs-multiprotocol\n", "version_latest": "2.4.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Book Manager and Automation (Sonarr for Ebooks)", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Readarr", "repository": "a0d7b954", "slug": "a0d7b954_readarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-readarr/tree/main/README.md", "version_latest": "0.2.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Manage custom Python modules in Home Assistant deps", "documentation": false, "homeassistant": "2021.7.0", "icon": false, "installed": false, "logo": true, "name": "Custom deps deployment", "repository": "77f1785d", "slug": "77f1785d_custom_deps", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development", "version_latest": "1.3.4", "version": null }, { "advanced": true, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Explore your SQLite database", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "SQLite Web", "repository": "a0d7b954", "slug": "a0d7b954_sqlite-web", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-sqlite-web/tree/main/README.md", "version_latest": "4.2.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Play Spotify music on your Home Assistant device", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Spotify Connect", "repository": "a0d7b954", "slug": "a0d7b954_spotify", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-spotify-connect", "version_latest": "0.13.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "A simple DNS server", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Dnsmasq", "repository": "core", "slug": "core_dnsmasq", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/dnsmasq", "version_latest": "1.8.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Manage battery backup (UPS) devices", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Network UPS Tools", "repository": "a0d7b954", "slug": "a0d7b954_nut", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-nut", "version_latest": "0.13.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Manage Nginx proxy hosts with a simple, powerful interface", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Nginx Proxy Manager", "repository": "a0d7b954", "slug": "a0d7b954_nginxproxymanager", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-nginx-proxy-manager", "version_latest": "1.0.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Browser-based log utility for Home Assistant", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "Log Viewer", "repository": "a0d7b954", "slug": "a0d7b954_logviewer", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-log-viewer", "version_latest": "0.17.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Protect your privacy and access Home Assistant via Tor", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Tor", "repository": "a0d7b954", "slug": "a0d7b954_tor", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-tor", "version_latest": "5.0.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Fast, modern, secure VPN tunnel", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "WireGuard", "repository": "a0d7b954", "slug": "a0d7b954_wireguard", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-wireguard", "version_latest": "0.10.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Recorded media, live TV, online news, and podcasts ready to stream.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Plex Media Server", "repository": "a0d7b954", "slug": "a0d7b954_plex", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-plex", "version_latest": "3.5.2", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "Text-to-speech with Piper", "documentation": true, "homeassistant": "2023.8.0.dev20230718", "icon": true, "installed": false, "logo": true, "name": "Piper", "repository": "core", "slug": "core_piper", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/blob/master/piper", "version_latest": "1.5.2", "version": null }, { "advanced": true, "arch": ["amd64", "armv7", "aarch64"], "available": true, "build": false, "description": "Beta version of ESPHome add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "ESPHome (beta)", "repository": "5c53de3b", "slug": "5c53de3b_esphome-beta", "stage": "experimental", "update_available": false, "url": "https://beta.esphome.io/", "version_latest": "2024.9.0b3", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": false, "build": false, "description": "Matter WebSocket Server for Home Assistant Matter support.", "documentation": true, "homeassistant": "2024.6.0", "icon": true, "installed": false, "logo": true, "name": "Matter Server", "repository": "core", "slug": "core_matter_server", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/matter_server", "version_latest": "6.5.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Movie organizer/manager for usenet and torrent users", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Radarr", "repository": "a0d7b954", "slug": "a0d7b954_radarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-radarr/tree/main/README.md", "version_latest": "0.8.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Fully configurable Z-Wave JS gateway and control panel", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Z-Wave JS UI", "repository": "a0d7b954", "slug": "a0d7b954_zwavejs2mqtt", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-zwave-js-ui", "version_latest": "3.11.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Monitoring and tracking tool for Plex Media Server", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Tautulli", "repository": "a0d7b954", "slug": "a0d7b954_tautulli", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-tautulli/tree/main/README.md", "version_latest": "4.0.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Flow-based programming for the Internet of Things", "documentation": true, "homeassistant": "2023.3.0", "icon": true, "installed": false, "logo": true, "name": "Node-RED", "repository": "a0d7b954", "slug": "a0d7b954_nodered", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-node-red", "version_latest": "18.0.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Create documents containing live code, equations, visualizations, and explanatory text", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "JupyterLab", "repository": "a0d7b954", "slug": "a0d7b954_jupyterlablite", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-jupyterlab", "version_latest": "0.15.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Request management and media discovery tool for the Plex ecosystem", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Overseerr", "repository": "a0d7b954", "slug": "a0d7b954_overseerr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-overseerr/tree/main/README.md", "version_latest": "0.1.0", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64", "armv7"], "available": true, "build": false, "description": "openWakeWord using the Wyoming protocol", "documentation": true, "homeassistant": "2023.9.0.dev20230809", "icon": false, "installed": false, "logo": false, "name": "openWakeWord", "repository": "core", "slug": "core_openwakeword", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/blob/master/openwakeword", "version_latest": "1.10.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Simple & Free Wiki Software", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Bookstack", "repository": "a0d7b954", "slug": "a0d7b954_bookstack", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-bookstack", "version_latest": "2.0.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Connected Home over IP (Matter) Tool example application.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "CHIP Tool", "repository": "77f1785d", "slug": "77f1785d_chip_tool", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/chip_tool", "version_latest": "0.4.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "A simple DHCP server", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "DHCP server", "repository": "core", "slug": "core_dhcp_server", "stage": "stable", "update_available": false, "url": "https://home-assistant.io/addons/dhcp_server/", "version_latest": "1.4.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "An Open Source MQTT broker", "documentation": true, "homeassistant": null, "icon": true, "installed": true, "logo": true, "name": "Mosquitto broker", "repository": "core", "slug": "core_mosquitto", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/mosquitto", "version_latest": "6.4.1", "version": "6.4.1" }, { "advanced": false, "arch": ["armv7", "aarch64", "amd64"], "available": true, "build": false, "description": "Zigbee NCP and OpenThread multiprotocol add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Silicon Labs Concurrent Multiprotocol", "repository": "77f1785d", "slug": "77f1785d_silabs_concurrent_multiprotocol", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/silabs-concurrent-multiprotocol", "version_latest": "0.1.5", "version": null }, { "advanced": false, "arch": ["amd64"], "available": false, "build": false, "description": "Fighting disease with a world wide distributed super computer", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Folding@home", "repository": "a0d7b954", "slug": "a0d7b954_foldingathome", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-foldingathome", "version_latest": "0.7.2", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Shutdown Windows machines remotely", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "RPC Shutdown", "repository": "core", "slug": "core_rpc_shutdown", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/rpc_shutdown", "version_latest": "2.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "ERP beyond your fridge! A groceries & household management solution for your home", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Grocy", "repository": "a0d7b954", "slug": "a0d7b954_grocy", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-grocy", "version_latest": "0.21.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Network-wide ads & trackers blocking DNS server", "documentation": true, "homeassistant": "0.113.2", "icon": true, "installed": false, "logo": true, "name": "AdGuard Home", "repository": "a0d7b954", "slug": "a0d7b954_adguard", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-adguard-home", "version_latest": "5.1.2", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Expose Home Assistant folders with SMB/CIFS", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Samba share", "repository": "core", "slug": "core_samba", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/samba", "version_latest": "12.3.2", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "amd64"], "available": true, "build": false, "description": "A virtual personal assistant developed by Google", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Google Assistant SDK", "repository": "core", "slug": "core_google_assistant", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/google_assistant", "version_latest": "2.5.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armhf", "armv7", "i386"], "available": true, "build": false, "description": "Simple, elegant and feature-rich CCTV/NVR for your cameras", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "motionEye", "repository": "a0d7b954", "slug": "a0d7b954_motioneye", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-motioneye", "version_latest": "0.20.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Free and easy binary newsreader", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "SABnzbd", "repository": "a0d7b954", "slug": "a0d7b954_sabnzbd", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-sabnzbd/tree/main/README.md", "version_latest": "0.2.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "It is a self-hosted monitoring tool like \"Uptime Robot\"", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Uptime Kuma", "repository": "a0d7b954", "slug": "a0d7b954_uptime-kuma", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-uptime-kuma", "version_latest": "0.12.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Radically simplify your network with a virtual networking layer that works the same everywhere", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "ZeroTier One", "repository": "a0d7b954", "slug": "a0d7b954_zerotier", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-zerotier", "version_latest": "0.18.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Open source password management solution", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Vaultwarden (Bitwarden)", "repository": "a0d7b954", "slug": "a0d7b954_bitwarden", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-bitwarden", "version_latest": "0.22.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Manage certificate from Let's Encrypt", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Let's Encrypt", "repository": "core", "slug": "core_letsencrypt", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/letsencrypt", "version_latest": "5.1.4", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A web interface for the official MariaDB add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "phpMyAdmin", "repository": "a0d7b954", "slug": "a0d7b954_phpmyadmin", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-phpmyadmin", "version_latest": "0.9.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A cross-platform system monitoring tool", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "Glances", "repository": "a0d7b954", "slug": "a0d7b954_glances", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-glances", "version_latest": "0.21.1", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Program the manufacturer and vendor strings to a SkyConnect's CP2102N chip", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "SkyConnect CP2102N Programmer", "repository": "77f1785d", "slug": "77f1785d_skyconnect_cp2102n_programmer", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/skyconnect_cp2102n_programmer", "version_latest": "1.0.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Python Apps and Dashboard using AppDaemon 4.x for Home Assistant", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "AppDaemon", "repository": "a0d7b954", "slug": "a0d7b954_appdaemon", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-appdaemon", "version_latest": "0.16.6", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Simple browser-based file editor for Home Assistant", "documentation": true, "homeassistant": "0.91.1", "icon": true, "installed": false, "logo": true, "name": "File editor", "repository": "core", "slug": "core_configurator", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/configurator", "version_latest": "5.8.0", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": true, "description": "Development (nightly) version of Music Assistant. Do not install this add-on unless you're specifically asked or a developer! This add-on installs the latest version from the main branch directly from git at startup.", "documentation": false, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Music Assistant Server (dev)", "repository": "d5369777", "slug": "d5369777_music_assistant_dev", "stage": "experimental", "update_available": false, "url": "https://music-assistant.io", "version_latest": "1.0.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "An SSL/TLS proxy", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "NGINX Home Assistant SSL proxy", "repository": "core", "slug": "core_nginx_proxy", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/nginx_proxy", "version_latest": "3.10.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Centrally manage all your Sonoff-Tasmota devices", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "TasmoAdmin", "repository": "a0d7b954", "slug": "a0d7b954_sonweb", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-tasmoadmin", "version_latest": "0.30.5", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Scalable datastore for metrics, events, and real-time analytics", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "InfluxDB", "repository": "a0d7b954", "slug": "a0d7b954_influxdb", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-influxdb", "version_latest": "5.0.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Remote Debug proxy for Supervisor/ptvsd", "documentation": false, "homeassistant": null, "icon": false, "installed": true, "logo": false, "name": "Remote ptvsd debugger", "repository": "77f1785d", "slug": "77f1785d_remote_debug", "stage": "experimental", "update_available": false, "url": "https://developers.home-assistant.io/docs/supervisor/debugging", "version_latest": "1.2.0", "version": "1.2.0" }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Indexer manager/proxy built on the popular arr stack to integrate with your various PVR apps", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Prowlarr", "repository": "a0d7b954", "slug": "a0d7b954_prowlarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-prowlarr/tree/main/README.md", "version_latest": "0.11.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A local NTP (Network Time Protocol) server for cameras etc.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "chrony", "repository": "a0d7b954", "slug": "a0d7b954_chrony", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-chrony", "version_latest": "4.0.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "AirPlay capabilities for your Chromecast devices.", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "AirCast", "repository": "a0d7b954", "slug": "a0d7b954_aircast", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-aircast", "version_latest": "4.2.2", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "Use Assist with local microphone", "documentation": true, "homeassistant": "2023.12.1", "icon": false, "installed": false, "logo": false, "name": "Assist Microphone", "repository": "core", "slug": "core_assist_microphone", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/blob/master/assist_microphone", "version_latest": "1.2.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "OpenThread Border Router add-on", "documentation": true, "homeassistant": "2023.6.0.dev20230531", "icon": true, "installed": false, "logo": true, "name": "OpenThread Border Router", "repository": "core", "slug": "core_openthread_border_router", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/openthread_border_router", "version_latest": "2.10.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Modern GPS Tracking Platform", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Traccar", "repository": "a0d7b954", "slug": "a0d7b954_traccar", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-traccar", "version_latest": "0.25.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "TellStick and TellStick Duo service", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "TellStick", "repository": "core", "slug": "core_tellstick", "stage": "deprecated", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/tellstick", "version_latest": "2.2.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Zero config VPN for building secure networks", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Tailscale", "repository": "a0d7b954", "slug": "a0d7b954_tailscale", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-tailscale", "version_latest": "0.21.0", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Free Dynamic DNS (DynDNS or DDNS) service with Let's Encrypt support", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Duck DNS", "repository": "core", "slug": "core_duckdns", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/duckdns", "version_latest": "1.18.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Remote API proxy for Home Assistant", "documentation": false, "homeassistant": null, "icon": false, "installed": true, "logo": false, "name": "Remote API proxy", "repository": "77f1785d", "slug": "77f1785d_remote_api", "stage": "experimental", "update_available": false, "url": "https://developers.home-assistant.io/docs/supervisor/development/#supervisor-api-access", "version_latest": "1.3.0", "version": "1.3.0" }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A secure and fast FTP server for Home Assistant", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "FTP", "repository": "a0d7b954", "slug": "a0d7b954_ftp", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-ftp", "version_latest": "5.1.0", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "BETA version of Music Assistant. Install this version if you want to keep up with the latest and greatest and don't mind some beta testing. Follow us on discord to report and discuss feedback!", "documentation": false, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Music Assistant Server (beta)", "repository": "d5369777", "slug": "d5369777_music_assistant_beta", "stage": "stable", "update_available": false, "url": "https://music-assistant.io", "version_latest": "2.3.0b24", "version": null }, { "advanced": false, "arch": ["amd64", "i386", "armv7", "aarch64"], "available": true, "build": false, "description": "Turn your device into a Media Player with VLC", "documentation": true, "homeassistant": "2021.3.0.dev20210216", "icon": true, "installed": false, "logo": true, "name": "VLC", "repository": "core", "slug": "core_vlc", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/vlc", "version_latest": "0.3.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Example add-on by Community Home Assistant Add-ons", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Example", "repository": "a0d7b954", "slug": "a0d7b954_example", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-example", "version_latest": "9.0.0", "version": null }, { "advanced": true, "arch": ["amd64", "armv7", "aarch64"], "available": true, "build": false, "description": "Development version of ESPHome add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "ESPHome (dev)", "repository": "5c53de3b", "slug": "5c53de3b_esphome-dev", "stage": "experimental", "update_available": false, "url": "https://next.esphome.io/", "version_latest": "2024.10.0-dev20240917", "version": null }, { "advanced": false, "arch": ["amd64", "i386", "armhf", "armv7", "aarch64"], "available": true, "build": false, "description": "Control a Z-Wave network with Home Assistant Z-Wave JS", "documentation": true, "homeassistant": "2021.2.0b0", "icon": true, "installed": false, "logo": true, "name": "Z-Wave JS", "repository": "core", "slug": "core_zwave_js", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/zwave_js", "version_latest": "0.7.2", "version": null }, { "advanced": false, "arch": ["amd64", "aarch64"], "available": true, "build": false, "description": "Speech-to-text with Whisper", "documentation": true, "homeassistant": "2023.8.0.dev20230728", "icon": true, "installed": false, "logo": true, "name": "Whisper", "repository": "core", "slug": "core_whisper", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/blob/master/whisper", "version_latest": "2.1.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Fully featured Visual Studio Code (VSCode) experience integrated in the Home Assistant frontend.", "documentation": true, "homeassistant": null, "icon": true, "installed": true, "logo": true, "name": "Studio Code Server", "repository": "a0d7b954", "slug": "a0d7b954_vscode", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-vscode", "version_latest": "5.15.0", "version": "5.15.0" }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "A SQL database server", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "MariaDB", "repository": "core", "slug": "core_mariadb", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/mariadb", "version_latest": "2.7.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A supercharged SSH & Web Terminal access to your Home Assistant instance", "documentation": true, "homeassistant": "0.92.0b2", "icon": true, "installed": false, "logo": true, "name": "Advanced SSH & Web Terminal", "repository": "a0d7b954", "slug": "a0d7b954_ssh", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-ssh", "version_latest": "19.0.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Download and manage subtitles for Sonarr and Radarr", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Bazarr", "repository": "a0d7b954", "slug": "a0d7b954_bazarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-bazarr/tree/main/README.md", "version_latest": "0.2.0", "version": null }, { "advanced": false, "arch": ["amd64", "armv7", "aarch64"], "available": true, "build": false, "description": "ESPHome add-on for intelligently managing all your ESP8266/ESP32 devices", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "ESPHome", "repository": "5c53de3b", "slug": "5c53de3b_esphome", "stage": "stable", "update_available": false, "url": "https://esphome.io/", "version_latest": "2024.8.3", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Expose GPIO modules and digital sensors via MQTT for remote control and monitoring.", "documentation": true, "homeassistant": null, "icon": false, "installed": false, "logo": false, "name": "MQTT IO", "repository": "a0d7b954", "slug": "a0d7b954_mqtt-io", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-mqtt-io", "version_latest": "0.4.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Manage your UniFi network using a web browser", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "UniFi Network Application", "repository": "a0d7b954", "slug": "a0d7b954_unifi", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-unifi", "version_latest": "3.2.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64"], "available": true, "build": false, "description": "Smart PVR for newsgroup and bittorrent users", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Sonarr", "repository": "a0d7b954", "slug": "a0d7b954_sonarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-sonarr/tree/main/README.md", "version_latest": "0.2.4", "version": null }, { "advanced": false, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Testing Home Assistant latency using MQTT.", "documentation": true, "homeassistant": null, "icon": false, "installed": false, "logo": false, "name": "Real-Time latency test", "repository": "77f1785d", "slug": "77f1785d_rt_test", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons-development/tree/master/rt_test", "version_latest": "0.1.0", "version": null }, { "advanced": true, "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], "available": true, "build": false, "description": "Simple git pull to update the local configuration", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Git pull", "repository": "core", "slug": "core_git_pull", "stage": "stable", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/git_pull", "version_latest": "7.14.1", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "Looks and smells like Sonarr but made for music", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Lidarr", "repository": "a0d7b954", "slug": "a0d7b954_lidarr", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-lidarr/tree/main/README.md", "version_latest": "0.5.0", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armv7"], "available": true, "build": false, "description": "A self-hosted web IRC client", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "The Lounge", "repository": "a0d7b954", "slug": "a0d7b954_thelounge", "stage": "stable", "update_available": false, "url": "https://github.com/hassio-addons/addon-thelounge", "version_latest": "0.19.2", "version": null }, { "advanced": false, "arch": ["aarch64", "amd64", "armhf", "armv7", "i386"], "available": true, "build": false, "description": "Silicon Labs firmware flasher add-on", "documentation": true, "homeassistant": null, "icon": true, "installed": false, "logo": true, "name": "Silicon Labs Flasher", "repository": "core", "slug": "core_silabs_flasher", "stage": "experimental", "update_available": false, "url": "https://github.com/home-assistant/addons/tree/master/silabs_flasher\n", "version_latest": "0.3.0", "version": null } ], "repositories": [ { "slug": "local", "name": "Local add-ons", "source": "local", "url": "https://home-assistant.io/hassio", "maintainer": "you" }, { "slug": "77f1785d", "name": "Home Assistant Developer Add-ons", "source": "https://github.com/home-assistant/addons-development", "url": "https://developers.home-assistant.io/", "maintainer": "Home Assistant Team " }, { "slug": "d5369777", "name": "Music Assistant", "source": "https://github.com/music-assistant/home-assistant-addon", "url": "https://github.com/music-assistant/core", "maintainer": "Music Assistant " }, { "slug": "core", "name": "Official add-ons", "source": "core", "url": "https://home-assistant.io/addons", "maintainer": "Home Assistant" }, { "slug": "5c53de3b", "name": "ESPHome", "source": "https://github.com/esphome/home-assistant-addon", "url": "https://esphome.io", "maintainer": "ESPHome " }, { "slug": "a0d7b954", "name": "Home Assistant Community Add-ons", "source": "https://github.com/hassio-addons/repository", "url": "https://addons.community", "maintainer": "Franck Nijhof " } ] } } python-supervisor-client-0.3.1/tests/fixtures/store_repositories_list.json000066400000000000000000000026131500244310200274240ustar00rootroot00000000000000{ "result": "ok", "data": [ { "slug": "local", "name": "Local add-ons", "source": "local", "url": "https://home-assistant.io/hassio", "maintainer": "you" }, { "slug": "77f1785d", "name": "Home Assistant Developer Add-ons", "source": "https://github.com/home-assistant/addons-development", "url": "https://developers.home-assistant.io/", "maintainer": "Home Assistant Team " }, { "slug": "d5369777", "name": "Music Assistant", "source": "https://github.com/music-assistant/home-assistant-addon", "url": "https://github.com/music-assistant/core", "maintainer": "Music Assistant " }, { "slug": "core", "name": "Official add-ons", "source": "core", "url": "https://home-assistant.io/addons", "maintainer": "Home Assistant" }, { "slug": "5c53de3b", "name": "ESPHome", "source": "https://github.com/esphome/home-assistant-addon", "url": "https://esphome.io", "maintainer": "ESPHome " }, { "slug": "a0d7b954", "name": "Home Assistant Community Add-ons", "source": "https://github.com/hassio-addons/repository", "url": "https://addons.community", "maintainer": "Franck Nijhof " } ] } python-supervisor-client-0.3.1/tests/fixtures/store_repository_info.json000066400000000000000000000003021500244310200270650ustar00rootroot00000000000000{ "result": "ok", "data": { "slug": "core", "name": "Official add-ons", "source": "core", "url": "https://home-assistant.io/addons", "maintainer": "Home Assistant" } } python-supervisor-client-0.3.1/tests/fixtures/supervisor_info.json000066400000000000000000000024441500244310200256640ustar00rootroot00000000000000{ "result": "ok", "data": { "version": "2024.09.1", "version_latest": "2024.09.1", "update_available": true, "channel": "stable", "arch": "aarch64", "supported": true, "healthy": true, "ip_address": "172.30.32.2", "timezone": "America/New_York", "logging": "info", "debug": true, "debug_block": false, "diagnostics": false, "auto_update": true, "country": null, "wait_boot": 5, "addons": [ { "name": "Terminal & SSH", "slug": "core_ssh", "version": "9.14.0", "version_latest": "9.14.0", "update_available": false, "state": "started", "repository": "core", "icon": true }, { "name": "Mosquitto broker", "slug": "core_mosquitto", "version": "6.4.1", "version_latest": "6.4.1", "update_available": false, "state": "started", "repository": "core", "icon": true } ], "addons_repositories": [ { "name": "Local add-ons", "slug": "local" }, { "name": "Music Assistant", "slug": "d5369777" }, { "name": "Official add-ons", "slug": "core" }, { "name": "ESPHome", "slug": "5c53de3b" }, { "name": "Home Assistant Community Add-ons", "slug": "a0d7b954" } ] } } python-supervisor-client-0.3.1/tests/fixtures/supervisor_stats.json000066400000000000000000000003641500244310200260660ustar00rootroot00000000000000{ "result": "ok", "data": { "cpu_percent": 0.04, "memory_usage": 243982336, "memory_limit": 3899138048, "memory_percent": 6.26, "network_rx": 176623, "network_tx": 114204, "blk_read": 0, "blk_write": 0 } } python-supervisor-client-0.3.1/tests/ruff.toml000066400000000000000000000006211500244310200215160ustar00rootroot00000000000000# This extend our general Ruff rules specifically for tests extend = "../pyproject.toml" lint.extend-select = [ "PT", # Use @pytest.fixture without parentheses ] lint.extend-ignore = [ "PLR2004", # Magic values are fine in tests "RUF018", # Tests are never run with asserts disabled "S101", # Use of assert detected "SLF001", # Tests will access private/protected members ] python-supervisor-client-0.3.1/tests/test_addons.py000066400000000000000000000207051500244310200225450ustar00rootroot00000000000000"""Test addons supervisor client.""" from ipaddress import IPv4Address from aioresponses import aioresponses from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import ( AddonBoot, AddonsOptions, AddonsSecurityOptions, AddonStage, AddonStartup, AddonState, AddonsUninstall, Capability, InstalledAddonComplete, StoreAddonComplete, SupervisorRole, ) from aiohasupervisor.models.base import Response from . import load_fixture from .const import SUPERVISOR_URL async def test_addons_list( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test addons list API.""" responses.get( f"{SUPERVISOR_URL}/addons", status=200, body=load_fixture("addons_list.json") ) addons = await supervisor_client.addons.list() assert addons[0].name == "Terminal & SSH" assert addons[0].slug == "core_ssh" assert addons[0].icon is True assert addons[0].logo is True assert addons[0].state == AddonState.STARTED assert addons[0].stage == AddonStage.STABLE assert addons[1].slug == "a0d7b954_vscode" async def test_addons_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test addons info API.""" responses.get( f"{SUPERVISOR_URL}/addons/core_ssh/info", status=200, body=load_fixture("addons_info.json"), ) addon = await supervisor_client.addons.addon_info("core_ssh") assert addon.name == "Terminal & SSH" assert addon.slug == "core_ssh" assert addon.documentation is True assert addon.changelog is True assert addon.watchdog is False assert addon.auto_update is False assert addon.ip_address == IPv4Address("172.30.33.0") assert Capability.NET_RAW in addon.privileged assert "not_real" in addon.privileged assert addon.supervisor_api is True assert addon.supervisor_role == SupervisorRole.MANAGER assert addon.system_managed is False assert addon.system_managed_config_entry is None assert addon.startup == AddonStartup.SERVICES async def test_addons_uninstall( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test uninstall addon API.""" responses.post(f"{SUPERVISOR_URL}/addons/core_ssh/uninstall", status=200) assert ( await supervisor_client.addons.uninstall_addon( "core_ssh", AddonsUninstall(remove_config=True) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/addons/core_ssh/uninstall")) } async def test_addons_start( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test start addon API.""" responses.post(f"{SUPERVISOR_URL}/addons/core_ssh/start", status=200) assert await supervisor_client.addons.start_addon("core_ssh") is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/addons/core_ssh/start")) } async def test_addons_stop( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test stop addon API.""" responses.post(f"{SUPERVISOR_URL}/addons/core_ssh/stop", status=200) assert await supervisor_client.addons.stop_addon("core_ssh") is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/addons/core_ssh/stop")) } async def test_addons_restart( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test restart addon API.""" responses.post(f"{SUPERVISOR_URL}/addons/core_ssh/restart", status=200) assert await supervisor_client.addons.restart_addon("core_ssh") is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/addons/core_ssh/restart")) } async def test_addons_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test addon options API.""" responses.post(f"{SUPERVISOR_URL}/addons/core_ssh/options", status=200) assert ( await supervisor_client.addons.set_addon_options( "core_ssh", AddonsOptions( config=None, boot=AddonBoot.AUTO, network={"22/tcp": 22, "1234/tcp": None}, watchdog=True, ), ) is None ) assert len(responses.requests) == 1 assert ( request := responses.requests[ ("POST", URL(f"{SUPERVISOR_URL}/addons/core_ssh/options")) ] ) assert request[0].kwargs["json"] == { "options": None, "boot": "auto", "network": {"22/tcp": 22, "1234/tcp": None}, "watchdog": True, } async def test_addons_config_validate( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test config validate API.""" responses.post( f"{SUPERVISOR_URL}/addons/core_ssh/options/validate", status=200, body=load_fixture("addons_config_validate.json"), ) validate = await supervisor_client.addons.addon_config_validate( "core_ssh", {"bad": "config"} ) assert validate.message == ( "Missing option 'server' in root in Terminal & SSH" " (core_ssh). Got {'bad': 'config'}" ) assert validate.valid is False assert validate.pwned is False async def test_addons_config( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test get addon config API.""" responses.get( f"{SUPERVISOR_URL}/addons/core_ssh/options/config", status=200, body=load_fixture("addons_options_config.json"), ) config = await supervisor_client.addons.addon_config("core_ssh") assert config["authorized_keys"] == [] assert config["password"] == "" async def test_addons_rebuild( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test rebuild addon API.""" responses.post(f"{SUPERVISOR_URL}/addons/local_example/rebuild", status=200) assert await supervisor_client.addons.rebuild_addon("local_example") is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/addons/local_example/rebuild")) } async def test_addons_stdin( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test addon stdin API.""" responses.post(f"{SUPERVISOR_URL}/addons/core_ssh/stdin", status=200) assert ( await supervisor_client.addons.write_addon_stdin("core_ssh", b"hello world") is None ) assert len(responses.requests) == 1 assert ( request := responses.requests[ ("POST", URL(f"{SUPERVISOR_URL}/addons/core_ssh/stdin")) ] ) assert request[0].kwargs["data"] == b"hello world" async def test_addons_security( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test addon security API.""" responses.post(f"{SUPERVISOR_URL}/addons/core_ssh/security", status=200) assert ( await supervisor_client.addons.set_addon_security( "core_ssh", AddonsSecurityOptions(protected=True) ) is None ) assert len(responses.requests) == 1 assert ( request := responses.requests[ ("POST", URL(f"{SUPERVISOR_URL}/addons/core_ssh/security")) ] ) assert request[0].kwargs["json"] == {"protected": True} async def test_addons_stats( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test addon stats API.""" responses.get( f"{SUPERVISOR_URL}/addons/core_ssh/stats", status=200, body=load_fixture("addon_stats.json"), ) stats = await supervisor_client.addons.addon_stats("core_ssh") assert stats.cpu_percent == 0 assert stats.memory_usage == 24588288 assert stats.network_rx == 1717120021 async def test_addons_serialize_by_alias() -> None: """Test serializing addons by alias.""" response = Response.from_json(load_fixture("store_addon_info.json")) store_addon_info = StoreAddonComplete.from_dict(response.data) assert store_addon_info.supervisor_api is False assert (store_addon_info.to_dict())["supervisor_api"] is False assert (store_addon_info.to_dict(by_alias=True))["hassio_api"] is False response = Response.from_json(load_fixture("addons_info.json")) addon_info = InstalledAddonComplete.from_dict(response.data) assert addon_info.supervisor_api is True assert (addon_info.to_dict())["supervisor_api"] is True assert (addon_info.to_dict(by_alias=True))["hassio_api"] is True python-supervisor-client-0.3.1/tests/test_backups.py000066400000000000000000000470541500244310200227330ustar00rootroot00000000000000"""Test backups supervisor client.""" import asyncio from collections.abc import AsyncIterator from datetime import UTC, datetime from pathlib import PurePath from typing import Any from aioresponses import CallbackResult, aioresponses import pytest from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import ( AddonSet, BackupLocationAttributes, BackupsOptions, DownloadBackupOptions, Folder, FreezeOptions, FullBackupOptions, FullRestoreOptions, PartialBackupOptions, PartialRestoreOptions, RemoveBackupOptions, UploadBackupOptions, ) from . import load_fixture from .const import SUPERVISOR_URL async def test_backups_list( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backups list API.""" responses.get( f"{SUPERVISOR_URL}/backups", status=200, body=load_fixture("backups_list.json") ) backups = await supervisor_client.backups.list() assert backups[0].slug == "58bc7491" assert backups[0].type == "full" assert backups[0].date == datetime(2024, 4, 6, 7, 5, 40, 0, UTC) assert backups[0].compressed is True assert backups[0].content.homeassistant is True assert backups[0].content.folders == ["share", "addons/local", "ssl", "media"] assert backups[1].slug == "69558789" assert backups[1].type == "partial" async def test_backups_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backups info API.""" responses.get( f"{SUPERVISOR_URL}/backups/info", status=200, body=load_fixture("backups_info.json"), ) info = await supervisor_client.backups.info() assert info.backups[0].slug == "58bc7491" assert info.backups[1].slug == "69558789" assert info.days_until_stale == 30 async def test_backups_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backups options API.""" responses.post(f"{SUPERVISOR_URL}/backups/options", status=200) assert ( await supervisor_client.backups.set_options(BackupsOptions(days_until_stale=10)) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/backups/options")) } async def test_backups_reload( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backups reload API.""" responses.post(f"{SUPERVISOR_URL}/backups/reload", status=200) assert await supervisor_client.backups.reload() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/backups/reload")) } @pytest.mark.parametrize("options", [None, FreezeOptions(timeout=1000)]) async def test_backups_freeze( responses: aioresponses, supervisor_client: SupervisorClient, options: FreezeOptions | None, ) -> None: """Test backups freeze API.""" responses.post(f"{SUPERVISOR_URL}/backups/freeze", status=200) assert await supervisor_client.backups.freeze(options) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/backups/freeze")) } async def test_backups_thaw( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backups thaw API.""" responses.post(f"{SUPERVISOR_URL}/backups/thaw", status=200) assert await supervisor_client.backups.thaw() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/backups/thaw")) } async def test_partial_backup_options() -> None: """Test partial backup options.""" assert PartialBackupOptions(name="good", addons={"a"}) assert PartialBackupOptions(name="good", folders={Folder.SSL}) assert PartialBackupOptions(name="good", homeassistant=True) with pytest.raises( ValueError, match="At least one of addons, folders, or homeassistant must have a value", ): PartialBackupOptions(name="bad") async def test_partial_restore_options() -> None: """Test partial restore options.""" assert PartialRestoreOptions(addons={"a"}) assert PartialRestoreOptions(folders={Folder.SSL}) assert PartialRestoreOptions(homeassistant=True) with pytest.raises( ValueError, match="At least one of addons, folders, or homeassistant must have a value", ): PartialRestoreOptions(background=True) async def test_backup_options_location() -> None: """Test location field in backup options.""" assert FullBackupOptions(location=["test", None]).to_dict() == { "location": ["test", None] } assert FullBackupOptions(location="test").to_dict() == {"location": "test"} assert FullBackupOptions().to_dict() == {} assert PartialBackupOptions( location=["test", ".local"], folders={Folder.SSL} ).to_dict() == { "location": ["test", ".local"], "folders": ["ssl"], } assert PartialBackupOptions(location="test", folders={Folder.SSL}).to_dict() == { "location": "test", "folders": ["ssl"], } assert PartialBackupOptions(folders={Folder.SSL}).to_dict() == {"folders": ["ssl"]} def backup_callback(url: str, **kwargs: dict[str, Any]) -> CallbackResult: # noqa: ARG001 """Return response based on whether backup was in background or not.""" if kwargs["json"] and kwargs["json"].get("background"): fixture = "backup_background.json" else: fixture = "backup_foreground.json" return CallbackResult(status=200, body=load_fixture(fixture)) @pytest.mark.parametrize( ("options", "slug"), [ (FullBackupOptions(name="Test", background=True), None), (FullBackupOptions(name="Test", background=False), "9ecf0028"), (FullBackupOptions(name="Test", background=False, location="test"), "9ecf0028"), ( FullBackupOptions( name="Test", background=False, location={".local", "test"} ), "9ecf0028", ), ( FullBackupOptions( name="Test", background=False, extra={"user": "test", "scheduled": True} ), "9ecf0028", ), (FullBackupOptions(name="Test", background=False, extra=None), "9ecf0028"), ( FullBackupOptions( name="test", background=False, filename=PurePath("backup.tar") ), "9ecf0028", ), (None, "9ecf0028"), ], ) async def test_backups_full_backup( responses: aioresponses, supervisor_client: SupervisorClient, options: FullBackupOptions | None, slug: str | None, ) -> None: """Test backups full backup API.""" responses.post( f"{SUPERVISOR_URL}/backups/new/full", callback=backup_callback, ) result = await supervisor_client.backups.full_backup(options) assert result.job_id.hex == "dc9dbc16f6ad4de592ffa72c807ca2bf" assert result.slug == slug @pytest.mark.parametrize( ("options", "slug"), [ (PartialBackupOptions(name="Test", background=True, addons={"core_ssh"}), None), ( PartialBackupOptions(name="Test", background=False, addons={"core_ssh"}), "9ecf0028", ), ( PartialBackupOptions( name="Test", background=False, location="test", addons={"core_ssh"} ), "9ecf0028", ), ( PartialBackupOptions( name="Test", background=False, location={".local", "test"}, addons={"core_ssh"}, ), "9ecf0028", ), ( PartialBackupOptions( name="Test", background=False, addons={"core_ssh"}, extra={"user": "test", "scheduled": True}, ), "9ecf0028", ), ( PartialBackupOptions( name="Test", background=False, addons={"core_ssh"}, extra=None ), "9ecf0028", ), ( PartialBackupOptions( name="Test", background=False, addons={"core_ssh"}, filename=PurePath("backup.tar"), ), "9ecf0028", ), ( PartialBackupOptions(name="Test", background=None, addons={"core_ssh"}), "9ecf0028", ), ( PartialBackupOptions(name="Test", background=None, addons=AddonSet.ALL), "9ecf0028", ), ], ) async def test_backups_partial_backup( responses: aioresponses, supervisor_client: SupervisorClient, options: PartialBackupOptions, slug: str | None, ) -> None: """Test backups full backup API.""" responses.post( f"{SUPERVISOR_URL}/backups/new/partial", callback=backup_callback, ) result = await supervisor_client.backups.partial_backup(options) assert result.job_id.hex == "dc9dbc16f6ad4de592ffa72c807ca2bf" assert result.slug == slug async def test_backup_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backup info API.""" responses.get( f"{SUPERVISOR_URL}/backups/69558789/info", status=200, body=load_fixture("backup_info.json"), ) result = await supervisor_client.backups.backup_info("69558789") assert result.slug == "69558789" assert result.type == "partial" assert result.date == datetime(2024, 5, 31, 0, 0, 0, 0, UTC) assert result.compressed is True assert result.addons[0].slug == "core_mosquitto" assert result.addons[0].name == "Mosquitto broker" assert result.addons[0].version == "6.4.0" assert result.addons[0].size == 0 assert result.repositories == [ "core", "local", "https://github.com/music-assistant/home-assistant-addon", "https://github.com/esphome/home-assistant-addon", "https://github.com/hassio-addons/repository", ] assert result.folders == [] assert result.homeassistant_exclude_database is None assert result.extra is None assert result.location_attributes[".local"].protected is False assert result.location_attributes[".local"].size_bytes == 10123 async def test_backup_info_no_homeassistant( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backup info API with no home assistant.""" responses.get( f"{SUPERVISOR_URL}/backups/d13dedd0/info", status=200, body=load_fixture("backup_info_no_homeassistant.json"), ) result = await supervisor_client.backups.backup_info("d13dedd0") assert result.slug == "d13dedd0" assert result.type == "partial" assert result.homeassistant is None async def test_backup_info_with_extra( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backup info API with extras set by client.""" responses.get( f"{SUPERVISOR_URL}/backups/d13dedd0/info", status=200, body=load_fixture("backup_info_with_extra.json"), ) result = await supervisor_client.backups.backup_info("d13dedd0") assert result.slug == "69558789" assert result.type == "partial" assert result.extra == {"user": "test", "scheduled": True} async def test_backup_info_with_multiple_locations( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test backup info API with multiple locations.""" responses.get( f"{SUPERVISOR_URL}/backups/d13dedd0/info", status=200, body=load_fixture("backup_info_with_locations.json"), ) result = await supervisor_client.backups.backup_info("d13dedd0") assert result.slug == "69558789" assert result.type == "partial" assert result.location_attributes[".local"].protected is False assert result.location_attributes[".local"].size_bytes == 10123 assert result.location_attributes["Test"].protected is False assert result.location_attributes["Test"].size_bytes == 10123 @pytest.mark.parametrize( "options", [None, RemoveBackupOptions(location={"test", None})] ) async def test_remove_backup( responses: aioresponses, supervisor_client: SupervisorClient, options: RemoveBackupOptions | None, ) -> None: """Test remove backup API.""" responses.delete(f"{SUPERVISOR_URL}/backups/abc123", status=200) assert await supervisor_client.backups.remove_backup("abc123", options) is None assert responses.requests.keys() == { ("DELETE", URL(f"{SUPERVISOR_URL}/backups/abc123")) } @pytest.mark.parametrize( "options", [ None, FullRestoreOptions(password="abc123"), # noqa: S106 FullRestoreOptions(background=True), FullRestoreOptions(location="test"), ], ) async def test_full_restore( responses: aioresponses, supervisor_client: SupervisorClient, options: FullRestoreOptions | None, ) -> None: """Test full restore API.""" responses.post( f"{SUPERVISOR_URL}/backups/abc123/restore/full", status=200, body=load_fixture("backup_restore.json"), ) result = await supervisor_client.backups.full_restore("abc123", options) assert result.job_id.hex == "dc9dbc16f6ad4de592ffa72c807ca2bf" @pytest.mark.parametrize( "options", [ PartialRestoreOptions(addons={"core_ssh"}), PartialRestoreOptions(homeassistant=True, location=".local"), PartialRestoreOptions(folders={Folder.SHARE, Folder.SSL}, location="test"), PartialRestoreOptions(addons={"core_ssh"}, background=True), PartialRestoreOptions(addons={"core_ssh"}, password="abc123"), # noqa: S106 ], ) async def test_partial_restore( responses: aioresponses, supervisor_client: SupervisorClient, options: PartialRestoreOptions, ) -> None: """Test partial restore API.""" responses.post( f"{SUPERVISOR_URL}/backups/abc123/restore/partial", status=200, body=load_fixture("backup_restore.json"), ) result = await supervisor_client.backups.partial_restore("abc123", options) assert result.job_id.hex == "dc9dbc16f6ad4de592ffa72c807ca2bf" @pytest.mark.parametrize( ("options", "query"), [ (None, ""), ( UploadBackupOptions(location={".local", "test"}), "?location=.local&location=test", ), (UploadBackupOptions(filename=PurePath("backup.tar")), "?filename=backup.tar"), ], ) async def test_upload_backup( responses: aioresponses, supervisor_client: SupervisorClient, options: UploadBackupOptions | None, query: str, ) -> None: """Test upload backup API.""" responses.post( f"{SUPERVISOR_URL}/backups/new/upload{query}", status=200, body=load_fixture("backup_uploaded.json"), ) data = asyncio.StreamReader(loop=asyncio.get_running_loop()) data.feed_data(b"backup test") data.feed_eof() result = await supervisor_client.backups.upload_backup(data, options) assert result == "7fed74c8" @pytest.mark.parametrize( ("options", "query"), [ (None, ""), (DownloadBackupOptions(location="test"), "?location=test"), (DownloadBackupOptions(location=None), "?location="), ], ) async def test_download_backup( responses: aioresponses, supervisor_client: SupervisorClient, options: DownloadBackupOptions | None, query: str, ) -> None: """Test download backup API.""" responses.get( f"{SUPERVISOR_URL}/backups/7fed74c8/download{query}", status=200, body=b"backup test", ) result = await supervisor_client.backups.download_backup("7fed74c8", options) assert isinstance(result, AsyncIterator) async for chunk in result: assert chunk == b"backup test" @pytest.mark.parametrize( ("options", "as_dict"), [ ( PartialBackupOptions(name="Test", folders={Folder.SHARE}), {"name": "Test", "folders": ["share"]}, ), (PartialBackupOptions(addons={"core_ssh"}), {"addons": ["core_ssh"]}), (PartialBackupOptions(addons=AddonSet.ALL), {"addons": "ALL"}), ( PartialBackupOptions( homeassistant=True, homeassistant_exclude_database=True ), {"homeassistant": True, "homeassistant_exclude_database": True}, ), ( PartialBackupOptions( folders={Folder.SSL}, compressed=True, background=True ), {"folders": ["ssl"], "compressed": True, "background": True}, ), ( PartialBackupOptions( homeassistant=True, location=[".cloud_backup", "test"] ), {"homeassistant": True, "location": [".cloud_backup", "test"]}, ), ( PartialBackupOptions(homeassistant=True, location="test"), {"homeassistant": True, "location": "test"}, ), ( PartialBackupOptions(homeassistant=True, filename=PurePath("backup.tar")), {"homeassistant": True, "filename": "backup.tar"}, ), ], ) async def test_partial_backup_model( options: PartialBackupOptions, as_dict: dict[str, Any] ) -> None: """Test partial backup model parsing and serializing.""" assert PartialBackupOptions.from_dict(as_dict) == options assert options.to_dict() == as_dict @pytest.mark.parametrize( ("options", "as_dict"), [ (FullBackupOptions(name="Test"), {"name": "Test"}), (FullBackupOptions(password="test"), {"password": "test"}), # noqa: S106 (FullBackupOptions(compressed=True), {"compressed": True}), ( FullBackupOptions(homeassistant_exclude_database=True), {"homeassistant_exclude_database": True}, ), (FullBackupOptions(background=True), {"background": True}), ( FullBackupOptions(location=[".cloud_backup", "test"]), {"location": [".cloud_backup", "test"]}, ), (FullBackupOptions(location="test"), {"location": "test"}), ( FullBackupOptions(filename=PurePath("backup.tar")), {"filename": "backup.tar"}, ), ], ) async def test_full_backup_model( options: FullBackupOptions, as_dict: dict[str, Any] ) -> None: """Test full backup model parsing and serializing.""" assert FullBackupOptions.from_dict(as_dict) == options assert options.to_dict() == as_dict async def test_backups_list_location_attributes( responses: aioresponses, supervisor_client: SupervisorClient, ) -> None: """Test location attributes field in backups list.""" responses.get( f"{SUPERVISOR_URL}/backups", status=200, body=load_fixture("backups_list_location_attributes.json"), ) result = await supervisor_client.backups.list() assert result[0].location_attributes == { ".local": BackupLocationAttributes( protected=True, size_bytes=10240, ), "test": BackupLocationAttributes( protected=True, size_bytes=10240, ), } async def test_backup_info_location_attributes( responses: aioresponses, supervisor_client: SupervisorClient, ) -> None: """Test location attributes field in backup info.""" responses.get( f"{SUPERVISOR_URL}/backups/d9c48f8b/info", status=200, body=load_fixture("backup_info_location_attributes.json"), ) result = await supervisor_client.backups.backup_info("d9c48f8b") assert result.location_attributes == { ".local": BackupLocationAttributes( protected=True, size_bytes=10240, ), "test": BackupLocationAttributes( protected=True, size_bytes=10240, ), } python-supervisor-client-0.3.1/tests/test_client.py000066400000000000000000000014251500244310200225510ustar00rootroot00000000000000"""Tests for client.""" import pytest from aiohasupervisor.client import _SupervisorClient from aiohasupervisor.exceptions import SupervisorError from .const import SUPERVISOR_URL @pytest.mark.parametrize("method", ["get", "post", "put", "delete"]) async def test_path_manipulation_blocked(method: str) -> None: """Test path manipulation prevented.""" client = _SupervisorClient(SUPERVISOR_URL, "abc123", 10) action = getattr(client, method) with pytest.raises(SupervisorError): # absolute path await action("/test/../bad") with pytest.raises(SupervisorError): # relative path await action("test/../bad") with pytest.raises(SupervisorError): # relative path with percent encoding await action("test/%2E%2E/bad") python-supervisor-client-0.3.1/tests/test_discovery.py000066400000000000000000000045171500244310200233070ustar00rootroot00000000000000"""Test discovery supervisor client.""" from uuid import UUID from aioresponses import aioresponses from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import DiscoveryConfig from . import load_fixture from .const import SUPERVISOR_URL async def test_discovery_list( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Discovery list API.""" responses.get( f"{SUPERVISOR_URL}/discovery", status=200, body=load_fixture("discovery_list.json"), ) disc_list = await supervisor_client.discovery.list() assert disc_list[0].addon == "core_mosquitto" assert disc_list[0].service == "mqtt" assert disc_list[0].uuid.hex == "889ca604cff84004894e53d181655b3a" assert disc_list[0].config["host"] == "core-mosquitto" async def test_get_discovery( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Discovery get API.""" uuid = UUID("889ca604cff84004894e53d181655b3a") responses.get( f"{SUPERVISOR_URL}/discovery/{uuid.hex}", status=200, body=load_fixture("discovery_get.json"), ) discovery = await supervisor_client.discovery.get(uuid) assert discovery.addon == "core_mosquitto" assert discovery.service == "mqtt" assert discovery.uuid == uuid assert discovery.config["host"] == "core-mosquitto" assert discovery.config["port"] == 1883 assert discovery.config["ssl"] is False async def test_delete_discovery( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Discovery delete API.""" uuid = UUID("889ca604cff84004894e53d181655b3a") responses.delete(f"{SUPERVISOR_URL}/discovery/{uuid.hex}", status=200) assert await supervisor_client.discovery.delete(uuid) is None assert responses.requests.keys() == { ("DELETE", URL(f"{SUPERVISOR_URL}/discovery/{uuid.hex}")) } async def test_set_discovery( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Discovery set API.""" responses.post( f"{SUPERVISOR_URL}/discovery", status=200, body=load_fixture("discovery_set.json"), ) assert await supervisor_client.discovery.set( DiscoveryConfig(service="mqtt", config={}) ) == UUID("889ca604cff84004894e53d181655b3a") python-supervisor-client-0.3.1/tests/test_homeassistant.py000066400000000000000000000115551500244310200241620ustar00rootroot00000000000000"""Test Home Assistant supervisor client.""" from ipaddress import IPv4Address from aioresponses import aioresponses import pytest from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import ( HomeAssistantOptions, HomeAssistantRebuildOptions, HomeAssistantRestartOptions, HomeAssistantStopOptions, HomeAssistantUpdateOptions, ) from . import load_fixture from .const import SUPERVISOR_URL async def test_homeassistant_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Home Assistant info API.""" responses.get( f"{SUPERVISOR_URL}/core/info", status=200, body=load_fixture("homeassistant_info.json"), ) info = await supervisor_client.homeassistant.info() assert info.version == "2024.9.0" assert info.update_available is False assert info.arch == "aarch64" assert info.ssl is False assert info.port == 8123 assert info.audio_input is None assert info.audio_output is None assert info.ip_address == IPv4Address("172.30.32.1") async def test_homeassistant_stats( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Home Assistant stats API.""" responses.get( f"{SUPERVISOR_URL}/core/stats", status=200, body=load_fixture("homeassistant_stats.json"), ) stats = await supervisor_client.homeassistant.stats() assert stats.cpu_percent == 0.01 assert stats.memory_usage == 678883328 assert stats.memory_percent == 17.41 async def test_homeassistant_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Home Assistant options API.""" responses.post(f"{SUPERVISOR_URL}/core/options", status=200) assert ( await supervisor_client.homeassistant.set_options( HomeAssistantOptions(watchdog=False, backups_exclude_database=True) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/core/options")) } @pytest.mark.parametrize("options", [None, HomeAssistantUpdateOptions(backup=False)]) async def test_homeassistant_update( responses: aioresponses, supervisor_client: SupervisorClient, options: HomeAssistantUpdateOptions | None, ) -> None: """Test Home Assistant update API.""" responses.post(f"{SUPERVISOR_URL}/core/update", status=200) assert await supervisor_client.homeassistant.update(options) is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/core/update"))} @pytest.mark.parametrize("options", [None, HomeAssistantRestartOptions(safe_mode=True)]) async def test_homeassistant_restart( responses: aioresponses, supervisor_client: SupervisorClient, options: HomeAssistantRestartOptions | None, ) -> None: """Test Home Assistant restart API.""" responses.post(f"{SUPERVISOR_URL}/core/restart", status=200) assert await supervisor_client.homeassistant.restart(options) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/core/restart")) } @pytest.mark.parametrize("options", [None, HomeAssistantStopOptions(force=True)]) async def test_homeassistant_stop( responses: aioresponses, supervisor_client: SupervisorClient, options: HomeAssistantStopOptions | None, ) -> None: """Test Home Assistant stop API.""" responses.post(f"{SUPERVISOR_URL}/core/stop", status=200) assert await supervisor_client.homeassistant.stop(options) is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/core/stop"))} async def test_homeassistant_start( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Home Assistant start API.""" responses.post(f"{SUPERVISOR_URL}/core/start", status=200) assert await supervisor_client.homeassistant.start() is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/core/start"))} async def test_homeassistant_check_config( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test Home Assistant check config API.""" responses.post(f"{SUPERVISOR_URL}/core/check", status=200) assert await supervisor_client.homeassistant.check_config() is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/core/check"))} @pytest.mark.parametrize("options", [None, HomeAssistantRebuildOptions(safe_mode=True)]) async def test_homeassistant_rebuild( responses: aioresponses, supervisor_client: SupervisorClient, options: HomeAssistantRebuildOptions | None, ) -> None: """Test Home Assistant rebuild API.""" responses.post(f"{SUPERVISOR_URL}/core/rebuild", status=200) assert await supervisor_client.homeassistant.rebuild(options) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/core/rebuild")) } python-supervisor-client-0.3.1/tests/test_host.py000066400000000000000000000071621500244310200222540ustar00rootroot00000000000000"""Test host supervisor client.""" from datetime import UTC, datetime from aioresponses import aioresponses import pytest from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import HostOptions, RebootOptions, ShutdownOptions from . import load_fixture from .const import SUPERVISOR_URL async def test_host_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test host info API.""" responses.get( f"{SUPERVISOR_URL}/host/info", status=200, body=load_fixture("host_info.json") ) result = await supervisor_client.host.info() assert result.agent_version == "1.6.0" assert result.chassis == "embedded" assert result.virtualization == "" assert result.disk_total == 27.9 assert result.disk_life_time == 10 assert result.features == [ "reboot", "shutdown", "services", "network", "hostname", "timedate", "os_agent", "haos", "resolved", "journal", "disk", "mount", ] assert result.hostname == "homeassistant" assert result.llmnr_hostname == "homeassistant3" assert result.dt_utc == datetime(2024, 10, 3, 0, 0, 0, 0, UTC) assert result.dt_synchronized is True assert result.startup_time == 1.966311 @pytest.mark.parametrize("options", [None, RebootOptions(force=True)]) async def test_host_reboot( responses: aioresponses, supervisor_client: SupervisorClient, options: RebootOptions | None, ) -> None: """Test host reboot API.""" responses.post(f"{SUPERVISOR_URL}/host/reboot", status=200) assert await supervisor_client.host.reboot(options) is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/host/reboot"))} @pytest.mark.parametrize("options", [None, ShutdownOptions(force=True)]) async def test_host_shutdown( responses: aioresponses, supervisor_client: SupervisorClient, options: ShutdownOptions | None, ) -> None: """Test host shutdown API.""" responses.post(f"{SUPERVISOR_URL}/host/shutdown", status=200) assert await supervisor_client.host.shutdown(options) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/host/shutdown")) } async def test_host_reload( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test host reload API.""" responses.post(f"{SUPERVISOR_URL}/host/reload", status=200) assert await supervisor_client.host.reload() is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/host/reload"))} async def test_host_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test host options API.""" responses.post(f"{SUPERVISOR_URL}/host/options", status=200) assert ( await supervisor_client.host.set_options(HostOptions(hostname="test")) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/host/options")) } async def test_host_services( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test host services API.""" responses.get( f"{SUPERVISOR_URL}/host/services", status=200, body=load_fixture("host_services.json"), ) result = await supervisor_client.host.services() assert result[0].name == "emergency.service" assert result[0].description == "Emergency Shell" assert result[0].state == "inactive" assert result[-1].name == "systemd-resolved.service" assert result[-1].description == "Network Name Resolution" assert result[-1].state == "active" python-supervisor-client-0.3.1/tests/test_jobs.py000066400000000000000000000077461500244310200222440ustar00rootroot00000000000000"""Test jobs supervisor client.""" from datetime import datetime from uuid import UUID from aioresponses import aioresponses from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import JobCondition, JobsOptions from . import load_fixture from .const import SUPERVISOR_URL async def test_jobs_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test jobs info API.""" responses.get( f"{SUPERVISOR_URL}/jobs/info", status=200, body=load_fixture("jobs_info.json") ) info = await supervisor_client.jobs.info() assert info.ignore_conditions == [JobCondition.FREE_SPACE] assert info.jobs[0].name == "backup_manager_partial_backup" assert info.jobs[0].reference == "89cafa67" assert info.jobs[0].uuid.hex == "2febe59311f94d6ba36f6f9f73357ca8" assert info.jobs[0].progress == 0 assert info.jobs[0].stage == "finishing_file" assert info.jobs[0].done is True assert info.jobs[0].errors == [] assert info.jobs[0].created == datetime.fromisoformat( "2025-01-30T20:55:12.859349+00:00" ) assert info.jobs[0].child_jobs[0].name == "backup_store_folders" assert info.jobs[0].child_jobs[0].child_jobs[0].name == "backup_folder_save" assert info.jobs[0].child_jobs[0].child_jobs[0].reference == "ssl" assert info.jobs[0].child_jobs[0].child_jobs[0].child_jobs == [] assert info.jobs[1].name == "backup_manager_partial_restore" assert info.jobs[1].reference == "cfddca18" assert info.jobs[1].errors[0].type == "BackupInvalidError" assert info.jobs[1].errors[0].message == "Invalid password for backup cfddca18" async def test_jobs_set_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test jobs set options API.""" responses.post(f"{SUPERVISOR_URL}/jobs/options", status=200) assert ( await supervisor_client.jobs.set_options( JobsOptions(ignore_conditions=[JobCondition.FREE_SPACE]) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/jobs/options")) } async def test_jobs_reset( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test jobs reset API.""" responses.post(f"{SUPERVISOR_URL}/jobs/reset", status=200) assert await supervisor_client.jobs.reset() is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/jobs/reset"))} async def test_jobs_get_job( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test jobs get job API.""" responses.get( f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8", status=200, body=load_fixture("jobs_get_job.json"), ) info = await supervisor_client.jobs.get_job( UUID("2febe59311f94d6ba36f6f9f73357ca8") ) assert info.name == "backup_manager_partial_backup" assert info.reference == "89cafa67" assert info.uuid.hex == "2febe59311f94d6ba36f6f9f73357ca8" assert info.progress == 0 assert info.stage == "finishing_file" assert info.done is True assert info.errors == [] assert info.created == datetime.fromisoformat("2025-01-30T20:55:12.859349+00:00") assert info.child_jobs[0].name == "backup_store_folders" assert info.child_jobs[0].child_jobs[0].name == "backup_folder_save" assert info.child_jobs[0].child_jobs[0].reference == "ssl" assert info.child_jobs[0].child_jobs[0].child_jobs == [] async def test_jobs_delete_job( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test jobs delete job API.""" responses.delete( f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8", status=200 ) assert ( await supervisor_client.jobs.delete_job( UUID("2febe59311f94d6ba36f6f9f73357ca8") ) is None ) assert responses.requests.keys() == { ("DELETE", URL(f"{SUPERVISOR_URL}/jobs/2febe59311f94d6ba36f6f9f73357ca8")) } python-supervisor-client-0.3.1/tests/test_mounts.py000066400000000000000000000124641500244310200226250ustar00rootroot00000000000000"""Test mounts supervisor client.""" from pathlib import PurePath from aioresponses import aioresponses import pytest from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import ( CIFSMountRequest, MountCifsVersion, MountsOptions, MountUsage, NFSMountRequest, ) from . import load_fixture from .const import SUPERVISOR_URL async def test_mounts_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test mounts info API.""" responses.get( f"{SUPERVISOR_URL}/mounts", status=200, body=load_fixture("mounts_info.json") ) info = await supervisor_client.mounts.info() assert info.default_backup_mount == "Test" assert info.mounts[0].name == "Test" assert info.mounts[0].server == "test.local" assert info.mounts[0].type == "cifs" assert info.mounts[0].share == "backup" assert info.mounts[0].usage == "backup" assert info.mounts[0].read_only is False assert info.mounts[0].version is None assert info.mounts[0].state == "active" assert info.mounts[0].user_path is None assert info.mounts[1].usage == "share" assert info.mounts[1].read_only is True assert info.mounts[1].version == "2.0" assert info.mounts[1].port == 12345 assert info.mounts[1].user_path == PurePath("/share/Test2") assert info.mounts[2].type == "nfs" assert info.mounts[2].usage == "media" assert info.mounts[2].path.as_posix() == "media" assert info.mounts[2].user_path == PurePath("/media/Test3") @pytest.mark.parametrize("mount_name", ["test", None]) async def test_mounts_options( responses: aioresponses, supervisor_client: SupervisorClient, mount_name: str | None ) -> None: """Test mounts options API.""" responses.post(f"{SUPERVISOR_URL}/mounts/options", status=200) assert ( await supervisor_client.mounts.options( MountsOptions(default_backup_mount=mount_name) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/mounts/options")) } @pytest.mark.parametrize( "mount_config", [ CIFSMountRequest( server="test.local", share="backup", usage=MountUsage.BACKUP, ), CIFSMountRequest( server="test.local", share="media", port=12345, usage=MountUsage.MEDIA, version=MountCifsVersion.LEGACY_2_0, read_only=True, username="test", password="test", # noqa: S106 ), NFSMountRequest( server="test.local", path=PurePath("share"), usage=MountUsage.SHARE, ), NFSMountRequest( server="test.local", path=PurePath("backups"), port=12345, read_only=False, usage=MountUsage.BACKUP, ), ], ) async def test_create_mount( responses: aioresponses, supervisor_client: SupervisorClient, mount_config: CIFSMountRequest | NFSMountRequest, ) -> None: """Test create mount API.""" responses.post(f"{SUPERVISOR_URL}/mounts", status=200) assert await supervisor_client.mounts.create_mount("test", mount_config) is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/mounts"))} @pytest.mark.parametrize( "mount_config", [ CIFSMountRequest( server="test.local", share="backup", usage=MountUsage.BACKUP, ), CIFSMountRequest( server="test.local", share="media", port=12345, usage=MountUsage.MEDIA, version=MountCifsVersion.LEGACY_2_0, read_only=True, username="test", password="test", # noqa: S106 ), NFSMountRequest( server="test.local", path=PurePath("share"), usage=MountUsage.SHARE, ), NFSMountRequest( server="test.local", path=PurePath("backups"), port=12345, read_only=False, usage=MountUsage.BACKUP, ), ], ) async def test_update_mount( responses: aioresponses, supervisor_client: SupervisorClient, mount_config: CIFSMountRequest | NFSMountRequest, ) -> None: """Test update mount API.""" responses.put(f"{SUPERVISOR_URL}/mounts/test", status=200) assert await supervisor_client.mounts.update_mount("test", mount_config) is None assert responses.requests.keys() == {("PUT", URL(f"{SUPERVISOR_URL}/mounts/test"))} async def test_delete_mount( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test delete mount API.""" responses.delete(f"{SUPERVISOR_URL}/mounts/test", status=200) assert await supervisor_client.mounts.delete_mount("test") is None assert responses.requests.keys() == { ("DELETE", URL(f"{SUPERVISOR_URL}/mounts/test")) } async def test_reload_mount( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test reload mount API.""" responses.post(f"{SUPERVISOR_URL}/mounts/test/reload", status=200) assert await supervisor_client.mounts.reload_mount("test") is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/mounts/test/reload")) } python-supervisor-client-0.3.1/tests/test_network.py000066400000000000000000000132231500244310200227630ustar00rootroot00000000000000"""Test network supervisor client.""" from ipaddress import IPv4Address, IPv4Interface from aioresponses import aioresponses import pytest from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import ( InterfaceMethod, IPv4Config, NetworkInterfaceConfig, VlanConfig, ) from . import load_fixture from .const import SUPERVISOR_URL async def test_network_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test network info API.""" responses.get( f"{SUPERVISOR_URL}/network/info", status=200, body=load_fixture("network_info.json"), ) result = await supervisor_client.network.info() assert result.interfaces[0].interface == "end0" assert result.interfaces[0].type == "ethernet" assert result.interfaces[0].enabled is True assert result.interfaces[0].mac == "00:11:22:33:44:55" assert result.interfaces[0].ipv4.method == "static" assert result.interfaces[0].ipv4.address[0].with_prefixlen == "192.168.1.2/24" assert result.interfaces[0].ipv4.nameservers[0].compressed == "192.168.1.1" assert result.interfaces[0].ipv4.gateway.compressed == "192.168.1.1" assert result.interfaces[0].ipv4.ready is True assert result.interfaces[0].ipv6.method == "disabled" assert ( result.interfaces[0].ipv6.address[0].with_prefixlen == "fe80::819d:c479:d712:7a77/64" ) assert result.interfaces[0].ipv6.gateway is None assert result.interfaces[0].wifi is None assert result.interfaces[0].vlan is None assert result.docker.interface == "hassio" assert result.docker.address.compressed == "172.30.32.0/23" assert result.docker.gateway.compressed == "172.30.32.1" assert result.docker.dns.compressed == "172.30.32.3" assert result.host_internet is True assert result.supervisor_internet is True async def test_network_reload( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test network reload API.""" responses.post(f"{SUPERVISOR_URL}/network/reload", status=200) assert await supervisor_client.network.reload() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/network/reload")) } async def test_network_interface_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test network interface info API.""" responses.get( f"{SUPERVISOR_URL}/network/interface/end0/info", status=200, body=load_fixture("network_interface_info.json"), ) result = await supervisor_client.network.interface_info("end0") assert result.interface == "end0" assert result.type == "ethernet" assert result.enabled is True assert result.mac == "00:11:22:33:44:55" assert result.ipv4.method == "static" assert result.ipv4.address[0].with_prefixlen == "192.168.1.2/24" assert result.ipv4.nameservers[0].compressed == "192.168.1.1" assert result.ipv4.gateway.compressed == "192.168.1.1" assert result.ipv4.ready is True assert result.ipv6.method == "disabled" assert result.ipv6.address[0].with_prefixlen == "fe80::819d:c479:d712:7a77/64" assert result.ipv6.gateway is None assert result.wifi is None assert result.vlan is None async def test_network_update_interface( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test network interface update API.""" responses.post(f"{SUPERVISOR_URL}/network/interface/end0/update", status=200) config = NetworkInterfaceConfig( ipv4=IPv4Config( method=InterfaceMethod.STATIC, address=[IPv4Interface("192.168.1.2/24")], gateway=IPv4Address("192.168.1.1"), nameservers=[IPv4Address("192.168.1.1")], ) ) assert ( await supervisor_client.network.update_interface("end0", config=config) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/network/interface/end0/update")) } async def test_network_access_points( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test network access points API.""" responses.get( f"{SUPERVISOR_URL}/network/interface/end0/accesspoints", status=200, body=load_fixture("network_access_points.json"), ) result = await supervisor_client.network.access_points("end0") assert result[0].mode == "infrastructure" assert result[0].ssid == "UPC4814466" assert result[0].frequency == 2462 assert result[0].signal == 47 assert result[0].mac == "AA:BB:CC:DD:EE:FF" assert result[1].ssid == "VQ@35(55720" @pytest.mark.parametrize( "config", [None, NetworkInterfaceConfig(ipv4=IPv4Config(method=InterfaceMethod.AUTO))], ) async def test_network_save_vlan( responses: aioresponses, supervisor_client: SupervisorClient, config: NetworkInterfaceConfig | None, ) -> None: """Test network save vlan API.""" responses.post(f"{SUPERVISOR_URL}/network/interface/end0/vlan/1", status=200) assert await supervisor_client.network.save_vlan("end0", 1, config=config) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/network/interface/end0/vlan/1")) } async def test_network_configs_cannot_be_empty() -> None: """Test network config instances require at least one field specified.""" # Network interface config for update calls with pytest.raises(ValueError, match="At least one field must have a value"): NetworkInterfaceConfig() # Vlan config for save vlan calls with pytest.raises(ValueError, match="At least one field must have a value"): VlanConfig() python-supervisor-client-0.3.1/tests/test_os.py000066400000000000000000000145321500244310200217170ustar00rootroot00000000000000"""Test OS supervisor client.""" from pathlib import PurePath from aioresponses import aioresponses import pytest from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import ( BootSlotName, GreenOptions, MigrateDataOptions, OSUpdate, SetBootSlotOptions, YellowOptions, ) from aiohasupervisor.models.os import SwapOptions from . import load_fixture from .const import SUPERVISOR_URL async def test_os_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS info API.""" responses.get( f"{SUPERVISOR_URL}/os/info", status=200, body=load_fixture("os_info.json"), ) info = await supervisor_client.os.info() assert info.version == "13.0" assert info.version_latest == "13.1" assert info.update_available is True assert info.boot_slots["A"].state == "inactive" assert info.boot_slots["B"].state == "booted" assert info.boot_slots["B"].status == "good" assert info.boot_slots["B"].version == "13.0" @pytest.mark.parametrize("options", [None, OSUpdate(version="13.0")]) async def test_os_update( responses: aioresponses, supervisor_client: SupervisorClient, options: OSUpdate | None, ) -> None: """Test OS update API.""" responses.post(f"{SUPERVISOR_URL}/os/update", status=200) assert await supervisor_client.os.update(options) is None assert responses.requests.keys() == {("POST", URL(f"{SUPERVISOR_URL}/os/update"))} async def test_os_swap_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS config swap API.""" responses.get( f"{SUPERVISOR_URL}/os/config/swap", status=200, body=load_fixture("os_config_swap.json"), ) info = await supervisor_client.os.swap_info() assert info.swap_size == "1G" assert info.swappiness == 1 async def test_os_set_swap_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS set swap options API.""" responses.post(f"{SUPERVISOR_URL}/os/config/swap", status=200) assert ( await supervisor_client.os.set_swap_options( SwapOptions(swap_size="1G", swappiness=20) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/os/config/swap")) } async def test_os_config_sync( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS config sync API.""" responses.post(f"{SUPERVISOR_URL}/os/config/sync", status=200) assert await supervisor_client.os.config_sync() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/os/config/sync")) } async def test_os_migrate_data( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS migrate data API.""" responses.post(f"{SUPERVISOR_URL}/os/datadisk/move", status=200) assert ( await supervisor_client.os.migrate_data(MigrateDataOptions(device="/dev/test")) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/os/datadisk/move")) } async def test_os_list_data_disks( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS datadisk list API.""" responses.get( f"{SUPERVISOR_URL}/os/datadisk/list", status=200, body=load_fixture("os_datadisk_list.json"), ) datadisks = await supervisor_client.os.list_data_disks() assert datadisks[0].vendor == "SSK" assert datadisks[0].model == "SSK Storage" assert datadisks[0].serial == "DF123" assert datadisks[0].name == "SSK SSK Storage (DF123)" assert datadisks[0].dev_path == PurePath("/dev/sda") async def test_os_wipe_data( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS wipe data API.""" responses.post(f"{SUPERVISOR_URL}/os/datadisk/wipe", status=200) assert await supervisor_client.os.wipe_data() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/os/datadisk/wipe")) } async def test_os_set_boot_slot( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS set boot slot API.""" responses.post(f"{SUPERVISOR_URL}/os/boot-slot", status=200) assert ( await supervisor_client.os.set_boot_slot( SetBootSlotOptions(boot_slot=BootSlotName.B) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/os/boot-slot")) } async def test_os_green_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS green board info API.""" responses.get( f"{SUPERVISOR_URL}/os/boards/green", status=200, body=load_fixture("os_green_info.json"), ) info = await supervisor_client.os.green_info() assert info.activity_led is True assert info.power_led is True assert info.system_health_led is True async def test_os_green_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS green board options API.""" responses.post(f"{SUPERVISOR_URL}/os/boards/green", status=200) assert ( await supervisor_client.os.set_green_options(GreenOptions(activity_led=False)) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/os/boards/green")) } async def test_os_yellow_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS yellow board info API.""" responses.get( f"{SUPERVISOR_URL}/os/boards/yellow", status=200, body=load_fixture("os_yellow_info.json"), ) info = await supervisor_client.os.yellow_info() assert info.disk_led is True assert info.heartbeat_led is True assert info.power_led is True async def test_os_yellow_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test OS yellow board options API.""" responses.post(f"{SUPERVISOR_URL}/os/boards/yellow", status=200) assert ( await supervisor_client.os.set_yellow_options( YellowOptions(heartbeat_led=False) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/os/boards/yellow")) } python-supervisor-client-0.3.1/tests/test_resolution.py000066400000000000000000000111671500244310200235020ustar00rootroot00000000000000"""Test resolution center supervisor client.""" from uuid import uuid4 from aioresponses import aioresponses from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import CheckOptions from . import load_fixture from .const import SUPERVISOR_URL async def test_resolution_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test resolution center info API.""" responses.get( f"{SUPERVISOR_URL}/resolution/info", status=200, body=load_fixture("resolution_info.json"), ) info = await supervisor_client.resolution.info() assert info.checks[4].enabled is True assert info.checks[4].slug == "backups" assert info.issues[0].context == "system" assert info.issues[0].type == "no_current_backup" assert info.issues[0].reference is None assert info.issues[0].uuid.hex == "7f0eac2b61c9456dab6970507a276c36" assert info.suggestions[0].auto is False assert info.suggestions[0].context == "system" assert info.suggestions[0].type == "create_full_backup" assert info.suggestions[0].reference is None assert info.suggestions[0].uuid.hex == "f87d3556f02c4004a47111c072c76fac" assert info.unhealthy == ["supervisor"] assert info.unsupported == [] async def test_resolution_check_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test resolution center check options API.""" responses.post(f"{SUPERVISOR_URL}/resolution/check/backups/options", status=200) assert ( await supervisor_client.resolution.check_options( "backups", CheckOptions(enabled=False) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/resolution/check/backups/options")) } async def test_resolution_run_check( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test resolution center run check API.""" responses.post(f"{SUPERVISOR_URL}/resolution/check/backups/run", status=200) assert await supervisor_client.resolution.run_check("backups") is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/resolution/check/backups/run")) } async def test_resolution_apply_suggestion( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test resolution center apply suggestion API.""" uuid = uuid4() responses.post(f"{SUPERVISOR_URL}/resolution/suggestion/{uuid.hex}", status=200) assert await supervisor_client.resolution.apply_suggestion(uuid) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/resolution/suggestion/{uuid.hex}")) } async def test_resolution_dismiss_suggestion( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test resolution center dismiss suggestion API.""" uuid = uuid4() responses.delete(f"{SUPERVISOR_URL}/resolution/suggestion/{uuid.hex}", status=200) assert await supervisor_client.resolution.dismiss_suggestion(uuid) is None assert responses.requests.keys() == { ("DELETE", URL(f"{SUPERVISOR_URL}/resolution/suggestion/{uuid.hex}")) } async def test_resolution_dismiss_issue( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test resolution center dismiss issue API.""" uuid = uuid4() responses.delete(f"{SUPERVISOR_URL}/resolution/issue/{uuid.hex}", status=200) assert await supervisor_client.resolution.dismiss_issue(uuid) is None assert responses.requests.keys() == { ("DELETE", URL(f"{SUPERVISOR_URL}/resolution/issue/{uuid.hex}")) } async def test_resolution_suggestions_for_issue( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test resolution center suggestions for issue API.""" uuid = uuid4() responses.get( f"{SUPERVISOR_URL}/resolution/issue/{uuid.hex}/suggestions", status=200, body=load_fixture("resolution_suggestions_for_issue.json"), ) result = await supervisor_client.resolution.suggestions_for_issue(uuid) assert result[0].auto is False assert result[0].context == "system" assert result[0].type == "create_full_backup" async def test_resolution_healthcheck( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test resolution center healthcheck API.""" responses.post(f"{SUPERVISOR_URL}/resolution/healthcheck", status=200) assert await supervisor_client.resolution.healthcheck() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/resolution/healthcheck")) } python-supervisor-client-0.3.1/tests/test_root.py000066400000000000000000000112741500244310200222610ustar00rootroot00000000000000"""Test root services on supervisor client.""" from json import dumps from aiohttp import ClientSession from aioresponses import aioresponses import pytest from yarl import URL from aiohasupervisor import ( SupervisorAuthenticationError, SupervisorBadRequestError, SupervisorClient, SupervisorError, SupervisorForbiddenError, SupervisorNotFoundError, SupervisorServiceUnavailableError, ) from aiohasupervisor.models import HostFeature, SupervisorState, UpdateType from . import load_fixture from .const import SUPERVISOR_URL async def test_using_own_session(responses: aioresponses) -> None: """Test passing in an existing session.""" responses.get( f"{SUPERVISOR_URL}/info", status=200, body=load_fixture("root_info.json") ) async with ClientSession() as session: client = SupervisorClient(SUPERVISOR_URL, "abc123", session=session) info = await client.info() assert info assert client._client.session == session assert not session.closed await client.close() assert not session.closed async def test_using_new_session(responses: aioresponses) -> None: """Test letting client create new session.""" responses.get( f"{SUPERVISOR_URL}/info", status=200, body=load_fixture("root_info.json") ) async with SupervisorClient(SUPERVISOR_URL, "abc123") as client: info = await client.info() assert info assert client._client.session assert not client._client.session.closed assert client._client.session.closed async def test_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test info API.""" responses.get( f"{SUPERVISOR_URL}/info", status=200, body=load_fixture("root_info.json") ) info = await supervisor_client.info() assert info.supervisor == "2024.07.1.dev3001" assert info.homeassistant == "2024.6.0.dev202405280218" assert info.hassos == "12.4.dev20240527" assert info.hostname == "homeassistant" assert info.arch == "aarch64" assert info.supported is True assert info.state == SupervisorState.RUNNING assert HostFeature.REBOOT in info.features assert "not_real" in info.features async def test_available_updates( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test available updates API.""" responses.get( f"{SUPERVISOR_URL}/available_updates", status=200, body=load_fixture("root_available_updates.json"), ) updates = await supervisor_client.available_updates() assert updates[0].update_type == UpdateType.CORE assert updates[0].panel_path == "/update-available/core" assert updates[0].version_latest == "2024.9.0.dev202408010224" assert updates[0].name is None assert updates[0].icon is None assert updates[1].update_type == UpdateType.OS async def test_reload_updates( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test reload updates API.""" responses.post(f"{SUPERVISOR_URL}/reload_updates", status=200) assert await supervisor_client.reload_updates() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/reload_updates")) } async def test_refresh_updates( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test refresh updates API.""" responses.post(f"{SUPERVISOR_URL}/refresh_updates", status=200) assert await supervisor_client.refresh_updates() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/refresh_updates")) } @pytest.mark.parametrize( ("status", "message", "expected_exc"), [ (400, "Bad input", SupervisorBadRequestError), (401, None, SupervisorAuthenticationError), (403, "Not allowed", SupervisorForbiddenError), (404, "Not found", SupervisorNotFoundError), (503, "DB migration in progress", SupervisorServiceUnavailableError), (500, "Unknown error", SupervisorError), ], ) async def test_error_handling( responses: aioresponses, supervisor_client: SupervisorClient, status: int, message: str | None, expected_exc: type[SupervisorError], ) -> None: """Test error handling scenarios.""" if message is not None: kwargs = { "body": dumps( { "result": "error", "message": message, } ) } else: kwargs = {"content_type": "text/plain"} responses.post(f"{SUPERVISOR_URL}/refresh_updates", status=status, **kwargs) with pytest.raises(expected_exc, match=message): await supervisor_client.refresh_updates() python-supervisor-client-0.3.1/tests/test_store.py000066400000000000000000000140371500244310200224320ustar00rootroot00000000000000"""Tests for store supervisor client.""" from aioresponses import aioresponses from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import StoreAddonUpdate, StoreAddRepository from . import load_fixture from .const import SUPERVISOR_URL async def test_store_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store info API.""" responses.get( f"{SUPERVISOR_URL}/store", status=200, body=load_fixture("store_info.json") ) store = await supervisor_client.store.info() assert len(store.repositories) == 6 assert store.repositories[0].slug == "local" assert store.repositories[0].name == "Local add-ons" assert len(store.addons) == 88 assert store.addons[0].slug == "d5369777_music_assistant" assert store.addons[0].name == "Music Assistant Server" async def test_store_addons_list( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store addons list API.""" responses.get( f"{SUPERVISOR_URL}/store/addons", status=200, body=load_fixture("store_addons_list.json"), ) addons = await supervisor_client.store.addons_list() assert len(addons) == 88 assert addons[0].slug == "d5369777_music_assistant" assert addons[0].name == "Music Assistant Server" async def test_store_repositories_list( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store repositories list API.""" responses.get( f"{SUPERVISOR_URL}/store/repositories", status=200, body=load_fixture("store_repositories_list.json"), ) repositories = await supervisor_client.store.repositories_list() assert len(repositories) == 6 assert repositories[0].slug == "local" assert repositories[0].name == "Local add-ons" async def test_store_addon_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store addon info API.""" responses.get( f"{SUPERVISOR_URL}/store/addons/core_mosquitto", status=200, body=load_fixture("store_addon_info.json"), ) info = await supervisor_client.store.addon_info("core_mosquitto") assert info.name == "Mosquitto broker" assert info.slug == "core_mosquitto" assert info.signed is True assert info.supervisor_api is False assert info.supervisor_role == "default" async def test_store_addon_changelog( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store addon info changelog API.""" responses.get( f"{SUPERVISOR_URL}/store/addons/core_mosquitto/changelog", status=200, body=load_fixture("store_addon_changelog.txt"), ) changelog = await supervisor_client.store.addon_changelog("core_mosquitto") assert changelog.startswith("# Changelog") async def test_store_addon_documentation( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store addon documentation API.""" responses.get( f"{SUPERVISOR_URL}/store/addons/core_mosquitto/documentation", status=200, body=load_fixture("store_addon_documentation.txt"), ) documentation = await supervisor_client.store.addon_documentation("core_mosquitto") assert documentation.startswith("# Home Assistant Add-on: Mosquitto broker") async def test_store_addon_install( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store addon install API.""" responses.post(f"{SUPERVISOR_URL}/store/addons/core_mosquitto/install", status=200) assert (await supervisor_client.store.install_addon("core_mosquitto")) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/store/addons/core_mosquitto/install")) } async def test_store_addon_update( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store addon update API.""" responses.post(f"{SUPERVISOR_URL}/store/addons/core_mosquitto/update", status=200) assert ( await supervisor_client.store.update_addon( "core_mosquitto", StoreAddonUpdate(backup=True) ) ) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/store/addons/core_mosquitto/update")) } async def test_store_reload( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store reload API.""" responses.post(f"{SUPERVISOR_URL}/store/reload", status=200) assert (await supervisor_client.store.reload()) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/store/reload")) } async def test_store_repository_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store repository info API.""" responses.get( f"{SUPERVISOR_URL}/store/repositories/core", status=200, body=load_fixture("store_repository_info.json"), ) repository = await supervisor_client.store.repository_info("core") assert repository.slug == "core" assert repository.name == "Official add-ons" assert repository.source == "core" async def test_store_add_repository( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store add repository API.""" responses.post(f"{SUPERVISOR_URL}/store/repositories", status=200) assert ( await supervisor_client.store.add_repository( StoreAddRepository(repository="test") ) ) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/store/repositories")) } async def test_store_remove_repository( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test store addon info API.""" responses.delete(f"{SUPERVISOR_URL}/store/repositories/test", status=200) assert (await supervisor_client.store.remove_repository("test")) is None assert responses.requests.keys() == { ("DELETE", URL(f"{SUPERVISOR_URL}/store/repositories/test")) } python-supervisor-client-0.3.1/tests/test_supervisor.py000066400000000000000000000077721500244310200235270ustar00rootroot00000000000000"""Test for supervisor management client.""" from ipaddress import IPv4Address from aioresponses import aioresponses import pytest from yarl import URL from aiohasupervisor import SupervisorClient from aiohasupervisor.models import SupervisorOptions, SupervisorUpdateOptions from . import load_fixture from .const import SUPERVISOR_URL async def test_supervisor_ping( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test supervisor ping API.""" responses.get(f"{SUPERVISOR_URL}/supervisor/ping", status=200) assert await supervisor_client.supervisor.ping() is None assert responses.requests.keys() == { ("GET", URL(f"{SUPERVISOR_URL}/supervisor/ping")) } async def test_supervisor_info( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test supervisor info API.""" responses.get( f"{SUPERVISOR_URL}/supervisor/info", status=200, body=load_fixture("supervisor_info.json"), ) info = await supervisor_client.supervisor.info() assert info.version == "2024.09.1" assert info.channel == "stable" assert info.arch == "aarch64" assert info.supported is True assert info.healthy is True assert info.logging == "info" assert info.ip_address == IPv4Address("172.30.32.2") assert info.country is None async def test_supervisor_stats( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test supervisor stats API.""" responses.get( f"{SUPERVISOR_URL}/supervisor/stats", status=200, body=load_fixture("supervisor_stats.json"), ) stats = await supervisor_client.supervisor.stats() assert stats.cpu_percent == 0.04 assert stats.memory_usage == 243982336 assert stats.memory_limit == 3899138048 assert stats.memory_percent == 6.26 @pytest.mark.parametrize( "options", [None, SupervisorUpdateOptions(version="2024.01.0")] ) async def test_supervisor_update( responses: aioresponses, supervisor_client: SupervisorClient, options: SupervisorUpdateOptions | None, ) -> None: """Test supervisor update API.""" responses.post(f"{SUPERVISOR_URL}/supervisor/update", status=200) assert await supervisor_client.supervisor.update(options) is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/supervisor/update")) } async def test_supervisor_reload( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test supervisor reload API.""" responses.post(f"{SUPERVISOR_URL}/supervisor/reload", status=200) assert await supervisor_client.supervisor.reload() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/supervisor/reload")) } async def test_supervisor_restart( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test supervisor restart API.""" responses.post(f"{SUPERVISOR_URL}/supervisor/restart", status=200) assert await supervisor_client.supervisor.restart() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/supervisor/restart")) } async def test_supervisor_options( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test supervisor options API.""" responses.post(f"{SUPERVISOR_URL}/supervisor/options", status=200) assert ( await supervisor_client.supervisor.set_options( SupervisorOptions(debug=True, debug_block=True) ) is None ) assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/supervisor/options")) } async def test_supervisor_repair( responses: aioresponses, supervisor_client: SupervisorClient ) -> None: """Test supervisor repair API.""" responses.post(f"{SUPERVISOR_URL}/supervisor/repair", status=200) assert await supervisor_client.supervisor.repair() is None assert responses.requests.keys() == { ("POST", URL(f"{SUPERVISOR_URL}/supervisor/repair")) }