Compare commits

..

4 Commits

Author SHA1 Message Date
Jan Čermák
7a1e378300 Sleep 1e-99s in async_block_till_done to workaround unfinished tasks 2026-02-05 18:08:07 +01:00
Jan Čermák
f1e1792e64 Revert "Try reverting changes from Python's gh-105836 patch"
This reverts commit 5de171f714.
2026-02-05 16:56:18 +01:00
Jan Čermák
5de171f714 Try reverting changes from Python's gh-105836 patch 2026-02-05 14:09:09 +01:00
Jan Čermák
6b07b2b8bc Bump base image to 2026.02.0 with Python 3.14.3, use 3.14.3 in CI
This also bumps libcec used in the base image to 7.1.1, full changelog:
* https://github.com/home-assistant/docker/releases/tag/2026.02.0

Python changelog:
* https://docs.python.org/release/3.14.3/whatsnew/changelog.html
2026-02-05 08:59:14 +01:00
1646 changed files with 13972 additions and 58856 deletions

View File

@@ -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/**

View File

@@ -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": [

View File

@@ -10,23 +10,19 @@ on:
env:
BUILD_TYPE: core
DEFAULT_PYTHON: "3.14.2"
DEFAULT_PYTHON: "3.14.3"
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: "2026.02.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
jobs:
init:
name: Initialize build
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
channel: ${{ steps.version.outputs.channel }}
@@ -35,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
@@ -103,8 +97,6 @@ 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'
@@ -173,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
@@ -198,7 +190,8 @@ jobs:
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"
@@ -232,7 +225,7 @@ jobs:
- 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
@@ -282,8 +275,6 @@ jobs:
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set build additional args
run: |
@@ -319,13 +310,9 @@ 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
@@ -368,10 +355,7 @@ jobs:
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'
@@ -492,8 +476,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
@@ -539,8 +521,6 @@ 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
@@ -550,7 +530,7 @@ jobs:
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
@@ -563,7 +543,7 @@ jobs:
- 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

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,6 @@ on:
schedule:
- cron: "30 18 * * 4"
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -24,15 +22,13 @@ jobs:
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@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
with:
category: "/language:python"

View File

@@ -231,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: |

View File

@@ -57,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: |

View File

@@ -5,15 +5,10 @@ on:
schedule:
- cron: "0 * * * *"
permissions: {}
jobs:
lock:
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:

View File

@@ -5,14 +5,9 @@ on:
issues:
types: [opened]
permissions: {}
jobs:
check-authorization:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
# Only run if this is a Task issue type (from the issue form)
if: github.event.issue.type.name == 'Task'
steps:

View File

@@ -6,15 +6,10 @@ on:
- cron: "0 * * * *"
workflow_dispatch:
permissions: {}
jobs:
stale:
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
# The 60 day stale policy for PRs
# Used for:

View File

@@ -9,10 +9,8 @@ on:
paths:
- "**strings.json"
permissions: {}
env:
DEFAULT_PYTHON: "3.14.2"
DEFAULT_PYTHON: "3.14.3"
jobs:
upload:
@@ -22,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

View File

@@ -17,9 +17,7 @@ on:
- "script/gen_requirements_all.py"
env:
DEFAULT_PYTHON: "3.14.2"
permissions: {}
DEFAULT_PYTHON: "3.14.3"
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
@@ -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

View File

@@ -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:

View File

@@ -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,7 +384,6 @@ 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.*
@@ -439,7 +435,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.*

25
CODEOWNERS generated
View File

@@ -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
@@ -1086,8 +1076,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
@@ -1180,8 +1170,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
@@ -1367,8 +1355,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
@@ -1575,7 +1561,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

View File

@@ -13,7 +13,6 @@
"microsoft",
"msteams",
"onedrive",
"onedrive_for_business",
"xbox"
]
}

View File

@@ -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:

View File

@@ -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}

View File

@@ -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"

View File

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

View File

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

View File

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

View File

@@ -49,9 +49,6 @@
}
},
"exceptions": {
"api_error": {
"message": "Failed to communicate with Actron Air device: {error}"
},
"auth_error": {
"message": "Authentication failed, please reauthenticate"
},

View File

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

View File

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

View File

@@ -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.",

View File

@@ -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)),
),
)

View File

@@ -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:

View File

@@ -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"

View File

@@ -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"

View File

@@ -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()

View File

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

View File

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

View File

@@ -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",
}

View File

@@ -1,15 +1,5 @@
{
"entity": {
"sensor": {
"voc_index": {
"default": "mdi:molecule"
}
}
},
"services": {
"send_info_skill": {
"service": "mdi:information"
},
"send_sound": {
"service": "mdi:cast-audio"
},

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==12.0.0"]
"requirements": ["aioamazondevices==11.1.1"]
}

View File

@@ -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(

View File

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

View File

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

View File

@@ -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": {

View File

@@ -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}

View File

@@ -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": {

View File

@@ -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(

View File

@@ -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(

View File

@@ -26,26 +26,21 @@ from homeassistant.const import (
UnitOfVolumetricFlux,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AmbientStationConfigEntry
from . import AmbientStation, AmbientStationConfigEntry
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 +58,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 +79,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 +144,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",
@@ -774,6 +683,22 @@ async def async_setup_entry(
class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity):
"""Define an Ambient sensor."""
def __init__(
self,
ambient: AmbientStation,
mac_address: str,
station_name: str,
description: EntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(ambient, mac_address, station_name, description)
if description.key == TYPE_SOLARRADIATION_LX:
# Since TYPE_SOLARRADIATION and TYPE_SOLARRADIATION_LX will have the same
# name in the UI, we influence the entity ID of TYPE_SOLARRADIATION_LX here
# to differentiate them:
self.entity_id = f"sensor.{station_name}_solar_rad_lx"
@callback
def update_from_latest_data(self) -> None:
"""Fetch new state data for the sensor."""

View File

@@ -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"
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(

View File

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

View File

@@ -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",

View File

@@ -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
@@ -629,34 +622,21 @@ class AnthropicBaseLLMEntity(Entity):
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 +680,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,7 +701,6 @@ 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",
)
@@ -746,24 +708,22 @@ class AnthropicBaseLLMEntity(Entity):
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 +731,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 +744,7 @@ class AnthropicBaseLLMEntity(Entity):
_transform_stream(
chat_log,
stream,
output_tool=structure_name or None,
output_tool=structure_name if structure else None,
),
)
]

View File

@@ -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"]
}

View File

@@ -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()

View File

@@ -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"
}
}
}
}

View File

@@ -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.16"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"

View File

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

View File

@@ -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"]
}

View File

@@ -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()

View File

@@ -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(

View File

@@ -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,

View File

@@ -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 {},
)

View File

@@ -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"
}

View File

@@ -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,

View File

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

View File

@@ -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:

View File

@@ -16,18 +16,12 @@ CONNECTION_TIMEOUT = 120 # 2 minutes
# Default TIMEOUT_FOR_UPLOAD is 128 seconds, which is too short for large backups
TIMEOUT_FOR_UPLOAD = 43200 # 12 hours
# Reduced retry count for download operations
# Default is 20 retries with exponential backoff, which can hang for 30+ minutes
# when there are persistent connection errors (e.g., SSL failures)
TRY_COUNT_DOWNLOAD = 3
class B2Http(BaseB2Http): # type: ignore[misc]
"""B2Http with extended timeouts for backup operations."""
CONNECTION_TIMEOUT = CONNECTION_TIMEOUT
TIMEOUT_FOR_UPLOAD = TIMEOUT_FOR_UPLOAD
TRY_COUNT_DOWNLOAD = TRY_COUNT_DOWNLOAD
class B2Session(BaseB2Session): # type: ignore[misc]

View File

@@ -40,10 +40,6 @@ CACHE_TTL = 300
# This prevents uploads from hanging indefinitely
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
# Timeout for metadata download operations (in seconds)
# This prevents the backup system from hanging when B2 connections fail
METADATA_DOWNLOAD_TIMEOUT = 60
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
"""Return the suggested filenames for the backup and metadata files."""
@@ -417,21 +413,12 @@ class BackblazeBackupAgent(BackupAgent):
backups = {}
for file_name, file_version in all_files_in_prefix.items():
if file_name.endswith(METADATA_FILE_SUFFIX):
try:
backup = await asyncio.wait_for(
self._hass.async_add_executor_job(
self._process_metadata_file_sync,
file_name,
file_version,
all_files_in_prefix,
),
timeout=METADATA_DOWNLOAD_TIMEOUT,
)
except TimeoutError:
_LOGGER.warning(
"Timeout downloading metadata file %s", file_name
)
continue
backup = await self._hass.async_add_executor_job(
self._process_metadata_file_sync,
file_name,
file_version,
all_files_in_prefix,
)
if backup:
backups[backup.backup_id] = backup
self._backup_list_cache = backups
@@ -455,18 +442,10 @@ class BackblazeBackupAgent(BackupAgent):
if not file or not metadata_file_version:
raise BackupNotFound(f"Backup {backup_id} not found")
try:
metadata_content = await asyncio.wait_for(
self._hass.async_add_executor_job(
self._download_and_parse_metadata_sync,
metadata_file_version,
),
timeout=METADATA_DOWNLOAD_TIMEOUT,
)
except TimeoutError:
raise BackupAgentError(
f"Timeout downloading metadata for backup {backup_id}"
) from None
metadata_content = await self._hass.async_add_executor_job(
self._download_and_parse_metadata_sync,
metadata_file_version,
)
_LOGGER.debug(
"Successfully retrieved metadata for backup ID %s from file %s",
@@ -489,27 +468,16 @@ class BackblazeBackupAgent(BackupAgent):
# Process metadata files sequentially to avoid exhausting executor pool
for file_name, file_version in all_files_in_prefix.items():
if file_name.endswith(METADATA_FILE_SUFFIX):
try:
(
result_backup_file,
result_metadata_file_version,
) = await asyncio.wait_for(
self._hass.async_add_executor_job(
self._process_metadata_file_for_id_sync,
file_name,
file_version,
backup_id,
all_files_in_prefix,
),
timeout=METADATA_DOWNLOAD_TIMEOUT,
)
except TimeoutError:
_LOGGER.warning(
"Timeout downloading metadata file %s while searching for backup %s",
file_name,
backup_id,
)
continue
(
result_backup_file,
result_metadata_file_version,
) = await self._hass.async_add_executor_job(
self._process_metadata_file_for_id_sync,
file_name,
file_version,
backup_id,
all_files_in_prefix,
)
if result_backup_file and result_metadata_file_version:
return result_backup_file, result_metadata_file_version

View File

@@ -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"}

View File

@@ -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("-", " ")

View File

@@ -80,16 +80,18 @@ SWITCHES = (
key=PLUG_AND_CHARGE,
translation_key=PLUG_AND_CHARGE,
function=set_plug_and_charge,
turn_on_off_fn=lambda evse_id, connector: update_on_value_and_activity(
PLUG_AND_CHARGE, evse_id, connector
turn_on_off_fn=lambda evse_id, connector: (
update_on_value_and_activity(PLUG_AND_CHARGE, evse_id, connector)
),
),
BlueCurrentSwitchEntityDescription(
key=LINKED_CHARGE_CARDS,
translation_key=LINKED_CHARGE_CARDS,
function=set_linked_charge_cards,
turn_on_off_fn=lambda evse_id, connector: update_on_value_and_activity(
PUBLIC_CHARGING, evse_id, connector, reverse_is_on=True
turn_on_off_fn=lambda evse_id, connector: (
update_on_value_and_activity(
PUBLIC_CHARGING, evse_id, connector, reverse_is_on=True
)
),
),
BlueCurrentSwitchEntityDescription(

View File

@@ -148,10 +148,8 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
device_class=BinarySensorDeviceClass.LOCK,
# device class lock: On means unlocked, Off means locked
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
value_fn=lambda v: (
v.doors_and_windows.door_lock_state
not in {LockState.LOCKED, LockState.SECURED}
),
value_fn=lambda v: v.doors_and_windows.door_lock_state
not in {LockState.LOCKED, LockState.SECURED},
attr_fn=lambda v, u: {
"door_lock_state": v.doors_and_windows.door_lock_state.value
},
@@ -191,11 +189,9 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
BMWBinarySensorEntityDescription(
key="is_pre_entry_climatization_enabled",
translation_key="is_pre_entry_climatization_enabled",
value_fn=lambda v: (
v.charging_profile.is_pre_entry_climatization_enabled
if v.charging_profile
else False
),
value_fn=lambda v: v.charging_profile.is_pre_entry_climatization_enabled
if v.charging_profile
else False,
is_available=lambda v: v.has_electric_drivetrain,
),
)

View File

@@ -40,9 +40,7 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
BMWButtonEntityDescription(
key="light_flash",
translation_key="light_flash",
remote_function=lambda vehicle: (
vehicle.remote_services.trigger_remote_light_flash()
),
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_light_flash(),
),
BMWButtonEntityDescription(
key="sound_horn",
@@ -52,24 +50,18 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
BMWButtonEntityDescription(
key="activate_air_conditioning",
translation_key="activate_air_conditioning",
remote_function=lambda vehicle: (
vehicle.remote_services.trigger_remote_air_conditioning()
),
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning(),
),
BMWButtonEntityDescription(
key="deactivate_air_conditioning",
translation_key="deactivate_air_conditioning",
remote_function=lambda vehicle: (
vehicle.remote_services.trigger_remote_air_conditioning_stop()
),
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(),
is_available=lambda vehicle: vehicle.is_remote_climate_stop_enabled,
),
BMWButtonEntityDescription(
key="find_vehicle",
translation_key="find_vehicle",
remote_function=lambda vehicle: (
vehicle.remote_services.trigger_remote_vehicle_finder()
),
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_vehicle_finder(),
),
)

View File

@@ -50,9 +50,7 @@ NUMBER_TYPES: list[BMWSwitchEntityDescription] = [
is_available=lambda v: v.is_remote_climate_stop_enabled,
value_fn=lambda v: v.climate.is_climate_on,
remote_service_on=lambda v: v.remote_services.trigger_remote_air_conditioning(),
remote_service_off=lambda v: (
v.remote_services.trigger_remote_air_conditioning_stop()
),
remote_service_off=lambda v: v.remote_services.trigger_remote_air_conditioning_stop(),
),
BMWSwitchEntityDescription(
key="charging",

View File

@@ -8,10 +8,11 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, service
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
@@ -40,10 +41,21 @@ SET_DATE_TIME_SCHEMA = vol.Schema(
async def async_set_panel_date(call: ServiceCall) -> None:
"""Set the date and time on a bosch alarm panel."""
config_entry: BoschAlarmConfigEntry | None
value: dt.datetime = call.data.get(ATTR_DATETIME, dt_util.now())
config_entry: BoschAlarmConfigEntry = service.async_get_config_entry(
call.hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID]
)
entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
if not (config_entry := call.hass.config_entries.async_get_entry(entry_id)):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="integration_not_found",
translation_placeholders={"target": entry_id},
)
if config_entry.state is not ConfigEntryState.LOADED:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="not_loaded",
translation_placeholders={"target": config_entry.title},
)
panel = config_entry.runtime_data
try:
await panel.set_panel_date(value)

View File

@@ -155,6 +155,12 @@
"incorrect_door_state": {
"message": "Door cannot be manipulated while it is momentarily unlocked."
},
"integration_not_found": {
"message": "Integration \"{target}\" not found in registry."
},
"not_loaded": {
"message": "{target} is not loaded."
},
"unknown_error": {
"message": "An unknown error occurred while setting the date and time on \"{target}\"."
}

View File

@@ -200,7 +200,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
"device": self.config_entry.title,
},
) from err
except BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff:
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff):
self.is_on = False
self.connected = False
_LOGGER.debug(

View File

@@ -63,9 +63,9 @@ SENSOR_DESCRIPTIONS: tuple[BringSensorEntityDescription, ...] = (
key=BringSensor.LIST_LANGUAGE,
translation_key=BringSensor.LIST_LANGUAGE,
value_fn=(
lambda lst, settings: (
x.lower() if (x := list_language(lst.lst.listUuid, settings)) else None
)
lambda lst, settings: x.lower()
if (x := list_language(lst.lst.listUuid, settings))
else None
),
entity_category=EntityCategory.DIAGNOSTIC,
options=[x.lower() for x in BRING_SUPPORTED_LOCALES],

View File

@@ -1,5 +1,7 @@
"""Actions for Bring! integration."""
from typing import TYPE_CHECKING
from bring_api import (
ActivityType,
BringAuthException,
@@ -11,6 +13,7 @@ import voluptuous as vol
from homeassistant.components.event import ATTR_EVENT_TYPE
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
@@ -43,6 +46,19 @@ SERVICE_ACTIVITY_STREAM_REACTION_SCHEMA = vol.Schema(
)
def get_config_entry(hass: HomeAssistant, entry_id: str) -> BringConfigEntry:
"""Return config entry or raise if not found or not loaded."""
entry = hass.config_entries.async_get_entry(entry_id)
if TYPE_CHECKING:
assert entry
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_loaded",
)
return entry
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for Bring! integration."""
@@ -62,9 +78,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
ATTR_ENTITY_ID: call.data[ATTR_ENTITY_ID],
},
)
config_entry: BringConfigEntry = service.async_get_config_entry(
hass, DOMAIN, entity.config_entry_id
)
config_entry = get_config_entry(hass, entity.config_entry_id)
coordinator = config_entry.runtime_data.data

View File

@@ -124,6 +124,10 @@
"entity_not_found": {
"message": "Failed to send reaction for Bring! — Unknown entity {entity_id}"
},
"entry_not_loaded": {
"message": "The account associated with this Bring! list is either not loaded or disabled in Home Assistant."
},
"notify_missing_argument": {
"message": "This action requires field {field}, please enter a valid value for {field}"
},

View File

@@ -173,7 +173,7 @@ class BroadlinkDevice[_ApiT: blk.Device = blk.Device]:
request = partial(function, *args, **kwargs)
try:
return await self.hass.async_add_executor_job(request)
except AuthorizationError, ConnectionClosedError:
except (AuthorizationError, ConnectionClosedError):
if not await self.async_auth():
raise
return await self.hass.async_add_executor_job(request)

View File

@@ -337,7 +337,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
await asyncio.sleep(1)
try:
code = await device.async_request(device.api.check_data)
except ReadError, StorageError:
except (ReadError, StorageError):
continue
return b64encode(code).decode("utf8")
@@ -413,7 +413,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
await asyncio.sleep(1)
try:
code = await device.async_request(device.api.check_data)
except ReadError, StorageError:
except (ReadError, StorageError):
continue
return b64encode(code).decode("utf8")

View File

@@ -127,7 +127,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
model, serial = await validate_input(self.hass, user_input)
except InvalidHost:
errors[CONF_HOST] = "wrong_host"
except ConnectionError, TimeoutError:
except (ConnectionError, TimeoutError):
errors["base"] = "cannot_connect"
except SnmpError:
errors["base"] = "snmp_error"
@@ -163,7 +163,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
await self.brother.async_update()
except UnsupportedModelError:
return self.async_abort(reason="unsupported_model")
except ConnectionError, SnmpError, TimeoutError:
except (ConnectionError, SnmpError, TimeoutError):
return self.async_abort(reason="cannot_connect")
# Check if already configured
@@ -211,7 +211,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
await validate_input(self.hass, user_input, entry.unique_id)
except InvalidHost:
errors[CONF_HOST] = "wrong_host"
except ConnectionError, TimeoutError:
except (ConnectionError, TimeoutError):
errors["base"] = "cannot_connect"
except SnmpError:
errors["base"] = "snmp_error"

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any, Final
from bsblan import BSBLANError, get_hvac_action_category
from bsblan import BSBLANError
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
@@ -13,7 +13,6 @@ from homeassistant.components.climate import (
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE
@@ -129,15 +128,6 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
return BSBLAN_TO_HA_HVAC_MODE.get(hvac_mode_value)
return try_parse_enum(HVACMode, hvac_mode_value)
@property
def hvac_action(self) -> HVACAction | None:
"""Return the current running hvac action."""
action = self.coordinator.data.state.hvac_action
if not action or not isinstance(action.value, int):
return None
category = get_hvac_action_category(action.value)
return HVACAction(category.name.lower())
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==4.2.0"],
"requirements": ["python-bsblan==4.1.0"],
"zeroconf": [
{
"name": "bsb-lan*",

View File

@@ -9,11 +9,10 @@ from bsblan import BSBLANError, SetHotWaterParam
from homeassistant.components.water_heater import (
STATE_ECO,
STATE_OFF,
STATE_PERFORMANCE,
WaterHeaterEntity,
WaterHeaterEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.const import ATTR_TEMPERATURE, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import format_mac
@@ -25,16 +24,14 @@ from .entity import BSBLanDualCoordinatorEntity
PARALLEL_UPDATES = 1
# Mapping between BSBLan operating mode values and HA operation modes
BSBLAN_TO_HA_OPERATION_MODE: dict[int, str] = {
0: STATE_OFF, # Protection mode
1: STATE_PERFORMANCE, # Continuous comfort mode
2: STATE_ECO, # Eco/automatic mode
# Mapping between BSBLan and HA operation modes
OPERATION_MODES = {
"Eco": STATE_ECO, # Energy saving mode
"Off": STATE_OFF, # Protection mode
"On": STATE_ON, # Continuous comfort mode
}
HA_TO_BSBLAN_OPERATION_MODE: dict[str, int] = {
v: k for k, v in BSBLAN_TO_HA_OPERATION_MODE.items()
}
OPERATION_MODES_REVERSE = {v: k for k, v in OPERATION_MODES.items()}
async def async_setup_entry(
@@ -66,14 +63,13 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
_attr_supported_features = (
WaterHeaterEntityFeature.TARGET_TEMPERATURE
| WaterHeaterEntityFeature.OPERATION_MODE
| WaterHeaterEntityFeature.ON_OFF
)
def __init__(self, data: BSBLanData) -> None:
"""Initialize BSBLAN water heater."""
super().__init__(data.fast_coordinator, data.slow_coordinator, data)
self._attr_unique_id = format_mac(data.device.MAC)
self._attr_operation_list = list(HA_TO_BSBLAN_OPERATION_MODE.keys())
self._attr_operation_list = list(OPERATION_MODES_REVERSE.keys())
# Set temperature unit
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
@@ -114,11 +110,8 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
"""Return current operation."""
if self.coordinator.data.dhw.operating_mode is None:
return None
# The operating_mode.value is an integer (0=Off, 1=On, 2=Eco)
current_mode_value = self.coordinator.data.dhw.operating_mode.value
if isinstance(current_mode_value, int):
return BSBLAN_TO_HA_OPERATION_MODE.get(current_mode_value)
return None
current_mode = self.coordinator.data.dhw.operating_mode.desc
return OPERATION_MODES.get(current_mode)
@property
def current_temperature(self) -> float | None:
@@ -151,12 +144,10 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new operation mode."""
# Base class validates operation_mode is in operation_list before calling
bsblan_mode = HA_TO_BSBLAN_OPERATION_MODE[operation_mode]
bsblan_mode = OPERATION_MODES_REVERSE.get(operation_mode)
try:
# Send numeric value as string - BSB-LAN API expects numeric mode values
await self.coordinator.client.set_hot_water(
SetHotWaterParam(operating_mode=str(bsblan_mode))
SetHotWaterParam(operating_mode=bsblan_mode)
)
except BSBLANError as err:
raise HomeAssistantError(
@@ -165,11 +156,3 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
) from err
await self.coordinator.async_request_refresh()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the water heater on."""
await self.async_set_operation_mode(STATE_PERFORMANCE)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the water heater off."""
await self.async_set_operation_mode(STATE_OFF)

View File

@@ -81,7 +81,7 @@ def get_event_types_by_event_class(event_class: str) -> set[str]:
but if there is only one button then it will be
button without a number postfix.
"""
return EVENT_TYPES_BY_EVENT_CLASS.get(event_class.split("_", maxsplit=1)[0], set())
return EVENT_TYPES_BY_EVENT_CLASS.get(event_class.split("_")[0], set())
async def async_validate_trigger_config(

View File

@@ -199,7 +199,7 @@ class BrData:
"""Return the temperature, or None."""
try:
return float(self.data.get(TEMPERATURE))
except ValueError, TypeError:
except (ValueError, TypeError):
return None
@property
@@ -207,7 +207,7 @@ class BrData:
"""Return the feeltemperature, or None."""
try:
return float(self.data.get(FEELTEMPERATURE))
except ValueError, TypeError:
except (ValueError, TypeError):
return None
@property
@@ -215,7 +215,7 @@ class BrData:
"""Return the pressure, or None."""
try:
return float(self.data.get(PRESSURE))
except ValueError, TypeError:
except (ValueError, TypeError):
return None
@property
@@ -223,7 +223,7 @@ class BrData:
"""Return the humidity, or None."""
try:
return int(self.data.get(HUMIDITY))
except ValueError, TypeError:
except (ValueError, TypeError):
return None
@property
@@ -231,7 +231,7 @@ class BrData:
"""Return the visibility, or None."""
try:
return int(self.data.get(VISIBILITY))
except ValueError, TypeError:
except (ValueError, TypeError):
return None
@property
@@ -239,7 +239,7 @@ class BrData:
"""Return the windgust, or None."""
try:
return float(self.data.get(WINDGUST))
except ValueError, TypeError:
except (ValueError, TypeError):
return None
@property
@@ -247,7 +247,7 @@ class BrData:
"""Return the windspeed, or None."""
try:
return float(self.data.get(WINDSPEED))
except ValueError, TypeError:
except (ValueError, TypeError):
return None
@property
@@ -255,7 +255,7 @@ class BrData:
"""Return the wind bearing, or None."""
try:
return int(self.data.get(WINDAZIMUTH))
except ValueError, TypeError:
except (ValueError, TypeError):
return None
@property

View File

@@ -691,7 +691,7 @@ class CalendarEventView(http.HomeAssistantView):
try:
start_date = dt_util.parse_datetime(start)
end_date = dt_util.parse_datetime(end)
except ValueError, AttributeError:
except (ValueError, AttributeError):
return web.Response(status=HTTPStatus.BAD_REQUEST)
if start_date is None or end_date is None:
return web.Response(status=HTTPStatus.BAD_REQUEST)

Some files were not shown because too many files have changed in this diff Show More