mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 18:24:50 -05:00
Merge branch 'dev' into frenck/recorder-purge-skip-empty-cycle
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: ha-pr-comment-audit
|
||||
description: Audits the review comment threads on a Home Assistant GitHub pull request, flagging unaddressed comments and requests for clarification. Use when checking whether PR feedback has been handled, either standalone or as part of a full PR review.
|
||||
---
|
||||
|
||||
# Check Home Assistant PR Review Comments
|
||||
|
||||
## Instructions:
|
||||
- Resolve the PR context first. If a PR number is not given, use 'gh pr view' to identify the current branch's PR.
|
||||
- Fetch the review comment threads for the PR (e.g. 'gh api' for review threads/comments).
|
||||
- Flag comments that have not been addressed. If the author has replied but has not implemented the suggestion, still flag it and summarize the reply.
|
||||
- Flag comments for which the author has asked for clarification.
|
||||
- Generate a summary of the flagged comments, including a link for each comment. Don't include comments that have been addressed.
|
||||
|
||||
|
||||
## IMPORTANT:
|
||||
- Only provide feedback in the CONSOLE. DO NOT ACT ON GITHUB.
|
||||
@@ -5,11 +5,11 @@ description: Reviews Home Assistant GitHub pull requests and provides feedback c
|
||||
|
||||
# Review GitHub Pull Request
|
||||
|
||||
## Follow these steps:
|
||||
1. Use 'gh pr view' to get the PR details and description.
|
||||
2. Use 'gh pr diff' to see all the changes in the PR.
|
||||
3. Review the changes following the `ha-review` skill. It is VERY IMPORTANT to follow the `ha-review` skill instructions.
|
||||
4. Check if all existing review comments have been addressed.
|
||||
## Instructions:
|
||||
- Use 'gh pr view' to get the PR details and description.
|
||||
- Use 'gh pr diff' to see all the changes in the PR.
|
||||
- Review the changes following the `ha-review` skill. It is VERY IMPORTANT to follow the `ha-review` skill instructions. Explicitly pass the PR's target/base branch to the `ha-review` skill (obtained via `gh pr view`) so it diffs against the correct base.
|
||||
- Run a subagent in parallel to check the PR review comments following the `ha-pr-comment-audit` skill.
|
||||
|
||||
## IMPORTANT:
|
||||
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
|
||||
|
||||
@@ -5,6 +5,9 @@ description: Reviews Home Assistant code changes and provides constructive feedb
|
||||
|
||||
# Review Code Changes
|
||||
|
||||
## Scope:
|
||||
- Unless instructed otherwise, review the full branch changes against the target branch. Resolve the base to an available ref (prefer `upstream/<base>`, then `origin/<base>`, then local `<base>`) and review `git diff "$(git merge-base "$BASE_REF" HEAD)"..HEAD`; use `dev` as the default base.
|
||||
|
||||
## Analyze the code changes for:
|
||||
- Code quality and style consistency
|
||||
- Potential bugs or issues
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template, please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/core/releases
|
||||
- Do not report issues for integrations if you are using custom components or integrations.
|
||||
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||
-->
|
||||
## The problem
|
||||
<!--
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us what you were trying to do and what happened.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Provide details about the versions you are using, which helps us to reproduce
|
||||
and find the issue quicker. Version information is found in the
|
||||
Home Assistant frontend: Settings -> About.
|
||||
-->
|
||||
|
||||
- Home Assistant Core release with the issue:
|
||||
- Last working Home Assistant Core release (if known):
|
||||
- Operating environment (OS/Container/Supervised/Core):
|
||||
- Integration causing this issue:
|
||||
- Link to integration documentation on our website:
|
||||
|
||||
## Problem-relevant `configuration.yaml`
|
||||
<!--
|
||||
An example configuration that caused the problem for you. Fill this out even
|
||||
if it seems unimportant to you. Please be sure to remove personal information
|
||||
like passwords, private URLs and other credentials.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Traceback/Error logs
|
||||
<!--
|
||||
If you come across any trace or error logs, please provide them.
|
||||
-->
|
||||
|
||||
```txt
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 102 KiB |
@@ -8,6 +8,131 @@
|
||||
- Do not comment on code style, formatting or linting issues.
|
||||
- Flag comments that over-explain straightforward code, narrate the obvious, or read like AI commentary (multi-sentence justifications for a single line).
|
||||
- A Pull Request with a dependency version bump should only contain changes required for the version bump. If the PR includes other changes, request that they are removed from the PR.
|
||||
- Check that the PR description is complete and filled in according to the PR template included below. Every section and checklist item from the template must be present, except the `## Breaking change` section which is optional. No content from the template should be missing, except for HTML comments. Even unchecked checkboxes or empty sections must be present. This is a hard requirement.
|
||||
|
||||
## Pull Request template
|
||||
|
||||
The PR description must follow this template (from `.github/PULL_REQUEST_TEMPLATE.md`):
|
||||
|
||||
```markdown
|
||||
<!--
|
||||
You are amazing! Thanks for contributing to our project!
|
||||
Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
|
||||
-->
|
||||
## Breaking change
|
||||
<!--
|
||||
If your PR contains a breaking change for existing users, it is important
|
||||
to tell them what breaks, how to make it work again and why we did this.
|
||||
This piece of text is published with the release notes, so it helps if you
|
||||
write it towards our users, not us.
|
||||
Note: Remove this section if this PR is NOT a breaking change.
|
||||
-->
|
||||
|
||||
|
||||
## Proposed change
|
||||
<!--
|
||||
Describe the big picture of your changes here to communicate to the
|
||||
maintainers why we should accept this pull request. If it fixes a bug
|
||||
or resolves a feature request, be sure to link to that issue in the
|
||||
additional information section.
|
||||
-->
|
||||
|
||||
|
||||
## Type of change
|
||||
<!--
|
||||
What type of change does your PR introduce to Home Assistant?
|
||||
NOTE: Please, check only 1! box!
|
||||
If your PR requires multiple boxes to be checked, you'll most likely need to
|
||||
split it into multiple PRs. This makes things easier and faster to code review.
|
||||
-->
|
||||
|
||||
- [ ] Dependency upgrade
|
||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||
- [ ] New integration (thank you!)
|
||||
- [ ] New feature (which adds functionality to an existing integration)
|
||||
- [ ] Deprecation (breaking change to happen in the future)
|
||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
|
||||
## Additional information
|
||||
<!--
|
||||
Details are important, and help maintainers processing your PR.
|
||||
Please be sure to fill out additional details, if applicable.
|
||||
-->
|
||||
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to frontend pull request:
|
||||
|
||||
## Checklist
|
||||
<!--
|
||||
Put an `x` in the boxes that apply. You can also fill these out after
|
||||
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||
We're here to help! This is simply a reminder of what we are going to look
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] I have followed the [development checklist][dev-checklist]
|
||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
||||
- [ ] The code has been formatted using Ruff (`ruff format homeassistant tests`)
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
|
||||
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
|
||||
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
|
||||
- [ ] The [manifest file][manifest-docs] has all fields filled out correctly.
|
||||
Updated and included derived files by running: `python3 -m script.hassfest`.
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
|
||||
Updated by running `python3 -m script.gen_requirements_all`.
|
||||
- [ ] For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.
|
||||
|
||||
<!--
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
Unfortunately, the number of incoming pull requests is higher than what our
|
||||
reviewers can review and merge so there is a long backlog of pull requests
|
||||
waiting for review. You can help here!
|
||||
|
||||
By reviewing another pull request, you will help raise the code quality of
|
||||
that pull request and the final review will be faster. This way the general
|
||||
pace of pull request reviews will go up and your wait time will go down.
|
||||
|
||||
When picking a pull request to review, try to choose one that hasn't yet
|
||||
been reviewed.
|
||||
|
||||
Thanks for helping out!
|
||||
-->
|
||||
|
||||
To help with the load of incoming pull requests:
|
||||
|
||||
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
|
||||
|
||||
[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure
|
||||
|
||||
<!--
|
||||
Thank you for contributing <3
|
||||
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
|
||||
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
|
||||
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
||||
```
|
||||
|
||||
# GitHub Copilot & Claude Code Instructions
|
||||
|
||||
|
||||
@@ -342,13 +342,13 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -378,7 +378,7 @@ jobs:
|
||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
with:
|
||||
images: ${{ matrix.registry }}/home-assistant
|
||||
sep-tags: ","
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.7.1
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v3.7.1
|
||||
|
||||
- name: Copy architecture images to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
@@ -521,14 +521,14 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -541,7 +541,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
# of a new uv cache entry after a version bump.
|
||||
echo "key=venv-${CACHE_VERSION}-${HA_SHORT_VERSION}-${HASH_REQUIREMENTS_TEST}-${HASH_REQUIREMENTS}-${HASH_REQUIREMENTS_ALL}-${HASH_PACKAGE_CONSTRAINTS}-${HASH_GEN_REQUIREMENTS}" >> $GITHUB_OUTPUT
|
||||
- name: Filter for core changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: core
|
||||
with:
|
||||
filters: .core_files.yaml
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
echo "Result:"
|
||||
cat .integration_paths.yaml
|
||||
- name: Filter for integration changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: integrations
|
||||
with:
|
||||
filters: .integration_paths.yaml
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
uses: j178/prek-action@e98a699c41eb69ab013a45817a0406469a748f8d # v2.0.5
|
||||
env:
|
||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
@@ -302,7 +302,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
uses: j178/prek-action@e98a699c41eb69ab013a45817a0406469a748f8d # v2.0.5
|
||||
with:
|
||||
extra-args: --all-files zizmor
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
name: E2E tests
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Image tag or digest to test (e.g. dev, 2026.7.1, 2026.8.0b0, sha256:0a1b2c3d…)"
|
||||
default: "dev"
|
||||
required: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
boot_check:
|
||||
name: Boot check ${{ matrix.arch }} core image
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
env:
|
||||
BASE_URL: http://localhost:8123
|
||||
services:
|
||||
homeassistant:
|
||||
image: ghcr.io/home-assistant/home-assistant${{ startsWith(inputs.version, 'sha256:') && '@' || ':' }}${{ inputs.version }} # zizmor: ignore[unpinned-images]
|
||||
ports:
|
||||
- 8123:8123
|
||||
# Gate steps until Home Assistant answers (60 x 5s ≈ 300s startup budget)
|
||||
options: >-
|
||||
--health-cmd="curl --fail --silent --max-time 10 --output /dev/null http://127.0.0.1:8123/"
|
||||
--health-start-period=10s
|
||||
--health-interval=5s
|
||||
--health-retries=60
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
package_json_file: tests/e2e/package.json
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
cache-dependency-path: tests/e2e/pnpm-lock.yaml
|
||||
|
||||
- name: Install E2E test dependencies
|
||||
working-directory: tests/e2e
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright browser
|
||||
working-directory: tests/e2e
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run Playwright E2E tests
|
||||
working-directory: tests/e2e
|
||||
run: pnpm exec playwright test
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-${{ matrix.arch }}
|
||||
path: |
|
||||
tests/e2e/playwright-report/
|
||||
tests/e2e/test-results/
|
||||
|
||||
- name: Dump container logs
|
||||
if: always()
|
||||
env:
|
||||
CONTAINER: ${{ job.services.homeassistant.id }}
|
||||
run: docker logs "$CONTAINER" > homeassistant.log 2>&1 || true
|
||||
|
||||
- name: Upload container logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: container-logs-${{ matrix.arch }}
|
||||
path: homeassistant.log
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
# - Issues
|
||||
# - No issues marked as no-stale or help-wanted
|
||||
- name: 60 days stale PRs policy and 90 days stale issue policy
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
remove-stale-when-updated: true
|
||||
|
||||
@@ -145,3 +145,7 @@ pytest_buckets.txt
|
||||
.claude/worktrees/
|
||||
.serena/
|
||||
|
||||
# Playwright e2e tests
|
||||
tests/e2e/node_modules/
|
||||
tests/e2e/playwright-report/
|
||||
tests/e2e/test-results/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.20
|
||||
rev: v0.15.21
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
||||
@@ -5,3 +5,4 @@ homeassistant/generated/*
|
||||
tests/components/lidarr/fixtures/initialize.js
|
||||
tests/components/lidarr/fixtures/initialize-wrong.js
|
||||
tests/fixtures/core/config/yaml_errors/
|
||||
tests/e2e/pnpm-lock.yaml
|
||||
|
||||
@@ -228,6 +228,7 @@ homeassistant.components.fujitsu_fglair.*
|
||||
homeassistant.components.fully_kiosk.*
|
||||
homeassistant.components.fumis.*
|
||||
homeassistant.components.fyta.*
|
||||
homeassistant.components.gatus.*
|
||||
homeassistant.components.generic_hygrostat.*
|
||||
homeassistant.components.generic_thermostat.*
|
||||
homeassistant.components.geo_location.*
|
||||
@@ -242,6 +243,7 @@ homeassistant.components.google.*
|
||||
homeassistant.components.google_assistant_sdk.*
|
||||
homeassistant.components.google_cloud.*
|
||||
homeassistant.components.google_drive.*
|
||||
homeassistant.components.google_health.*
|
||||
homeassistant.components.google_photos.*
|
||||
homeassistant.components.google_sheets.*
|
||||
homeassistant.components.google_weather.*
|
||||
@@ -254,6 +256,7 @@ homeassistant.components.guntamatic.*
|
||||
homeassistant.components.habitica.*
|
||||
homeassistant.components.hardkernel.*
|
||||
homeassistant.components.hardware.*
|
||||
homeassistant.components.harman_luxury.*
|
||||
homeassistant.components.hdfury.*
|
||||
homeassistant.components.heos.*
|
||||
homeassistant.components.here_travel_time.*
|
||||
@@ -335,6 +338,7 @@ homeassistant.components.lawn_mower.*
|
||||
homeassistant.components.lcn.*
|
||||
homeassistant.components.ld2410_ble.*
|
||||
homeassistant.components.led_ble.*
|
||||
homeassistant.components.led_infrared.*
|
||||
homeassistant.components.lektrico.*
|
||||
homeassistant.components.letpot.*
|
||||
homeassistant.components.lg_infrared.*
|
||||
@@ -346,7 +350,9 @@ homeassistant.components.lifx.*
|
||||
homeassistant.components.light.*
|
||||
homeassistant.components.linkplay.*
|
||||
homeassistant.components.litejet.*
|
||||
homeassistant.components.litellm.*
|
||||
homeassistant.components.litterrobot.*
|
||||
homeassistant.components.llama_cpp.*
|
||||
homeassistant.components.local_ip.*
|
||||
homeassistant.components.local_todo.*
|
||||
homeassistant.components.lock.*
|
||||
@@ -393,6 +399,7 @@ homeassistant.components.nam.*
|
||||
homeassistant.components.namecheapdns.*
|
||||
homeassistant.components.nasweb.*
|
||||
homeassistant.components.neato.*
|
||||
homeassistant.components.neopool.*
|
||||
homeassistant.components.nest.*
|
||||
homeassistant.components.netatmo.*
|
||||
homeassistant.components.network.*
|
||||
|
||||
Generated
+39
-14
@@ -607,6 +607,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/frontend/ @home-assistant/frontend
|
||||
/homeassistant/components/frontier_silicon/ @wlcrs
|
||||
/tests/components/frontier_silicon/ @wlcrs
|
||||
/homeassistant/components/fuelprices_dk/ @MTrab
|
||||
/tests/components/fuelprices_dk/ @MTrab
|
||||
/homeassistant/components/fujitsu_fglair/ @crevetor
|
||||
/tests/components/fujitsu_fglair/ @crevetor
|
||||
/homeassistant/components/fully_kiosk/ @cgarwood
|
||||
@@ -623,6 +625,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/gardena_bluetooth/ @elupus
|
||||
/homeassistant/components/gate/ @home-assistant/core
|
||||
/tests/components/gate/ @home-assistant/core
|
||||
/homeassistant/components/gatus/ @TN-1
|
||||
/tests/components/gatus/ @TN-1
|
||||
/homeassistant/components/gdacs/ @exxamalte
|
||||
/tests/components/gdacs/ @exxamalte
|
||||
/homeassistant/components/generic/ @davet2001
|
||||
@@ -675,6 +679,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/google_drive/ @tronikos
|
||||
/homeassistant/components/google_generative_ai_conversation/ @tronikos @ivanlh
|
||||
/tests/components/google_generative_ai_conversation/ @tronikos @ivanlh
|
||||
/homeassistant/components/google_health/ @allenporter
|
||||
/tests/components/google_health/ @allenporter
|
||||
/homeassistant/components/google_mail/ @tkdrob
|
||||
/tests/components/google_mail/ @tkdrob
|
||||
/homeassistant/components/google_photos/ @allenporter
|
||||
@@ -713,10 +719,14 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/habitica/ @tr4nt0r
|
||||
/homeassistant/components/hanna/ @bestycame
|
||||
/tests/components/hanna/ @bestycame
|
||||
/homeassistant/components/harbor/ @Lash-L @afgarcia86
|
||||
/tests/components/harbor/ @Lash-L @afgarcia86
|
||||
/homeassistant/components/hardkernel/ @home-assistant/core
|
||||
/tests/components/hardkernel/ @home-assistant/core
|
||||
/homeassistant/components/hardware/ @home-assistant/core
|
||||
/tests/components/hardware/ @home-assistant/core
|
||||
/homeassistant/components/harman_luxury/ @sbesh91
|
||||
/tests/components/harman_luxury/ @sbesh91
|
||||
/homeassistant/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/homeassistant/components/hassio/ @home-assistant/supervisor
|
||||
@@ -777,8 +787,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/homematicip_cloud/ @hahn-th @lackas
|
||||
/homeassistant/components/homevolt/ @danielhiversen @liudger
|
||||
/tests/components/homevolt/ @danielhiversen @liudger
|
||||
/homeassistant/components/homewizard/ @DCSBL
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/homewizard/ @DCSBL @lexpostma
|
||||
/tests/components/homewizard/ @DCSBL @lexpostma
|
||||
/homeassistant/components/honeywell/ @mkmer
|
||||
/tests/components/honeywell/ @mkmer
|
||||
/homeassistant/components/honeywell_string_lights/ @balloob
|
||||
@@ -938,6 +948,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/kaiterra/ @Michsior14
|
||||
/homeassistant/components/kaleidescape/ @SteveEasley
|
||||
/tests/components/kaleidescape/ @SteveEasley
|
||||
/homeassistant/components/karakeep/ @sli-cka
|
||||
/tests/components/karakeep/ @sli-cka
|
||||
/homeassistant/components/keba/ @dannerph
|
||||
/homeassistant/components/keenetic_ndms2/ @foxel
|
||||
/tests/components/keenetic_ndms2/ @foxel
|
||||
@@ -993,6 +1005,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/leaone/ @bdraco
|
||||
/homeassistant/components/led_ble/ @bdraco
|
||||
/tests/components/led_ble/ @bdraco
|
||||
/homeassistant/components/led_infrared/ @tr4nt0r
|
||||
/tests/components/led_infrared/ @tr4nt0r
|
||||
/homeassistant/components/lektrico/ @lektrico
|
||||
/tests/components/lektrico/ @lektrico
|
||||
/homeassistant/components/letpot/ @jpelgrom
|
||||
@@ -1022,10 +1036,14 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/linux_battery/ @fabaff
|
||||
/homeassistant/components/litejet/ @joncar
|
||||
/tests/components/litejet/ @joncar
|
||||
/homeassistant/components/litellm/ @luismalves
|
||||
/tests/components/litellm/ @luismalves
|
||||
/homeassistant/components/litterrobot/ @natekspencer @tkdrob
|
||||
/tests/components/litterrobot/ @natekspencer @tkdrob
|
||||
/homeassistant/components/livisi/ @StefanIacobLivisi @planbnet
|
||||
/tests/components/livisi/ @StefanIacobLivisi @planbnet
|
||||
/homeassistant/components/llama_cpp/ @allenporter
|
||||
/tests/components/llama_cpp/ @allenporter
|
||||
/homeassistant/components/llm/ @home-assistant/core
|
||||
/tests/components/llm/ @home-assistant/core
|
||||
/homeassistant/components/local_calendar/ @allenporter
|
||||
@@ -1051,6 +1069,7 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/lovelace/ @home-assistant/frontend
|
||||
/tests/components/lovelace/ @home-assistant/frontend
|
||||
/homeassistant/components/luci/ @mzdrale
|
||||
/tests/components/luci/ @mzdrale
|
||||
/homeassistant/components/luftdaten/ @fabaff @frenck
|
||||
/tests/components/luftdaten/ @fabaff @frenck
|
||||
/homeassistant/components/lunatone/ @MoonDevLT
|
||||
@@ -1061,6 +1080,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/lutron/ @cdheiser @wilburCForce
|
||||
/homeassistant/components/lutron_caseta/ @swails @danaues @eclair4151
|
||||
/tests/components/lutron_caseta/ @swails @danaues @eclair4151
|
||||
/homeassistant/components/lyngdorf/ @fishloa
|
||||
/tests/components/lyngdorf/ @fishloa
|
||||
/homeassistant/components/lyric/ @timmo001
|
||||
/tests/components/lyric/ @timmo001
|
||||
/homeassistant/components/madvr/ @iloveicedgreentea
|
||||
@@ -1115,8 +1136,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/microbees/ @microBeesTech
|
||||
/homeassistant/components/miele/ @astrandb
|
||||
/tests/components/miele/ @astrandb
|
||||
/homeassistant/components/mikrotik/ @engrbm87
|
||||
/tests/components/mikrotik/ @engrbm87
|
||||
/homeassistant/components/mikrotik/ @engrbm87 @chemelli74
|
||||
/tests/components/mikrotik/ @engrbm87 @chemelli74
|
||||
/homeassistant/components/mill/ @danielhiversen
|
||||
/tests/components/mill/ @danielhiversen
|
||||
/homeassistant/components/min_max/ @gjohansson-ST
|
||||
@@ -1139,8 +1160,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/homeassistant/components/moisture/ @home-assistant/core
|
||||
/tests/components/moisture/ @home-assistant/core
|
||||
/homeassistant/components/monarch_money/ @jeeftor
|
||||
/tests/components/monarch_money/ @jeeftor
|
||||
/homeassistant/components/monarch_money/ @jeeftor @bradleyseanf
|
||||
/tests/components/monarch_money/ @jeeftor @bradleyseanf
|
||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||
/tests/components/monoprice/ @etsinko @OnFreund
|
||||
/homeassistant/components/monzo/ @jakemartin-icl
|
||||
@@ -1189,6 +1210,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/nasweb/ @nasWebio
|
||||
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
|
||||
/tests/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
|
||||
/homeassistant/components/neopool/ @svasek
|
||||
/tests/components/neopool/ @svasek
|
||||
/homeassistant/components/ness_alarm/ @nickw444 @poshy163
|
||||
/tests/components/ness_alarm/ @nickw444 @poshy163
|
||||
/homeassistant/components/nest/ @allenporter
|
||||
@@ -1360,8 +1383,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/peco/ @IceBotYT
|
||||
/homeassistant/components/pegel_online/ @mib1185
|
||||
/tests/components/pegel_online/ @mib1185
|
||||
/homeassistant/components/permobil/ @IsakNyberg
|
||||
/tests/components/permobil/ @IsakNyberg
|
||||
/homeassistant/components/persistent_notification/ @home-assistant/core
|
||||
/tests/components/persistent_notification/ @home-assistant/core
|
||||
/homeassistant/components/pglab/ @pglab-electronics
|
||||
@@ -1577,6 +1598,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/schlage/ @dknowles2
|
||||
/tests/components/schlage/ @dknowles2
|
||||
/homeassistant/components/schluter/ @prairieapps
|
||||
/homeassistant/components/scorpiontrack/ @Herbertmt978
|
||||
/tests/components/scorpiontrack/ @Herbertmt978
|
||||
/homeassistant/components/scrape/ @fabaff @gjohansson-ST
|
||||
/tests/components/scrape/ @fabaff @gjohansson-ST
|
||||
/homeassistant/components/screenlogic/ @dieselrabbit @bdraco
|
||||
@@ -1696,8 +1719,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/sonarr/ @ctalkington
|
||||
/homeassistant/components/songpal/ @rytilahti @shenxn
|
||||
/tests/components/songpal/ @rytilahti @shenxn
|
||||
/homeassistant/components/sonos/ @jjlawren @peterager
|
||||
/tests/components/sonos/ @jjlawren @peterager
|
||||
/homeassistant/components/sonos/ @peterager @jjlawren
|
||||
/tests/components/sonos/ @peterager @jjlawren
|
||||
/homeassistant/components/soundtouch/ @kroimon
|
||||
/tests/components/soundtouch/ @kroimon
|
||||
/homeassistant/components/spaceapi/ @fabaff
|
||||
@@ -1718,8 +1741,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/starline/ @anonym-tsk
|
||||
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||
/homeassistant/components/steam_online/ @tkdrob
|
||||
/tests/components/steam_online/ @tkdrob
|
||||
/homeassistant/components/steam_online/ @tr4nt0r @tkdrob
|
||||
/tests/components/steam_online/ @tr4nt0r @tkdrob
|
||||
/homeassistant/components/steamist/ @bdraco
|
||||
/tests/components/steamist/ @bdraco
|
||||
/homeassistant/components/stiebel_eltron/ @fucm @ThyMYthOS
|
||||
@@ -1805,8 +1828,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
/tests/components/tesla_fleet/ @Bre77
|
||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||
/tests/components/tesla_wall_connector/ @einarhauks
|
||||
/homeassistant/components/tesla_wall_connector/ @einarhauks @sarabveer
|
||||
/tests/components/tesla_wall_connector/ @einarhauks @sarabveer
|
||||
/homeassistant/components/teslemetry/ @Bre77
|
||||
/tests/components/teslemetry/ @Bre77
|
||||
/homeassistant/components/tessie/ @Bre77
|
||||
@@ -1946,6 +1969,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/version/ @ludeeus
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/homeassistant/components/vibration/ @home-assistant/core
|
||||
/tests/components/vibration/ @home-assistant/core
|
||||
/homeassistant/components/vicare/ @CFenner @lackas
|
||||
/tests/components/vicare/ @CFenner @lackas
|
||||
/homeassistant/components/victron_ble/ @rajlaud
|
||||
|
||||
@@ -47,7 +47,7 @@ from .components import (
|
||||
file_upload as file_upload_pre_import, # noqa: F401
|
||||
group as group_pre_import, # noqa: F401
|
||||
history as history_pre_import, # noqa: F401
|
||||
http, # not named pre_import since it has requirements
|
||||
http as http_import, # noqa: F401 - not named pre_import since it has requirements
|
||||
image_upload as image_upload_import, # noqa: F401 - not named pre_import since it has requirements
|
||||
logbook as logbook_pre_import, # noqa: F401
|
||||
lovelace as lovelace_pre_import, # noqa: F401
|
||||
@@ -264,6 +264,7 @@ DEFAULT_INTEGRATIONS = {
|
||||
"occupancy",
|
||||
"power",
|
||||
"temperature",
|
||||
"vibration",
|
||||
"window",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
@@ -414,12 +415,7 @@ async def async_setup_hass(
|
||||
_LOGGER.info("Starting in recovery mode")
|
||||
hass.config.recovery_mode = True
|
||||
|
||||
http_conf = (await http.async_get_last_config(hass)) or {}
|
||||
|
||||
await async_from_config_dict(
|
||||
{"recovery_mode": {}, "http": http_conf},
|
||||
hass,
|
||||
)
|
||||
await async_from_config_dict({"recovery_mode": {}}, hass)
|
||||
|
||||
if runtime_config.open_ui:
|
||||
hass.add_job(open_hass_ui, hass)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"google_cloud",
|
||||
"google_drive",
|
||||
"google_generative_ai_conversation",
|
||||
"google_health",
|
||||
"google_mail",
|
||||
"google_maps",
|
||||
"google_photos",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "mitsubishi",
|
||||
"name": "Mitsubishi",
|
||||
"integrations": ["melcloud", "mitsubishi_comfort"]
|
||||
"integrations": ["melcloud", "melcloud_home", "mitsubishi_comfort"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from aioacaia.acaiascale import AcaiaScale
|
||||
from aioacaia import AcaiaScale
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, override
|
||||
|
||||
from aioacaia.acaiascale import AcaiaScale
|
||||
from aioacaia import AcaiaScale
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from aioacaia.discovery import is_new_scale
|
||||
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice
|
||||
from aioacaia.helpers import is_new_scale
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from aioacaia.acaiascale import AcaiaScale
|
||||
from aioacaia import AcaiaScale
|
||||
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
||||
|
||||
from homeassistant.components.bluetooth import async_get_scanner
|
||||
|
||||
@@ -26,5 +26,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioacaia"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioacaia==0.1.18"]
|
||||
"requirements": ["aioacaia==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from aioacaia.acaiascale import AcaiaDeviceState, AcaiaScale
|
||||
from aioacaia import AcaiaScale
|
||||
from aioacaia.const import UnitMass as AcaiaUnitOfMass
|
||||
from aioacaia.scale import AcaiaDeviceState
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
RestoreSensor,
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_source import local_source
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
|
||||
from homeassistant.core import (
|
||||
@@ -34,6 +35,7 @@ from .const import (
|
||||
)
|
||||
from .entity import AITaskEntity
|
||||
from .http import async_setup as async_setup_http
|
||||
from .media_source import async_get_media_source
|
||||
from .task import (
|
||||
GenDataTask,
|
||||
GenDataTaskResult,
|
||||
@@ -88,6 +90,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||
await hass.data[DATA_PREFERENCES].async_load()
|
||||
async_setup_http(hass)
|
||||
if hass.config.media_dirs:
|
||||
source = await async_get_media_source(hass)
|
||||
hass.http.register_view(local_source.LocalMediaView(hass, source))
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GENERATE_DATA,
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from homeassistant.components.media_source import MediaSource, local_source
|
||||
from homeassistant.components.media_source import local_source
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from .const import DATA_MEDIA_SOURCE, DOMAIN, IMAGE_DIR
|
||||
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
@singleton(DATA_MEDIA_SOURCE, async_=True)
|
||||
async def async_get_media_source(hass: HomeAssistant) -> local_source.LocalSource:
|
||||
"""Set up local media source."""
|
||||
media_dirs = list(hass.config.media_dirs.values())
|
||||
|
||||
@@ -20,11 +22,10 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
|
||||
media_dir = Path(media_dirs[0]) / DOMAIN / IMAGE_DIR
|
||||
|
||||
hass.data[DATA_MEDIA_SOURCE] = source = local_source.LocalSource(
|
||||
return local_source.LocalSource(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"AI generated images",
|
||||
{IMAGE_DIR: str(media_dir)},
|
||||
f"/{DOMAIN}",
|
||||
)
|
||||
return source
|
||||
|
||||
@@ -163,6 +163,4 @@ class AidotDeviceManagerCoordinator(DataUpdateCoordinator[None]):
|
||||
):
|
||||
if not set(device.identifiers) & identifiers:
|
||||
_LOGGER.debug("Removing obsolete device entry %s", device.name)
|
||||
device_reg.async_update_device(
|
||||
device.id, remove_config_entry_id=self.config_entry.entry_id
|
||||
)
|
||||
device_reg.async_remove_device(device.id)
|
||||
|
||||
@@ -38,11 +38,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
|
||||
|
||||
lat = data[CONF_LATITUDE]
|
||||
lng = data[CONF_LONGITUDE]
|
||||
distance = data[CONF_RADIUS]
|
||||
|
||||
# Check that the provided latitude/longitude provide a response
|
||||
try:
|
||||
test_data = await client.observations.latLong(lat, lng, distance=distance)
|
||||
test_data = await client.observations.latLong(lat, lng)
|
||||
|
||||
except InvalidKeyError as exc:
|
||||
raise InvalidAuth from exc
|
||||
|
||||
@@ -77,7 +77,6 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
obs = await self.airnow.observations.latLong(
|
||||
self.latitude,
|
||||
self.longitude,
|
||||
distance=self.distance,
|
||||
)
|
||||
|
||||
except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairnow"],
|
||||
"requirements": ["pyairnow==1.3.1"]
|
||||
"requirements": ["pyairnow==1.4.0"]
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@ from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, override
|
||||
|
||||
from pyairobotrest.exceptions import (
|
||||
AirobotConnectionError,
|
||||
AirobotError,
|
||||
AirobotTimeoutError,
|
||||
)
|
||||
from pyairobotrest.exceptions import AirobotError
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
@@ -84,10 +80,6 @@ class AirobotButton(AirobotEntity, ButtonEntity):
|
||||
"""Handle the button press."""
|
||||
try:
|
||||
await self.entity_description.press_fn(self.coordinator)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except AirobotConnectionError, AirobotTimeoutError:
|
||||
# Connection errors during reboot are expected as device restarts
|
||||
pass
|
||||
except AirobotError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairobotrest"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyairobotrest==0.3.0"]
|
||||
"requirements": ["pyairobotrest==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.const import (
|
||||
EntityCategory,
|
||||
UnitOfDensity,
|
||||
UnitOfPressure,
|
||||
UnitOfRadiationConcentration,
|
||||
UnitOfRatio,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
@@ -33,36 +34,34 @@ from .coordinator import AirthingsDataUpdateCoordinator
|
||||
SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"radonShortTermAvg": SensorEntityDescription(
|
||||
key="radonShortTermAvg",
|
||||
native_unit_of_measurement="Bq/m³",
|
||||
translation_key="radon",
|
||||
device_class=SensorDeviceClass.RADON,
|
||||
native_unit_of_measurement=(
|
||||
UnitOfRadiationConcentration.BECQUEREL_PER_CUBIC_METER
|
||||
),
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"temp": SensorEntityDescription(
|
||||
key="temp",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
"humidity": SensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"pressure": SensorEntityDescription(
|
||||
key="pressure",
|
||||
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
"sla": SensorEntityDescription(
|
||||
key="sla",
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"battery": SensorEntityDescription(
|
||||
@@ -70,47 +69,40 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"co2": SensorEntityDescription(
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"voc": SensorEntityDescription(
|
||||
key="voc",
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"light": SensorEntityDescription(
|
||||
key="light",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
translation_key="light",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"lux": SensorEntityDescription(
|
||||
key="lux",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"virusRisk": SensorEntityDescription(
|
||||
key="virusRisk",
|
||||
translation_key="virus_risk",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"mold": SensorEntityDescription(
|
||||
key="mold",
|
||||
translation_key="mold",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"rssi": SensorEntityDescription(
|
||||
@@ -119,21 +111,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"pm1": SensorEntityDescription(
|
||||
key="pm1",
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM1,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"pm25": SensorEntityDescription(
|
||||
key="pm25",
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
}
|
||||
@@ -145,8 +134,8 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Airthings sensor."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities = [
|
||||
AirthingsDeviceSensor(
|
||||
coordinator,
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
"mold": {
|
||||
"name": "Mold"
|
||||
},
|
||||
"radon": {
|
||||
"name": "Radon"
|
||||
},
|
||||
"virus_risk": {
|
||||
"name": "Virus Risk"
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ from airthings_ble import AirthingsDeviceType
|
||||
DOMAIN = "airthings_ble"
|
||||
MFCT_ID = 820
|
||||
|
||||
VOLUME_BECQUEREL = "Bq/m³"
|
||||
VOLUME_PICOCURIE = "pCi/L"
|
||||
|
||||
DEVICE_MODEL = "device_model"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = 300
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
@@ -36,9 +35,7 @@ class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: AirthingsBLEConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.airthings = AirthingsBluetoothDeviceData(
|
||||
_LOGGER, hass.config.units is METRIC_SYSTEM
|
||||
)
|
||||
self.airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric=True)
|
||||
|
||||
device_model = entry.data.get(DEVICE_MODEL)
|
||||
interval = DEVICE_SPECIFIC_SCAN_INTERVAL.get(
|
||||
|
||||
@@ -9,15 +9,9 @@
|
||||
"smartlink": "mdi:hub"
|
||||
}
|
||||
},
|
||||
"radon_1day_avg": {
|
||||
"default": "mdi:radioactive"
|
||||
},
|
||||
"radon_1day_level": {
|
||||
"default": "mdi:radioactive"
|
||||
},
|
||||
"radon_longterm_avg": {
|
||||
"default": "mdi:radioactive"
|
||||
},
|
||||
"radon_longterm_level": {
|
||||
"default": "mdi:radioactive"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for airthings ble sensors."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import dataclasses
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import override
|
||||
@@ -19,6 +18,7 @@ from homeassistant.const import (
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfPressure,
|
||||
UnitOfRadiationConcentration,
|
||||
UnitOfRatio,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
@@ -33,9 +33,8 @@ from homeassistant.helpers.entity_registry import (
|
||||
)
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -65,15 +64,15 @@ SENSORS_MAPPING_TEMPLATE: dict[str, AirthingsBLESensorEntityDescription] = {
|
||||
"radon_1day_avg": AirthingsBLESensorEntityDescription(
|
||||
key="radon_1day_avg",
|
||||
translation_key="radon_1day_avg",
|
||||
native_unit_of_measurement=VOLUME_BECQUEREL,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.RADON,
|
||||
native_unit_of_measurement=UnitOfRadiationConcentration.BECQUEREL_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"radon_longterm_avg": AirthingsBLESensorEntityDescription(
|
||||
key="radon_longterm_avg",
|
||||
translation_key="radon_longterm_avg",
|
||||
native_unit_of_measurement=VOLUME_BECQUEREL,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.RADON,
|
||||
native_unit_of_measurement=UnitOfRadiationConcentration.BECQUEREL_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"radon_1day_level": AirthingsBLESensorEntityDescription(
|
||||
@@ -210,26 +209,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Airthings BLE sensors."""
|
||||
is_metric = hass.config.units is METRIC_SYSTEM
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# we need to change some units
|
||||
sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy()
|
||||
if not is_metric:
|
||||
for key, val in sensors_mapping.items():
|
||||
if val.native_unit_of_measurement is not VOLUME_BECQUEREL:
|
||||
continue
|
||||
sensors_mapping[key] = dataclasses.replace(
|
||||
val,
|
||||
native_unit_of_measurement=VOLUME_PICOCURIE,
|
||||
suggested_display_precision=1,
|
||||
)
|
||||
|
||||
entities = []
|
||||
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
|
||||
for sensor_type, sensor_value in coordinator.data.sensors.items():
|
||||
if sensor_type not in sensors_mapping:
|
||||
if sensor_type not in SENSORS_MAPPING_TEMPLATE:
|
||||
_LOGGER.debug(
|
||||
"Unknown sensor type detected: %s, %s",
|
||||
sensor_type,
|
||||
@@ -238,7 +223,9 @@ async def async_setup_entry(
|
||||
continue
|
||||
async_migrate(hass, coordinator.data.address, sensor_type)
|
||||
entities.append(
|
||||
AirthingsSensor(coordinator, coordinator.data, sensors_mapping[sensor_type])
|
||||
AirthingsSensor(
|
||||
coordinator, coordinator.data, SENSORS_MAPPING_TEMPLATE[sensor_type]
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -9,14 +9,13 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
ATTR_STATE,
|
||||
CONF_COUNTRY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATE,
|
||||
EntityStateAttribute,
|
||||
UnitOfDensity,
|
||||
UnitOfRatio,
|
||||
)
|
||||
@@ -191,12 +190,14 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
|
||||
)
|
||||
|
||||
if self.coordinator.config_entry.options[CONF_SHOW_ON_MAP]:
|
||||
self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude
|
||||
self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude
|
||||
self._attr_extra_state_attributes[EntityStateAttribute.LATITUDE] = latitude
|
||||
self._attr_extra_state_attributes[EntityStateAttribute.LONGITUDE] = (
|
||||
longitude
|
||||
)
|
||||
self._attr_extra_state_attributes.pop("lati", None)
|
||||
self._attr_extra_state_attributes.pop("long", None)
|
||||
else:
|
||||
self._attr_extra_state_attributes["lati"] = latitude
|
||||
self._attr_extra_state_attributes["long"] = longitude
|
||||
self._attr_extra_state_attributes.pop(ATTR_LATITUDE, None)
|
||||
self._attr_extra_state_attributes.pop(ATTR_LONGITUDE, None)
|
||||
self._attr_extra_state_attributes.pop(EntityStateAttribute.LATITUDE, None)
|
||||
self._attr_extra_state_attributes.pop(EntityStateAttribute.LONGITUDE, None)
|
||||
|
||||
@@ -111,6 +111,4 @@ def remove_stale_devices(
|
||||
break
|
||||
|
||||
if device_id and device_id not in all_device_ids:
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
device_registry.async_remove_device(device_entry.id)
|
||||
|
||||
@@ -41,7 +41,6 @@ class AsyncConfigEntryAuth(Auth):
|
||||
@override
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
if not self._oauth_session.valid_token:
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return cast(str, self._oauth_session.token["access_token"])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Alexa Devices integration."""
|
||||
|
||||
from homeassistant.const import CONF_COUNTRY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_COUNTRY, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv, httpx_client
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2
|
||||
@@ -56,7 +56,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
||||
on_reauth_required=_on_http2_reauth_required,
|
||||
)
|
||||
|
||||
entry.async_on_unload(coordinator.api.stop_http2_processing)
|
||||
async def _async_stop_http2(_event: Event | None = None) -> None:
|
||||
"""Stop HTTP/2 processing on entry unload or HA shutdown."""
|
||||
await coordinator.api.stop_http2_processing()
|
||||
|
||||
entry.async_on_unload(_async_stop_http2)
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_http2)
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
"""Support for buttons."""
|
||||
|
||||
from typing import override
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, override
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator, alexa_api_call
|
||||
from .entity import AmazonServiceEntity
|
||||
from .entity import AmazonEntity, AmazonServiceEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Amazon Devices button entity description."""
|
||||
|
||||
capability: str
|
||||
|
||||
|
||||
DEVICE_BUTTONS: Final = {
|
||||
AmazonButtonEntityDescription(
|
||||
key="restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
capability="ALEXA_DEVICE_REBOOT",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
@@ -24,8 +47,9 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
known_routines: set[str] = set()
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_routines() -> None:
|
||||
def _check_routines_devices() -> None:
|
||||
current_routines = set(coordinator.api.routines)
|
||||
new_routines = current_routines - known_routines
|
||||
if new_routines:
|
||||
@@ -34,8 +58,19 @@ async def async_setup_entry(
|
||||
AmazonRoutineButton(coordinator, routine) for routine in new_routines
|
||||
)
|
||||
|
||||
_check_routines()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_routines))
|
||||
current_devices = set(coordinator.data)
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
AmazonDeviceButton(coordinator, serial_num, button_desc)
|
||||
for button_desc in DEVICE_BUTTONS
|
||||
for serial_num in new_devices
|
||||
if button_desc.capability in coordinator.data[serial_num].capabilities
|
||||
)
|
||||
|
||||
_check_routines_devices()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_routines_devices))
|
||||
|
||||
|
||||
class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
|
||||
@@ -54,3 +89,13 @@ class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
|
||||
"""Handle button press action."""
|
||||
async with alexa_api_call(self.coordinator):
|
||||
await self.coordinator.api.call_routine(self._routine)
|
||||
|
||||
|
||||
class AmazonDeviceButton(AmazonEntity, ButtonEntity):
|
||||
"""Button entity for Alexa device."""
|
||||
|
||||
@override
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press action."""
|
||||
async with alexa_api_call(self.coordinator):
|
||||
await self.coordinator.api.restart_device(self.device)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==14.1.8"]
|
||||
"requirements": ["aioamazondevices==14.2.0"]
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# 1 -> 2: Unique ID format changed, so delete and re-import:
|
||||
if version == 1:
|
||||
dev_reg = dr.async_get(hass)
|
||||
dev_reg.async_clear_config_entry(entry.entry_id)
|
||||
dev_reg.async_clear_config_entry(entry.entry_id, entry.domain)
|
||||
|
||||
en_reg = er.async_get(hass)
|
||||
en_reg.async_clear_config_entry(entry.entry_id)
|
||||
|
||||
@@ -28,9 +28,9 @@ from homeassistant.components.recorder import (
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE,
|
||||
ATTR_DOMAIN,
|
||||
BASE_PLATFORMS,
|
||||
EntityStateAttribute,
|
||||
__version__ as HA_VERSION,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
@@ -915,7 +915,9 @@ async def _async_snapshot_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
||||
# It is also not present, if entity is not in the state machine,
|
||||
# which can happen for disabled entities.
|
||||
"assumed_state": (
|
||||
entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
|
||||
entity_state.attributes.get(
|
||||
EntityStateAttribute.ASSUMED_STATE, False
|
||||
)
|
||||
if entity_state is not None
|
||||
else None
|
||||
),
|
||||
|
||||
@@ -51,7 +51,7 @@ rules:
|
||||
status: done
|
||||
comment: |
|
||||
The coordinator handles this.
|
||||
parallel-updates: todo
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
|
||||
@@ -20,6 +20,8 @@ from . import AnalyticsInsightsConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AnalyticsData, HomeassistantAnalyticsDataUpdateCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AnalyticsSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.components.diagnostics import (
|
||||
async_redact_data,
|
||||
device_entry_as_dict,
|
||||
entity_entry_as_dict,
|
||||
)
|
||||
from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS, CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
@@ -40,7 +42,7 @@ async def async_get_config_entry_diagnostics(
|
||||
return data
|
||||
|
||||
data["device"] = {
|
||||
**async_redact_data(attr.asdict(hass_device), TO_REDACT_DEV),
|
||||
**async_redact_data(device_entry_as_dict(hass_device), TO_REDACT_DEV),
|
||||
"entities": {},
|
||||
}
|
||||
|
||||
@@ -60,13 +62,11 @@ async def async_get_config_entry_diagnostics(
|
||||
# The context doesn't provide useful information in this case.
|
||||
state_dict.pop("context", None)
|
||||
|
||||
entity_dict = entity_entry_as_dict(entity_entry)
|
||||
# The entity_id is already provided at root level (the key).
|
||||
del entity_dict["entity_id"]
|
||||
data["device"]["entities"][entity_entry.entity_id] = {
|
||||
**async_redact_data(
|
||||
attr.asdict(
|
||||
entity_entry, filter=lambda attr, value: attr.name != "entity_id"
|
||||
),
|
||||
TO_REDACT,
|
||||
),
|
||||
**async_redact_data(entity_dict, TO_REDACT),
|
||||
"state": state_dict,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
|
||||
from .const import CONF_CHAT_MODEL, DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
|
||||
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
|
||||
@@ -137,7 +137,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
# Device and entity registries will set the disabled_by flag to None
|
||||
# when moving a device or entity disabled by CONFIG_ENTRY to an enabled
|
||||
# config entry, but we want to set it to USER instead,
|
||||
device_disabled_by = device.disabled_by
|
||||
device_disabled_by: dr.DeviceEntryDisabler | UndefinedType = UNDEFINED
|
||||
if (
|
||||
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
@@ -147,20 +147,9 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
device.id,
|
||||
disabled_by=device_disabled_by,
|
||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
add_config_subentry_id=subentry.subentry_id,
|
||||
add_config_entry_id=parent_entry.entry_id,
|
||||
new_config_entry_id=parent_entry.entry_id,
|
||||
new_config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
if parent_entry.entry_id != entry.entry_id:
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
)
|
||||
else:
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
if not use_existing:
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
|
||||
@@ -19,12 +19,11 @@ from homeassistant.config_entries import (
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_API_KEY,
|
||||
CONF_LLM_HASS_API,
|
||||
CONF_NAME,
|
||||
CONF_PROMPT,
|
||||
EntityStateAttribute,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, llm
|
||||
@@ -569,8 +568,8 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Where are the following coordinates located: "
|
||||
f"({zone_home.attributes[ATTR_LATITUDE]},"
|
||||
f" {zone_home.attributes[ATTR_LONGITUDE]})?",
|
||||
f"({zone_home.attributes[EntityStateAttribute.LATITUDE]},"
|
||||
f" {zone_home.attributes[EntityStateAttribute.LONGITUDE]})?",
|
||||
}
|
||||
],
|
||||
max_tokens=cast(int, DEFAULT[CONF_MAX_TOKENS]),
|
||||
|
||||
@@ -233,8 +233,9 @@ def _convert_content( # noqa: C901
|
||||
"""Transform HA chat_log content into Anthropic API format."""
|
||||
messages: list[MessageParam] = []
|
||||
container_id: str | None = None
|
||||
contents = list(chat_content)
|
||||
|
||||
for content in chat_content:
|
||||
for index, content in enumerate(contents):
|
||||
if isinstance(content, conversation.ToolResultContent):
|
||||
external_tool = True
|
||||
if content.tool_name == "web_search":
|
||||
@@ -322,14 +323,26 @@ def _convert_content( # noqa: C901
|
||||
else:
|
||||
messages[-1]["content"].append(tool_result_block) # type: ignore[attr-defined]
|
||||
elif isinstance(content, conversation.UserContent):
|
||||
has_text = bool(content.content.strip())
|
||||
# Attachments are only appended to the last message afterwards, so
|
||||
# an empty message is only useful for attachments if it is last
|
||||
has_attachments = bool(content.attachments) and index == len(contents) - 1
|
||||
if not has_text and not has_attachments:
|
||||
# The API rejects whitespace-only text blocks and empty
|
||||
# messages, so drop content that carries neither text nor
|
||||
# usable attachments
|
||||
continue
|
||||
# Combine consequent user messages
|
||||
if not messages or messages[-1]["role"] != "user":
|
||||
messages.append(
|
||||
MessageParam(
|
||||
role="user",
|
||||
content=content.content,
|
||||
content=content.content if has_text else [],
|
||||
)
|
||||
)
|
||||
elif not has_text:
|
||||
# Attachments are appended to the last user message later
|
||||
continue
|
||||
elif isinstance(messages[-1]["content"], str):
|
||||
messages[-1]["content"] = [
|
||||
TextBlockParam(type="text", text=messages[-1]["content"]),
|
||||
@@ -375,7 +388,7 @@ def _convert_content( # noqa: C901
|
||||
):
|
||||
container_id = content.native.container.id
|
||||
|
||||
if content.content:
|
||||
if content.content and content.content.strip():
|
||||
current_index = 0
|
||||
for detail in (
|
||||
content.native.citation_details
|
||||
@@ -455,7 +468,11 @@ def _convert_content( # noqa: C901
|
||||
]
|
||||
)
|
||||
|
||||
if (
|
||||
if not messages[-1]["content"]:
|
||||
# Drop assistant messages that ended up without any content
|
||||
# (e.g. whitespace-only text): the API rejects empty messages
|
||||
messages.pop()
|
||||
elif (
|
||||
isinstance(messages[-1]["content"], list)
|
||||
and len(messages[-1]["content"]) == 1
|
||||
and messages[-1]["content"][0]["type"] == "text"
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "gold",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["anthropic==0.108.0"]
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ from homeassistant.exceptions import (
|
||||
Unauthorized,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, recorder, template
|
||||
from homeassistant.helpers.http import MIN_COMPRESSED_RESPONSE_SIZE
|
||||
from homeassistant.helpers.json import json_dumps, json_fragment
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -223,12 +224,14 @@ class APIStatesView(HomeAssistantView):
|
||||
for state in hass.states.async_all()
|
||||
if entity_perm(state.entity_id, POLICY_READ)
|
||||
)
|
||||
body = b"".join((b"[", b",".join(states), b"]"))
|
||||
response = web.Response(
|
||||
body=b"".join((b"[", b",".join(states), b"]")),
|
||||
body=body,
|
||||
content_type=CONTENT_TYPE_JSON,
|
||||
zlib_executor_size=32768,
|
||||
)
|
||||
response.enable_compression()
|
||||
if len(body) > MIN_COMPRESSED_RESPONSE_SIZE:
|
||||
response.enable_compression()
|
||||
return response
|
||||
|
||||
|
||||
@@ -297,11 +300,12 @@ class APIEntityStateView(HomeAssistantView):
|
||||
return self.json_message(
|
||||
"Error storing state.", HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
resp = self.json(state.as_dict(), status_code)
|
||||
|
||||
resp.headers.add("Location", f"/api/states/{entity_id}")
|
||||
|
||||
return resp
|
||||
return web.Response(
|
||||
body=state.as_dict_json,
|
||||
content_type=CONTENT_TYPE_JSON,
|
||||
status=status_code,
|
||||
headers={"Location": f"/api/states/{entity_id}"},
|
||||
)
|
||||
|
||||
@ha.callback
|
||||
def delete(self, request: web.Request, entity_id: str) -> web.Response:
|
||||
|
||||
@@ -204,7 +204,7 @@ class AppleTvMediaPlayer(
|
||||
return MediaPlayerState.PLAYING
|
||||
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
|
||||
return MediaPlayerState.PAUSED
|
||||
return MediaPlayerState.IDLE # Bad or unknown state?
|
||||
return MediaPlayerState.IDLE # type: ignore[unreachable] # Bad or unknown state?
|
||||
return None
|
||||
|
||||
@callback
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Support for AquaLogic devices."""
|
||||
|
||||
import contextlib
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import threading
|
||||
@@ -9,22 +10,19 @@ from typing import override
|
||||
from aqualogic.core import AquaLogic
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, PLATFORMS, UPDATE_TOPIC
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "aqualogic"
|
||||
UPDATE_TOPIC = f"{DOMAIN}_update"
|
||||
CONF_UNIT = "unit"
|
||||
RECONNECT_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -36,19 +34,84 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
type AquaLogicConfigEntry = ConfigEntry[AquaLogicProcessor]
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up AquaLogic platform."""
|
||||
host = config[DOMAIN][CONF_HOST]
|
||||
port = config[DOMAIN][CONF_PORT]
|
||||
processor = AquaLogicProcessor(hass, host, port)
|
||||
hass.data[DOMAIN] = processor
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, processor.start_listen)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, processor.shutdown)
|
||||
_LOGGER.debug("AquaLogicProcessor %s:%i initialized", host, port)
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the AquaLogic component."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
hass.async_create_task(_async_import(hass, config[DOMAIN]))
|
||||
return True
|
||||
|
||||
|
||||
async def _async_import(hass: HomeAssistant, conf: dict) -> None:
|
||||
"""Import AquaLogic configuration from YAML and surface appropriate issues."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_HOST: conf[CONF_HOST], CONF_PORT: conf[CONF_PORT]},
|
||||
)
|
||||
|
||||
if (
|
||||
result.get("type") is FlowResultType.ABORT
|
||||
and result.get("reason") != "already_configured"
|
||||
):
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_import_issue_cannot_connect",
|
||||
breaks_in_ha_version="2027.2.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue_cannot_connect",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "AquaLogic",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2027.2.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "AquaLogic",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AquaLogicConfigEntry) -> bool:
|
||||
"""Set up AquaLogic from a config entry."""
|
||||
processor = AquaLogicProcessor(hass, entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||
entry.runtime_data = processor
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
processor.start()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AquaLogicConfigEntry) -> bool:
|
||||
"""Unload an AquaLogic config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
processor = entry.runtime_data
|
||||
processor.shutdown()
|
||||
await hass.async_add_executor_job(lambda: processor.join(timeout=5))
|
||||
if processor.is_alive():
|
||||
_LOGGER.warning("Processor thread did not stop within timeout")
|
||||
return unload_ok
|
||||
|
||||
|
||||
class AquaLogicProcessor(threading.Thread):
|
||||
"""AquaLogic event processor thread."""
|
||||
|
||||
@@ -59,17 +122,15 @@ class AquaLogicProcessor(threading.Thread):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._shutdown = False
|
||||
self._panel = None
|
||||
self._panel: AquaLogic | None = None
|
||||
|
||||
def start_listen(self, event: Event) -> None:
|
||||
"""Start event-processing thread."""
|
||||
_LOGGER.debug("Event processing thread started")
|
||||
self.start()
|
||||
|
||||
def shutdown(self, event: Event) -> None:
|
||||
def shutdown(self) -> None:
|
||||
"""Signal shutdown of processing event."""
|
||||
_LOGGER.debug("Event processing signaled exit")
|
||||
self._shutdown = True
|
||||
if (panel := self._panel) is not None and panel._socket is not None: # noqa: SLF001
|
||||
with contextlib.suppress(OSError):
|
||||
panel._socket.close() # noqa: SLF001
|
||||
|
||||
def data_changed(self, panel: AquaLogic) -> None:
|
||||
"""Aqualogic data changed callback."""
|
||||
@@ -82,13 +143,26 @@ class AquaLogicProcessor(threading.Thread):
|
||||
while True:
|
||||
panel = AquaLogic()
|
||||
self._panel = panel
|
||||
panel.connect(self._host, self._port)
|
||||
panel.process(self.data_changed)
|
||||
try:
|
||||
panel.connect(self._host, self._port)
|
||||
panel.process(self.data_changed)
|
||||
except OSError:
|
||||
pass
|
||||
except Exception as err:
|
||||
_LOGGER.exception(
|
||||
"Unexpected error in AquaLogic processor: %s",
|
||||
type(err).__name__,
|
||||
)
|
||||
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
_LOGGER.error("Connection to %s:%d lost", self._host, self._port)
|
||||
_LOGGER.warning(
|
||||
"Connection to %s:%d lost, retrying in %d seconds",
|
||||
self._host,
|
||||
self._port,
|
||||
int(RECONNECT_INTERVAL.total_seconds()),
|
||||
)
|
||||
time.sleep(RECONNECT_INTERVAL.total_seconds())
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Config flow for AquaLogic."""
|
||||
|
||||
import contextlib
|
||||
import threading
|
||||
from typing import Any, override
|
||||
|
||||
from aqualogic.core import AquaLogic
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
}
|
||||
)
|
||||
|
||||
# Worst case scenario, this covers both a plain socket timeout (READ_TIMEOUT)
|
||||
# and an additional frame-scan timeout (another READ_TIMEOUT), plus one second.
|
||||
_PROBE_TIMEOUT = AquaLogic.READ_TIMEOUT * 2 + 1
|
||||
|
||||
|
||||
class CannotConnect(Exception):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidDevice(Exception):
|
||||
"""Error to indicate the device is not an AquaLogic panel."""
|
||||
|
||||
|
||||
def _verify_device(host: str, port: int) -> None:
|
||||
"""Connect and verify the device is an AquaLogic panel.
|
||||
|
||||
Raises CannotConnect if the host is unreachable.
|
||||
Raises InvalidDevice if no valid AquaLogic data is received within the timeout.
|
||||
"""
|
||||
confirmed = threading.Event()
|
||||
|
||||
def _on_data(_: AquaLogic) -> None:
|
||||
confirmed.set()
|
||||
|
||||
panel = AquaLogic()
|
||||
try:
|
||||
panel.connect(host, port)
|
||||
except OSError as err:
|
||||
raise CannotConnect from err
|
||||
|
||||
probe = threading.Thread(target=panel.process, args=(_on_data,), daemon=True)
|
||||
probe.start()
|
||||
try:
|
||||
confirmed.wait(timeout=_PROBE_TIMEOUT)
|
||||
finally:
|
||||
if (sock := panel._socket) is not None: # noqa: SLF001
|
||||
with contextlib.suppress(OSError):
|
||||
sock.close()
|
||||
|
||||
if not confirmed.is_set():
|
||||
raise InvalidDevice
|
||||
|
||||
|
||||
class AquaLogicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for AquaLogic."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
_verify_device, user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidDevice:
|
||||
errors["base"] = "invalid_device"
|
||||
else:
|
||||
return self.async_create_entry(title="AquaLogic", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import AquaLogic config from configuration.yaml."""
|
||||
self._async_abort_entries_match(
|
||||
{CONF_HOST: import_data[CONF_HOST], CONF_PORT: import_data[CONF_PORT]}
|
||||
)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
_verify_device, import_data[CONF_HOST], import_data[CONF_PORT]
|
||||
)
|
||||
except CannotConnect, InvalidDevice:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return self.async_create_entry(title="AquaLogic", data=import_data)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Constants for the AquaLogic integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "aqualogic"
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
UPDATE_TOPIC = f"{DOMAIN}_update"
|
||||
@@ -2,9 +2,10 @@
|
||||
"domain": "aqualogic",
|
||||
"name": "AquaLogic",
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aqualogic",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aqualogic"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["aqualogic==2.6"]
|
||||
}
|
||||
|
||||
@@ -3,27 +3,18 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
PERCENTAGE,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfPower, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, UPDATE_TOPIC, AquaLogicProcessor
|
||||
from . import AquaLogicConfigEntry, AquaLogicProcessor
|
||||
from .const import UPDATE_TOPIC
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -101,34 +92,18 @@ SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_KEYS)]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
entry: AquaLogicConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
processor: AquaLogicProcessor = hass.data[DOMAIN]
|
||||
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
|
||||
"""Set up the sensor entities."""
|
||||
processor = entry.runtime_data
|
||||
|
||||
entities = [
|
||||
AquaLogicSensor(processor, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in monitored_conditions
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
AquaLogicSensor(processor, description) for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
class AquaLogicSensor(SensorEntity):
|
||||
@@ -172,4 +147,5 @@ class AquaLogicSensor(SensorEntity):
|
||||
self._attr_native_value = getattr(panel, self.entity_description.key)
|
||||
self.async_write_ha_state()
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = None
|
||||
self._attr_native_value = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_device": "The device at the given address is not an AquaLogic panel"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your AquaLogic controller",
|
||||
"port": "TCP port used to connect to the AquaLogic controller"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your YAML configuration, the AquaLogic device could not be reached or did not respond as an AquaLogic panel. Please ensure the device is accessible and restart Home Assistant to retry, or remove the {domain} key from your configuration and set up the integration via the UI.",
|
||||
"title": "The {integration_title} YAML configuration is being removed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,55 +3,39 @@
|
||||
from typing import Any, override
|
||||
|
||||
from aqualogic.core import States
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, UPDATE_TOPIC, AquaLogicProcessor
|
||||
from . import AquaLogicConfigEntry, AquaLogicProcessor
|
||||
from .const import UPDATE_TOPIC
|
||||
|
||||
SWITCH_TYPES = {
|
||||
"lights": "Lights",
|
||||
"filter": "Filter",
|
||||
"filter_low_speed": "Filter Low Speed",
|
||||
"aux_1": "Aux 1",
|
||||
"aux_2": "Aux 2",
|
||||
"aux_3": "Aux 3",
|
||||
"aux_4": "Aux 4",
|
||||
"aux_5": "Aux 5",
|
||||
"aux_6": "Aux 6",
|
||||
"aux_7": "Aux 7",
|
||||
_SWITCH_MAP: dict[str, tuple[str, States]] = {
|
||||
"lights": ("Lights", States.LIGHTS),
|
||||
"filter": ("Filter", States.FILTER),
|
||||
"filter_low_speed": ("Filter Low Speed", States.FILTER_LOW_SPEED),
|
||||
"aux_1": ("Aux 1", States.AUX_1),
|
||||
"aux_2": ("Aux 2", States.AUX_2),
|
||||
"aux_3": ("Aux 3", States.AUX_3),
|
||||
"aux_4": ("Aux 4", States.AUX_4),
|
||||
"aux_5": ("Aux 5", States.AUX_5),
|
||||
"aux_6": ("Aux 6", States.AUX_6),
|
||||
"aux_7": ("Aux 7", States.AUX_7),
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): vol.All(
|
||||
cv.ensure_list, [vol.In(SWITCH_TYPES)]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
entry: AquaLogicConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
processor: AquaLogicProcessor = hass.data[DOMAIN]
|
||||
"""Set up the switch entities."""
|
||||
processor = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AquaLogicSwitch(processor, switch_type)
|
||||
for switch_type in config[CONF_MONITORED_CONDITIONS]
|
||||
AquaLogicSwitch(processor, switch_type) for switch_type in _SWITCH_MAP
|
||||
)
|
||||
|
||||
|
||||
@@ -62,20 +46,10 @@ class AquaLogicSwitch(SwitchEntity):
|
||||
|
||||
def __init__(self, processor: AquaLogicProcessor, switch_type: str) -> None:
|
||||
"""Initialize switch."""
|
||||
name, state = _SWITCH_MAP[switch_type]
|
||||
self._processor = processor
|
||||
self._state_name = {
|
||||
"lights": States.LIGHTS,
|
||||
"filter": States.FILTER,
|
||||
"filter_low_speed": States.FILTER_LOW_SPEED,
|
||||
"aux_1": States.AUX_1,
|
||||
"aux_2": States.AUX_2,
|
||||
"aux_3": States.AUX_3,
|
||||
"aux_4": States.AUX_4,
|
||||
"aux_5": States.AUX_5,
|
||||
"aux_6": States.AUX_6,
|
||||
"aux_7": States.AUX_7,
|
||||
}[switch_type]
|
||||
self._attr_name = f"AquaLogic {SWITCH_TYPES[switch_type]}"
|
||||
self._state_name = state
|
||||
self._attr_name = f"AquaLogic {name}"
|
||||
|
||||
@property
|
||||
@override
|
||||
|
||||
@@ -201,22 +201,15 @@ class AqvifyAggrDataCoordinator(
|
||||
|
||||
self.api_client = api_client
|
||||
|
||||
@staticmethod
|
||||
def _get_times() -> tuple[str, str]:
|
||||
"""Determine strings for time parameters for aggregated data from API."""
|
||||
date_time_fmt = "%Y-%m-%dT%H:%MZ"
|
||||
base_time = utcnow() - timedelta(hours=1)
|
||||
beg_time = base_time.replace(minute=0).strftime(date_time_fmt)
|
||||
end_time = base_time.replace(minute=59).strftime(date_time_fmt)
|
||||
return beg_time, end_time
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, AqvifyHourAggregatedValues]:
|
||||
"""Fetch device state."""
|
||||
devices = self.config_entry.runtime_data.coordinator.data.devices
|
||||
|
||||
device_data: dict[str, AqvifyHourAggregatedValues] = {}
|
||||
beg_time, end_time = self._get_times()
|
||||
base_time = utcnow() - timedelta(hours=1)
|
||||
beg_time = base_time.replace(minute=0, second=0, microsecond=0)
|
||||
end_time = base_time.replace(minute=59, second=0, microsecond=0)
|
||||
for device in devices.devices.values():
|
||||
device_key = device.device_key
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.components import (
|
||||
wake_word,
|
||||
websocket_api,
|
||||
)
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
|
||||
from homeassistant.const import MATCH_ALL, EntityStateAttribute
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
@@ -1255,7 +1255,7 @@ class PipelineRun:
|
||||
if (
|
||||
intent_agent_state := self.hass.states.get(self.intent_agent.id)
|
||||
) and intent_agent_state.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0
|
||||
EntityStateAttribute.SUPPORTED_FEATURES, 0
|
||||
) & conversation.ConversationEntityFeature.CONTROL:
|
||||
intent_filter = _async_local_fallback_intent_filter
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""LLM tools for the assist_satellite integration."""
|
||||
|
||||
from homeassistant.components.llm import LLMTools
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.helpers.llm import LLM_API_ASSIST, IntentTool, LLMContext, Tool
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_tools(
|
||||
hass: HomeAssistant, llm_context: LLMContext, api_id: str
|
||||
) -> LLMTools | None:
|
||||
"""Return the broadcast LLM tool."""
|
||||
if api_id != LLM_API_ASSIST:
|
||||
return None
|
||||
|
||||
# assist_satellite registers the broadcast intent when it is set up, and
|
||||
# this platform is only queried once that has happened.
|
||||
tools: list[Tool] = [
|
||||
IntentTool(handler.intent_type, handler)
|
||||
for handler in intent.async_get(hass)
|
||||
if handler.intent_type == intent.INTENT_BROADCAST
|
||||
]
|
||||
return LLMTools(tools=tools)
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.components.diagnostics import (
|
||||
async_redact_data,
|
||||
device_entry_as_dict,
|
||||
entity_entry_as_dict,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_CONNECTIONS,
|
||||
ATTR_IDENTIFIERS,
|
||||
@@ -39,7 +41,7 @@ async def async_get_config_entry_diagnostics(
|
||||
return data
|
||||
|
||||
data["device"] = {
|
||||
**async_redact_data(attr.asdict(hass_device), TO_REDACT_DEV),
|
||||
**async_redact_data(device_entry_as_dict(hass_device), TO_REDACT_DEV),
|
||||
"entities": {},
|
||||
"tracked_devices": [],
|
||||
}
|
||||
@@ -60,13 +62,11 @@ async def async_get_config_entry_diagnostics(
|
||||
# The context doesn't provide useful information in this case.
|
||||
state_dict.pop("context", None)
|
||||
|
||||
entity_dict = entity_entry_as_dict(entity_entry)
|
||||
# The entity_id is already provided at root level (the key).
|
||||
del entity_dict["entity_id"]
|
||||
data["device"]["entities"][entity_entry.entity_id] = {
|
||||
**async_redact_data(
|
||||
attr.asdict(
|
||||
entity_entry, filter=lambda attr, value: attr.name != "entity_id"
|
||||
),
|
||||
TO_REDACT,
|
||||
),
|
||||
**async_redact_data(entity_dict, TO_REDACT),
|
||||
"state": state_dict,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ from yalexs.activity import ActivityType
|
||||
from yalexs.lock import Lock, LockOperation, LockStatus
|
||||
from yalexs.util import get_latest_activity, update_lock_detail_from_activity
|
||||
|
||||
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity, LockEntityFeature
|
||||
from homeassistant.components.lock import (
|
||||
LockEntity,
|
||||
LockEntityFeature,
|
||||
LockEntityStateAttribute,
|
||||
)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -134,7 +138,7 @@ class AugustLock(AugustEntity, RestoreEntity, LockEntity):
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore ATTR_CHANGED_BY on startup.
|
||||
"""Restore changed_by on startup.
|
||||
|
||||
It is likely no longer in the activity log.
|
||||
"""
|
||||
@@ -143,5 +147,7 @@ class AugustLock(AugustEntity, RestoreEntity, LockEntity):
|
||||
if not (last_state := await self.async_get_last_state()):
|
||||
return
|
||||
|
||||
if ATTR_CHANGED_BY in last_state.attributes:
|
||||
self._attr_changed_by = last_state.attributes[ATTR_CHANGED_BY]
|
||||
if LockEntityStateAttribute.CHANGED_BY in last_state.attributes:
|
||||
self._attr_changed_by = last_state.attributes[
|
||||
LockEntityStateAttribute.CHANGED_BY
|
||||
]
|
||||
|
||||
@@ -17,10 +17,10 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE,
|
||||
PERCENTAGE,
|
||||
STATE_UNAVAILABLE,
|
||||
EntityCategory,
|
||||
EntityStateAttribute,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -170,7 +170,7 @@ class AugustOperatorSensor(AugustEntity, RestoreSensor):
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore ATTR_CHANGED_BY on startup.
|
||||
"""Restore attributes on startup.
|
||||
|
||||
It is likely no longer in the activity log.
|
||||
"""
|
||||
@@ -187,8 +187,8 @@ class AugustOperatorSensor(AugustEntity, RestoreSensor):
|
||||
|
||||
self._attr_native_value = last_sensor_state.native_value
|
||||
last_attrs = last_state.attributes
|
||||
if ATTR_ENTITY_PICTURE in last_attrs:
|
||||
self._attr_entity_picture = last_attrs[ATTR_ENTITY_PICTURE]
|
||||
if EntityStateAttribute.ENTITY_PICTURE in last_attrs:
|
||||
self._attr_entity_picture = last_attrs[EntityStateAttribute.ENTITY_PICTURE]
|
||||
if ATTR_OPERATION_REMOTE in last_attrs:
|
||||
self._operated_remote = last_attrs[ATTR_OPERATION_REMOTE]
|
||||
if ATTR_OPERATION_KEYPAD in last_attrs:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The aurora component."""
|
||||
"""The Aurora integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The aurora component."""
|
||||
"""The Aurora integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The aurora component."""
|
||||
"""The Aurora integration."""
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==72"],
|
||||
"requirements": ["axis==74"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -123,8 +123,4 @@ rules:
|
||||
comment: |
|
||||
The b2sdk library does not support custom HTTP session injection.
|
||||
It manages HTTP connections internally through its own session management.
|
||||
strict-typing:
|
||||
status: exempt
|
||||
comment: |
|
||||
The b2sdk dependency does not include a py.typed file and is not PEP 561 compliant.
|
||||
This is outside the integration's control as it's a third-party library requirement.
|
||||
strict-typing: todo
|
||||
|
||||
@@ -62,9 +62,7 @@ async def async_setup_entry(
|
||||
if device.model == BeoModel.BEOREMOTE_ONE and device.serial_number not in {
|
||||
remote.serial_number for remote in remotes
|
||||
}:
|
||||
device_registry.async_update_device(
|
||||
device.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
device_registry.async_remove_device(device.id)
|
||||
|
||||
async_add_entities(new_entities=entities)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ BINARY_SENSOR_TYPES = (
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="input",
|
||||
translation_key="input",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ async def async_setup_entry(
|
||||
class BleBoxButtonEntity(BleBoxEntity[blebox_uniapi.button.Button], ButtonEntity):
|
||||
"""Representation of BleBox buttons."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.button.Button
|
||||
) -> None:
|
||||
|
||||
@@ -70,6 +70,16 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"input": { "name": "Input" }
|
||||
},
|
||||
"button": {
|
||||
"close": { "name": "Close" },
|
||||
"down": { "name": "Down" },
|
||||
"fav": { "name": "Favorite" },
|
||||
"open": { "name": "Open" },
|
||||
"up": { "name": "Up" }
|
||||
},
|
||||
"light": { "channel": { "name": "Channel {index}" } },
|
||||
"sensor": {
|
||||
"active_power": { "name": "Active power" },
|
||||
|
||||
@@ -22,7 +22,7 @@ from .const import DOMAIN
|
||||
from .coordinator import BleBoxCoordinator
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
PARALLEL_UPDATES = 1
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -43,6 +42,7 @@ class BlinkSyncModuleHA(
|
||||
):
|
||||
"""Representation of a Blink Alarm Control Panel."""
|
||||
|
||||
_attr_attribution = DEFAULT_ATTRIBUTION
|
||||
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
_attr_code_arm_required = False
|
||||
_attr_has_entity_name = True
|
||||
@@ -77,7 +77,6 @@ class BlinkSyncModuleHA(
|
||||
"""Update attributes for alarm control panel."""
|
||||
self.sync.attributes["network_info"] = self.api.networks
|
||||
self.sync.attributes["associated_cameras"] = list(self.sync.cameras)
|
||||
self.sync.attributes[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
|
||||
self._attr_extra_state_attributes = self.sync.attributes
|
||||
self._attr_alarm_state = (
|
||||
AlarmControlPanelState.ARMED_AWAY
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.22",
|
||||
"habluetooth==6.26.2"
|
||||
"habluetooth==6.26.5"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class BoschAlarmEntity(Entity):
|
||||
"""Stop observing state changes."""
|
||||
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
|
||||
if self._observe_faults:
|
||||
self.panel.faults_observer.attach(self.schedule_update_ha_state)
|
||||
self.panel.faults_observer.detach(self.schedule_update_ha_state)
|
||||
|
||||
|
||||
class BoschAlarmAreaEntity(BoschAlarmEntity):
|
||||
@@ -92,7 +92,7 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Stop observing state changes."""
|
||||
await super().async_added_to_hass()
|
||||
await super().async_will_remove_from_hass()
|
||||
if self._observe_alarms:
|
||||
self._area.alarm_observer.detach(self.schedule_update_ha_state)
|
||||
if self._observe_ready:
|
||||
@@ -126,7 +126,7 @@ class BoschAlarmPointEntity(BoschAlarmEntity):
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Stop observing state changes."""
|
||||
await super().async_added_to_hass()
|
||||
await super().async_will_remove_from_hass()
|
||||
self._point.status_observer.detach(self.schedule_update_ha_state)
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ class BoschAlarmDoorEntity(BoschAlarmEntity):
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Stop observing state changes."""
|
||||
await super().async_added_to_hass()
|
||||
await super().async_will_remove_from_hass()
|
||||
self._door.status_observer.detach(self.schedule_update_ha_state)
|
||||
|
||||
|
||||
@@ -184,5 +184,5 @@ class BoschAlarmOutputEntity(BoschAlarmEntity):
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Stop observing state changes."""
|
||||
await super().async_added_to_hass()
|
||||
await super().async_will_remove_from_hass()
|
||||
self._output.status_observer.detach(self.schedule_update_ha_state)
|
||||
|
||||
@@ -176,9 +176,7 @@ class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
|
||||
):
|
||||
if not set(device.identifiers) & identifiers:
|
||||
_LOGGER.debug("Removing obsolete device entry %s", device.name)
|
||||
device_reg.async_update_device(
|
||||
device.id, remove_config_entry_id=self.config_entry.entry_id
|
||||
)
|
||||
device_reg.async_remove_device(device.id)
|
||||
|
||||
|
||||
class BringActivityCoordinator(BringBaseCoordinator[dict[str, BringActivityData]]):
|
||||
|
||||
@@ -9,7 +9,7 @@ from bring_api import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.event import ATTR_EVENT_TYPE
|
||||
from homeassistant.components.event import EventEntityStateAttribute
|
||||
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
@@ -70,7 +70,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
list_uuid = entity.unique_id.split("_")[1]
|
||||
|
||||
activity = state.attributes[ATTR_EVENT_TYPE]
|
||||
activity = state.attributes[EventEntityStateAttribute.EVENT_TYPE]
|
||||
|
||||
reaction: ReactionType = call.data[ATTR_REACTION]
|
||||
|
||||
|
||||
@@ -90,6 +90,11 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]): # noqa: UP046
|
||||
self.device.api.model,
|
||||
self.device.api.host[0],
|
||||
)
|
||||
if not self.coordinator.last_update_success:
|
||||
# When the previous refresh already failed, the coordinator
|
||||
# will skip listener notification, so notify explicitly to
|
||||
# ensure entities flip to unavailable on this transition.
|
||||
self.coordinator.async_update_listeners()
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
if self.available is False:
|
||||
|
||||
@@ -257,10 +257,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
# Perform first refresh of fast coordinator (required for entities)
|
||||
await fast_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Refresh slow coordinator - don't fail if DHW is not available
|
||||
# This allows the integration to work even if the device doesn't support DHW
|
||||
await slow_coordinator.async_refresh()
|
||||
|
||||
entry.runtime_data = BSBLanData(
|
||||
client=bsblan,
|
||||
fast_coordinator=fast_coordinator,
|
||||
@@ -271,6 +267,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
available_circuits=circuits,
|
||||
)
|
||||
|
||||
# Fetch slow data in the background so it does not block startup.
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
slow_coordinator.async_refresh(),
|
||||
name=f"{DOMAIN}_slow_data_fetch_{entry.entry_id}",
|
||||
)
|
||||
|
||||
# Register main device before forwarding platforms, so sub-devices
|
||||
# (heating circuits, water heater) can reference it via via_device
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any, Final, override
|
||||
|
||||
from bsblan import BSBLANError, State, get_hvac_action_category
|
||||
from bsblan import BSBLANError, EntityInfo, State, get_hvac_action_category
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
@@ -54,6 +54,14 @@ BSBLAN_TO_HA_HVAC_MODE: Final[dict[int, HVACMode]] = {
|
||||
}
|
||||
|
||||
|
||||
def _resolve_temperature_bound(*sources: EntityInfo[float] | None) -> float | None:
|
||||
"""Return the first usable temperature bound from the given sources."""
|
||||
for source in sources:
|
||||
if source is not None and source.value is not None:
|
||||
return source.value
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BSBLanConfigEntry,
|
||||
@@ -97,12 +105,23 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity):
|
||||
else:
|
||||
self._attr_unique_id = f"{mac}-climate-{circuit}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
|
||||
|
||||
# Set temperature range from per-circuit static data
|
||||
# Set temperature range from per-circuit static data. Standard BSB/LPB
|
||||
# circuits expose the bounds via heating_protective_setpoint (714) and
|
||||
# comfort_setpoint_max (716); min_temp/max_temp (15006/15007) exist
|
||||
# only on PPS devices. Inactive parameters ("---") have value None.
|
||||
if (static := data.static.get(circuit)) is not None:
|
||||
if (min_temp := static.min_temp) is not None and min_temp.value is not None:
|
||||
self._attr_min_temp = min_temp.value
|
||||
if (max_temp := static.max_temp) is not None and max_temp.value is not None:
|
||||
self._attr_max_temp = max_temp.value
|
||||
if (
|
||||
min_temp := _resolve_temperature_bound(
|
||||
static.heating_protective_setpoint, static.min_temp
|
||||
)
|
||||
) is not None:
|
||||
self._attr_min_temp = min_temp
|
||||
if (
|
||||
max_temp := _resolve_temperature_bound(
|
||||
static.comfort_setpoint_max, static.max_temp
|
||||
)
|
||||
) is not None:
|
||||
self._attr_max_temp = max_temp
|
||||
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==6.1.4"],
|
||||
"requirements": ["python-bsblan==6.1.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from datetime import time
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from bsblan import BSBLANError, DaySchedule, DHWSchedule, TimeSlot
|
||||
import voluptuous as vol
|
||||
@@ -29,6 +29,16 @@ ATTR_FRIDAY_SLOTS = "friday_slots"
|
||||
ATTR_SATURDAY_SLOTS = "saturday_slots"
|
||||
ATTR_SUNDAY_SLOTS = "sunday_slots"
|
||||
|
||||
_DAY_NAME_SLOT_ATTR_PAIRS: tuple[tuple[str, str], ...] = (
|
||||
("monday", ATTR_MONDAY_SLOTS),
|
||||
("tuesday", ATTR_TUESDAY_SLOTS),
|
||||
("wednesday", ATTR_WEDNESDAY_SLOTS),
|
||||
("thursday", ATTR_THURSDAY_SLOTS),
|
||||
("friday", ATTR_FRIDAY_SLOTS),
|
||||
("saturday", ATTR_SATURDAY_SLOTS),
|
||||
("sunday", ATTR_SUNDAY_SLOTS),
|
||||
)
|
||||
|
||||
|
||||
# Schema for a single time slot
|
||||
_SLOT_SCHEMA = vol.Schema(
|
||||
@@ -39,16 +49,16 @@ _SLOT_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
_WEEKLY_SCHEDULE_FIELDS: Final[dict[vol.Marker, Any]] = {
|
||||
vol.Optional(slot_attr): vol.All(cv.ensure_list, [_SLOT_SCHEMA])
|
||||
for _, slot_attr in _DAY_NAME_SLOT_ATTR_PAIRS
|
||||
}
|
||||
|
||||
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
vol.Optional(ATTR_MONDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_TUESDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_WEDNESDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_THURSDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_FRIDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_SATURDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_SUNDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
**_WEEKLY_SCHEDULE_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -98,11 +108,26 @@ def _convert_time_slots_to_day_schedule(
|
||||
return DaySchedule(slots=time_slots)
|
||||
|
||||
|
||||
async def set_hot_water_schedule(service_call: ServiceCall) -> None:
|
||||
"""Set hot water heating schedule."""
|
||||
device_id = service_call.data[ATTR_DEVICE_ID]
|
||||
def _build_weekly_schedule_days(
|
||||
service_call: ServiceCall,
|
||||
) -> dict[str, DaySchedule | None]:
|
||||
"""Build day-name -> schedule values from the service call data.
|
||||
|
||||
Days omitted from the service call map to None, which tells python-bsblan not to
|
||||
modify that day.
|
||||
"""
|
||||
return {
|
||||
day_name: _convert_time_slots_to_day_schedule(service_call.data.get(attr_name))
|
||||
for day_name, attr_name in _DAY_NAME_SLOT_ATTR_PAIRS
|
||||
}
|
||||
|
||||
|
||||
def _resolve_config_entry(
|
||||
service_call: ServiceCall,
|
||||
) -> tuple[BSBLanConfigEntry, dr.DeviceEntry]:
|
||||
"""Resolve device_id from a service call into a loaded BSBLAN config entry."""
|
||||
device_id: str = service_call.data[ATTR_DEVICE_ID]
|
||||
|
||||
# Get the device and config entry
|
||||
device_registry = dr.async_get(service_call.hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
|
||||
@@ -137,56 +162,38 @@ async def set_hot_water_schedule(service_call: ServiceCall) -> None:
|
||||
translation_placeholders={"device_name": device_entry.name or device_id},
|
||||
)
|
||||
|
||||
return entry, device_entry
|
||||
|
||||
|
||||
def _device_name(device_entry: dr.DeviceEntry) -> str:
|
||||
"""Return the best available display name for a device."""
|
||||
return device_entry.name_by_user or device_entry.name or device_entry.id
|
||||
|
||||
|
||||
def _ensure_water_heater_device(device_entry: dr.DeviceEntry) -> None:
|
||||
"""Validate the service targets the water heater sub-device."""
|
||||
for domain, identifier in device_entry.identifiers:
|
||||
if domain == DOMAIN and identifier.endswith("-water-heater"):
|
||||
return
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_a_water_heater_device",
|
||||
translation_placeholders={"device_name": _device_name(device_entry)},
|
||||
)
|
||||
|
||||
|
||||
async def set_hot_water_schedule(service_call: ServiceCall) -> None:
|
||||
"""Set hot water heating schedule."""
|
||||
entry, device_entry = _resolve_config_entry(service_call)
|
||||
_ensure_water_heater_device(device_entry)
|
||||
client = entry.runtime_data.client
|
||||
|
||||
# Convert time slots to DaySchedule objects
|
||||
monday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_MONDAY_SLOTS)
|
||||
)
|
||||
tuesday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_TUESDAY_SLOTS)
|
||||
)
|
||||
wednesday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_WEDNESDAY_SLOTS)
|
||||
)
|
||||
thursday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_THURSDAY_SLOTS)
|
||||
)
|
||||
friday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_FRIDAY_SLOTS)
|
||||
)
|
||||
saturday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_SATURDAY_SLOTS)
|
||||
)
|
||||
sunday = _convert_time_slots_to_day_schedule(
|
||||
service_call.data.get(ATTR_SUNDAY_SLOTS)
|
||||
)
|
||||
days = _build_weekly_schedule_days(service_call)
|
||||
dhw_schedule = DHWSchedule(**days)
|
||||
|
||||
# Create the DHWSchedule object
|
||||
dhw_schedule = DHWSchedule(
|
||||
monday=monday,
|
||||
tuesday=tuesday,
|
||||
wednesday=wednesday,
|
||||
thursday=thursday,
|
||||
friday=friday,
|
||||
saturday=saturday,
|
||||
sunday=sunday,
|
||||
)
|
||||
|
||||
LOGGER.debug(
|
||||
"Setting hot water schedule - Monday: %s, Tuesday: %s, Wednesday: %s, "
|
||||
"Thursday: %s, Friday: %s, Saturday: %s, Sunday: %s",
|
||||
monday,
|
||||
tuesday,
|
||||
wednesday,
|
||||
thursday,
|
||||
friday,
|
||||
saturday,
|
||||
sunday,
|
||||
)
|
||||
LOGGER.debug("Setting hot water schedule: %s", dhw_schedule)
|
||||
|
||||
try:
|
||||
# Call the BSB-LAN API to set the schedule
|
||||
await client.set_hot_water_schedule(dhw_schedule)
|
||||
except BSBLANError as err:
|
||||
raise HomeAssistantError(
|
||||
@@ -201,45 +208,11 @@ async def set_hot_water_schedule(service_call: ServiceCall) -> None:
|
||||
|
||||
async def async_sync_time(service_call: ServiceCall) -> None:
|
||||
"""Synchronize BSB-LAN device time with Home Assistant."""
|
||||
device_id: str = service_call.data[ATTR_DEVICE_ID]
|
||||
|
||||
# Get the device and config entry
|
||||
device_registry = dr.async_get(service_call.hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
|
||||
if device_entry is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device_id",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
# Find the config entry for this device
|
||||
matching_entries: list[BSBLanConfigEntry] = [
|
||||
entry
|
||||
for entry in service_call.hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.entry_id in device_entry.config_entries
|
||||
]
|
||||
|
||||
if not matching_entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_config_entry_for_device",
|
||||
translation_placeholders={"device_id": device_entry.name or device_id},
|
||||
)
|
||||
|
||||
entry = matching_entries[0]
|
||||
|
||||
# Verify the config entry is loaded
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_loaded",
|
||||
translation_placeholders={"device_name": device_entry.name or device_id},
|
||||
)
|
||||
|
||||
entry, device_entry = _resolve_config_entry(service_call)
|
||||
client = entry.runtime_data.client
|
||||
await async_sync_device_time(client, device_entry.name or device_id)
|
||||
await async_sync_device_time(
|
||||
client, device_entry.name or service_call.data[ATTR_DEVICE_ID]
|
||||
)
|
||||
|
||||
|
||||
SYNC_TIME_SCHEMA = vol.Schema(
|
||||
|
||||
@@ -6,6 +6,8 @@ sync_time:
|
||||
selector:
|
||||
device:
|
||||
integration: bsblan
|
||||
entity:
|
||||
- domain: button
|
||||
|
||||
set_hot_water_schedule:
|
||||
fields:
|
||||
@@ -15,6 +17,8 @@ set_hot_water_schedule:
|
||||
selector:
|
||||
device:
|
||||
integration: bsblan
|
||||
entity:
|
||||
- domain: water_heater
|
||||
monday_slots:
|
||||
selector:
|
||||
object:
|
||||
|
||||
@@ -127,6 +127,9 @@
|
||||
"no_config_entry_for_device": {
|
||||
"message": "No configuration entry found for device: {device_id}"
|
||||
},
|
||||
"not_a_water_heater_device": {
|
||||
"message": "The selected device ({device_name}) is not a water heater. Please select the water heater sub-device."
|
||||
},
|
||||
"set_data_error": {
|
||||
"message": "An error occurred while sending the data to the BSB-LAN device"
|
||||
},
|
||||
|
||||
@@ -80,32 +80,49 @@ class BSBLANWaterHeater(BSBLanWaterHeaterDeviceEntity, WaterHeaterEntity):
|
||||
# Initialize available attribute to resolve multiple inheritance conflict
|
||||
self._attr_available = True
|
||||
|
||||
# Set temperature limits based on device capabilities from slow coordinator
|
||||
@property
|
||||
@override
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum temperature.
|
||||
|
||||
Derived from the slow-coordinator DHW config, which may still be
|
||||
pending when the platform is set up. Falls back to the default until
|
||||
the config becomes available.
|
||||
"""
|
||||
dhw_config = (
|
||||
data.slow_coordinator.data.dhw_config
|
||||
if data.slow_coordinator.data
|
||||
self.slow_coordinator.data.dhw_config
|
||||
if self.slow_coordinator.data
|
||||
else None
|
||||
)
|
||||
|
||||
# For min_temp: Use reduced_setpoint from config data (slow polling)
|
||||
if (
|
||||
dhw_config is not None
|
||||
and dhw_config.reduced_setpoint is not None
|
||||
and dhw_config.reduced_setpoint.value is not None
|
||||
):
|
||||
self._attr_min_temp = dhw_config.reduced_setpoint.value
|
||||
else:
|
||||
self._attr_min_temp = 10.0 # Default minimum
|
||||
return dhw_config.reduced_setpoint.value
|
||||
return 10.0 # Default minimum
|
||||
|
||||
# For max_temp: Use nominal_setpoint_max from config data (slow polling)
|
||||
@property
|
||||
@override
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum temperature.
|
||||
|
||||
Derived from the slow-coordinator DHW config, which may still be
|
||||
pending when the platform is set up. Falls back to the default until
|
||||
the config becomes available.
|
||||
"""
|
||||
dhw_config = (
|
||||
self.slow_coordinator.data.dhw_config
|
||||
if self.slow_coordinator.data
|
||||
else None
|
||||
)
|
||||
if (
|
||||
dhw_config is not None
|
||||
and dhw_config.nominal_setpoint_max is not None
|
||||
and dhw_config.nominal_setpoint_max.value is not None
|
||||
):
|
||||
self._attr_max_temp = dhw_config.nominal_setpoint_max.value
|
||||
else:
|
||||
self._attr_max_temp = 65.0 # Default maximum
|
||||
return dhw_config.nominal_setpoint_max.value
|
||||
return 65.0 # Default maximum
|
||||
|
||||
@property
|
||||
def _dhw(self) -> HotWaterState:
|
||||
|
||||
@@ -28,7 +28,6 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
@@ -905,17 +904,15 @@ class BrSensor(SensorEntity):
|
||||
|
||||
# update all other sensors
|
||||
self._attr_native_value = data.get(sensor_type)
|
||||
self._attr_attribution = data.get(ATTRIBUTION)
|
||||
if sensor_type.startswith(PRECIPITATION_FORECAST):
|
||||
result = {ATTR_ATTRIBUTION: data.get(ATTRIBUTION)}
|
||||
result = {}
|
||||
if self._timeframe is not None:
|
||||
result[TIMEFRAME_LABEL] = f"{self._timeframe} min"
|
||||
|
||||
self._attr_extra_state_attributes = result
|
||||
|
||||
result = {
|
||||
ATTR_ATTRIBUTION: data.get(ATTRIBUTION),
|
||||
STATIONNAME_LABEL: data.get(STATIONNAME),
|
||||
}
|
||||
result = {STATIONNAME_LABEL: data.get(STATIONNAME)}
|
||||
if self._measured is not None:
|
||||
# convert datetime (Europe/Amsterdam) into local datetime
|
||||
local_dt = dt_util.as_local(self._measured)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Library for working with CalDAV api."""
|
||||
# pylint: disable=home-assistant-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""LLM tools for the calendar integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from operator import attrgetter
|
||||
from typing import cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.homeassistant import async_should_expose
|
||||
from homeassistant.components.llm import LLMTools
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er, intent
|
||||
from homeassistant.helpers.llm import LLM_API_ASSIST, LLMContext, Tool, ToolInput
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
|
||||
from . import SERVICE_GET_EVENTS
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class CalendarGetEventsTool(Tool):
|
||||
"""LLM Tool allowing querying a calendar."""
|
||||
|
||||
name = "calendar_get_events"
|
||||
description = (
|
||||
"Get events from a calendar. "
|
||||
"When asked if something happens, search the whole week. "
|
||||
"Results are RFC 5545 which means 'end' is exclusive."
|
||||
)
|
||||
|
||||
def __init__(self, calendars: list[str]) -> None:
|
||||
"""Init the get events tool."""
|
||||
self.parameters = vol.Schema(
|
||||
{
|
||||
vol.Required("calendar"): vol.In(calendars),
|
||||
vol.Required("range"): vol.In(["today", "week"]),
|
||||
}
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_call(
|
||||
self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext
|
||||
) -> JsonObjectType:
|
||||
"""Query a calendar."""
|
||||
data = self.parameters(tool_input.tool_args)
|
||||
result = intent.async_match_targets(
|
||||
hass,
|
||||
intent.MatchTargetsConstraints(
|
||||
name=data["calendar"],
|
||||
domains=[DOMAIN],
|
||||
assistant=llm_context.assistant,
|
||||
),
|
||||
)
|
||||
if not result.is_match:
|
||||
return {"success": False, "error": "Calendar not found"}
|
||||
|
||||
entity_id = result.states[0].entity_id
|
||||
if data["range"] == "today":
|
||||
start = dt_util.now()
|
||||
end = dt_util.start_of_local_day() + timedelta(days=1)
|
||||
elif data["range"] == "week":
|
||||
start = dt_util.now()
|
||||
end = dt_util.start_of_local_day() + timedelta(days=7)
|
||||
|
||||
service_data = {
|
||||
"entity_id": entity_id,
|
||||
"start_date_time": start.isoformat(),
|
||||
"end_date_time": end.isoformat(),
|
||||
}
|
||||
|
||||
service_result = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_EVENTS,
|
||||
service_data,
|
||||
context=llm_context.context,
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
events = [
|
||||
event if "T" in event["start"] else {**event, "all_day": True}
|
||||
for event in cast(dict, service_result)[entity_id]["events"]
|
||||
]
|
||||
|
||||
return {"success": True, "result": events}
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_tools(
|
||||
hass: HomeAssistant, llm_context: LLMContext, api_id: str
|
||||
) -> LLMTools | None:
|
||||
"""Return the calendar LLM tools when a calendar is exposed."""
|
||||
if api_id != LLM_API_ASSIST:
|
||||
return None
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
names: list[str] = []
|
||||
for state in sorted(hass.states.async_all(DOMAIN), key=attrgetter("name")):
|
||||
if not async_should_expose(hass, llm_context.assistant, state.entity_id):
|
||||
continue
|
||||
entity_entry = entity_registry.async_get(state.entity_id)
|
||||
names.extend(intent.async_get_entity_aliases(hass, entity_entry, state=state))
|
||||
|
||||
if not names:
|
||||
return None
|
||||
return LLMTools(tools=[CalendarGetEventsTool(names)])
|
||||
@@ -160,26 +160,32 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the target temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
data = self.data
|
||||
assert data is not None
|
||||
await self.coordinator.async_set_temperature(
|
||||
self._ac_index, self.data, temperature, kwargs.get(ATTR_HVAC_MODE)
|
||||
self._ac_index, data, temperature, kwargs.get(ATTR_HVAC_MODE)
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set the hvac mode."""
|
||||
await self.coordinator.async_set_hvac_mode(self._ac_index, self.data, hvac_mode)
|
||||
data = self.data
|
||||
assert data is not None
|
||||
await self.coordinator.async_set_hvac_mode(self._ac_index, data, hvac_mode)
|
||||
|
||||
@override
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set the fan mode."""
|
||||
await self.coordinator.async_set_fan_mode(self._ac_index, self.data, fan_mode)
|
||||
data = self.data
|
||||
assert data is not None
|
||||
await self.coordinator.async_set_fan_mode(self._ac_index, data, fan_mode)
|
||||
|
||||
@override
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Set the swing mode."""
|
||||
await self.coordinator.async_set_swing_mode(
|
||||
self._ac_index, self.data, swing_mode
|
||||
)
|
||||
data = self.data
|
||||
assert data is not None
|
||||
await self.coordinator.async_set_swing_mode(self._ac_index, data, swing_mode)
|
||||
|
||||
@override
|
||||
async def async_turn_off(self) -> None:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["py_ccm15==1.0.0"]
|
||||
"requirements": ["py_ccm15==1.1.2"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -103,28 +103,6 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = device_id
|
||||
|
||||
@property
|
||||
@override
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of temperature in Home Assistant format.
|
||||
|
||||
It can change over time based on the device settings,
|
||||
so we fetch it dynamically from the client.
|
||||
"""
|
||||
unit = self.client.temperature_unit()
|
||||
|
||||
if not unit:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
normalized = unit.strip().lower()
|
||||
|
||||
if normalized in {"c", "°c", "celsius"}:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
if normalized in {"f", "°f", "fahrenheit"}:
|
||||
return UnitOfTemperature.FAHRENHEIT
|
||||
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
@property
|
||||
@override
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
|
||||
@@ -11,12 +11,16 @@ from homeassistant.const import Platform
|
||||
DOMAIN: Final = "cielo_home"
|
||||
PLATFORMS: Final[list[Platform]] = [
|
||||
Platform.CLIMATE,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
DEFAULT_NAME: Final = "Cielo Home"
|
||||
DEFAULT_SCAN_INTERVAL: Final[int] = 2 * 60
|
||||
TIMEOUT: Final[int] = 20
|
||||
LOGGER: Final = logging.getLogger(__package__)
|
||||
|
||||
SENSOR_TEMPERATURE: Final = "temperature"
|
||||
SENSOR_HUMIDITY: Final = "humidity"
|
||||
|
||||
CIELO_ERRORS: Final[tuple] = (
|
||||
ClientError,
|
||||
TimeoutError,
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import override
|
||||
from cieloconnectapi.device import CieloDeviceAPI
|
||||
from cieloconnectapi.model import CieloDevice
|
||||
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -12,6 +13,26 @@ from .const import DOMAIN
|
||||
from .coordinator import CieloDataUpdateCoordinator
|
||||
|
||||
|
||||
def normalize_temp_unit(client: CieloDeviceAPI) -> str:
|
||||
"""Normalize a raw device temperature unit to a UnitOfTemperature value.
|
||||
|
||||
Unrecognized or empty values fall back to Celsius.
|
||||
"""
|
||||
unit = client.temperature_unit()
|
||||
|
||||
if not unit:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
normalized = unit.strip().lower()
|
||||
|
||||
if normalized in {"c", "°c", "celsius"}:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
if normalized in {"f", "°f", "fahrenheit"}:
|
||||
return UnitOfTemperature.FAHRENHEIT
|
||||
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
|
||||
class CieloBaseEntity(CoordinatorEntity[CieloDataUpdateCoordinator]):
|
||||
"""Representation of a Cielo base entity."""
|
||||
|
||||
@@ -74,3 +95,14 @@ class CieloDeviceEntity(CieloBaseEntity):
|
||||
configuration_url="https://home.cielowigle.com/",
|
||||
suggested_area=device.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of temperature for the device.
|
||||
|
||||
The unit can change over time based on the device settings,
|
||||
so it is fetched dynamically from the client. This dynamic
|
||||
nature means that if a user changes the device's temperature
|
||||
unit, historical statistics may be affected.
|
||||
"""
|
||||
return normalize_temp_unit(self.client)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user