mirror of
https://github.com/home-assistant/core.git
synced 2026-02-23 18:51:08 +01:00
Compare commits
89 Commits
knx-name-r
...
2026.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c640fe0fa | ||
|
|
62145e5f9e | ||
|
|
c0fc414bb9 | ||
|
|
69411a05ff | ||
|
|
06c9ec861d | ||
|
|
946df1755f | ||
|
|
d0678e0641 | ||
|
|
ec56f183da | ||
|
|
033005e0de | ||
|
|
91f9f5a826 | ||
|
|
ac4fcab827 | ||
|
|
d0eea77178 | ||
|
|
fb38fa3844 | ||
|
|
440efb953e | ||
|
|
7ce47cca0d | ||
|
|
a5f607bb91 | ||
|
|
b03043aa6f | ||
|
|
0f3c7ca277 | ||
|
|
3abf7c22f3 | ||
|
|
292e1de126 | ||
|
|
2d776a8193 | ||
|
|
039bbbb48c | ||
|
|
ad5565df95 | ||
|
|
3e6bc29a6a | ||
|
|
ec8067a5a8 | ||
|
|
6f47716d0a | ||
|
|
efba5c6bcc | ||
|
|
d10e78079f | ||
|
|
6d4581580f | ||
|
|
0d9a41a540 | ||
|
|
cd69e6db73 | ||
|
|
1320367d0d | ||
|
|
dfa4698887 | ||
|
|
b426115de7 | ||
|
|
fb79fa37f8 | ||
|
|
6a5f7bf424 | ||
|
|
142ca6dec1 | ||
|
|
0f986c24d0 | ||
|
|
01f2b7b6f6 | ||
|
|
b9469027f5 | ||
|
|
fbb94af748 | ||
|
|
148bdf6e3a | ||
|
|
91999f8871 | ||
|
|
aecca4eb99 | ||
|
|
bf8aa49bae | ||
|
|
4423425683 | ||
|
|
44202da53d | ||
|
|
9f7dfb72c4 | ||
|
|
de07a69e4f | ||
|
|
bbf4c38115 | ||
|
|
e1bb5d52ef | ||
|
|
eb64b6bdee | ||
|
|
ecb288b735 | ||
|
|
a419c9c420 | ||
|
|
dd29133324 | ||
|
|
90f22ea516 | ||
|
|
9db1428265 | ||
|
|
a696b05b0d | ||
|
|
77ddb63b73 | ||
|
|
4180a6e176 | ||
|
|
6d74c912d2 | ||
|
|
8a01dfcc00 | ||
|
|
9722898dc6 | ||
|
|
7438c71fcb | ||
|
|
0b5e55b923 | ||
|
|
61ed959e8e | ||
|
|
3989532465 | ||
|
|
28027ddca4 | ||
|
|
fe0d7b3cca | ||
|
|
0dcc4e9527 | ||
|
|
b13b189703 | ||
|
|
150829f599 | ||
|
|
57dd9d9c23 | ||
|
|
e2056cb12c | ||
|
|
fa2c8992cf | ||
|
|
ddf5c7fe3a | ||
|
|
7034ed6d3f | ||
|
|
9015b53c1b | ||
|
|
1cfa6561f7 | ||
|
|
eead02dcca | ||
|
|
456e51a221 | ||
|
|
5d984ce186 | ||
|
|
61f45489ac | ||
|
|
f72c643b38 | ||
|
|
27bc26e886 | ||
|
|
0e9f03cbc1 | ||
|
|
9480c33fb0 | ||
|
|
3e6b8663e8 | ||
|
|
1c69a83793 |
@@ -22,7 +22,6 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/calendar/**
|
||||
- homeassistant/components/camera/**
|
||||
- homeassistant/components/climate/**
|
||||
- homeassistant/components/conversation/**
|
||||
- homeassistant/components/cover/**
|
||||
- homeassistant/components/date/**
|
||||
- homeassistant/components/datetime/**
|
||||
@@ -54,7 +53,6 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/update/**
|
||||
- homeassistant/components/vacuum/**
|
||||
- homeassistant/components/valve/**
|
||||
- homeassistant/components/wake_word/**
|
||||
- homeassistant/components/water_heater/**
|
||||
- homeassistant/components/weather/**
|
||||
|
||||
@@ -72,6 +70,7 @@ components: &components
|
||||
- homeassistant/components/cloud/**
|
||||
- homeassistant/components/config/**
|
||||
- homeassistant/components/configurator/**
|
||||
- homeassistant/components/conversation/**
|
||||
- homeassistant/components/demo/**
|
||||
- homeassistant/components/device_automation/**
|
||||
- homeassistant/components/dhcp/**
|
||||
|
||||
@@ -60,13 +60,7 @@
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[yaml]": {
|
||||
"[json][jsonc][yaml]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"json.schemas": [
|
||||
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -9,5 +9,3 @@ updates:
|
||||
labels:
|
||||
- dependency
|
||||
- github_actions
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
164
.github/workflows/builder.yml
vendored
164
.github/workflows/builder.yml
vendored
@@ -10,27 +10,19 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2026.01.0"
|
||||
BASE_IMAGE_VERSION: "2025.12.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
init:
|
||||
name: Initialize build
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
channel: ${{ steps.version.outputs.channel }}
|
||||
@@ -39,8 +31,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -49,16 +39,16 @@ jobs:
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
uses: home-assistant/actions/helpers/info@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/info@master
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
uses: home-assistant/actions/helpers/version@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/version@master
|
||||
with:
|
||||
type: ${{ env.BUILD_TYPE }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
with:
|
||||
ignore-dev: true
|
||||
|
||||
@@ -92,9 +82,9 @@ jobs:
|
||||
needs: init
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
id-token: write # For cosign signing
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -107,12 +97,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -123,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: OHF-Voice/intents-package
|
||||
@@ -143,12 +131,11 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
UV_PRERELEASE: allow
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
python3 -m pip install "$(grep '^uv' < requirements.txt)"
|
||||
uv pip install packaging tomli
|
||||
uv pip install .
|
||||
python3 script/version_bump.py nightly --set-nightly-version "${VERSION}"
|
||||
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
|
||||
|
||||
if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then
|
||||
echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}"
|
||||
@@ -178,7 +165,7 @@ jobs:
|
||||
sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \
|
||||
homeassistant/package_constraints.txt
|
||||
|
||||
sed -i "s|home-assistant-intents==.*||" requirements_all.txt requirements.txt
|
||||
sed -i "s|home-assistant-intents==.*||" requirements_all.txt
|
||||
fi
|
||||
|
||||
- name: Download translations
|
||||
@@ -194,16 +181,17 @@ jobs:
|
||||
- name: Write meta info file
|
||||
shell: bash
|
||||
run: |
|
||||
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Cosign
|
||||
- &install_cosign
|
||||
name: Install Cosign
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.5.3"
|
||||
@@ -214,36 +202,30 @@ jobs:
|
||||
- name: Build variables
|
||||
id: vars
|
||||
shell: bash
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
echo "base_image=ghcr.io/home-assistant/${ARCH}-homeassistant-base:${BASE_IMAGE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "cache_image=ghcr.io/home-assistant/${ARCH}-homeassistant:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "base_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ env.BASE_IMAGE_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||
echo "cache_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify base image signature
|
||||
env:
|
||||
BASE_IMAGE: ${{ steps.vars.outputs.base_image }}
|
||||
run: |
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
|
||||
"${BASE_IMAGE}"
|
||||
"${{ steps.vars.outputs.base_image }}"
|
||||
|
||||
- name: Verify cache image signature
|
||||
id: cache
|
||||
continue-on-error: true
|
||||
env:
|
||||
CACHE_IMAGE: ${{ steps.vars.outputs.cache_image }}
|
||||
run: |
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
|
||||
"${CACHE_IMAGE}"
|
||||
"${{ steps.vars.outputs.cache_image }}"
|
||||
|
||||
- name: Build base image
|
||||
id: build
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -253,7 +235,6 @@ jobs:
|
||||
build-args: |
|
||||
BUILD_FROM=${{ steps.vars.outputs.base_image }}
|
||||
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
outputs: type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true
|
||||
labels: |
|
||||
io.hass.arch=${{ matrix.arch }}
|
||||
io.hass.version=${{ needs.init.outputs.version }}
|
||||
@@ -261,12 +242,8 @@ jobs:
|
||||
org.opencontainers.image.version=${{ needs.init.outputs.version }}
|
||||
|
||||
- name: Sign image
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
cosign sign --yes "ghcr.io/home-assistant/${ARCH}-homeassistant:${VERSION}@${DIGEST}"
|
||||
cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}"
|
||||
|
||||
build_machine:
|
||||
name: Build ${{ matrix.machine }} machine core image
|
||||
@@ -274,9 +251,9 @@ jobs:
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
id-token: write # For cosign signing
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
machine:
|
||||
@@ -297,31 +274,28 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set build additional args
|
||||
env:
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
# Create general tags
|
||||
if [[ "${VERSION}" =~ d ]]; then
|
||||
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
|
||||
elif [[ "${VERSION}" =~ b ]]; then
|
||||
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# home-assistant/builder doesn't support sha pinning
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@21bc64d76dad7a5184c67826aab41c6b6f89023a # 2025.11.0
|
||||
uses: home-assistant/builder@2025.11.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -335,23 +309,19 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_machine"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
with:
|
||||
name: ${{ secrets.GIT_NAME }}
|
||||
email: ${{ secrets.GIT_EMAIL }}
|
||||
token: ${{ secrets.GIT_TOKEN }}
|
||||
|
||||
- name: Update version file
|
||||
uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/version-push@master
|
||||
with:
|
||||
key: "homeassistant[]"
|
||||
key-description: "Home Assistant Core"
|
||||
@@ -361,7 +331,7 @@ jobs:
|
||||
|
||||
- name: Update version file (stable -> beta)
|
||||
if: needs.init.outputs.channel == 'stable'
|
||||
uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/version-push@master
|
||||
with:
|
||||
key: "homeassistant[]"
|
||||
key-description: "Home Assistant Core"
|
||||
@@ -376,28 +346,25 @@ jobs:
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
id-token: write # For cosign signing
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.5.3"
|
||||
- *install_cosign
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -405,17 +372,14 @@ jobs:
|
||||
|
||||
- name: Verify architecture image signatures
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
for arch in $ARCHS; do
|
||||
echo "Verifying ${arch} image signature..."
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity-regexp https://github.com/home-assistant/core/.* \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||
done
|
||||
echo "✓ All images verified successfully"
|
||||
|
||||
@@ -446,19 +410,16 @@ jobs:
|
||||
- name: Copy architecture images to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
# Use imagetools to copy image blobs directly between registries
|
||||
# This preserves provenance/attestations and seems to be much faster than pull/push
|
||||
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
for arch in $ARCHS; do
|
||||
echo "Copying ${arch} image to DockerHub..."
|
||||
for attempt in 1 2 3; do
|
||||
if docker buildx imagetools create \
|
||||
--tag "docker.io/homeassistant/${arch}-homeassistant:${VERSION}" \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"; then
|
||||
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
|
||||
break
|
||||
fi
|
||||
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
|
||||
@@ -468,28 +429,23 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${VERSION}"
|
||||
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||
done
|
||||
|
||||
- name: Create and push multi-arch manifests
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
REGISTRY: ${{ matrix.registry }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
# Build list of architecture images dynamically
|
||||
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
ARCH_IMAGES=()
|
||||
for arch in $ARCHS; do
|
||||
ARCH_IMAGES+=("${REGISTRY}/${arch}-homeassistant:${VERSION}")
|
||||
ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
|
||||
done
|
||||
|
||||
# Build list of all tags for single manifest creation
|
||||
# Note: Using sep-tags=',' in metadata-action for easier parsing
|
||||
TAG_ARGS=()
|
||||
IFS=',' read -ra TAGS <<< "${META_TAGS}"
|
||||
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
|
||||
for tag in "${TAGS[@]}"; do
|
||||
TAG_ARGS+=("--tag" "${tag}")
|
||||
done
|
||||
@@ -513,14 +469,12 @@ jobs:
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
id-token: write # For PyPI trusted publishing
|
||||
contents: read
|
||||
id-token: write
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -554,10 +508,10 @@ jobs:
|
||||
name: Build and test hassfest image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
attestations: write # For build provenance attestation
|
||||
id-token: write # For build provenance attestation
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
needs: ["init"]
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
env:
|
||||
@@ -566,18 +520,16 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -585,12 +537,12 @@ jobs:
|
||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||
|
||||
- name: Run hassfest against core
|
||||
run: docker run --rm -v "${GITHUB_WORKSPACE}":/github/workspace "${HASSFEST_IMAGE_TAG}" --core-path=/github/workspace
|
||||
run: docker run --rm -v ${{ github.workspace }}:/github/workspace ${{ env.HASSFEST_IMAGE_TAG }} --core-path=/github/workspace
|
||||
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
887
.github/workflows/ci.yaml
vendored
887
.github/workflows/ci.yaml
vendored
File diff suppressed because it is too large
Load Diff
14
.github/workflows/codeql.yml
vendored
14
.github/workflows/codeql.yml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
schedule:
|
||||
- cron: "30 18 * * 4"
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -17,22 +15,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 360
|
||||
permissions:
|
||||
actions: read # To read workflow information for CodeQL
|
||||
contents: read # To check out the repository
|
||||
security-events: write # To upload CodeQL results
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
13
.github/workflows/detect-duplicate-issues.yml
vendored
13
.github/workflows/detect-duplicate-issues.yml
vendored
@@ -5,18 +5,13 @@ on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
detect-duplicates:
|
||||
name: Detect duplicate issues
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on and label issues
|
||||
models: read # For AI-based duplicate detection
|
||||
|
||||
steps:
|
||||
- name: Check if integration label was added and extract details
|
||||
@@ -236,7 +231,7 @@ jobs:
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@a380166897b5408b8fb7dddd148142794cb5624a # v2.0.6
|
||||
uses: actions/ai-inference@a6101c89c6feaecc585efdd8d461f18bb7896f20 # v2.0.5
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt: |
|
||||
|
||||
13
.github/workflows/detect-non-english-issues.yml
vendored
13
.github/workflows/detect-non-english-issues.yml
vendored
@@ -5,18 +5,13 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
detect-language:
|
||||
name: Detect non-English issues
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on, label, and close issues
|
||||
models: read # For AI-based language detection
|
||||
|
||||
steps:
|
||||
- name: Check issue language
|
||||
@@ -62,7 +57,7 @@ jobs:
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@a380166897b5408b8fb7dddd148142794cb5624a # v2.0.6
|
||||
uses: actions/ai-inference@a6101c89c6feaecc585efdd8d461f18bb7896f20 # v2.0.5
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
|
||||
10
.github/workflows/lock.yml
vendored
10
.github/workflows/lock.yml
vendored
@@ -5,20 +5,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
name: Lock inactive threads
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To lock issues
|
||||
pull-requests: write # To lock pull requests
|
||||
steps:
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
with:
|
||||
|
||||
30
.github/workflows/restrict-task-creation.yml
vendored
30
.github/workflows/restrict-task-creation.yml
vendored
@@ -5,39 +5,9 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
add-no-stale:
|
||||
name: Add no-stale label
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To add labels to issues
|
||||
if: >-
|
||||
github.event.issue.type.name == 'Task'
|
||||
|| github.event.issue.type.name == 'Epic'
|
||||
|| github.event.issue.type.name == 'Opportunity'
|
||||
steps:
|
||||
- name: Add no-stale label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['no-stale']
|
||||
});
|
||||
|
||||
check-authorization:
|
||||
name: Check authorization
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To read CODEOWNERS file
|
||||
issues: write # To comment on, label, and close issues
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
|
||||
10
.github/workflows/stale.yml
vendored
10
.github/workflows/stale.yml
vendored
@@ -6,20 +6,10 @@ on:
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: Mark stale issues and PRs
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To label and close stale issues
|
||||
pull-requests: write # To label and close stale PRs
|
||||
steps:
|
||||
# The 60 day stale policy for PRs
|
||||
# Used for:
|
||||
|
||||
13
.github/workflows/translations.yml
vendored
13
.github/workflows/translations.yml
vendored
@@ -9,14 +9,8 @@ on:
|
||||
paths:
|
||||
- "**strings.json"
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
@@ -26,8 +20,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -35,7 +27,6 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Upload Translations
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
run: |
|
||||
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
||||
python3 -m script.translations upload
|
||||
|
||||
64
.github/workflows/wheels.yml
vendored
64
.github/workflows/wheels.yml
vendored
@@ -17,9 +17,7 @@ on:
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
|
||||
permissions: {}
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
@@ -31,10 +29,9 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
- &checkout
|
||||
name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
@@ -53,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Create requirements_diff file
|
||||
run: |
|
||||
if [[ "${GITHUB_EVENT_NAME}" =~ (schedule|workflow_dispatch) ]]; then
|
||||
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
|
||||
touch requirements_diff.txt
|
||||
else
|
||||
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt
|
||||
@@ -77,7 +74,7 @@ jobs:
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
@@ -85,7 +82,7 @@ jobs:
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: *actions-upload-artifact
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@@ -97,7 +94,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: *actions-upload-artifact
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
@@ -109,7 +106,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
matrix: &matrix-build
|
||||
abi: ["cp313", "cp314"]
|
||||
arch: ["amd64", "aarch64"]
|
||||
include:
|
||||
@@ -118,18 +115,17 @@ jobs:
|
||||
- arch: aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- *checkout
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
- &download-env-file
|
||||
name: Download env_file
|
||||
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
- &download-requirements-diff
|
||||
name: Download requirements_diff
|
||||
uses: *actions-download-artifact
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -140,7 +136,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -160,32 +156,16 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp313", "cp314"]
|
||||
arch: ["amd64", "aarch64"]
|
||||
include:
|
||||
- arch: amd64
|
||||
os: ubuntu-latest
|
||||
- arch: aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
matrix: *matrix-build
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- *checkout
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: env_file
|
||||
- *download-env-file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
- *download-requirements-diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: *actions-download-artifact
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
@@ -198,7 +178,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
uses: *home-assistant-wheels
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.1
|
||||
rev: v0.14.13
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
@@ -17,12 +17,6 @@ repos:
|
||||
- --quiet-level=2
|
||||
exclude_types: [csv, json, html]
|
||||
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.22.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
args:
|
||||
- --pedantic
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.14
|
||||
3.13
|
||||
|
||||
@@ -84,7 +84,6 @@ homeassistant.components.androidtv_remote.*
|
||||
homeassistant.components.anel_pwrctrl.*
|
||||
homeassistant.components.anova.*
|
||||
homeassistant.components.anthemav.*
|
||||
homeassistant.components.anthropic.*
|
||||
homeassistant.components.apache_kafka.*
|
||||
homeassistant.components.apcupsd.*
|
||||
homeassistant.components.api.*
|
||||
@@ -222,7 +221,6 @@ homeassistant.components.generic_hygrostat.*
|
||||
homeassistant.components.generic_thermostat.*
|
||||
homeassistant.components.geo_location.*
|
||||
homeassistant.components.geocaching.*
|
||||
homeassistant.components.ghost.*
|
||||
homeassistant.components.gios.*
|
||||
homeassistant.components.github.*
|
||||
homeassistant.components.glances.*
|
||||
@@ -243,7 +241,6 @@ homeassistant.components.guardian.*
|
||||
homeassistant.components.habitica.*
|
||||
homeassistant.components.hardkernel.*
|
||||
homeassistant.components.hardware.*
|
||||
homeassistant.components.hdfury.*
|
||||
homeassistant.components.heos.*
|
||||
homeassistant.components.here_travel_time.*
|
||||
homeassistant.components.history.*
|
||||
@@ -289,7 +286,6 @@ homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.input_text.*
|
||||
homeassistant.components.integration.*
|
||||
homeassistant.components.intelliclima.*
|
||||
homeassistant.components.intent.*
|
||||
homeassistant.components.intent_script.*
|
||||
homeassistant.components.ios.*
|
||||
@@ -366,6 +362,7 @@ homeassistant.components.my.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.myuplink.*
|
||||
homeassistant.components.nam.*
|
||||
homeassistant.components.nanoleaf.*
|
||||
homeassistant.components.nasweb.*
|
||||
homeassistant.components.neato.*
|
||||
homeassistant.components.nest.*
|
||||
@@ -387,13 +384,11 @@ homeassistant.components.ohme.*
|
||||
homeassistant.components.onboarding.*
|
||||
homeassistant.components.oncue.*
|
||||
homeassistant.components.onedrive.*
|
||||
homeassistant.components.onedrive_for_business.*
|
||||
homeassistant.components.onewire.*
|
||||
homeassistant.components.onkyo.*
|
||||
homeassistant.components.open_meteo.*
|
||||
homeassistant.components.open_router.*
|
||||
homeassistant.components.openai_conversation.*
|
||||
homeassistant.components.openevse.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
homeassistant.components.openuv.*
|
||||
@@ -439,7 +434,6 @@ homeassistant.components.raspberry_pi.*
|
||||
homeassistant.components.rdw.*
|
||||
homeassistant.components.recollect_waste.*
|
||||
homeassistant.components.recorder.*
|
||||
homeassistant.components.redgtech.*
|
||||
homeassistant.components.remember_the_milk.*
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.remote_calendar.*
|
||||
@@ -496,7 +490,6 @@ homeassistant.components.smtp.*
|
||||
homeassistant.components.snooz.*
|
||||
homeassistant.components.solarlog.*
|
||||
homeassistant.components.sonarr.*
|
||||
homeassistant.components.spaceapi.*
|
||||
homeassistant.components.speedtestdotnet.*
|
||||
homeassistant.components.spotify.*
|
||||
homeassistant.components.sql.*
|
||||
|
||||
31
CODEOWNERS
generated
31
CODEOWNERS
generated
@@ -15,7 +15,7 @@
|
||||
.yamllint @home-assistant/core
|
||||
pyproject.toml @home-assistant/core
|
||||
requirements_test.txt @home-assistant/core
|
||||
/.devcontainer/ @home-assistant/core @edenhaus
|
||||
/.devcontainer/ @home-assistant/core
|
||||
/.github/ @home-assistant/core
|
||||
/.vscode/ @home-assistant/core
|
||||
/homeassistant/*.py @home-assistant/core
|
||||
@@ -595,8 +595,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/geonetnz_quakes/ @exxamalte
|
||||
/homeassistant/components/geonetnz_volcano/ @exxamalte
|
||||
/tests/components/geonetnz_volcano/ @exxamalte
|
||||
/homeassistant/components/ghost/ @johnonolan
|
||||
/tests/components/ghost/ @johnonolan
|
||||
/homeassistant/components/gios/ @bieniu
|
||||
/tests/components/gios/ @bieniu
|
||||
/homeassistant/components/github/ @timmo001 @ludeeus
|
||||
@@ -672,8 +670,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/hdmi_cec/ @inytar
|
||||
/tests/components/hdmi_cec/ @inytar
|
||||
/homeassistant/components/heatmiser/ @andylockran
|
||||
/homeassistant/components/hegel/ @boazca
|
||||
/tests/components/hegel/ @boazca
|
||||
/homeassistant/components/heos/ @andrewsayre
|
||||
/tests/components/heos/ @andrewsayre
|
||||
/homeassistant/components/here_travel_time/ @eifinger
|
||||
@@ -717,10 +713,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/homekit_controller/ @Jc2k @bdraco
|
||||
/homeassistant/components/homematic/ @pvizeli
|
||||
/tests/components/homematic/ @pvizeli
|
||||
/homeassistant/components/homematicip_cloud/ @hahn-th @lackas
|
||||
/tests/components/homematicip_cloud/ @hahn-th @lackas
|
||||
/homeassistant/components/homevolt/ @danielhiversen
|
||||
/tests/components/homevolt/ @danielhiversen
|
||||
/homeassistant/components/homematicip_cloud/ @hahn-th
|
||||
/tests/components/homematicip_cloud/ @hahn-th
|
||||
/homeassistant/components/homewizard/ @DCSBL
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
@@ -762,8 +756,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/icloud/ @Quentame @nzapponi
|
||||
/homeassistant/components/idasen_desk/ @abmantis
|
||||
/tests/components/idasen_desk/ @abmantis
|
||||
/homeassistant/components/idrive_e2/ @patrickvorgers
|
||||
/tests/components/idrive_e2/ @patrickvorgers
|
||||
/homeassistant/components/igloohome/ @keithle888
|
||||
/tests/components/igloohome/ @keithle888
|
||||
/homeassistant/components/ign_sismologia/ @exxamalte
|
||||
@@ -808,8 +800,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/insteon/ @teharris1
|
||||
/homeassistant/components/integration/ @dgomes
|
||||
/tests/components/integration/ @dgomes
|
||||
/homeassistant/components/intelliclima/ @dvdinth
|
||||
/tests/components/intelliclima/ @dvdinth
|
||||
/homeassistant/components/intellifire/ @jeeftor
|
||||
/tests/components/intellifire/ @jeeftor
|
||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
@@ -931,8 +921,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/libre_hardware_monitor/ @Sab44
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/liebherr/ @mettolen
|
||||
/tests/components/liebherr/ @mettolen
|
||||
/homeassistant/components/lifx/ @Djelibeybi
|
||||
/tests/components/lifx/ @Djelibeybi
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
@@ -1068,8 +1056,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco
|
||||
/tests/components/mqtt/ @emontnemery @jbouwh @bdraco
|
||||
/homeassistant/components/msteams/ @peroyvind
|
||||
/homeassistant/components/mta/ @OnFreund
|
||||
/tests/components/mta/ @OnFreund
|
||||
/homeassistant/components/mullvad/ @meichthys
|
||||
/tests/components/mullvad/ @meichthys
|
||||
/homeassistant/components/music_assistant/ @music-assistant @arturpragacz
|
||||
@@ -1088,8 +1074,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/nam/ @bieniu
|
||||
/homeassistant/components/namecheapdns/ @tr4nt0r
|
||||
/tests/components/namecheapdns/ @tr4nt0r
|
||||
/homeassistant/components/nanoleaf/ @milanmeu @joostlek @loebi-ch @JaspervRijbroek @jonathanrobichaud4
|
||||
/tests/components/nanoleaf/ @milanmeu @joostlek @loebi-ch @JaspervRijbroek @jonathanrobichaud4
|
||||
/homeassistant/components/nanoleaf/ @milanmeu @joostlek
|
||||
/tests/components/nanoleaf/ @milanmeu @joostlek
|
||||
/homeassistant/components/nasweb/ @nasWebio
|
||||
/tests/components/nasweb/ @nasWebio
|
||||
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
|
||||
@@ -1182,8 +1168,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ondilo_ico/ @JeromeHXP
|
||||
/homeassistant/components/onedrive/ @zweckj
|
||||
/tests/components/onedrive/ @zweckj
|
||||
/homeassistant/components/onedrive_for_business/ @zweckj
|
||||
/tests/components/onedrive_for_business/ @zweckj
|
||||
/homeassistant/components/onewire/ @garbled1 @epenet
|
||||
/tests/components/onewire/ @garbled1 @epenet
|
||||
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
|
||||
@@ -1369,8 +1353,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/recorder/ @home-assistant/core
|
||||
/homeassistant/components/recovery_mode/ @home-assistant/core
|
||||
/tests/components/recovery_mode/ @home-assistant/core
|
||||
/homeassistant/components/redgtech/ @jonhsady @luan-nvg
|
||||
/tests/components/redgtech/ @jonhsady @luan-nvg
|
||||
/homeassistant/components/refoss/ @ashionky
|
||||
/tests/components/refoss/ @ashionky
|
||||
/homeassistant/components/rehlko/ @bdraco @peterager
|
||||
@@ -1577,7 +1559,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
||||
/tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
||||
/homeassistant/components/splunk/ @Bre77
|
||||
/tests/components/splunk/ @Bre77
|
||||
/homeassistant/components/spotify/ @frenck @joostlek
|
||||
/tests/components/spotify/ @frenck @joostlek
|
||||
/homeassistant/components/sql/ @gjohansson-ST @dougiteixeira
|
||||
@@ -1897,8 +1878,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/worldclock/ @fabaff
|
||||
/homeassistant/components/ws66i/ @ssaenger
|
||||
/tests/components/ws66i/ @ssaenger
|
||||
/homeassistant/components/wsdot/ @ucodery
|
||||
/tests/components/wsdot/ @ucodery
|
||||
/homeassistant/components/wyoming/ @synesthesiam
|
||||
/tests/components/wyoming/ @synesthesiam
|
||||
/homeassistant/components/xbox/ @hunterjm @tr4nt0r
|
||||
|
||||
@@ -52,9 +52,6 @@ RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||||
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
|
||||
# Claude Code native install
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"microsoft",
|
||||
"msteams",
|
||||
"onedrive",
|
||||
"onedrive_for_business",
|
||||
"xbox"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class AbodeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
errors = {"base": "cannot_connect"}
|
||||
|
||||
except ConnectTimeout, HTTPError:
|
||||
except (ConnectTimeout, HTTPError):
|
||||
errors = {"base": "cannot_connect"}
|
||||
|
||||
if errors:
|
||||
|
||||
@@ -99,7 +99,7 @@ class AbodeLight(AbodeDevice, LightEntity):
|
||||
return _hs
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode:
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
"""Return the color mode of the light."""
|
||||
if self._device.is_dimmable and self._device.is_color_capable:
|
||||
if self.hs_color is not None:
|
||||
@@ -110,7 +110,7 @@ class AbodeLight(AbodeDevice, LightEntity):
|
||||
return ColorMode.ONOFF
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[ColorMode]:
|
||||
def supported_color_modes(self) -> set[ColorMode] | None:
|
||||
"""Flag supported color modes."""
|
||||
if self._device.is_dimmable and self._device.is_color_capable:
|
||||
return {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||
|
||||
@@ -43,7 +43,7 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
longitude=user_input[CONF_LONGITUDE],
|
||||
)
|
||||
await accuweather.async_get_location()
|
||||
except ApiError, ClientConnectorError, TimeoutError, ClientError:
|
||||
except (ApiError, ClientConnectorError, TimeoutError, ClientError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidApiKeyError:
|
||||
errors[CONF_API_KEY] = "invalid_api_key"
|
||||
@@ -104,7 +104,7 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
longitude=self._longitude,
|
||||
)
|
||||
await accuweather.async_get_location()
|
||||
except ApiError, ClientConnectorError, TimeoutError, ClientError:
|
||||
except (ApiError, ClientConnectorError, TimeoutError, ClientError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidApiKeyError:
|
||||
errors["base"] = "invalid_api_key"
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
|
||||
from .entity import ActronAirAcEntity, ActronAirZoneEntity, handle_actron_api_errors
|
||||
from .entity import ActronAirAcEntity, ActronAirZoneEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -136,19 +136,16 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
|
||||
"""Return the target temperature."""
|
||||
return self._status.user_aircon_settings.temperature_setpoint_cool_c
|
||||
|
||||
@handle_actron_api_errors
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set a new fan mode."""
|
||||
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode)
|
||||
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
|
||||
|
||||
@handle_actron_api_errors
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set the HVAC mode."""
|
||||
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode)
|
||||
await self._status.ac_system.set_system_mode(ac_mode)
|
||||
|
||||
@handle_actron_api_errors
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the temperature."""
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -212,13 +209,11 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
|
||||
"""Return the target temperature."""
|
||||
return self._zone.temperature_setpoint_cool_c
|
||||
|
||||
@handle_actron_api_errors
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set the HVAC mode."""
|
||||
is_enabled = hvac_mode != HVACMode.OFF
|
||||
await self._zone.enable(is_enabled)
|
||||
|
||||
@handle_actron_api_errors
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the temperature."""
|
||||
await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE))
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
"""Base entity classes for Actron Air integration."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
from actron_neo_api import ActronAirZone
|
||||
|
||||
from actron_neo_api import ActronAirAPIError, ActronAirZone
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -14,26 +9,6 @@ from .const import DOMAIN
|
||||
from .coordinator import ActronAirSystemCoordinator
|
||||
|
||||
|
||||
def handle_actron_api_errors[_EntityT: ActronAirEntity, **_P](
|
||||
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
|
||||
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||
"""Decorate Actron Air API calls to handle ActronAirAPIError exceptions."""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
"""Wrap API calls with exception handling."""
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except ActronAirAPIError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class ActronAirEntity(CoordinatorEntity[ActronAirSystemCoordinator]):
|
||||
"""Base class for Actron Air entities."""
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ rules:
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
|
||||
@@ -49,9 +49,6 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_error": {
|
||||
"message": "Failed to communicate with Actron Air device: {error}"
|
||||
},
|
||||
"auth_error": {
|
||||
"message": "Authentication failed, please reauthenticate"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
|
||||
from .entity import ActronAirAcEntity, handle_actron_api_errors
|
||||
from .entity import ActronAirAcEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -29,42 +29,30 @@ SWITCHES: tuple[ActronAirSwitchEntityDescription, ...] = (
|
||||
key="away_mode",
|
||||
translation_key="away_mode",
|
||||
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.away_mode,
|
||||
set_fn=lambda coordinator, enabled: (
|
||||
coordinator.data.user_aircon_settings.set_away_mode(enabled)
|
||||
),
|
||||
set_fn=lambda coordinator,
|
||||
enabled: coordinator.data.user_aircon_settings.set_away_mode(enabled),
|
||||
),
|
||||
ActronAirSwitchEntityDescription(
|
||||
key="continuous_fan",
|
||||
translation_key="continuous_fan",
|
||||
is_on_fn=lambda coordinator: (
|
||||
coordinator.data.user_aircon_settings.continuous_fan_enabled
|
||||
),
|
||||
set_fn=lambda coordinator, enabled: (
|
||||
coordinator.data.user_aircon_settings.set_continuous_mode(enabled)
|
||||
),
|
||||
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.continuous_fan_enabled,
|
||||
set_fn=lambda coordinator,
|
||||
enabled: coordinator.data.user_aircon_settings.set_continuous_mode(enabled),
|
||||
),
|
||||
ActronAirSwitchEntityDescription(
|
||||
key="quiet_mode",
|
||||
translation_key="quiet_mode",
|
||||
is_on_fn=lambda coordinator: (
|
||||
coordinator.data.user_aircon_settings.quiet_mode_enabled
|
||||
),
|
||||
set_fn=lambda coordinator, enabled: (
|
||||
coordinator.data.user_aircon_settings.set_quiet_mode(enabled)
|
||||
),
|
||||
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.quiet_mode_enabled,
|
||||
set_fn=lambda coordinator,
|
||||
enabled: coordinator.data.user_aircon_settings.set_quiet_mode(enabled),
|
||||
),
|
||||
ActronAirSwitchEntityDescription(
|
||||
key="turbo_mode",
|
||||
translation_key="turbo_mode",
|
||||
is_on_fn=lambda coordinator: (
|
||||
coordinator.data.user_aircon_settings.turbo_enabled
|
||||
),
|
||||
set_fn=lambda coordinator, enabled: (
|
||||
coordinator.data.user_aircon_settings.set_turbo_mode(enabled)
|
||||
),
|
||||
is_supported_fn=lambda coordinator: (
|
||||
coordinator.data.user_aircon_settings.turbo_supported
|
||||
),
|
||||
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_enabled,
|
||||
set_fn=lambda coordinator,
|
||||
enabled: coordinator.data.user_aircon_settings.set_turbo_mode(enabled),
|
||||
is_supported_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_supported,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -105,12 +93,10 @@ class ActronAirSwitch(ActronAirAcEntity, SwitchEntity):
|
||||
"""Return true if the switch is on."""
|
||||
return self.entity_description.is_on_fn(self.coordinator)
|
||||
|
||||
@handle_actron_api_errors
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self.entity_description.set_fn(self.coordinator, True)
|
||||
|
||||
@handle_actron_api_errors
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self.entity_description.set_fn(self.coordinator, False)
|
||||
|
||||
@@ -20,10 +20,9 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_FORCE,
|
||||
@@ -46,7 +45,6 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
|
||||
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.UPDATE]
|
||||
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
|
||||
|
||||
@@ -59,69 +57,6 @@ class AdGuardData:
|
||||
version: str
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
|
||||
def _get_adguard_instances(hass: HomeAssistant) -> list[AdGuardHome]:
|
||||
"""Get the AdGuardHome instances."""
|
||||
entries: list[AdGuardConfigEntry] = hass.config_entries.async_loaded_entries(
|
||||
DOMAIN
|
||||
)
|
||||
if not entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
|
||||
)
|
||||
return [entry.runtime_data.client for entry in entries]
|
||||
|
||||
async def add_url(call: ServiceCall) -> None:
|
||||
"""Service call to add a new filter subscription to AdGuard Home."""
|
||||
for adguard in _get_adguard_instances(call.hass):
|
||||
await adguard.filtering.add_url(
|
||||
allowlist=False, name=call.data[CONF_NAME], url=call.data[CONF_URL]
|
||||
)
|
||||
|
||||
async def remove_url(call: ServiceCall) -> None:
|
||||
"""Service call to remove a filter subscription from AdGuard Home."""
|
||||
for adguard in _get_adguard_instances(call.hass):
|
||||
await adguard.filtering.remove_url(allowlist=False, url=call.data[CONF_URL])
|
||||
|
||||
async def enable_url(call: ServiceCall) -> None:
|
||||
"""Service call to enable a filter subscription in AdGuard Home."""
|
||||
for adguard in _get_adguard_instances(call.hass):
|
||||
await adguard.filtering.enable_url(allowlist=False, url=call.data[CONF_URL])
|
||||
|
||||
async def disable_url(call: ServiceCall) -> None:
|
||||
"""Service call to disable a filter subscription in AdGuard Home."""
|
||||
for adguard in _get_adguard_instances(call.hass):
|
||||
await adguard.filtering.disable_url(
|
||||
allowlist=False, url=call.data[CONF_URL]
|
||||
)
|
||||
|
||||
async def refresh(call: ServiceCall) -> None:
|
||||
"""Service call to refresh the filter subscriptions in AdGuard Home."""
|
||||
for adguard in _get_adguard_instances(call.hass):
|
||||
await adguard.filtering.refresh(
|
||||
allowlist=False, force=call.data[CONF_FORCE]
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REMOVE_URL, remove_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ENABLE_URL, enable_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DISABLE_URL, disable_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REFRESH, refresh, schema=SERVICE_REFRESH_SCHEMA
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
|
||||
"""Set up AdGuard Home from a config entry."""
|
||||
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
|
||||
@@ -144,9 +79,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> b
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def add_url(call: ServiceCall) -> None:
|
||||
"""Service call to add a new filter subscription to AdGuard Home."""
|
||||
await adguard.filtering.add_url(
|
||||
allowlist=False, name=call.data[CONF_NAME], url=call.data[CONF_URL]
|
||||
)
|
||||
|
||||
async def remove_url(call: ServiceCall) -> None:
|
||||
"""Service call to remove a filter subscription from AdGuard Home."""
|
||||
await adguard.filtering.remove_url(allowlist=False, url=call.data[CONF_URL])
|
||||
|
||||
async def enable_url(call: ServiceCall) -> None:
|
||||
"""Service call to enable a filter subscription in AdGuard Home."""
|
||||
await adguard.filtering.enable_url(allowlist=False, url=call.data[CONF_URL])
|
||||
|
||||
async def disable_url(call: ServiceCall) -> None:
|
||||
"""Service call to disable a filter subscription in AdGuard Home."""
|
||||
await adguard.filtering.disable_url(allowlist=False, url=call.data[CONF_URL])
|
||||
|
||||
async def refresh(call: ServiceCall) -> None:
|
||||
"""Service call to refresh the filter subscriptions in AdGuard Home."""
|
||||
await adguard.filtering.refresh(allowlist=False, force=call.data[CONF_FORCE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REMOVE_URL, remove_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ENABLE_URL, enable_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DISABLE_URL, disable_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REFRESH, refresh, schema=SERVICE_REFRESH_SCHEMA
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
|
||||
"""Unload AdGuard Home config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
# This is the last loaded instance of AdGuard, deregister any services
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -76,11 +76,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"config_entry_not_loaded": {
|
||||
"message": "Config entry not loaded."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"add_url": {
|
||||
"description": "Adds a new filter subscription to AdGuard Home.",
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: todo
|
||||
comment: https://developers.home-assistant.io/blog/2025/09/25/entity-services-api-changes/
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules:
|
||||
status: todo
|
||||
comment: |
|
||||
Move coordinator from __init__.py to coordinator.py.
|
||||
Consider using entity descriptions for binary_sensor and switch.
|
||||
Consider simplifying climate supported features flow.
|
||||
config-flow-test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Add mock_setup_entry common fixture.
|
||||
Test unique_id of the entry in happy flow.
|
||||
Split duplicate entry test from happy flow, use mock_config_entry.
|
||||
Error flow should end in CREATE_ENTRY to test recovery.
|
||||
Add data_description for ip_address (and port) to strings.json - tests fail with:
|
||||
"Translation not found for advantage_air: config.step.user.data_description.ip_address"
|
||||
config-flow:
|
||||
status: todo
|
||||
comment: Data descriptions missing
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: todo
|
||||
docs-removal-instructions: todo
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Entities do not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data:
|
||||
status: done
|
||||
comment: Consider extending coordinator to access API via coordinator and remove extra dataclass.
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No options to be set.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable:
|
||||
status: todo
|
||||
comment: MyZone temp entity should be unavailable when MyZone is disabled rather than returning None.
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: Integration connects to local device without authentication.
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Patch the library instead of mocking at integration level.
|
||||
Split binary sensor tests into multiple tests (enable entities etc).
|
||||
Split tests into Creation (right entities with right values), Actions (right library calls), and Other behaviors.
|
||||
|
||||
# Gold
|
||||
devices:
|
||||
status: todo
|
||||
comment: Consider making every zone its own device for better naming and room assignment. Breaking change to split cover entities to separate devices.
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Device is a generic Android device (android-xxxxxxxx) indistinguishable from other Android devices, not discoverable.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Check mDNS, DHCP, SSDP confirmed not feasible. Device is a generic Android device (android-xxxxxxxx) indistinguishable from other Android devices.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: AC zones are static per unit and configured on the device itself.
|
||||
entity-category: done
|
||||
entity-device-class:
|
||||
status: todo
|
||||
comment: Consider using UPDATE device class for app update binary sensor instead of custom.
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: todo
|
||||
exception-translations:
|
||||
status: todo
|
||||
comment: UpdateFailed in the coordinator
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: Integration does not raise repair issues.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: Zones are part of the AC unit, not separate removable devices.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
@@ -7,12 +7,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, SERVER_URL
|
||||
from .services import async_setup_services
|
||||
|
||||
ATTRIBUTION = "ispyconnect.com"
|
||||
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
||||
@@ -21,14 +19,6 @@ PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
||||
|
||||
AgentDVRConfigEntry = ConfigEntry[Agent]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
|
||||
|
||||
@@ -9,7 +9,10 @@ from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
|
||||
@@ -18,6 +21,20 @@ SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DEV_EN_ALT = "enable_alerts"
|
||||
_DEV_DS_ALT = "disable_alerts"
|
||||
_DEV_EN_REC = "start_recording"
|
||||
_DEV_DS_REC = "stop_recording"
|
||||
_DEV_SNAP = "snapshot"
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_DEV_EN_ALT: "async_enable_alerts",
|
||||
_DEV_DS_ALT: "async_disable_alerts",
|
||||
_DEV_EN_REC: "async_start_recording",
|
||||
_DEV_DS_REC: "async_stop_recording",
|
||||
_DEV_SNAP: "async_snapshot",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -40,6 +57,10 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(cameras)
|
||||
|
||||
platform = async_get_current_platform()
|
||||
for service, method in CAMERA_SERVICES.items():
|
||||
platform.async_register_entity_service(service, None, method)
|
||||
|
||||
|
||||
class AgentCamera(MjpegCamera):
|
||||
"""Representation of an Agent Device Stream."""
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Services for Agent DVR."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_DEV_EN_ALT = "enable_alerts"
|
||||
_DEV_DS_ALT = "disable_alerts"
|
||||
_DEV_EN_REC = "start_recording"
|
||||
_DEV_DS_REC = "stop_recording"
|
||||
_DEV_SNAP = "snapshot"
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_DEV_EN_ALT: "async_enable_alerts",
|
||||
_DEV_DS_ALT: "async_disable_alerts",
|
||||
_DEV_EN_REC: "async_start_recording",
|
||||
_DEV_DS_REC: "async_stop_recording",
|
||||
_DEV_SNAP: "async_snapshot",
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
for service_name, method in CAMERA_SERVICES.items():
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
service_name,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func=method,
|
||||
)
|
||||
@@ -133,9 +133,8 @@ CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = (
|
||||
value_fn=lambda config: _get_value(
|
||||
config.co2_automatic_baseline_calibration_days, ABC_DAYS
|
||||
),
|
||||
set_value_fn=lambda client, value: (
|
||||
client.set_co2_automatic_baseline_calibration(int(value))
|
||||
),
|
||||
set_value_fn=lambda client,
|
||||
value: client.set_co2_automatic_baseline_calibration(int(value)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class AirobotButton(AirobotEntity, ButtonEntity):
|
||||
"""Handle the button press."""
|
||||
try:
|
||||
await self.entity_description.press_fn(self.coordinator)
|
||||
except AirobotConnectionError, AirobotTimeoutError:
|
||||
except (AirobotConnectionError, AirobotTimeoutError):
|
||||
# Connection errors during reboot are expected as device restarts
|
||||
pass
|
||||
except AirobotError as err:
|
||||
|
||||
@@ -114,7 +114,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
self.errors["base"] = "cannot_connect"
|
||||
except AirOSConnectionAuthenticationError, AirOSDataMissingError:
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
self.errors["base"] = "invalid_auth"
|
||||
except AirOSKeyDataMissingError:
|
||||
self.errors["base"] = "key_data_missing"
|
||||
|
||||
@@ -130,7 +130,7 @@ class AirVisualFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
await coro
|
||||
except InvalidKeyError, KeyExpiredError, UnauthorizedError:
|
||||
except (InvalidKeyError, KeyExpiredError, UnauthorizedError):
|
||||
errors[CONF_API_KEY] = "invalid_api_key"
|
||||
except NotFoundError:
|
||||
errors[CONF_CITY] = "location_not_found"
|
||||
|
||||
@@ -100,7 +100,7 @@ class AirZoneCloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
await self.airzone.login()
|
||||
except AirzoneCloudError, LoginError:
|
||||
except (AirzoneCloudError, LoginError):
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return await self.async_step_inst_pick()
|
||||
|
||||
@@ -18,15 +18,12 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_BAUD,
|
||||
CONF_DEVICE_PATH,
|
||||
DOMAIN,
|
||||
PROTOCOL_SERIAL,
|
||||
PROTOCOL_SOCKET,
|
||||
SIGNAL_PANEL_MESSAGE,
|
||||
@@ -35,11 +32,9 @@ from .const import (
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -59,12 +54,6 @@ class AlarmDecoderData:
|
||||
restart: bool
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AlarmDecoderConfigEntry
|
||||
) -> bool:
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -23,6 +27,11 @@ from .const import (
|
||||
)
|
||||
from .entity import AlarmDecoderEntity
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
|
||||
ATTR_KEYPRESS = "keypress"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -41,6 +50,23 @@ async def async_setup_entry(
|
||||
)
|
||||
async_add_entities([entity])
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_TOGGLE_CHIME,
|
||||
{
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
},
|
||||
"alarm_toggle_chime",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_KEYPRESS,
|
||||
{
|
||||
vol.Required(ATTR_KEYPRESS): cv.string,
|
||||
},
|
||||
"alarm_keypress",
|
||||
)
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Support for AlarmDecoder-based alarm control panels (Honeywell/DSC)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
|
||||
ATTR_KEYPRESS = "keypress"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_TOGGLE_CHIME,
|
||||
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
|
||||
schema={
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
},
|
||||
func="alarm_toggle_chime",
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_KEYPRESS,
|
||||
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
|
||||
schema={
|
||||
vol.Required(ATTR_KEYPRESS): cv.string,
|
||||
},
|
||||
func="alarm_keypress",
|
||||
)
|
||||
@@ -123,7 +123,7 @@ class Auth:
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Timeout calling LWA to get auth token")
|
||||
return None
|
||||
|
||||
|
||||
@@ -358,7 +358,7 @@ async def async_send_changereport_message(
|
||||
"""
|
||||
try:
|
||||
token = await config.async_get_access_token()
|
||||
except RequireRelink, NoTokenAvailable:
|
||||
except (RequireRelink, NoTokenAvailable):
|
||||
await config.set_authorized(False)
|
||||
_LOGGER.error(
|
||||
"Error when sending ChangeReport to Alexa, could not get access token"
|
||||
@@ -392,7 +392,7 @@ async def async_send_changereport_message(
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Timeout sending report to Alexa for %s", alexa_entity.entity_id)
|
||||
return
|
||||
|
||||
@@ -549,7 +549,7 @@ async def async_send_doorbell_event_message(
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Timeout sending report to Alexa for %s", alexa_entity.entity_id)
|
||||
return
|
||||
|
||||
|
||||
@@ -29,24 +29,3 @@ COUNTRY_DOMAINS = {
|
||||
|
||||
CATEGORY_SENSORS = "sensors"
|
||||
CATEGORY_NOTIFICATIONS = "notifications"
|
||||
|
||||
# Map service translation keys to Alexa API
|
||||
INFO_SKILLS_MAPPING = {
|
||||
"calendar_today": "Alexa.Calendar.PlayToday",
|
||||
"calendar_tomorrow": "Alexa.Calendar.PlayTomorrow",
|
||||
"calendar_next": "Alexa.Calendar.PlayNext",
|
||||
"date": "Alexa.Date.Play",
|
||||
"time": "Alexa.Time.Play",
|
||||
"national_news": "Alexa.News.NationalNews",
|
||||
"flash_briefing": "Alexa.FlashBriefing.Play",
|
||||
"traffic": "Alexa.Traffic.Play",
|
||||
"weather": "Alexa.Weather.Play",
|
||||
"cleanup": "Alexa.CleanUp.Play",
|
||||
"good_morning": "Alexa.GoodMorning.Play",
|
||||
"sing_song": "Alexa.SingASong.Play",
|
||||
"fun_fact": "Alexa.FunFact.Play",
|
||||
"tell_joke": "Alexa.Joke.Play",
|
||||
"tell_story": "Alexa.TellStory.Play",
|
||||
"im_home": "Alexa.ImHome.Play",
|
||||
"goodnight": "Alexa.GoodNight.Play",
|
||||
}
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"voc_index": {
|
||||
"default": "mdi:molecule"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_info_skill": {
|
||||
"service": "mdi:information"
|
||||
},
|
||||
"send_sound": {
|
||||
"service": "mdi:cast-audio"
|
||||
},
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==12.0.0"]
|
||||
"requirements": ["aioamazondevices==11.1.3"]
|
||||
}
|
||||
|
||||
@@ -20,13 +20,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.const import LIGHT_LUX, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -83,41 +77,6 @@ SENSORS: Final = (
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AmazonSensorEntityDescription(
|
||||
key="Humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AmazonSensorEntityDescription(
|
||||
key="PM10",
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AmazonSensorEntityDescription(
|
||||
key="PM25",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AmazonSensorEntityDescription(
|
||||
key="CO",
|
||||
device_class=SensorDeviceClass.CO,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AmazonSensorEntityDescription(
|
||||
key="VOC",
|
||||
# No device class as this is an index not a concentration
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="voc_index",
|
||||
),
|
||||
AmazonSensorEntityDescription(
|
||||
key="Air Quality",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
NOTIFICATIONS: Final = (
|
||||
AmazonNotificationEntityDescription(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Support for services."""
|
||||
|
||||
from aioamazondevices.const.metadata import ALEXA_INFO_SKILLS
|
||||
from aioamazondevices.const.sounds import SOUNDS_LIST
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -10,15 +9,13 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
from .const import DOMAIN, INFO_SKILLS_MAPPING
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
ATTR_TEXT_COMMAND = "text_command"
|
||||
ATTR_SOUND = "sound"
|
||||
ATTR_INFO_SKILL = "info_skill"
|
||||
SERVICE_TEXT_COMMAND = "send_text_command"
|
||||
SERVICE_SOUND_NOTIFICATION = "send_sound"
|
||||
SERVICE_INFO_SKILL = "send_info_skill"
|
||||
|
||||
SCHEMA_SOUND_SERVICE = vol.Schema(
|
||||
{
|
||||
@@ -32,12 +29,6 @@ SCHEMA_CUSTOM_COMMAND = vol.Schema(
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
SCHEMA_INFO_SKILL = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_INFO_SKILL): cv.string,
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -95,17 +86,6 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
||||
await coordinator.api.call_alexa_text_command(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
elif attribute == ATTR_INFO_SKILL:
|
||||
info_skill = INFO_SKILLS_MAPPING.get(value)
|
||||
if info_skill not in ALEXA_INFO_SKILLS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_info_skill_value",
|
||||
translation_placeholders={"info_skill": value},
|
||||
)
|
||||
await coordinator.api.call_alexa_info_skill(
|
||||
coordinator.data[device.serial_number], info_skill
|
||||
)
|
||||
|
||||
|
||||
async def async_send_sound_notification(call: ServiceCall) -> None:
|
||||
@@ -118,11 +98,6 @@ async def async_send_text_command(call: ServiceCall) -> None:
|
||||
await _async_execute_action(call, ATTR_TEXT_COMMAND)
|
||||
|
||||
|
||||
async def async_send_info_skill(call: ServiceCall) -> None:
|
||||
"""Send an info skill command to a AmazonDevice."""
|
||||
await _async_execute_action(call, ATTR_INFO_SKILL)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Amazon Devices integration."""
|
||||
@@ -137,10 +112,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
async_send_text_command,
|
||||
SCHEMA_CUSTOM_COMMAND,
|
||||
),
|
||||
(
|
||||
SERVICE_INFO_SKILL,
|
||||
async_send_info_skill,
|
||||
SCHEMA_INFO_SKILL,
|
||||
),
|
||||
):
|
||||
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
|
||||
|
||||
@@ -67,36 +67,3 @@ send_sound:
|
||||
- squeaky_12
|
||||
- zap_01
|
||||
translation_key: sound
|
||||
|
||||
send_info_skill:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: alexa_devices
|
||||
info_skill:
|
||||
required: true
|
||||
example: date
|
||||
default: date
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- calendar_today
|
||||
- calendar_tomorrow
|
||||
- calendar_next
|
||||
- date
|
||||
- time
|
||||
- national_news
|
||||
- flash_briefing
|
||||
- traffic
|
||||
- weather
|
||||
- cleanup
|
||||
- good_morning
|
||||
- sing_song
|
||||
- fun_fact
|
||||
- tell_joke
|
||||
- tell_story
|
||||
- im_home
|
||||
- goodnight
|
||||
translation_key: info_skill
|
||||
|
||||
@@ -75,9 +75,6 @@
|
||||
},
|
||||
"timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"voc_index": {
|
||||
"name": "Volatile organic compounds index"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
@@ -93,9 +90,6 @@
|
||||
"cannot_retrieve_data_with_error": {
|
||||
"message": "Error retrieving data: {error}"
|
||||
},
|
||||
"config_entry_not_found": {
|
||||
"message": "Config entry not found: {device_id}"
|
||||
},
|
||||
"device_serial_number_missing": {
|
||||
"message": "Device serial number missing: {device_id}"
|
||||
},
|
||||
@@ -105,35 +99,11 @@
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
"invalid_info_skill_value": {
|
||||
"message": "Invalid info skill {info_skill} specified"
|
||||
},
|
||||
"invalid_sound_value": {
|
||||
"message": "Invalid sound {sound} specified"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"info_skill": {
|
||||
"options": {
|
||||
"calendar_next": "Calendar: Next event",
|
||||
"calendar_today": "Calendar: Today's Calendar",
|
||||
"calendar_tomorrow": "Calendar: Tomorrow's Calendar",
|
||||
"cleanup": "Encourage me to clean up",
|
||||
"date": "Date",
|
||||
"flash_briefing": "Flash Briefing",
|
||||
"fun_fact": "Tell me a fun fact",
|
||||
"good_morning": "Good morning",
|
||||
"goodnight": "Wish me a good night",
|
||||
"im_home": "Welcome me home",
|
||||
"national_news": "National News",
|
||||
"sing_song": "Sing a song",
|
||||
"tell_joke": "Tell me a joke",
|
||||
"tell_story": "Tell me a story",
|
||||
"time": "Time",
|
||||
"traffic": "Traffic",
|
||||
"weather": "Weather"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"options": {
|
||||
"air_horn_03": "Air horn",
|
||||
@@ -181,20 +151,6 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_info_skill": {
|
||||
"description": "Sends an info skill command to a device",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"description": "[%key:component::alexa_devices::common::device_id_description%]",
|
||||
"name": "Device"
|
||||
},
|
||||
"info_skill": {
|
||||
"description": "The info skill command to send.",
|
||||
"name": "Alexa info skill command"
|
||||
}
|
||||
},
|
||||
"name": "Send info skill command"
|
||||
},
|
||||
"send_sound": {
|
||||
"description": "Sends a sound to a device",
|
||||
"fields": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from amberelectric.models.channel import ChannelType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
@@ -12,7 +13,6 @@ from homeassistant.core import (
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import service
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
from homeassistant.util.json import JsonValueType
|
||||
|
||||
@@ -37,6 +37,23 @@ GET_FORECASTS_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> AmberConfigEntry:
|
||||
"""Get the Amber config entry."""
|
||||
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="integration_not_found",
|
||||
translation_placeholders={"target": config_entry_id},
|
||||
)
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": entry.title},
|
||||
)
|
||||
return entry
|
||||
|
||||
|
||||
def get_forecasts(channel_type: str, data: dict) -> list[JsonValueType]:
|
||||
"""Return an array of forecasts."""
|
||||
results: list[JsonValueType] = []
|
||||
@@ -92,9 +109,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def handle_get_forecasts(call: ServiceCall) -> ServiceResponse:
|
||||
channel_type = call.data[ATTR_CHANNEL_TYPE]
|
||||
entry: AmberConfigEntry = service.async_get_config_entry(
|
||||
hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID]
|
||||
)
|
||||
entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||
coordinator = entry.runtime_data
|
||||
forecasts = get_forecasts(channel_type, coordinator.data)
|
||||
return {"forecasts": forecasts}
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"exceptions": {
|
||||
"channel_not_found": {
|
||||
"message": "There is no {channel_type} channel at this site."
|
||||
},
|
||||
"integration_not_found": {
|
||||
"message": "Config entry \"{target}\" not found in registry."
|
||||
},
|
||||
"not_loaded": {
|
||||
"message": "{target} is not loaded."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
||||
@@ -77,11 +77,9 @@ class AmbientNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# Filter out indoor stations
|
||||
self._stations = dict(
|
||||
filter(
|
||||
lambda item: (
|
||||
not item[1]
|
||||
.get(API_STATION_INFO, {})
|
||||
.get(API_STATION_INDOOR, False)
|
||||
),
|
||||
lambda item: not item[1]
|
||||
.get(API_STATION_INFO, {})
|
||||
.get(API_STATION_INDOOR, False),
|
||||
self._stations.items(),
|
||||
)
|
||||
)
|
||||
@@ -115,7 +113,7 @@ class AmbientNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id=CONF_USER, data_schema=schema, errors=errors or {}
|
||||
step_id=CONF_USER, data_schema=schema, errors=errors if errors else {}
|
||||
)
|
||||
|
||||
async def async_step_station(
|
||||
|
||||
@@ -31,7 +31,7 @@ class AmbientStationFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.data_schema,
|
||||
errors=errors or {},
|
||||
errors=errors if errors else {},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
|
||||
@@ -33,19 +33,13 @@ from .const import ATTR_LAST_DATA, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX
|
||||
from .entity import AmbientWeatherEntity
|
||||
|
||||
TYPE_24HOURRAININ = "24hourrainin"
|
||||
TYPE_AQI_PM10_24H_AQIN = "aqi_pm10_24h_aqin"
|
||||
TYPE_AQI_PM10_AQIN = "aqi_pm10_aqin"
|
||||
TYPE_AQI_PM25 = "aqi_pm25"
|
||||
TYPE_AQI_PM25_24H = "aqi_pm25_24h"
|
||||
TYPE_AQI_PM25_24H_AQIN = "aqi_pm25_24h_aqin"
|
||||
TYPE_AQI_PM25_AQIN = "aqi_pm25_aqin"
|
||||
TYPE_AQI_PM25_IN = "aqi_pm25_in"
|
||||
TYPE_AQI_PM25_IN_24H = "aqi_pm25_in_24h"
|
||||
TYPE_BAROMABSIN = "baromabsin"
|
||||
TYPE_BAROMRELIN = "baromrelin"
|
||||
TYPE_CO2 = "co2"
|
||||
TYPE_CO2_IN_24H_AQIN = "co2_in_24h_aqin"
|
||||
TYPE_CO2_IN_AQIN = "co2_in_aqin"
|
||||
TYPE_DAILYRAININ = "dailyrainin"
|
||||
TYPE_DEWPOINT = "dewPoint"
|
||||
TYPE_EVENTRAININ = "eventrainin"
|
||||
@@ -63,23 +57,17 @@ TYPE_HUMIDITY7 = "humidity7"
|
||||
TYPE_HUMIDITY8 = "humidity8"
|
||||
TYPE_HUMIDITY9 = "humidity9"
|
||||
TYPE_HUMIDITYIN = "humidityin"
|
||||
TYPE_LASTLIGHTNING = "lightning_time"
|
||||
TYPE_LASTLIGHTNING_DISTANCE = "lightning_distance"
|
||||
TYPE_LASTRAIN = "lastRain"
|
||||
TYPE_LIGHTNING_PER_DAY = "lightning_day"
|
||||
TYPE_LIGHTNING_PER_HOUR = "lightning_hour"
|
||||
TYPE_LASTLIGHTNING_DISTANCE = "lightning_distance"
|
||||
TYPE_LASTLIGHTNING = "lightning_time"
|
||||
TYPE_MAXDAILYGUST = "maxdailygust"
|
||||
TYPE_MONTHLYRAININ = "monthlyrainin"
|
||||
TYPE_PM_IN_HUMIDITY_AQIN = "pm_in_humidity_aqin"
|
||||
TYPE_PM_IN_TEMP_AQIN = "pm_in_temp_aqin"
|
||||
TYPE_PM10_IN_24H_AQIN = "pm10_in_24h_aqin"
|
||||
TYPE_PM10_IN_AQIN = "pm10_in_aqin"
|
||||
TYPE_PM25 = "pm25"
|
||||
TYPE_PM25_24H = "pm25_24h"
|
||||
TYPE_PM25_IN = "pm25_in"
|
||||
TYPE_PM25_IN_24H = "pm25_in_24h"
|
||||
TYPE_PM25_IN_24H_AQIN = "pm25_in_24h_aqin"
|
||||
TYPE_PM25_IN_AQIN = "pm25_in_aqin"
|
||||
TYPE_SOILHUM1 = "soilhum1"
|
||||
TYPE_SOILHUM10 = "soilhum10"
|
||||
TYPE_SOILHUM2 = "soilhum2"
|
||||
@@ -90,8 +78,8 @@ TYPE_SOILHUM6 = "soilhum6"
|
||||
TYPE_SOILHUM7 = "soilhum7"
|
||||
TYPE_SOILHUM8 = "soilhum8"
|
||||
TYPE_SOILHUM9 = "soilhum9"
|
||||
TYPE_SOILTEMP10F = "soiltemp10f"
|
||||
TYPE_SOILTEMP1F = "soiltemp1f"
|
||||
TYPE_SOILTEMP10F = "soiltemp10f"
|
||||
TYPE_SOILTEMP2F = "soiltemp2f"
|
||||
TYPE_SOILTEMP3F = "soiltemp3f"
|
||||
TYPE_SOILTEMP4F = "soiltemp4f"
|
||||
@@ -155,86 +143,6 @@ SENSOR_DESCRIPTIONS = (
|
||||
translation_key="pm25_indoor_aqi_24h_average",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_IN_AQIN,
|
||||
translation_key="pm25_indoor_aqin",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_IN_24H_AQIN,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
translation_key="pm25_indoor_24h_aqin",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM10_IN_AQIN,
|
||||
translation_key="pm10_indoor_aqin",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM10_IN_24H_AQIN,
|
||||
translation_key="pm10_indoor_24h_aqin",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_CO2_IN_AQIN,
|
||||
translation_key="co2_indoor_aqin",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_CO2_IN_24H_AQIN,
|
||||
translation_key="co2_indoor_24h_aqin",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM_IN_TEMP_AQIN,
|
||||
translation_key="pm_indoor_temp_aqin",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM_IN_HUMIDITY_AQIN,
|
||||
translation_key="pm_indoor_humidity_aqin",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_AQIN,
|
||||
translation_key="pm25_aqi_aqin",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_24H_AQIN,
|
||||
translation_key="pm25_aqi_24h_aqin",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM10_AQIN,
|
||||
translation_key="pm10_aqi_aqin",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM10_24H_AQIN,
|
||||
translation_key="pm10_aqi_24h_aqin",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_BAROMABSIN,
|
||||
translation_key="absolute_pressure",
|
||||
|
||||
@@ -156,12 +156,6 @@
|
||||
"absolute_pressure": {
|
||||
"name": "Absolute pressure"
|
||||
},
|
||||
"co2_indoor_24h_aqin": {
|
||||
"name": "CO2 Indoor 24h Average AQIN"
|
||||
},
|
||||
"co2_indoor_aqin": {
|
||||
"name": "CO2 Indoor AQIN"
|
||||
},
|
||||
"daily_rain": {
|
||||
"name": "Daily rain"
|
||||
},
|
||||
@@ -234,39 +228,18 @@
|
||||
"monthly_rain": {
|
||||
"name": "Monthly rain"
|
||||
},
|
||||
"pm10_aqi_24h_aqin": {
|
||||
"name": "PM10 Indoor AQI 24h Average AQIN"
|
||||
},
|
||||
"pm10_aqi_aqin": {
|
||||
"name": "PM10 Indoor AQI AQIN"
|
||||
},
|
||||
"pm10_indoor_24h_aqin": {
|
||||
"name": "PM10 Indoor 24h Average AQIN"
|
||||
},
|
||||
"pm10_indoor_aqin": {
|
||||
"name": "PM10 Indoor AQIN"
|
||||
},
|
||||
"pm25_24h_average": {
|
||||
"name": "PM2.5 24 hour average"
|
||||
},
|
||||
"pm25_aqi": {
|
||||
"name": "PM2.5 AQI"
|
||||
},
|
||||
"pm25_aqi_24h_aqin": {
|
||||
"name": "PM2.5 Indoor AQI 24h Average AQIN"
|
||||
},
|
||||
"pm25_aqi_24h_average": {
|
||||
"name": "PM2.5 AQI 24 hour average"
|
||||
},
|
||||
"pm25_aqi_aqin": {
|
||||
"name": "PM2.5 Indoor AQI AQIN"
|
||||
},
|
||||
"pm25_indoor": {
|
||||
"name": "PM2.5 indoor"
|
||||
},
|
||||
"pm25_indoor_24h_aqin": {
|
||||
"name": "PM2.5 Indoor 24h AQIN"
|
||||
},
|
||||
"pm25_indoor_24h_average": {
|
||||
"name": "PM2.5 indoor 24 hour average"
|
||||
},
|
||||
@@ -276,15 +249,6 @@
|
||||
"pm25_indoor_aqi_24h_average": {
|
||||
"name": "PM2.5 indoor AQI"
|
||||
},
|
||||
"pm25_indoor_aqin": {
|
||||
"name": "PM2.5 Indoor AQIN"
|
||||
},
|
||||
"pm_indoor_humidity_aqin": {
|
||||
"name": "Indoor Humidity AQIN"
|
||||
},
|
||||
"pm_indoor_temp_aqin": {
|
||||
"name": "Indoor Temperature AQIN"
|
||||
},
|
||||
"relative_pressure": {
|
||||
"name": "Relative pressure"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from collections.abc import AsyncIterator, Callable
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
@@ -202,7 +202,7 @@ class AmcrestChecker(ApiWrapper):
|
||||
@asynccontextmanager
|
||||
async def async_stream_command(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> AsyncGenerator[httpx.Response]:
|
||||
) -> AsyncIterator[httpx.Response]:
|
||||
"""amcrest.ApiWrapper.command wrapper to catch errors."""
|
||||
async with (
|
||||
self._async_command_wrapper(),
|
||||
@@ -211,7 +211,7 @@ class AmcrestChecker(ApiWrapper):
|
||||
yield ret
|
||||
|
||||
@asynccontextmanager
|
||||
async def _async_command_wrapper(self) -> AsyncGenerator[None]:
|
||||
async def _async_command_wrapper(self) -> AsyncIterator[None]:
|
||||
try:
|
||||
yield
|
||||
except LoginError as ex:
|
||||
|
||||
@@ -73,21 +73,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
started = False
|
||||
|
||||
async def _async_handle_labs_update(
|
||||
event_data: labs.EventLabsUpdatedData,
|
||||
event: Event[labs.EventLabsUpdatedData],
|
||||
) -> None:
|
||||
"""Handle labs feature toggle."""
|
||||
await analytics.save_preferences({ATTR_SNAPSHOTS: event_data["enabled"]})
|
||||
await analytics.save_preferences({ATTR_SNAPSHOTS: event.data["enabled"]})
|
||||
if started:
|
||||
await analytics.async_schedule()
|
||||
|
||||
@callback
|
||||
def _async_labs_event_filter(event_data: labs.EventLabsUpdatedData) -> bool:
|
||||
"""Filter labs events for this integration's snapshot feature."""
|
||||
return (
|
||||
event_data["domain"] == DOMAIN
|
||||
and event_data["preview_feature"] == LABS_SNAPSHOT_FEATURE
|
||||
)
|
||||
|
||||
async def start_schedule(_event: Event) -> None:
|
||||
"""Start the send schedule after the started event."""
|
||||
nonlocal started
|
||||
started = True
|
||||
await analytics.async_schedule()
|
||||
|
||||
labs.async_subscribe_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
|
||||
hass.bus.async_listen(
|
||||
labs.EVENT_LABS_UPDATED,
|
||||
_async_handle_labs_update,
|
||||
event_filter=_async_labs_event_filter,
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
||||
try:
|
||||
return await self._async_start_pair()
|
||||
except CannotConnect, ConnectionClosed:
|
||||
except (CannotConnect, ConnectionClosed):
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
user_input = {}
|
||||
@@ -135,7 +135,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# Attempt to pair again.
|
||||
try:
|
||||
return await self._async_start_pair()
|
||||
except CannotConnect, ConnectionClosed:
|
||||
except (CannotConnect, ConnectionClosed):
|
||||
# Device doesn't respond to the specified host. Abort.
|
||||
# If we are in the user flow we could go back to the user step to allow
|
||||
# them to enter a new IP address but we cannot do that for the zeroconf
|
||||
@@ -203,7 +203,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
try:
|
||||
return await self._async_start_pair()
|
||||
except CannotConnect, ConnectionClosed:
|
||||
except (CannotConnect, ConnectionClosed):
|
||||
# Device became network unreachable after discovery.
|
||||
# Abort and let discovery find it again later.
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
@@ -229,7 +229,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
try:
|
||||
return await self._async_start_pair()
|
||||
except CannotConnect, ConnectionClosed:
|
||||
except (CannotConnect, ConnectionClosed):
|
||||
# Device is network unreachable. Abort.
|
||||
errors["base"] = "cannot_connect"
|
||||
return self.async_show_form(
|
||||
@@ -264,7 +264,7 @@ class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithReload):
|
||||
@callback
|
||||
def _save_config(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Save the updated options."""
|
||||
new_data = {k: v for k, v in data.items() if k != CONF_APPS}
|
||||
new_data = {k: v for k, v in data.items() if k not in [CONF_APPS]}
|
||||
if self._apps:
|
||||
new_data[CONF_APPS] = self._apps
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ async def validate_account(auth: MSOB2CAuth, account_number: str) -> str | MSOB2
|
||||
_aw = AnglianWater(authenticator=auth)
|
||||
try:
|
||||
await _aw.validate_smart_meter(account_number)
|
||||
except InvalidAccountIdError, SmartMeterUnavailableError:
|
||||
except (InvalidAccountIdError, SmartMeterUnavailableError):
|
||||
return "smart_meter_unavailable"
|
||||
return auth
|
||||
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
import anthropic
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -41,13 +42,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
|
||||
"""Set up Anthropic from a config entry."""
|
||||
client = anthropic.AsyncAnthropic(
|
||||
api_key=entry.data[CONF_API_KEY], http_client=get_async_client(hass)
|
||||
client = await hass.async_add_executor_job(
|
||||
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
|
||||
)
|
||||
try:
|
||||
await client.models.list(timeout=10.0)
|
||||
except anthropic.AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
LOGGER.error("Invalid API key: %s", err)
|
||||
return False
|
||||
except anthropic.AnthropicError as err:
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
|
||||
@@ -76,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Anthropic."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -104,7 +106,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
if not any(entry.version == 1 for entry in entries):
|
||||
return
|
||||
|
||||
api_keys_entries: dict[str, tuple[AnthropicConfigEntry, bool]] = {}
|
||||
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from json import JSONDecodeError
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components import ai_task, conversation
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -14,15 +14,12 @@ from homeassistant.util.json import json_loads
|
||||
|
||||
from .entity import AnthropicBaseLLMEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AnthropicConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AnthropicConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AI Task entities."""
|
||||
@@ -53,9 +50,7 @@ class AnthropicTaskEntity(
|
||||
chat_log: conversation.ChatLog,
|
||||
) -> ai_task.GenDataTaskResult:
|
||||
"""Handle a generate data task."""
|
||||
await self._async_handle_chat_log(
|
||||
chat_log, task.name, task.structure, max_iterations=1000
|
||||
)
|
||||
await self._async_handle_chat_log(chat_log, task.name, task.structure)
|
||||
|
||||
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import Any, cast
|
||||
|
||||
import anthropic
|
||||
import voluptuous as vol
|
||||
@@ -14,7 +14,7 @@ from voluptuous_openapi import convert
|
||||
|
||||
from homeassistant.components.zone import ENTITY_ID_HOME
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
@@ -30,14 +30,12 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.selector import (
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
TemplateSelector,
|
||||
)
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
@@ -49,7 +47,6 @@ from .const import (
|
||||
CONF_RECOMMENDED,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_THINKING_BUDGET,
|
||||
CONF_THINKING_EFFORT,
|
||||
CONF_WEB_SEARCH,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
@@ -61,14 +58,10 @@ from .const import (
|
||||
DEFAULT_AI_TASK_NAME,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DOMAIN,
|
||||
NON_ADAPTIVE_THINKING_MODELS,
|
||||
NON_THINKING_MODELS,
|
||||
WEB_SEARCH_UNSUPPORTED_MODELS,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AnthropicConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
@@ -93,8 +86,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
client = anthropic.AsyncAnthropic(
|
||||
api_key=data[CONF_API_KEY], http_client=get_async_client(hass)
|
||||
client = await hass.async_add_executor_job(
|
||||
partial(anthropic.AsyncAnthropic, api_key=data[CONF_API_KEY])
|
||||
)
|
||||
await client.models.list(timeout=10.0)
|
||||
|
||||
@@ -118,7 +111,6 @@ async def get_model_list(client: anthropic.AsyncAnthropic) -> list[SelectOptionD
|
||||
"claude-3-5-haiku-20241022",
|
||||
"claude-3-opus-20240229",
|
||||
)
|
||||
and model_info.id[-2:-1] != "-"
|
||||
else model_info.id
|
||||
)
|
||||
if short_form.search(model_alias):
|
||||
@@ -166,10 +158,6 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data_updates=user_input
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title="Claude",
|
||||
data=user_input,
|
||||
@@ -190,34 +178,13 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors or None,
|
||||
description_placeholders={
|
||||
"instructions_url": "https://www.home-assistant.io/integrations/anthropic/#generating-an-api-key",
|
||||
},
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors or None
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: AnthropicConfigEntry
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this integration."""
|
||||
return {
|
||||
@@ -387,9 +354,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
model = self.options[CONF_CHAT_MODEL]
|
||||
|
||||
if not model.startswith(tuple(NON_THINKING_MODELS)) and model.startswith(
|
||||
tuple(NON_ADAPTIVE_THINKING_MODELS)
|
||||
):
|
||||
if not model.startswith(tuple(NON_THINKING_MODELS)):
|
||||
step_schema[
|
||||
vol.Optional(
|
||||
CONF_THINKING_BUDGET, default=DEFAULT[CONF_THINKING_BUDGET]
|
||||
@@ -406,22 +371,6 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
else:
|
||||
self.options.pop(CONF_THINKING_BUDGET, None)
|
||||
|
||||
if not model.startswith(tuple(NON_ADAPTIVE_THINKING_MODELS)):
|
||||
step_schema[
|
||||
vol.Optional(
|
||||
CONF_THINKING_EFFORT,
|
||||
default=DEFAULT[CONF_THINKING_EFFORT],
|
||||
)
|
||||
] = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=["none", "low", "medium", "high", "max"],
|
||||
translation_key=CONF_THINKING_EFFORT,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.options.pop(CONF_THINKING_EFFORT, None)
|
||||
|
||||
if not model.startswith(tuple(WEB_SEARCH_UNSUPPORTED_MODELS)):
|
||||
step_schema.update(
|
||||
{
|
||||
@@ -486,9 +435,11 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
async def _get_model_list(self) -> list[SelectOptionDict]:
|
||||
"""Get list of available models."""
|
||||
client = anthropic.AsyncAnthropic(
|
||||
api_key=self._get_entry().data[CONF_API_KEY],
|
||||
http_client=get_async_client(self.hass),
|
||||
client = await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
anthropic.AsyncAnthropic,
|
||||
api_key=self._get_entry().data[CONF_API_KEY],
|
||||
)
|
||||
)
|
||||
return await get_model_list(client)
|
||||
|
||||
@@ -497,9 +448,11 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
location_data: dict[str, str] = {}
|
||||
zone_home = self.hass.states.get(ENTITY_ID_HOME)
|
||||
if zone_home is not None:
|
||||
client = anthropic.AsyncAnthropic(
|
||||
api_key=self._get_entry().data[CONF_API_KEY],
|
||||
http_client=get_async_client(self.hass),
|
||||
client = await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
anthropic.AsyncAnthropic,
|
||||
api_key=self._get_entry().data[CONF_API_KEY],
|
||||
)
|
||||
)
|
||||
location_schema = vol.Schema(
|
||||
{
|
||||
@@ -520,24 +473,22 @@ 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[ATTR_LONGITUDE]})? Please respond "
|
||||
"only with a JSON object using the following schema:\n"
|
||||
f"{convert(location_schema)}",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "{", # hints the model to skip any preamble
|
||||
},
|
||||
],
|
||||
max_tokens=cast(int, DEFAULT[CONF_MAX_TOKENS]),
|
||||
output_config={
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"schema": {
|
||||
**convert(location_schema),
|
||||
"additionalProperties": False,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
_LOGGER.debug("Model response: %s", response.content)
|
||||
location_data = location_schema(
|
||||
json.loads(
|
||||
"".join(
|
||||
"{"
|
||||
+ "".join(
|
||||
block.text
|
||||
for block in response.content
|
||||
if isinstance(block, anthropic.types.TextBlock)
|
||||
|
||||
@@ -14,7 +14,6 @@ CONF_CHAT_MODEL = "chat_model"
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
CONF_THINKING_BUDGET = "thinking_budget"
|
||||
CONF_THINKING_EFFORT = "thinking_effort"
|
||||
CONF_WEB_SEARCH = "web_search"
|
||||
CONF_WEB_SEARCH_USER_LOCATION = "user_location"
|
||||
CONF_WEB_SEARCH_MAX_USES = "web_search_max_uses"
|
||||
@@ -30,7 +29,6 @@ DEFAULT = {
|
||||
CONF_MAX_TOKENS: 3000,
|
||||
CONF_TEMPERATURE: 1.0,
|
||||
CONF_THINKING_BUDGET: 0,
|
||||
CONF_THINKING_EFFORT: "low",
|
||||
CONF_WEB_SEARCH: False,
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
CONF_WEB_SEARCH_MAX_USES: 5,
|
||||
@@ -44,27 +42,6 @@ NON_THINKING_MODELS = [
|
||||
"claude-3-haiku",
|
||||
]
|
||||
|
||||
NON_ADAPTIVE_THINKING_MODELS = [
|
||||
"claude-opus-4-5",
|
||||
"claude-sonnet-4-5",
|
||||
"claude-haiku-4-5",
|
||||
"claude-opus-4-1",
|
||||
"claude-opus-4-0",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-sonnet-4-0",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-3",
|
||||
]
|
||||
|
||||
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS = [
|
||||
"claude-opus-4-1",
|
||||
"claude-opus-4-0",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-sonnet-4-0",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-3",
|
||||
]
|
||||
|
||||
WEB_SEARCH_UNSUPPORTED_MODELS = [
|
||||
"claude-3-haiku",
|
||||
"claude-3-opus",
|
||||
|
||||
@@ -20,11 +20,9 @@ from anthropic.types import (
|
||||
DocumentBlockParam,
|
||||
ImageBlockParam,
|
||||
InputJSONDelta,
|
||||
JSONOutputFormatParam,
|
||||
MessageDeltaUsage,
|
||||
MessageParam,
|
||||
MessageStreamEvent,
|
||||
OutputConfigParam,
|
||||
RawContentBlockDeltaEvent,
|
||||
RawContentBlockStartEvent,
|
||||
RawContentBlockStopEvent,
|
||||
@@ -43,7 +41,6 @@ from anthropic.types import (
|
||||
TextDelta,
|
||||
ThinkingBlock,
|
||||
ThinkingBlockParam,
|
||||
ThinkingConfigAdaptiveParam,
|
||||
ThinkingConfigDisabledParam,
|
||||
ThinkingConfigEnabledParam,
|
||||
ThinkingDelta,
|
||||
@@ -81,7 +78,6 @@ from .const import (
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_THINKING_BUDGET,
|
||||
CONF_THINKING_EFFORT,
|
||||
CONF_WEB_SEARCH,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
@@ -93,9 +89,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
MIN_THINKING_BUDGET,
|
||||
NON_ADAPTIVE_THINKING_MODELS,
|
||||
NON_THINKING_MODELS,
|
||||
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS,
|
||||
)
|
||||
|
||||
# Max number of back and forth with the LLM to generate a response
|
||||
@@ -599,7 +593,6 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
chat_log: conversation.ChatLog,
|
||||
structure_name: str | None = None,
|
||||
structure: vol.Schema | None = None,
|
||||
max_iterations: int = MAX_TOOL_ITERATIONS,
|
||||
) -> None:
|
||||
"""Generate an answer for the chat log."""
|
||||
options = self.subentry.data
|
||||
@@ -607,16 +600,6 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
system = chat_log.content[0]
|
||||
if not isinstance(system, conversation.SystemContent):
|
||||
raise TypeError("First message must be a system message")
|
||||
|
||||
# System prompt with caching enabled
|
||||
system_prompt: list[TextBlockParam] = [
|
||||
TextBlockParam(
|
||||
type="text",
|
||||
text=system.content,
|
||||
cache_control={"type": "ephemeral"},
|
||||
)
|
||||
]
|
||||
|
||||
messages = _convert_content(chat_log.content[1:])
|
||||
|
||||
model = options.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
|
||||
@@ -625,38 +608,25 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
model=model,
|
||||
messages=messages,
|
||||
max_tokens=options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
|
||||
system=system_prompt,
|
||||
system=system.content,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
if not model.startswith(tuple(NON_ADAPTIVE_THINKING_MODELS)):
|
||||
thinking_effort = options.get(
|
||||
CONF_THINKING_EFFORT, DEFAULT[CONF_THINKING_EFFORT]
|
||||
thinking_budget = options.get(
|
||||
CONF_THINKING_BUDGET, DEFAULT[CONF_THINKING_BUDGET]
|
||||
)
|
||||
if (
|
||||
not model.startswith(tuple(NON_THINKING_MODELS))
|
||||
and thinking_budget >= MIN_THINKING_BUDGET
|
||||
):
|
||||
model_args["thinking"] = ThinkingConfigEnabledParam(
|
||||
type="enabled", budget_tokens=thinking_budget
|
||||
)
|
||||
if thinking_effort != "none":
|
||||
model_args["thinking"] = ThinkingConfigAdaptiveParam(type="adaptive")
|
||||
model_args["output_config"] = OutputConfigParam(effort=thinking_effort)
|
||||
else:
|
||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||
model_args["temperature"] = options.get(
|
||||
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
|
||||
)
|
||||
else:
|
||||
thinking_budget = options.get(
|
||||
CONF_THINKING_BUDGET, DEFAULT[CONF_THINKING_BUDGET]
|
||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||
model_args["temperature"] = options.get(
|
||||
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
|
||||
)
|
||||
if (
|
||||
not model.startswith(tuple(NON_THINKING_MODELS))
|
||||
and thinking_budget >= MIN_THINKING_BUDGET
|
||||
):
|
||||
model_args["thinking"] = ThinkingConfigEnabledParam(
|
||||
type="enabled", budget_tokens=thinking_budget
|
||||
)
|
||||
else:
|
||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||
model_args["temperature"] = options.get(
|
||||
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
|
||||
)
|
||||
|
||||
tools: list[ToolUnionParam] = []
|
||||
if chat_log.llm_api:
|
||||
@@ -700,25 +670,8 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
)
|
||||
|
||||
if structure and structure_name:
|
||||
if not model.startswith(tuple(UNSUPPORTED_STRUCTURED_OUTPUT_MODELS)):
|
||||
# Native structured output for those models who support it.
|
||||
structure_name = None
|
||||
model_args.setdefault("output_config", OutputConfigParam())[
|
||||
"format"
|
||||
] = JSONOutputFormatParam(
|
||||
type="json_schema",
|
||||
schema={
|
||||
**convert(
|
||||
structure,
|
||||
custom_serializer=chat_log.llm_api.custom_serializer
|
||||
if chat_log.llm_api
|
||||
else llm.selector_serializer,
|
||||
),
|
||||
"additionalProperties": False,
|
||||
},
|
||||
)
|
||||
elif model_args["thinking"]["type"] == "disabled":
|
||||
structure_name = slugify(structure_name)
|
||||
structure_name = slugify(structure_name)
|
||||
if model_args["thinking"]["type"] == "disabled":
|
||||
if not tools:
|
||||
# Simplest case: no tools and no extended thinking
|
||||
# Add a tool and force its use
|
||||
@@ -738,32 +691,33 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
# force tool use or disable text responses, so we add a hint to the
|
||||
# system prompt instead. With extended thinking, the model should be
|
||||
# smart enough to use the tool.
|
||||
structure_name = slugify(structure_name)
|
||||
model_args["tool_choice"] = ToolChoiceAutoParam(
|
||||
type="auto",
|
||||
)
|
||||
|
||||
if isinstance(model_args["system"], str):
|
||||
model_args["system"] = [
|
||||
TextBlockParam(type="text", text=model_args["system"])
|
||||
]
|
||||
model_args["system"].append( # type: ignore[union-attr]
|
||||
TextBlockParam(
|
||||
type="text",
|
||||
text=f"Claude MUST use the '{structure_name}' tool to provide "
|
||||
"the final answer instead of plain text.",
|
||||
text=f"Claude MUST use the '{structure_name}' tool to provide the final answer instead of plain text.",
|
||||
)
|
||||
)
|
||||
|
||||
if structure_name:
|
||||
tools.append(
|
||||
ToolParam(
|
||||
name=structure_name,
|
||||
description="Use this tool to reply to the user",
|
||||
input_schema=convert(
|
||||
structure,
|
||||
custom_serializer=chat_log.llm_api.custom_serializer
|
||||
if chat_log.llm_api
|
||||
else llm.selector_serializer,
|
||||
),
|
||||
)
|
||||
tools.append(
|
||||
ToolParam(
|
||||
name=structure_name,
|
||||
description="Use this tool to reply to the user",
|
||||
input_schema=convert(
|
||||
structure,
|
||||
custom_serializer=chat_log.llm_api.custom_serializer
|
||||
if chat_log.llm_api
|
||||
else llm.selector_serializer,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if tools:
|
||||
model_args["tools"] = tools
|
||||
@@ -771,7 +725,7 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
client = self.entry.runtime_data
|
||||
|
||||
# To prevent infinite loops, we limit the number of iterations
|
||||
for _iteration in range(max_iterations):
|
||||
for _iteration in range(MAX_TOOL_ITERATIONS):
|
||||
try:
|
||||
stream = await client.messages.create(**model_args)
|
||||
|
||||
@@ -784,7 +738,7 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
_transform_stream(
|
||||
chat_log,
|
||||
stream,
|
||||
output_tool=structure_name or None,
|
||||
output_tool=structure_name if structure else None,
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["anthropic==0.78.0"]
|
||||
"requirements": ["anthropic==0.75.0"]
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
from homeassistant.config_entries import ConfigEntryState, ConfigSubentry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigSubentry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
@@ -23,9 +23,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AnthropicConfigEntry
|
||||
|
||||
|
||||
class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
"""Handler for an issue fixing flow."""
|
||||
@@ -113,7 +110,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
|
||||
async def _async_next_target(
|
||||
self,
|
||||
) -> tuple[AnthropicConfigEntry, ConfigSubentry, str] | None:
|
||||
) -> tuple[ConfigEntry, ConfigSubentry, str] | None:
|
||||
"""Return the next deprecated subentry target."""
|
||||
if self._subentry_iter is None:
|
||||
self._subentry_iter = self._iter_deprecated_subentries()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"error": {
|
||||
"authentication_error": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
@@ -11,23 +10,10 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::anthropic::config::step::user::data_description::api_key%]"
|
||||
},
|
||||
"description": "Reauthentication required. Please enter your updated API key."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "Your Anthropic API key."
|
||||
},
|
||||
"description": "Set up Anthropic integration by providing your Anthropic API key. Instructions to obtain an API key can be found in [the documentation]({instructions_url})."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -49,11 +35,6 @@
|
||||
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::max_tokens%]",
|
||||
"temperature": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::temperature%]"
|
||||
},
|
||||
"data_description": {
|
||||
"chat_model": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::chat_model%]",
|
||||
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::max_tokens%]",
|
||||
"temperature": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::temperature%]"
|
||||
},
|
||||
"title": "[%key:component::anthropic::config_subentries::conversation::step::advanced::title%]"
|
||||
},
|
||||
"init": {
|
||||
@@ -61,23 +42,17 @@
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"recommended": "[%key:component::anthropic::config_subentries::conversation::step::init::data::recommended%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "[%key:component::anthropic::config_subentries::conversation::step::init::data_description::name%]",
|
||||
"recommended": "[%key:component::anthropic::config_subentries::conversation::step::init::data_description::recommended%]"
|
||||
},
|
||||
"title": "[%key:component::anthropic::config_subentries::conversation::step::init::title%]"
|
||||
},
|
||||
"model": {
|
||||
"data": {
|
||||
"thinking_budget": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_budget%]",
|
||||
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_effort%]",
|
||||
"user_location": "[%key:component::anthropic::config_subentries::conversation::step::model::data::user_location%]",
|
||||
"web_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data::web_search%]",
|
||||
"web_search_max_uses": "[%key:component::anthropic::config_subentries::conversation::step::model::data::web_search_max_uses%]"
|
||||
},
|
||||
"data_description": {
|
||||
"thinking_budget": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_budget%]",
|
||||
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_effort%]",
|
||||
"user_location": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::user_location%]",
|
||||
"web_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::web_search%]",
|
||||
"web_search_max_uses": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::web_search_max_uses%]"
|
||||
@@ -103,11 +78,6 @@
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"temperature": "Temperature"
|
||||
},
|
||||
"data_description": {
|
||||
"chat_model": "The model to serve the responses.",
|
||||
"max_tokens": "Limit the number of response tokens.",
|
||||
"temperature": "Control the randomness of the response, trading off between creativity and coherence."
|
||||
},
|
||||
"title": "Advanced settings"
|
||||
},
|
||||
"init": {
|
||||
@@ -118,24 +88,19 @@
|
||||
"recommended": "Recommended model settings"
|
||||
},
|
||||
"data_description": {
|
||||
"llm_hass_api": "Allow the LLM to control Home Assistant.",
|
||||
"name": "The name of this configuration",
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"recommended": "Use default configuration"
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template."
|
||||
},
|
||||
"title": "Basic settings"
|
||||
},
|
||||
"model": {
|
||||
"data": {
|
||||
"thinking_budget": "Thinking budget",
|
||||
"thinking_effort": "Thinking effort",
|
||||
"user_location": "Include home location",
|
||||
"web_search": "Enable web search",
|
||||
"web_search_max_uses": "Maximum web searches"
|
||||
},
|
||||
"data_description": {
|
||||
"thinking_budget": "The number of tokens the model can use to think about the response out of the total maximum number of tokens. Set to 1024 or greater to enable extended thinking.",
|
||||
"thinking_effort": "Control how many tokens Claude uses when responding, trading off between response thoroughness and token efficiency",
|
||||
"user_location": "Localize search results based on home location",
|
||||
"web_search": "The web search tool gives Claude direct access to real-time web content, allowing it to answer questions with up-to-date information beyond its knowledge cutoff",
|
||||
"web_search_max_uses": "Limit the number of searches performed per response"
|
||||
@@ -153,9 +118,6 @@
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]"
|
||||
},
|
||||
"data_description": {
|
||||
"chat_model": "Select the new model to use."
|
||||
},
|
||||
"description": "You are updating {subentry_name} ({subentry_type}) in {entry_name}. The current model {model} is deprecated. Select a supported model to continue.",
|
||||
"title": "Update model"
|
||||
}
|
||||
@@ -163,16 +125,5 @@
|
||||
},
|
||||
"title": "Model deprecated"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"thinking_effort": {
|
||||
"options": {
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"max": "Max",
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"none": "None"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["py-aosmith==1.0.17"]
|
||||
"requirements": ["py-aosmith==1.0.15"]
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
|
||||
return MODE_AOSMITH_TO_HA.get(self.device.status.current_mode, STATE_OFF)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self) -> bool:
|
||||
def is_away_mode_on(self):
|
||||
"""Return True if away mode is on."""
|
||||
return self.device.status.current_mode == AOSmithOperationMode.VACATION
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
async with asyncio.timeout(CONNECTION_TIMEOUT):
|
||||
data = APCUPSdData(await aioapcaccess.request_status(host, port))
|
||||
except OSError, asyncio.IncompleteReadError, TimeoutError:
|
||||
except (OSError, asyncio.IncompleteReadError, TimeoutError):
|
||||
errors = {"base": "cannot_connect"}
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=_SCHEMA, errors=errors
|
||||
@@ -77,7 +77,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
async with asyncio.timeout(CONNECTION_TIMEOUT):
|
||||
data = APCUPSdData(await aioapcaccess.request_status(host, port))
|
||||
except OSError, asyncio.IncompleteReadError, TimeoutError:
|
||||
except (OSError, asyncio.IncompleteReadError, TimeoutError):
|
||||
errors = {"base": "cannot_connect"}
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure", data_schema=_SCHEMA, errors=errors
|
||||
|
||||
@@ -547,7 +547,7 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
||||
|
||||
try:
|
||||
self._attr_native_value = dateutil.parser.parse(data)
|
||||
except dateutil.parser.ParserError, OverflowError:
|
||||
except (dateutil.parser.ParserError, OverflowError):
|
||||
# If parsing fails we should mark it as unknown, with a log for further debugging.
|
||||
_LOGGER.warning('Failed to parse date for %s: "%s"', key, data)
|
||||
self._attr_native_value = None
|
||||
|
||||
@@ -181,9 +181,9 @@ async def async_import_client_credential(
|
||||
CONF_DOMAIN: domain,
|
||||
CONF_CLIENT_ID: credential.client_id,
|
||||
CONF_CLIENT_SECRET: credential.client_secret,
|
||||
CONF_AUTH_DOMAIN: auth_domain or domain,
|
||||
CONF_AUTH_DOMAIN: auth_domain if auth_domain else domain,
|
||||
}
|
||||
item[CONF_NAME] = credential.name or DEFAULT_IMPORT_NAME
|
||||
item[CONF_NAME] = credential.name if credential.name else DEFAULT_IMPORT_NAME
|
||||
await hass.data[DATA_COMPONENT].async_import_item(item)
|
||||
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
|
||||
|
||||
name = data.get(Attribute.NAME) if data else None
|
||||
|
||||
return name or "Aprilaire"
|
||||
return name if name else "Aprilaire"
|
||||
|
||||
def get_hw_version(self, data: dict[str, Any]) -> str:
|
||||
"""Get the hardware version."""
|
||||
|
||||
@@ -41,7 +41,7 @@ class APsystemsLocalAPIFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
try:
|
||||
device_info = await api.get_device_info()
|
||||
except TimeoutError, ClientConnectionError:
|
||||
except (TimeoutError, ClientConnectionError):
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(device_info.deviceId)
|
||||
|
||||
@@ -64,7 +64,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
async def _async_setup(self) -> None:
|
||||
try:
|
||||
device_info = await self.api.get_device_info()
|
||||
except ConnectionError, TimeoutError:
|
||||
except (ConnectionError, TimeoutError):
|
||||
raise UpdateFailed from None
|
||||
self.api.max_power = device_info.maxPower
|
||||
self.api.min_power = device_info.minPower
|
||||
|
||||
@@ -49,7 +49,7 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
||||
"""Set the state with the value fetched from the inverter."""
|
||||
try:
|
||||
status = await self._api.get_max_power()
|
||||
except TimeoutError, ClientConnectorError:
|
||||
except (TimeoutError, ClientConnectorError):
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
|
||||
@@ -43,7 +43,7 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
|
||||
"""Update switch status and availability."""
|
||||
try:
|
||||
status = await self._api.get_device_power_status()
|
||||
except TimeoutError, ClientConnectionError, InverterReturnedError:
|
||||
except (TimeoutError, ClientConnectionError, InverterReturnedError):
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
|
||||
@@ -56,7 +56,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
refresh_token = await api.authenticate(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except ApiException, TimeoutError:
|
||||
except (ApiException, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except AuthenticationFailed:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -94,7 +94,7 @@ def _retry[_SharpAquosTVDeviceT: SharpAquosTVDevice, **_P](
|
||||
try:
|
||||
func(obj, *args, **kwargs)
|
||||
break
|
||||
except OSError, TypeError, ValueError:
|
||||
except (OSError, TypeError, ValueError):
|
||||
update_retries -= 1
|
||||
if update_retries == 0:
|
||||
obj.set_state(MediaPlayerState.OFF)
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aranet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aranet4==2.6.0"]
|
||||
"requirements": ["aranet4==2.5.1"]
|
||||
}
|
||||
|
||||
@@ -201,5 +201,5 @@ class ArwnSensor(SensorEntity):
|
||||
ev: dict[str, Any] = {}
|
||||
ev.update(event)
|
||||
self._attr_extra_state_attributes = ev
|
||||
self._attr_native_value = ev.get(self._state_key)
|
||||
self._attr_native_value = ev.get(self._state_key, None)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -969,7 +969,7 @@ class PipelineRun:
|
||||
metadata,
|
||||
self._speech_to_text_stream(audio_stream=stream, stt_vad=stt_vad),
|
||||
)
|
||||
except asyncio.CancelledError, TimeoutError:
|
||||
except (asyncio.CancelledError, TimeoutError):
|
||||
raise # expected
|
||||
except hass_nabucasa.auth.Unauthenticated as src_error:
|
||||
raise SpeechToTextError(
|
||||
|
||||
@@ -189,7 +189,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
await api.async_connect()
|
||||
|
||||
except AsusRouterError, OSError:
|
||||
except (AsusRouterError, OSError):
|
||||
_LOGGER.error(
|
||||
"Error connecting to the AsusWrt router at %s using protocol %s",
|
||||
host,
|
||||
|
||||
@@ -51,5 +51,5 @@ class AtagConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(DATA_SCHEMA),
|
||||
errors=errors or {},
|
||||
errors=errors if errors else {},
|
||||
)
|
||||
|
||||
@@ -37,15 +37,15 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.coordinator.atag.dhw.temperature
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str:
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
operation = self.coordinator.atag.dhw.current_operation
|
||||
return operation if operation in OPERATION_LIST else STATE_OFF
|
||||
return operation if operation in self.operation_list else STATE_OFF
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
@@ -53,7 +53,7 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
def target_temperature(self):
|
||||
"""Return the setpoint if water demand, otherwise return base temp (comfort level)."""
|
||||
return self.coordinator.atag.dhw.target_temperature
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
"title": "Set up one-time password delivered by notify component"
|
||||
},
|
||||
"setup": {
|
||||
"data": {
|
||||
"code": "Code"
|
||||
},
|
||||
"description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:",
|
||||
"title": "Verify setup"
|
||||
}
|
||||
@@ -45,9 +42,6 @@
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"code": "Code"
|
||||
},
|
||||
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator]({google_authenticator_url}) or [Authy]({authy_url}).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.",
|
||||
"title": "Set up two-factor authentication using TOTP"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import labs, websocket_api
|
||||
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
|
||||
from homeassistant.components.labs import async_subscribe_preview_feature
|
||||
from homeassistant.components.labs import async_listen as async_labs_listen
|
||||
from homeassistant.const import (
|
||||
ATTR_AREA_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
@@ -386,13 +386,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
schema=vol.Schema({vol.Optional(CONF_ID): str}),
|
||||
)
|
||||
|
||||
async def new_triggers_conditions_listener(
|
||||
_event_data: labs.EventLabsUpdatedData,
|
||||
) -> None:
|
||||
@callback
|
||||
def new_triggers_conditions_listener() -> None:
|
||||
"""Handle new_triggers_conditions flag change."""
|
||||
await reload_helper.execute_service(ServiceCall(hass, DOMAIN, SERVICE_RELOAD))
|
||||
hass.async_create_task(
|
||||
reload_helper.execute_service(ServiceCall(hass, DOMAIN, SERVICE_RELOAD))
|
||||
)
|
||||
|
||||
async_subscribe_preview_feature(
|
||||
async_labs_listen(
|
||||
hass,
|
||||
DOMAIN,
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
|
||||
|
||||
@@ -304,7 +304,7 @@ async def _try_async_validate_config_item(
|
||||
"""Validate config item."""
|
||||
try:
|
||||
return await _async_validate_config_item(hass, config, False, True)
|
||||
except vol.Invalid, HomeAssistantError:
|
||||
except (vol.Invalid, HomeAssistantError):
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import functools
|
||||
import json
|
||||
import logging
|
||||
from time import time
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from botocore.exceptions import BotoCoreError
|
||||
|
||||
@@ -189,68 +189,48 @@ class S3BackupAgent(BackupAgent):
|
||||
)
|
||||
upload_id = multipart_upload["UploadId"]
|
||||
try:
|
||||
parts: list[dict[str, Any]] = []
|
||||
parts = []
|
||||
part_number = 1
|
||||
buffer = bytearray() # bytes buffer to store the data
|
||||
offset = 0 # start index of unread data inside buffer
|
||||
buffer_size = 0 # bytes
|
||||
buffer: list[bytes] = []
|
||||
|
||||
stream = await open_stream()
|
||||
async for chunk in stream:
|
||||
buffer.extend(chunk)
|
||||
buffer_size += len(chunk)
|
||||
buffer.append(chunk)
|
||||
|
||||
# Upload parts of exactly MULTIPART_MIN_PART_SIZE_BYTES to ensure
|
||||
# all non-trailing parts have the same size (defensive implementation)
|
||||
view = memoryview(buffer)
|
||||
try:
|
||||
while len(buffer) - offset >= MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
start = offset
|
||||
end = offset + MULTIPART_MIN_PART_SIZE_BYTES
|
||||
part_data = view[start:end]
|
||||
offset = end
|
||||
|
||||
_LOGGER.debug(
|
||||
"Uploading part number %d, size %d",
|
||||
part_number,
|
||||
len(part_data),
|
||||
)
|
||||
part = await cast(Any, self._client).upload_part(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
PartNumber=part_number,
|
||||
UploadId=upload_id,
|
||||
Body=part_data.tobytes(),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
part_number += 1
|
||||
finally:
|
||||
view.release()
|
||||
|
||||
# Compact the buffer if the consumed offset has grown large enough. This
|
||||
# avoids unnecessary memory copies when compacting after every part upload.
|
||||
if offset and offset >= MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
buffer = bytearray(buffer[offset:])
|
||||
offset = 0
|
||||
# If buffer size meets minimum part size, upload it as a part
|
||||
if buffer_size >= MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
_LOGGER.debug(
|
||||
"Uploading part number %d, size %d", part_number, buffer_size
|
||||
)
|
||||
part = await self._client.upload_part(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
PartNumber=part_number,
|
||||
UploadId=upload_id,
|
||||
Body=b"".join(buffer),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
part_number += 1
|
||||
buffer_size = 0
|
||||
buffer = []
|
||||
|
||||
# Upload the final buffer as the last part (no minimum size requirement)
|
||||
# Offset should be 0 after the last compaction, but we use it as the start
|
||||
# index to be defensive in case the buffer was not compacted.
|
||||
if offset < len(buffer):
|
||||
remaining_data = memoryview(buffer)[offset:]
|
||||
if buffer:
|
||||
_LOGGER.debug(
|
||||
"Uploading final part number %d, size %d",
|
||||
part_number,
|
||||
len(remaining_data),
|
||||
"Uploading final part number %d, size %d", part_number, buffer_size
|
||||
)
|
||||
part = await cast(Any, self._client).upload_part(
|
||||
part = await self._client.upload_part(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
PartNumber=part_number,
|
||||
UploadId=upload_id,
|
||||
Body=remaining_data.tobytes(),
|
||||
Body=b"".join(buffer),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
|
||||
await cast(Any, self._client).complete_multipart_upload(
|
||||
await self._client.complete_multipart_upload(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
UploadId=upload_id,
|
||||
@@ -317,14 +297,14 @@ class S3BackupAgent(BackupAgent):
|
||||
return self._backup_cache
|
||||
|
||||
backups = {}
|
||||
paginator = self._client.get_paginator("list_objects_v2")
|
||||
metadata_files: list[dict[str, Any]] = []
|
||||
async for page in paginator.paginate(Bucket=self._bucket):
|
||||
metadata_files.extend(
|
||||
obj
|
||||
for obj in page.get("Contents", [])
|
||||
if obj["Key"].endswith(".metadata.json")
|
||||
)
|
||||
response = await self._client.list_objects_v2(Bucket=self._bucket)
|
||||
|
||||
# Filter for metadata files only
|
||||
metadata_files = [
|
||||
obj
|
||||
for obj in response.get("Contents", [])
|
||||
if obj["Key"].endswith(".metadata.json")
|
||||
]
|
||||
|
||||
for metadata_file in metadata_files:
|
||||
try:
|
||||
|
||||
@@ -44,7 +44,7 @@ async def async_get_config_entry_diagnostics(
|
||||
account_data["allowed"], TO_REDACT_ACCOUNT_DATA_ALLOWED
|
||||
)
|
||||
|
||||
except AttributeError, TypeError, ValueError, KeyError:
|
||||
except (AttributeError, TypeError, ValueError, KeyError):
|
||||
bucket_info = {"name": "unknown", "id": "unknown"}
|
||||
account_data = {"error": "Failed to retrieve detailed account information"}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ EXCLUDE_FROM_BACKUP = [
|
||||
"tmp_backups/*.tar",
|
||||
"OZW_Log.txt",
|
||||
"tts/*",
|
||||
".cache/*",
|
||||
]
|
||||
|
||||
EXCLUDE_DATABASE_FROM_BACKUP = [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for Baidu speech service."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aip import AipSpeech
|
||||
import voluptuous as vol
|
||||
@@ -10,7 +9,6 @@ from homeassistant.components.tts import (
|
||||
CONF_LANG,
|
||||
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
|
||||
Provider,
|
||||
TtsAudioType,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -87,17 +85,17 @@ class BaiduTTSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self._lang
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
def supported_languages(self):
|
||||
"""Return a list of supported languages."""
|
||||
return SUPPORTED_LANGUAGES
|
||||
|
||||
@property
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
def default_options(self):
|
||||
"""Return a dict including default options."""
|
||||
return {
|
||||
CONF_PERSON: self._speech_conf_data[_OPTIONS[CONF_PERSON]],
|
||||
@@ -107,16 +105,11 @@ class BaiduTTSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
def supported_options(self):
|
||||
"""Return a list of supported options."""
|
||||
return SUPPORTED_OPTIONS
|
||||
|
||||
def get_tts_audio(
|
||||
self,
|
||||
message: str,
|
||||
language: str,
|
||||
options: dict[str, Any],
|
||||
) -> TtsAudioType:
|
||||
def get_tts_audio(self, message, language, options):
|
||||
"""Load TTS from BaiduTTS."""
|
||||
|
||||
aip_speech = AipSpeech(
|
||||
|
||||
@@ -145,7 +145,7 @@ class BeoConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
async with self._client:
|
||||
try:
|
||||
await self._client.get_beolink_self(_request_timeout=3)
|
||||
except ClientConnectorError, TimeoutError:
|
||||
except (ClientConnectorError, TimeoutError):
|
||||
return self.async_abort(reason="invalid_address")
|
||||
|
||||
self._model = discovery_info.hostname[:-16].replace("-", " ")
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["mozart-api==5.3.1.108.2"],
|
||||
"requirements": ["mozart-api==5.3.1.108.0"],
|
||||
"zeroconf": ["_bangolufsen._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ from datetime import timedelta
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from uuid import UUID
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from mozart_api import __version__ as MOZART_API_VERSION
|
||||
@@ -736,7 +735,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
||||
await self._client.set_active_source(source_id=key)
|
||||
else:
|
||||
# Video
|
||||
await self._client.post_remote_trigger(id=UUID(key))
|
||||
await self._client.post_remote_trigger(id=key)
|
||||
|
||||
async def async_select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Select a sound mode."""
|
||||
@@ -895,7 +894,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
||||
translation_key="play_media_error",
|
||||
translation_placeholders={
|
||||
"media_type": media_type,
|
||||
"error_message": json.loads(cast(str, error.body))["message"],
|
||||
"error_message": json.loads(error.body)["message"],
|
||||
},
|
||||
) from error
|
||||
|
||||
|
||||
@@ -6,9 +6,16 @@ from typing import Any
|
||||
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy.blinkpy import Blink
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import CONF_SCAN_INTERVAL
|
||||
from homeassistant.const import (
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
CONF_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -20,6 +27,13 @@ from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string}
|
||||
)
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string})
|
||||
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILE_PATH): cv.string}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@@ -9,23 +9,35 @@ from typing import Any
|
||||
from blinkpy.auth import UnauthorizedError
|
||||
from blinkpy.camera import BlinkCamera as BlinkCameraAPI
|
||||
from requests.exceptions import ChunkedEncodingError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
HomeAssistantError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_BRAND, DOMAIN
|
||||
from .const import (
|
||||
DEFAULT_BRAND,
|
||||
DOMAIN,
|
||||
SERVICE_RECORD,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
SERVICE_TRIGGER,
|
||||
)
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_VIDEO_CLIP = "video"
|
||||
ATTR_IMAGE = "image"
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@@ -44,6 +56,20 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(SERVICE_RECORD, None, "record")
|
||||
platform.async_register_entity_service(SERVICE_TRIGGER, None, "trigger_camera")
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{vol.Required(CONF_FILE_PATH): cv.string},
|
||||
"save_recent_clips",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{vol.Required(CONF_FILENAME): cv.string},
|
||||
"save_video",
|
||||
)
|
||||
|
||||
|
||||
class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
||||
"""An implementation of a Blink Camera."""
|
||||
|
||||
@@ -20,6 +20,11 @@ TYPE_TEMPERATURE = "temperature"
|
||||
TYPE_BATTERY = "battery"
|
||||
TYPE_WIFI_STRENGTH = "wifi_strength"
|
||||
|
||||
SERVICE_RECORD = "record"
|
||||
SERVICE_TRIGGER = "trigger_camera"
|
||||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
|
||||
@@ -4,27 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir, service
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SERVICE_SEND_PIN
|
||||
|
||||
SERVICE_RECORD = "record"
|
||||
SERVICE_TRIGGER = "trigger_camera"
|
||||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
|
||||
|
||||
# Deprecated
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
@@ -66,36 +52,3 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
_send_pin,
|
||||
schema=SERVICE_SEND_PIN_SCHEMA,
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_RECORD,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func="record",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_TRIGGER,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func="trigger_camera",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={vol.Required(CONF_FILE_PATH): cv.string},
|
||||
func="save_recent_clips",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={vol.Required(CONF_FILENAME): cv.string},
|
||||
func="save_video",
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user