This commit is contained in:
Franck Nijhof
2023-07-05 17:20:03 +02:00
committed by GitHub
1803 changed files with 58326 additions and 14646 deletions

View File

@@ -27,6 +27,7 @@ base_platforms: &base_platforms
- homeassistant/components/fan/**
- homeassistant/components/geo_location/**
- homeassistant/components/humidifier/**
- homeassistant/components/image/**
- homeassistant/components/image_processing/**
- homeassistant/components/light/**
- homeassistant/components/lock/**

View File

@@ -90,6 +90,8 @@ omit =
homeassistant/components/atome/*
homeassistant/components/aurora/__init__.py
homeassistant/components/aurora/binary_sensor.py
homeassistant/components/aurora/coordinator.py
homeassistant/components/aurora/entity.py
homeassistant/components/aurora/sensor.py
homeassistant/components/avea/light.py
homeassistant/components/avion/light.py
@@ -122,7 +124,6 @@ omit =
homeassistant/components/bluetooth_tracker/*
homeassistant/components/bmw_connected_drive/__init__.py
homeassistant/components/bmw_connected_drive/binary_sensor.py
homeassistant/components/bmw_connected_drive/button.py
homeassistant/components/bmw_connected_drive/coordinator.py
homeassistant/components/bmw_connected_drive/lock.py
homeassistant/components/bmw_connected_drive/notify.py
@@ -202,6 +203,9 @@ omit =
homeassistant/components/discogs/sensor.py
homeassistant/components/discord/__init__.py
homeassistant/components/discord/notify.py
homeassistant/components/discovergy/__init__.py
homeassistant/components/discovergy/sensor.py
homeassistant/components/discovergy/coordinator.py
homeassistant/components/dlib_face_detect/image_processing.py
homeassistant/components/dlib_face_identify/image_processing.py
homeassistant/components/dlink/data.py
@@ -302,22 +306,10 @@ omit =
homeassistant/components/escea/climate.py
homeassistant/components/escea/discovery.py
homeassistant/components/esphome/__init__.py
homeassistant/components/esphome/binary_sensor.py
homeassistant/components/esphome/bluetooth/*
homeassistant/components/esphome/button.py
homeassistant/components/esphome/camera.py
homeassistant/components/esphome/climate.py
homeassistant/components/esphome/cover.py
homeassistant/components/esphome/domain_data.py
homeassistant/components/esphome/entry_data.py
homeassistant/components/esphome/fan.py
homeassistant/components/esphome/light.py
homeassistant/components/esphome/lock.py
homeassistant/components/esphome/media_player.py
homeassistant/components/esphome/number.py
homeassistant/components/esphome/select.py
homeassistant/components/esphome/sensor.py
homeassistant/components/esphome/switch.py
homeassistant/components/etherscan/sensor.py
homeassistant/components/eufy/*
homeassistant/components/eufylife_ble/__init__.py
@@ -327,6 +319,7 @@ omit =
homeassistant/components/ezviz/__init__.py
homeassistant/components/ezviz/binary_sensor.py
homeassistant/components/ezviz/camera.py
homeassistant/components/ezviz/light.py
homeassistant/components/ezviz/coordinator.py
homeassistant/components/ezviz/number.py
homeassistant/components/ezviz/entity.py
@@ -362,10 +355,13 @@ omit =
homeassistant/components/fitbit/*
homeassistant/components/fivem/__init__.py
homeassistant/components/fivem/binary_sensor.py
homeassistant/components/fivem/coordinator.py
homeassistant/components/fivem/entity.py
homeassistant/components/fivem/sensor.py
homeassistant/components/fixer/sensor.py
homeassistant/components/fjaraskupan/__init__.py
homeassistant/components/fjaraskupan/binary_sensor.py
homeassistant/components/fjaraskupan/coordinator.py
homeassistant/components/fjaraskupan/fan.py
homeassistant/components/fjaraskupan/light.py
homeassistant/components/fjaraskupan/number.py
@@ -615,7 +611,6 @@ omit =
homeassistant/components/kwb/sensor.py
homeassistant/components/lacrosse/sensor.py
homeassistant/components/lannouncer/notify.py
homeassistant/components/lastfm/sensor.py
homeassistant/components/launch_library/__init__.py
homeassistant/components/launch_library/sensor.py
homeassistant/components/lcn/climate.py
@@ -946,6 +941,8 @@ omit =
homeassistant/components/pyload/sensor.py
homeassistant/components/qbittorrent/__init__.py
homeassistant/components/qbittorrent/sensor.py
homeassistant/components/qnap/__init__.py
homeassistant/components/qnap/coordinator.py
homeassistant/components/qnap/sensor.py
homeassistant/components/qrcode/image_processing.py
homeassistant/components/quantum_gateway/device_tracker.py
@@ -973,6 +970,10 @@ omit =
homeassistant/components/rainmachine/switch.py
homeassistant/components/rainmachine/update.py
homeassistant/components/rainmachine/util.py
homeassistant/components/renson/__init__.py
homeassistant/components/renson/const.py
homeassistant/components/renson/entity.py
homeassistant/components/renson/sensor.py
homeassistant/components/raspyrfm/*
homeassistant/components/recollect_waste/sensor.py
homeassistant/components/recorder/repack.py
@@ -1046,12 +1047,6 @@ omit =
homeassistant/components/sense/__init__.py
homeassistant/components/sense/binary_sensor.py
homeassistant/components/sense/sensor.py
homeassistant/components/senseme/__init__.py
homeassistant/components/senseme/discovery.py
homeassistant/components/senseme/entity.py
homeassistant/components/senseme/fan.py
homeassistant/components/senseme/light.py
homeassistant/components/senseme/switch.py
homeassistant/components/senz/__init__.py
homeassistant/components/senz/api.py
homeassistant/components/senz/climate.py

View File

@@ -10,7 +10,7 @@ on:
env:
BUILD_TYPE: core
DEFAULT_PYTHON: "3.10"
DEFAULT_PYTHON: "3.11"
jobs:
init:
@@ -24,7 +24,7 @@ jobs:
publish: ${{ steps.version.outputs.publish }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
with:
fetch-depth: 0
@@ -48,18 +48,6 @@ jobs:
with:
ignore-dev: true
- name: Generate meta info
shell: bash
run: |
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > OFFICIAL_IMAGE
- name: Signing meta info file
uses: home-assistant/actions/helpers/codenotary@master
with:
source: file://${{ github.workspace }}/OFFICIAL_IMAGE
asset: OFFICIAL_IMAGE-${{ steps.version.outputs.version }}
token: ${{ secrets.CAS_TOKEN }}
build_python:
name: Build PyPi package
environment: ${{ needs.init.outputs.channel }}
@@ -68,7 +56,7 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1
@@ -101,12 +89,16 @@ jobs:
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
strategy:
matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
@@ -182,7 +174,7 @@ jobs:
# will drop the platform in the near future (they consider it
# "flimsy" on 386). The following packages depend on pandas,
# so we comment them out.
sed -i "s|env_canada|# env_canada|g" requirements_all.txt
sed -i "s|env-canada|# env-canada|g" requirements_all.txt
sed -i "s|noaa-coops|# noaa-coops|g" requirements_all.txt
sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
@@ -197,25 +189,20 @@ jobs:
run: |
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.1.0
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2023.03.0
uses: home-assistant/builder@2023.06.1
with:
args: |
$BUILD_ARGS \
--${{ matrix.arch }} \
--cosign \
--target /data \
--generic ${{ needs.init.outputs.version }}
env:
@@ -237,6 +224,10 @@ jobs:
if: github.repository_owner == 'home-assistant'
needs: ["init", "build_base"]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
strategy:
matrix:
machine:
@@ -262,7 +253,7 @@ jobs:
- yellow
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set build additional args
run: |
@@ -275,25 +266,20 @@ jobs:
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
fi
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.1.0
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2023.03.0
uses: home-assistant/builder@2023.06.1
with:
args: |
$BUILD_ARGS \
--target /data/machine \
--cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
env:
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
@@ -306,7 +292,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
@@ -338,34 +324,32 @@ jobs:
if: github.repository_owner == 'home-assistant'
needs: ["init", "build_base"]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
registry:
- "ghcr.io/home-assistant"
- "homeassistant"
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Install Cosign
uses: sigstore/cosign-installer@v3.1.1
with:
cosign-release: "v2.0.2"
- name: Login to DockerHub
if: matrix.registry == 'homeassistant'
uses: docker/login-action@v2.1.0
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v2.1.0
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install CAS tools
uses: home-assistant/actions/helpers/cas@master
- name: Build Meta Image
shell: bash
run: |
@@ -375,55 +359,78 @@ jobs:
local tag_l=${1}
local tag_r=${2}
docker manifest create "${{ matrix.registry }}/home-assistant:${tag_l}" \
"${{ matrix.registry }}/amd64-homeassistant:${tag_r}" \
"${{ matrix.registry }}/i386-homeassistant:${tag_r}" \
"${{ matrix.registry }}/armhf-homeassistant:${tag_r}" \
"${{ matrix.registry }}/armv7-homeassistant:${tag_r}" \
"${{ matrix.registry }}/aarch64-homeassistant:${tag_r}"
for registry in "ghcr.io/home-assistant" "docker.io/homeassistant"
do
docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \
"${{ matrix.registry }}/amd64-homeassistant:${tag_r}" \
--os linux --arch amd64
docker manifest create "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \
"${registry}/i386-homeassistant:${tag_r}" \
"${registry}/armhf-homeassistant:${tag_r}" \
"${registry}/armv7-homeassistant:${tag_r}" \
"${registry}/aarch64-homeassistant:${tag_r}"
docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \
"${{ matrix.registry }}/i386-homeassistant:${tag_r}" \
--os linux --arch 386
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \
--os linux --arch amd64
docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \
"${{ matrix.registry }}/armhf-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v6
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/i386-homeassistant:${tag_r}" \
--os linux --arch 386
docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \
"${{ matrix.registry }}/armv7-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v7
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/armhf-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v6
docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \
"${{ matrix.registry }}/aarch64-homeassistant:${tag_r}" \
--os linux --arch arm64 --variant=v8
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/armv7-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v7
docker manifest push --purge "${{ matrix.registry }}/home-assistant:${tag_l}"
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/aarch64-homeassistant:${tag_r}" \
--os linux --arch arm64 --variant=v8
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
cosign sign --yes "${registry}/home-assistant:${tag_l}"
done
}
function validate_image() {
local image=${1}
if ! cas authenticate --signerID notary@home-assistant.io "docker://${image}"; then
if ! cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp https://github.com/home-assistant/core/.* "${image}"; then
echo "Invalid signature!"
exit 1
fi
}
docker pull "${{ matrix.registry }}/amd64-homeassistant:${{ needs.init.outputs.version }}"
docker pull "${{ matrix.registry }}/i386-homeassistant:${{ needs.init.outputs.version }}"
docker pull "${{ matrix.registry }}/armhf-homeassistant:${{ needs.init.outputs.version }}"
docker pull "${{ matrix.registry }}/armv7-homeassistant:${{ needs.init.outputs.version }}"
docker pull "${{ matrix.registry }}/aarch64-homeassistant:${{ needs.init.outputs.version }}"
function push_dockerhub() {
local image=${1}
local tag=${2}
validate_image "${{ matrix.registry }}/amd64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "${{ matrix.registry }}/i386-homeassistant:${{ needs.init.outputs.version }}"
validate_image "${{ matrix.registry }}/armhf-homeassistant:${{ needs.init.outputs.version }}"
validate_image "${{ matrix.registry }}/armv7-homeassistant:${{ needs.init.outputs.version }}"
validate_image "${{ matrix.registry }}/aarch64-homeassistant:${{ needs.init.outputs.version }}"
docker tag "ghcr.io/home-assistant/${image}:${tag}" "docker.io/homeassistant/${image}:${tag}"
docker push "docker.io/homeassistant/${image}:${tag}"
cosign sign --yes "docker.io/homeassistant/${image}:${tag}"
}
# Pull images from github container registry and verify signature
docker pull "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/i386-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/armhf-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/i386-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/armhf-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
# Upload images to dockerhub
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
# Create version tag
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"

View File

@@ -32,7 +32,7 @@ env:
CACHE_VERSION: 5
PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 4
HA_SHORT_VERSION: 2023.6
HA_SHORT_VERSION: 2023.7
DEFAULT_PYTHON: "3.10"
ALL_PYTHON_VERSIONS: "['3.10', '3.11']"
# 10.3 is the oldest supported version
@@ -82,7 +82,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: >-
@@ -206,7 +206,7 @@ jobs:
- info
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.6.1
@@ -251,7 +251,7 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1
id: python
@@ -297,7 +297,7 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1
id: python
@@ -338,44 +338,6 @@ jobs:
shopt -s globstar
pre-commit run --hook-stage manual ruff --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*} --show-diff-on-failure
lint-isort:
name: Check isort
runs-on: ubuntu-22.04
needs:
- info
- pre-commit
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v3.3.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Run isort
run: |
. venv/bin/activate
pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure
lint-other:
name: Check other linters
runs-on: ubuntu-22.04
@@ -384,7 +346,7 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1
id: python
@@ -468,19 +430,6 @@ jobs:
with:
args: hadolint Dockerfile.dev
- name: Run bandit (fully)
if: needs.info.outputs.test_full_suite == 'true'
run: |
. venv/bin/activate
pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure
- name: Run bandit (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
shopt -s globstar
pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*} --show-diff-on-failure
base:
name: Prepare dependencies
runs-on: ubuntu-22.04
@@ -491,7 +440,7 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.6.1
@@ -543,10 +492,10 @@ jobs:
python -m venv venv
. venv/bin/activate
python --version
pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<23.2" setuptools wheel
pip install --cache-dir=$PIP_CACHE -U "pip>=21.3.1,<23.2" setuptools wheel
pip install --cache-dir=$PIP_CACHE -r requirements_all.txt
pip install --cache-dir=$PIP_CACHE -r requirements_test.txt
pip install -e .
pip install -e . --config-settings editable_mode=compat
hassfest:
name: Check hassfest
@@ -559,7 +508,7 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.6.1
@@ -591,7 +540,7 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.6.1
@@ -624,7 +573,7 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.6.1
@@ -668,7 +617,7 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.6.1
@@ -732,7 +681,6 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-isort
- lint-other
- lint-ruff
- mypy
@@ -751,7 +699,7 @@ jobs:
bluez \
ffmpeg
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.6.1
@@ -857,7 +805,6 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-isort
- lint-other
- lint-ruff
- mypy
@@ -877,7 +824,7 @@ jobs:
ffmpeg \
libmariadb-dev-compat
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.6.1
@@ -965,7 +912,6 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-isort
- lint-other
- lint-ruff
- mypy
@@ -985,7 +931,7 @@ jobs:
ffmpeg \
postgresql-server-dev-14
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.6.1
@@ -1062,12 +1008,12 @@ jobs:
timeout-minutes: 10
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Download all coverage artifacts
uses: actions/download-artifact@v3
- name: Upload coverage to Codecov (full coverage)
if: needs.info.outputs.test_full_suite == 'true'
uses: Wandalen/wretry.action@v1.0.36
uses: Wandalen/wretry.action@v1.3.0
with:
action: codecov/codecov-action@v3.1.3
with: |
@@ -1077,7 +1023,7 @@ jobs:
attempt_delay: 30000
- name: Upload coverage to Codecov (partial coverage)
if: needs.info.outputs.test_full_suite == 'false'
uses: Wandalen/wretry.action@v1.0.36
uses: Wandalen/wretry.action@v1.3.0
with:
action: codecov/codecov-action@v3.1.3
with: |

View File

@@ -10,7 +10,7 @@ jobs:
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4.0.0
- uses: dessant/lock-threads@v4.0.1
with:
github-token: ${{ github.token }}
issue-inactive-days: "30"

View File

@@ -10,7 +10,7 @@ on:
- "**strings.json"
env:
DEFAULT_PYTHON: "3.10"
DEFAULT_PYTHON: "3.11"
jobs:
upload:
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1

View File

@@ -26,7 +26,7 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Get information
id: info
@@ -47,10 +47,7 @@ jobs:
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true"
# GRPC on armv7 needs -lexecinfo (issue #56669) since home assistant installs
# execinfo-dev when building wheels. The setuptools build setup does not have an option for
# adding a single LDFLAG so copy all relevant linux flags here (as of 1.43.0)
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc -lexecinfo"
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc"
# Fix out of memory issues with rust
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
@@ -83,11 +80,11 @@ jobs:
strategy:
fail-fast: false
matrix:
abi: ["cp310", "cp311"]
abi: ["cp311"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Download env_file
uses: actions/download-artifact@v3
@@ -113,100 +110,6 @@ jobs:
requirements-diff: "requirements_diff.txt"
requirements: "requirements.txt"
integrations_cp310:
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
abi: ["cp310"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
- name: Download env_file
uses: actions/download-artifact@v3
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@v3
with:
name: requirements_diff
- name: Uncomment packages
run: |
requirement_files="requirements_all.txt requirements_diff.txt"
for requirement_file in ${requirement_files}; do
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
sed -i "s|# evdev|evdev|g" ${requirement_file}
sed -i "s|# pycups|pycups|g" ${requirement_file}
sed -i "s|# homekit|homekit|g" ${requirement_file}
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file}
done
- name: Split requirements all
run: |
# We split requirements all into two different files.
# This is to prevent the build from running out of memory when
# resolving packages on 32-bits systems (like armhf, armv7).
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 2) requirements_all.txt requirements_all.txt
- name: Adjust build env
run: |
if [ "${{ matrix.arch }}" = "i386" ]; then
echo "NPY_DISABLE_SVML=1" >> .env_file
fi
(
# cmake > 3.22.2 have issue on arm
# Tested until 3.22.5
echo "cmake==3.22.2"
) >> homeassistant/package_constraints.txt
# Do not pin numpy in wheels building
sed -i "/numpy/d" homeassistant/package_constraints.txt
- name: Build wheels (part 1)
uses: home-assistant/wheels@2023.04.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
- name: Build wheels (part 2)
uses: home-assistant/wheels@2023.04.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
# Wheels building for the cp311 ABI is currently split
# This is mainly until we have figured out to get all wheels built.
# Without harming our current workflow.
integrations_cp311:
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
if: github.repository_owner == 'home-assistant'
@@ -219,31 +122,12 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Write alternative env-file for cp311
run: |
(
echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false"
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true"
# GRPC on armv7 needed -lexecinfo (issue #56669) since home assistant installed
# execinfo-dev when building wheels. However, this package is no longer available
# Alpine 3.17, which we use for the cp311 ABI, so the flag should no longer be needed.
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc" # -lexecinfo
# Fix out of memory issues with rust
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
# OpenCV headless installation
echo "CI_BUILD=1"
echo "ENABLE_HEADLESS=1"
# Use C-Extension for sqlalchemy
echo "REQUIRE_SQLALCHEMY_CEXT=1"
) > .env_file
- name: Download env_file
uses: actions/download-artifact@v3
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@v3
@@ -254,27 +138,12 @@ jobs:
run: |
requirement_files="requirements_all.txt requirements_diff.txt"
for requirement_file in ${requirement_files}; do
# PyBluez no longer compiles. Commented it out for now.
# It need further cleanup down the line, as all machine images
# try to install it.
# sed -i "s|# pybluez|pybluez|g" ${requirement_file}
# beacontools requires PyBluez.
# sed -i "s|# beacontools|beacontools|g" ${requirement_file}
# It doesn't build for some reason, so we skip it for now.
# Bumping to the latest version (4.7.0.72) supporting Python 3.11
# doesn't help. Reverted bump in #91871. There are 8 registered
# instances using this integration according to analytics.
# sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file}
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
sed -i "s|# evdev|evdev|g" ${requirement_file}
sed -i "s|# pycups|pycups|g" ${requirement_file}
sed -i "s|# homekit|homekit|g" ${requirement_file}
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
sed -i "s|# decora-wifi|decora-wifi|g" ${requirement_file}
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
# Some packages are not buildable on armhf anymore
@@ -284,7 +153,7 @@ jobs:
# will drop the platform in the near future (they consider it
# "flimsy" on 386). The following packages depend on pandas,
# so we comment them out.
sed -i "s|env_canada|# env_canada|g" ${requirement_file}
sed -i "s|env-canada|# env-canada|g" ${requirement_file}
sed -i "s|noaa-coops|# noaa-coops|g" ${requirement_file}
sed -i "s|pyezviz|# pyezviz|g" ${requirement_file}
sed -i "s|pykrakenapi|# pykrakenapi|g" ${requirement_file}
@@ -297,7 +166,7 @@ jobs:
# This is to prevent the build from running out of memory when
# resolving packages on 32-bits systems (like armhf, armv7).
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 2) requirements_all.txt requirements_all.txt
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all.txt requirements_all.txt
- name: Adjust build env
run: |
@@ -305,13 +174,6 @@ jobs:
echo "NPY_DISABLE_SVML=1" >> .env_file
fi
# Probably not an issue anymore. Removing for now.
# (
# # cmake > 3.22.2 have issue on arm
# # Tested until 3.22.5
# echo "cmake==3.22.2"
# ) >> homeassistant/package_constraints.txt
# Do not pin numpy in wheels building
sed -i "/numpy/d" homeassistant/package_constraints.txt
@@ -342,3 +204,17 @@ jobs:
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
- name: Build wheels (part 3)
uses: home-assistant/wheels@2023.04.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.262
rev: v0.0.272
hooks:
- id: ruff
args:
@@ -22,19 +22,6 @@ repos:
- --quiet-level=2
exclude_types: [csv, json]
exclude: ^tests/fixtures/|homeassistant/generated/
- repo: https://github.com/PyCQA/bandit
rev: 1.7.4
hooks:
- id: bandit
args:
- --quiet
- --format=custom
- --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:

View File

@@ -172,6 +172,7 @@ homeassistant.components.huawei_lte.*
homeassistant.components.hydrawise.*
homeassistant.components.hyperion.*
homeassistant.components.ibeacon.*
homeassistant.components.image.*
homeassistant.components.image_processing.*
homeassistant.components.image_upload.*
homeassistant.components.imap.*
@@ -243,6 +244,7 @@ homeassistant.components.overkiz.*
homeassistant.components.peco.*
homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.*
homeassistant.components.ping.*
homeassistant.components.powerwall.*
homeassistant.components.proximity.*
homeassistant.components.prusalink.*
@@ -275,7 +277,6 @@ homeassistant.components.scene.*
homeassistant.components.schedule.*
homeassistant.components.scrape.*
homeassistant.components.select.*
homeassistant.components.senseme.*
homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.*
homeassistant.components.sensor.*
@@ -308,6 +309,7 @@ homeassistant.components.tag.*
homeassistant.components.tailscale.*
homeassistant.components.tautulli.*
homeassistant.components.tcp.*
homeassistant.components.text.*
homeassistant.components.threshold.*
homeassistant.components.tibber.*
homeassistant.components.tile.*

View File

@@ -275,6 +275,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/discogs/ @thibmaek
/homeassistant/components/discord/ @tkdrob
/tests/components/discord/ @tkdrob
/homeassistant/components/discovergy/ @jpbede
/tests/components/discovergy/ @jpbede
/homeassistant/components/discovery/ @home-assistant/core
/tests/components/discovery/ @home-assistant/core
/homeassistant/components/dlink/ @tkdrob
@@ -289,6 +291,8 @@ build.json @home-assistant/supervisor
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
/homeassistant/components/dormakaba_dkey/ @emontnemery
/tests/components/dormakaba_dkey/ @emontnemery
/homeassistant/components/dremel_3d_printer/ @tkdrob
/tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/dsmr/ @Robbie1221 @frenck
/tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @depl0y @glodenox
@@ -352,8 +356,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/eq3btsmart/ @rytilahti
/homeassistant/components/escea/ @lazdavila
/tests/components/escea/ @lazdavila
/homeassistant/components/esphome/ @OttoWinter @jesserockz
/tests/components/esphome/ @OttoWinter @jesserockz
/homeassistant/components/esphome/ @OttoWinter @jesserockz @bdraco
/tests/components/esphome/ @OttoWinter @jesserockz @bdraco
/homeassistant/components/eufylife_ble/ @bdr99
/tests/components/eufylife_ble/ @bdr99
/homeassistant/components/evil_genius_labs/ @balloob
@@ -448,8 +452,8 @@ build.json @home-assistant/supervisor
/tests/components/glances/ @engrbm87
/homeassistant/components/goalzero/ @tkdrob
/tests/components/goalzero/ @tkdrob
/homeassistant/components/gogogate2/ @vangorra @bdraco
/tests/components/gogogate2/ @vangorra @bdraco
/homeassistant/components/gogogate2/ @vangorra
/tests/components/gogogate2/ @vangorra
/homeassistant/components/goodwe/ @mletenay @starkillerOG
/tests/components/goodwe/ @mletenay @starkillerOG
/homeassistant/components/google/ @allenporter
@@ -559,12 +563,14 @@ build.json @home-assistant/supervisor
/tests/components/icloud/ @Quentame @nzapponi
/homeassistant/components/ign_sismologia/ @exxamalte
/tests/components/ign_sismologia/ @exxamalte
/homeassistant/components/image/ @home-assistant/core
/tests/components/image/ @home-assistant/core
/homeassistant/components/image_processing/ @home-assistant/core
/tests/components/image_processing/ @home-assistant/core
/homeassistant/components/image_upload/ @home-assistant/core
/tests/components/image_upload/ @home-assistant/core
/homeassistant/components/imap/ @engrbm87 @jbouwh
/tests/components/imap/ @engrbm87 @jbouwh
/homeassistant/components/imap/ @jbouwh
/tests/components/imap/ @jbouwh
/homeassistant/components/incomfort/ @zxdavb
/homeassistant/components/influxdb/ @mdegat01
/tests/components/influxdb/ @mdegat01
@@ -697,6 +703,8 @@ build.json @home-assistant/supervisor
/tests/components/logi_circle/ @evanjd
/homeassistant/components/lookin/ @ANMalko @bdraco
/tests/components/lookin/ @ANMalko @bdraco
/homeassistant/components/loqed/ @mikewoudenberg
/tests/components/loqed/ @mikewoudenberg
/homeassistant/components/lovelace/ @home-assistant/frontend
/tests/components/lovelace/ @home-assistant/frontend
/homeassistant/components/luci/ @mzdrale
@@ -779,11 +787,12 @@ build.json @home-assistant/supervisor
/tests/components/mutesync/ @currentoor
/homeassistant/components/my/ @home-assistant/core
/tests/components/my/ @home-assistant/core
/homeassistant/components/myq/ @bdraco @ehendrix23
/tests/components/myq/ @bdraco @ehendrix23
/homeassistant/components/myq/ @ehendrix23
/tests/components/myq/ @ehendrix23
/homeassistant/components/mysensors/ @MartinHjelmare @functionpointer
/tests/components/mysensors/ @MartinHjelmare @functionpointer
/homeassistant/components/mystrom/ @fabaff
/tests/components/mystrom/ @fabaff
/homeassistant/components/nam/ @bieniu
/tests/components/nam/ @bieniu
/homeassistant/components/nanoleaf/ @milanmeu
@@ -878,6 +887,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/opengarage/ @danielhiversen
/tests/components/opengarage/ @danielhiversen
/homeassistant/components/openhome/ @bazwilliams
/tests/components/openhome/ @bazwilliams
/homeassistant/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23
/tests/components/opentherm_gw/ @mvn23
@@ -961,6 +971,8 @@ build.json @home-assistant/supervisor
/tests/components/qingping/ @bdraco @skgsergio
/homeassistant/components/qld_bushfire/ @exxamalte
/tests/components/qld_bushfire/ @exxamalte
/homeassistant/components/qnap/ @disforw
/tests/components/qnap/ @disforw
/homeassistant/components/qnap_qsw/ @Noltari
/tests/components/qnap_qsw/ @Noltari
/homeassistant/components/quantum_gateway/ @cisasteelersfan
@@ -999,6 +1011,8 @@ build.json @home-assistant/supervisor
/tests/components/remote/ @home-assistant/core
/homeassistant/components/renault/ @epenet
/tests/components/renault/ @epenet
/homeassistant/components/renson/ @jimmyd-be
/tests/components/renson/ @jimmyd-be
/homeassistant/components/reolink/ @starkillerOG
/tests/components/reolink/ @starkillerOG
/homeassistant/components/repairs/ @home-assistant/core
@@ -1068,8 +1082,6 @@ build.json @home-assistant/supervisor
/tests/components/select/ @home-assistant/core
/homeassistant/components/sense/ @kbickar
/tests/components/sense/ @kbickar
/homeassistant/components/senseme/ @mikelawrence @bdraco
/tests/components/senseme/ @mikelawrence @bdraco
/homeassistant/components/sensibo/ @andrey-git @gjohansson-ST
/tests/components/sensibo/ @andrey-git @gjohansson-ST
/homeassistant/components/sensirion_ble/ @akx
@@ -1292,6 +1304,8 @@ build.json @home-assistant/supervisor
/tests/components/twentemilieu/ @frenck
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221
/tests/components/twinkly/ @dr1rrb @Robbie1221
/homeassistant/components/twitch/ @joostlek
/tests/components/twitch/ @joostlek
/homeassistant/components/ukraine_alarm/ @PaulAnnekov
/tests/components/ukraine_alarm/ @PaulAnnekov
/homeassistant/components/unifi/ @Kane610

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

View File

@@ -21,7 +21,7 @@ If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e
:target: https://www.home-assistant.io/join-chat/
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/master/docs/screenshots.png
:target: https://demo.home-assistant.io
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/docs/screenshot-integrations.png

View File

@@ -1,14 +1,16 @@
image: homeassistant/{arch}-homeassistant
shadow_repository: ghcr.io/home-assistant
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.1
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
cosign:
base_identity: https://github.com/home-assistant/docker/.*
identity: https://github.com/home-assistant/core/.*
labels:
io.hass.type: core
org.opencontainers.image.title: Home Assistant

View File

@@ -13,8 +13,8 @@ from homeassistant.const import CONF_COMMAND
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
from ..models import Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
CONF_ARGS = "args"
CONF_META = "meta"

View File

@@ -16,8 +16,8 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.storage import Store
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
from ..models import Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
STORAGE_VERSION = 1
STORAGE_KEY = "auth_provider.homeassistant"

View File

@@ -11,8 +11,8 @@ from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
from ..models import Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
USER_SCHEMA = vol.Schema(
{

View File

@@ -15,8 +15,8 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
from ..models import Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
AUTH_PROVIDER_TYPE = "legacy_api_password"
CONF_API_PASSWORD = "api_password"

View File

@@ -23,9 +23,9 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
from .. import InvalidAuthError
from ..models import Credentials, RefreshToken, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
IPAddress = IPv4Address | IPv6Address
IPNetwork = IPv4Network | IPv6Network

View File

@@ -0,0 +1,72 @@
"""Functools backports from standard lib."""
from __future__ import annotations
from collections.abc import Callable
from types import GenericAlias
from typing import Any, Generic, TypeVar, overload
from typing_extensions import Self
_T = TypeVar("_T")
_R = TypeVar("_R")
class cached_property(Generic[_T, _R]): # pylint: disable=invalid-name
"""Backport of Python 3.12's cached_property.
Includes https://github.com/python/cpython/pull/101890/files
"""
def __init__(self, func: Callable[[_T], _R]) -> None:
"""Initialize."""
self.func = func
self.attrname: Any = None
self.__doc__ = func.__doc__
def __set_name__(self, owner: type[_T], name: str) -> None:
"""Set name."""
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
raise TypeError(
"Cannot assign the same cached_property to two different names "
f"({self.attrname!r} and {name!r})."
)
@overload
def __get__(self, instance: None, owner: type[_T]) -> Self:
...
@overload
def __get__(self, instance: _T, owner: type[_T]) -> _R:
...
def __get__(self, instance: _T | None, owner: type[_T] | None = None) -> _R | Self:
"""Get."""
if instance is None:
return self
if self.attrname is None:
raise TypeError(
"Cannot use cached_property instance without calling __set_name__ on it."
)
try:
cache = instance.__dict__
# not all objects have __dict__ (e.g. class defines slots)
except AttributeError:
msg = (
f"No '__dict__' attribute on {type(instance).__name__!r} "
f"instance to cache {self.attrname!r} property."
)
raise TypeError(msg) from None
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
f"does not support item assignment for caching {self.attrname!r} property."
)
raise TypeError(msg) from None
return val
__class_getitem__ = classmethod(GenericAlias)

View File

@@ -34,6 +34,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
"""An alarm_control_panel implementation for Abode."""
_attr_icon = ICON
_attr_name = None
_attr_code_arm_required = False
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME

View File

@@ -42,6 +42,7 @@ async def async_setup_entry(
class AbodeBinarySensor(AbodeDevice, BinarySensorEntity):
"""A binary sensor implementation for Abode device."""
_attr_name = None
_device: ABBinarySensor
@property

View File

@@ -39,6 +39,7 @@ class AbodeCamera(AbodeDevice, Camera):
"""Representation of an Abode camera."""
_device: AbodeCam
_attr_name = None
def __init__(self, data: AbodeSystem, device: AbodeDev, event: Event) -> None:
"""Initialize the Abode device."""

View File

@@ -29,6 +29,7 @@ class AbodeCover(AbodeDevice, CoverEntity):
"""Representation of an Abode cover."""
_device: AbodeCV
_attr_name = None
@property
def is_closed(self) -> bool:

View File

@@ -42,6 +42,7 @@ class AbodeLight(AbodeDevice, LightEntity):
"""Representation of an Abode light."""
_device: AbodeLT
_attr_name = None
def turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""

View File

@@ -29,6 +29,7 @@ class AbodeLock(AbodeDevice, LockEntity):
"""Representation of an Abode lock."""
_device: AbodeLK
_attr_name = None
def lock(self, **kwargs: Any) -> None:
"""Lock the device."""

View File

@@ -22,17 +22,14 @@ from .const import DOMAIN
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=CONST.TEMP_STATUS_KEY,
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
),
SensorEntityDescription(
key=CONST.HUMI_STATUS_KEY,
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
),
SensorEntityDescription(
key=CONST.LUX_STATUS_KEY,
name="Lux",
device_class=SensorDeviceClass.ILLUMINANCE,
),
)

View File

@@ -44,6 +44,7 @@ class AbodeSwitch(AbodeDevice, SwitchEntity):
"""Representation of an Abode switch."""
_device: AbodeSW
_attr_name = None
def turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""

View File

@@ -4,10 +4,13 @@ from __future__ import annotations
from typing import cast
from homeassistant.components.weather import (
ATTR_FORECAST_CLOUD_COVERAGE,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_APPARENT_TEMP,
ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
@@ -29,7 +32,16 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherDataUpdateCoordinator
from .const import API_METRIC, ATTR_FORECAST, ATTRIBUTION, CONDITION_CLASSES, DOMAIN
from .const import (
API_METRIC,
ATTR_DIRECTION,
ATTR_FORECAST,
ATTR_SPEED,
ATTR_VALUE,
ATTRIBUTION,
CONDITION_CLASSES,
DOMAIN,
)
PARALLEL_UPDATES = 1
@@ -50,6 +62,7 @@ class AccuWeatherEntity(
"""Define an AccuWeather entity."""
_attr_has_entity_name = True
_attr_name = None
def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None:
"""Initialize."""
@@ -78,35 +91,61 @@ class AccuWeatherEntity(
except IndexError:
return None
@property
def cloud_coverage(self) -> float:
"""Return the Cloud coverage in %."""
return cast(float, self.coordinator.data["CloudCover"])
@property
def native_apparent_temperature(self) -> float:
"""Return the apparent temperature."""
return cast(
float, self.coordinator.data["ApparentTemperature"][API_METRIC][ATTR_VALUE]
)
@property
def native_temperature(self) -> float:
"""Return the temperature."""
return cast(float, self.coordinator.data["Temperature"][API_METRIC]["Value"])
return cast(float, self.coordinator.data["Temperature"][API_METRIC][ATTR_VALUE])
@property
def native_pressure(self) -> float:
"""Return the pressure."""
return cast(float, self.coordinator.data["Pressure"][API_METRIC]["Value"])
return cast(float, self.coordinator.data["Pressure"][API_METRIC][ATTR_VALUE])
@property
def native_dew_point(self) -> float:
"""Return the dew point."""
return cast(float, self.coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE])
@property
def humidity(self) -> int:
"""Return the humidity."""
return cast(int, self.coordinator.data["RelativeHumidity"])
@property
def native_wind_gust_speed(self) -> float:
"""Return the wind gust speed."""
return cast(
float, self.coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][ATTR_VALUE]
)
@property
def native_wind_speed(self) -> float:
"""Return the wind speed."""
return cast(float, self.coordinator.data["Wind"]["Speed"][API_METRIC]["Value"])
return cast(
float, self.coordinator.data["Wind"][ATTR_SPEED][API_METRIC][ATTR_VALUE]
)
@property
def wind_bearing(self) -> int:
"""Return the wind bearing."""
return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"])
return cast(int, self.coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"])
@property
def native_visibility(self) -> float:
"""Return the visibility."""
return cast(float, self.coordinator.data["Visibility"][API_METRIC]["Value"])
return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE])
@property
def forecast(self) -> list[Forecast] | None:
@@ -117,14 +156,23 @@ class AccuWeatherEntity(
return [
{
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"]["Value"],
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"]["Value"],
ATTR_FORECAST_NATIVE_PRECIPITATION: item["TotalLiquidDay"]["Value"],
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][
ATTR_VALUE
],
ATTR_FORECAST_NATIVE_PRECIPITATION: item["TotalLiquidDay"][ATTR_VALUE],
ATTR_FORECAST_PRECIPITATION_PROBABILITY: item[
"PrecipitationProbabilityDay"
],
ATTR_FORECAST_NATIVE_WIND_SPEED: item["WindDay"]["Speed"]["Value"],
ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"],
ATTR_FORECAST_NATIVE_WIND_SPEED: item["WindDay"][ATTR_SPEED][
ATTR_VALUE
],
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item["WindGustDay"][ATTR_SPEED][
ATTR_VALUE
],
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
ATTR_FORECAST_CONDITION: [
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
][0],

View File

@@ -14,6 +14,7 @@ class AcmedaBase(entity.Entity):
"""Base representation of an Acmeda roller."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(self, roller: aiopulse.Roller) -> None:
"""Initialize the roller."""
@@ -72,11 +73,6 @@ class AcmedaBase(entity.Entity):
"""Return the ID of this roller."""
return self.roller.id
@property
def name(self) -> str | None:
"""Return the name of roller."""
return self.roller.name
@property
def device_info(self) -> entity.DeviceInfo:
"""Return the device info."""

View File

@@ -45,7 +45,9 @@ async def async_setup_entry(
class AcmedaCover(AcmedaBase, CoverEntity):
"""Representation of a Acmeda cover device."""
"""Representation of an Acmeda cover device."""
_attr_name = None
@property
def current_cover_position(self) -> int | None:

View File

@@ -40,16 +40,11 @@ async def async_setup_entry(
class AcmedaBattery(AcmedaBase, SensorEntity):
"""Representation of a Acmeda cover device."""
"""Representation of an Acmeda cover sensor."""
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
@property
def name(self) -> str:
"""Return the name of roller."""
return f"{super().name} Battery"
@property
def native_value(self) -> float | int | None:
"""Return the state of the device."""

View File

@@ -1,7 +1,7 @@
"""Support for Adax wifi-enabled home heaters."""
from __future__ import annotations
from typing import Any
from typing import Any, cast
from adax import Adax
from adax_local import Adax as AdaxLocal
@@ -79,7 +79,10 @@ class AdaxDevice(ClimateEntity):
self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, heater_data["id"])},
name=self.name,
# Instead of setting the device name to the entity name, adax
# should be updated to set has_entity_name = True, and set the entity
# name to None
name=cast(str | None, self.name),
manufacturer="Adax",
)

View File

@@ -39,56 +39,56 @@ class AdGuardHomeEntityDescription(
SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
AdGuardHomeEntityDescription(
key="dns_queries",
name="DNS queries",
translation_key="dns_queries",
icon="mdi:magnify",
native_unit_of_measurement="queries",
value_fn=lambda adguard: adguard.stats.dns_queries(),
),
AdGuardHomeEntityDescription(
key="blocked_filtering",
name="DNS queries blocked",
translation_key="dns_queries_blocked",
icon="mdi:magnify-close",
native_unit_of_measurement="queries",
value_fn=lambda adguard: adguard.stats.blocked_filtering(),
),
AdGuardHomeEntityDescription(
key="blocked_percentage",
name="DNS queries blocked ratio",
translation_key="dns_queries_blocked_ratio",
icon="mdi:magnify-close",
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda adguard: adguard.stats.blocked_percentage(),
),
AdGuardHomeEntityDescription(
key="blocked_parental",
name="Parental control blocked",
translation_key="parental_control_blocked",
icon="mdi:human-male-girl",
native_unit_of_measurement="requests",
value_fn=lambda adguard: adguard.stats.replaced_parental(),
),
AdGuardHomeEntityDescription(
key="blocked_safebrowsing",
name="Safe browsing blocked",
translation_key="safe_browsing_blocked",
icon="mdi:shield-half-full",
native_unit_of_measurement="requests",
value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(),
),
AdGuardHomeEntityDescription(
key="enforced_safesearch",
name="Safe searches enforced",
translation_key="safe_searches_enforced",
icon="mdi:shield-search",
native_unit_of_measurement="requests",
value_fn=lambda adguard: adguard.stats.replaced_safesearch(),
),
AdGuardHomeEntityDescription(
key="average_speed",
name="Average processing speed",
translation_key="average_processing_speed",
icon="mdi:speedometer",
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
value_fn=lambda adguard: adguard.stats.avg_processing_time(),
),
AdGuardHomeEntityDescription(
key="rules_count",
name="Rules count",
translation_key="rules_count",
icon="mdi:counter",
native_unit_of_measurement="rules",
value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False),

View File

@@ -24,5 +24,53 @@
"existing_instance_updated": "Updated existing configuration.",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
},
"entity": {
"sensor": {
"dns_queries": {
"name": "DNS queries"
},
"dns_queries_blocked": {
"name": "DNS queries blocked"
},
"dns_queries_blocked_ratio": {
"name": "DNS queries blocked ratio"
},
"parental_control_blocked": {
"name": "Parental control blocked"
},
"safe_browsing_blocked": {
"name": "Safe browsing blocked"
},
"safe_searches_enforced": {
"name": "Safe searches enforced"
},
"average_processing_speed": {
"name": "Average processing speed"
},
"rules_count": {
"name": "Rules count"
}
},
"switch": {
"protection": {
"name": "Protection"
},
"parental": {
"name": "Parental control"
},
"safe_search": {
"name": "Safe search"
},
"safe_browsing": {
"name": "Safe browsing"
},
"filtering": {
"name": "Filtering"
},
"query_log": {
"name": "Query log"
}
}
}
}

View File

@@ -40,7 +40,7 @@ class AdGuardHomeSwitchEntityDescription(
SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
AdGuardHomeSwitchEntityDescription(
key="protection",
name="Protection",
translation_key="protection",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.protection_enabled,
turn_on_fn=lambda adguard: adguard.enable_protection,
@@ -48,7 +48,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
),
AdGuardHomeSwitchEntityDescription(
key="parental",
name="Parental control",
translation_key="parental",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.parental.enabled,
turn_on_fn=lambda adguard: adguard.parental.enable,
@@ -56,7 +56,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
),
AdGuardHomeSwitchEntityDescription(
key="safesearch",
name="Safe search",
translation_key="safe_search",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.safesearch.enabled,
turn_on_fn=lambda adguard: adguard.safesearch.enable,
@@ -64,7 +64,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
),
AdGuardHomeSwitchEntityDescription(
key="safebrowsing",
name="Safe browsing",
translation_key="safe_browsing",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.safebrowsing.enabled,
turn_on_fn=lambda adguard: adguard.safebrowsing.enable,
@@ -72,7 +72,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
),
AdGuardHomeSwitchEntityDescription(
key="filtering",
name="Filtering",
translation_key="filtering",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.filtering.enabled,
turn_on_fn=lambda adguard: adguard.filtering.enable,
@@ -80,7 +80,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
),
AdGuardHomeSwitchEntityDescription(
key="querylog",
name="Query log",
translation_key="query_log",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.querylog.enabled,
turn_on_fn=lambda adguard: adguard.querylog.enable,

View File

@@ -10,6 +10,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from .. import ads
from . import (
ADS_TYPEMAP,
CONF_ADS_FACTOR,
@@ -18,7 +19,6 @@ from . import (
STATE_KEY_STATE,
AdsEntity,
)
from .. import ads
DEFAULT_NAME = "ADS sensor"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(

View File

@@ -90,6 +90,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
_attr_target_temperature_step = PRECISION_WHOLE
_attr_max_temp = 32
_attr_min_temp = 16
_attr_name = None
_attr_hvac_modes = [
HVACMode.OFF,

View File

@@ -9,17 +9,30 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
TO_REDACT = ["dealerPhoneNumber", "latitude", "logoPIN", "longitude", "postCode"]
TO_REDACT = [
"dealerPhoneNumber",
"latitude",
"logoPIN",
"longitude",
"postCode",
"rid",
"deviceNames",
"deviceIds",
"deviceIdsV2",
"backupId",
]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]["coordinator"].data
data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id].coordinator.data
# Return only the relevant children
return {
"aircons": data["aircons"],
"aircons": data.get("aircons"),
"myLights": data.get("myLights"),
"myThings": data.get("myThings"),
"system": async_redact_data(data["system"], TO_REDACT),
}

View File

@@ -84,6 +84,8 @@ class AdvantageAirZoneEntity(AdvantageAirAcEntity):
class AdvantageAirThingEntity(AdvantageAirEntity):
"""Parent class for Advantage Air Things Entities."""
_attr_name = None
def __init__(self, instance: AdvantageAirData, thing: dict[str, Any]) -> None:
"""Initialize common aspects of an Advantage Air Things entity."""
super().__init__(instance)

View File

@@ -41,6 +41,7 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
"""Representation of Advantage Air Light."""
_attr_supported_color_modes = {ColorMode.ONOFF}
_attr_name = None
def __init__(self, instance: AdvantageAirData, light: dict[str, Any]) -> None:
"""Initialize an Advantage Air Light."""

View File

@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["advantage_air"],
"quality_scale": "platinum",
"requirements": ["advantage_air==0.4.4"]
"requirements": ["advantage-air==0.4.4"]
}

View File

@@ -80,7 +80,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_PM1,
device_class=SensorDeviceClass.PM1,
translation_key="pm1",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
@@ -88,7 +87,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_PM25,
device_class=SensorDeviceClass.PM25,
translation_key="pm25",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
@@ -100,7 +98,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_PM10,
device_class=SensorDeviceClass.PM10,
translation_key="pm10",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
@@ -112,7 +109,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_HUMIDITY,
device_class=SensorDeviceClass.HUMIDITY,
translation_key="humidity",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
@@ -120,7 +116,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
translation_key="pressure",
native_unit_of_measurement=UnitOfPressure.HPA,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
@@ -128,7 +123,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
@@ -147,7 +141,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_NO2,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
translation_key="no2",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
@@ -159,7 +152,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_SO2,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
translation_key="so2",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
@@ -171,7 +163,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_O3,
device_class=SensorDeviceClass.OZONE,
translation_key="o3",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,

View File

@@ -32,35 +32,8 @@
"caqi": {
"name": "Common air quality index"
},
"pm1": {
"name": "[%key:component::sensor::entity_component::pm1::name%]"
},
"pm25": {
"name": "[%key:component::sensor::entity_component::pm25::name%]"
},
"pm10": {
"name": "[%key:component::sensor::entity_component::pm10::name%]"
},
"humidity": {
"name": "[%key:component::sensor::entity_component::humidity::name%]"
},
"pressure": {
"name": "[%key:component::sensor::entity_component::pressure::name%]"
},
"temperature": {
"name": "[%key:component::sensor::entity_component::temperature::name%]"
},
"co": {
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
},
"no2": {
"name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]"
},
"so2": {
"name": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
},
"o3": {
"name": "[%key:component::sensor::entity_component::ozone::name%]"
}
}
}

View File

@@ -17,5 +17,3 @@ ATTR_API_STATION_LATITUDE = "Latitude"
ATTR_API_STATION_LONGITUDE = "Longitude"
DEFAULT_NAME = "AirNow"
DOMAIN = "airnow"
SENSOR_AQI_ATTR_DESCR = "description"
SENSOR_AQI_ATTR_LEVEL = "level"

View File

@@ -1,7 +1,12 @@
"""Support for the AirNow sensor service."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
@@ -12,7 +17,10 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirNowDataUpdateCoordinator
@@ -22,36 +30,60 @@ from .const import (
ATTR_API_AQI_LEVEL,
ATTR_API_O3,
ATTR_API_PM25,
DEFAULT_NAME,
DOMAIN,
SENSOR_AQI_ATTR_DESCR,
SENSOR_AQI_ATTR_LEVEL,
)
ATTRIBUTION = "Data provided by AirNow"
PARALLEL_UPDATES = 1
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
ATTR_DESCR = "description"
ATTR_LEVEL = "level"
@dataclass
class AirNowEntityDescriptionMixin:
"""Mixin for required keys."""
value_fn: Callable[[Any], StateType]
extra_state_attributes_fn: Callable[[Any], dict[str, str]] | None
@dataclass
class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMixin):
"""Describes Airnow sensor entity."""
SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_AQI,
icon="mdi:blur",
name=ATTR_API_AQI,
native_unit_of_measurement="aqi",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.AQI,
value_fn=lambda data: data.get(ATTR_API_AQI),
extra_state_attributes_fn=lambda data: {
ATTR_DESCR: data[ATTR_API_AQI_DESCRIPTION],
ATTR_LEVEL: data[ATTR_API_AQI_LEVEL],
},
),
SensorEntityDescription(
AirNowEntityDescription(
key=ATTR_API_PM25,
icon="mdi:blur",
name=ATTR_API_PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM25,
value_fn=lambda data: data.get(ATTR_API_PM25),
extra_state_attributes_fn=None,
),
SensorEntityDescription(
AirNowEntityDescription(
key=ATTR_API_O3,
translation_key="o3",
icon="mdi:blur",
name=ATTR_API_O3,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.get(ATTR_API_O3),
extra_state_attributes_fn=None,
),
)
@@ -73,38 +105,38 @@ class AirNowSensor(CoordinatorEntity[AirNowDataUpdateCoordinator], SensorEntity)
"""Define an AirNow sensor."""
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
entity_description: AirNowEntityDescription
def __init__(
self,
coordinator: AirNowDataUpdateCoordinator,
description: SensorEntityDescription,
description: AirNowEntityDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self.entity_description = description
self._state = None
self._attrs: dict[str, str] = {}
self._attr_name = f"AirNow {description.name}"
self._attr_unique_id = (
f"{coordinator.latitude}-{coordinator.longitude}-{description.key.lower()}"
)
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer=DEFAULT_NAME,
name=DEFAULT_NAME,
)
@property
def native_value(self):
def native_value(self) -> StateType:
"""Return the state."""
self._state = self.coordinator.data.get(self.entity_description.key)
return self._state
return self.entity_description.value_fn(self.coordinator.data)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the state attributes."""
if self.entity_description.key == ATTR_API_AQI:
self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[
ATTR_API_AQI_DESCRIPTION
]
self._attrs[SENSOR_AQI_ATTR_LEVEL] = self.coordinator.data[
ATTR_API_AQI_LEVEL
]
return self._attrs
if self.entity_description.extra_state_attributes_fn:
return self.entity_description.extra_state_attributes_fn(
self.coordinator.data
)
return None

View File

@@ -20,5 +20,12 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"sensor": {
"o3": {
"name": "[%key:component::sensor::entity_component::ozone::name%]"
}
}
}
}

View File

@@ -53,63 +53,62 @@ class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin)
SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="c2h4o",
name="Acetaldehyde",
translation_key="acetaldehyde",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c2h4o"),
),
AirQEntityDescription(
key="nh3_MR100",
name="Ammonia",
translation_key="ammonia",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("nh3_MR100"),
),
AirQEntityDescription(
key="ash3",
name="Arsine",
translation_key="arsine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ash3"),
),
AirQEntityDescription(
key="br2",
name="Bromine",
translation_key="bromine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("br2"),
),
AirQEntityDescription(
key="ch4s",
name="CH4S",
translation_key="methanethiol",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch4s"),
),
AirQEntityDescription(
key="cl2_M20",
name="Chlorine",
translation_key="chlorine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("cl2_M20"),
),
AirQEntityDescription(
key="clo2",
name="ClO2",
translation_key="chlorine_dioxide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("clo2"),
),
AirQEntityDescription(
key="co",
name="CO",
translation_key="carbon_monoxide",
native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("co"),
),
AirQEntityDescription(
key="co2",
name="CO2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
@@ -117,14 +116,14 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="cs2",
name="CS2",
translation_key="carbon_disulfide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("cs2"),
),
AirQEntityDescription(
key="dewpt",
name="Dew point",
translation_key="dew_point",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("dewpt"),
@@ -132,63 +131,63 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="ethanol",
name="Ethanol",
translation_key="ethanol",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ethanol"),
),
AirQEntityDescription(
key="c2h4",
name="Ethylene",
translation_key="ethylene",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c2h4"),
),
AirQEntityDescription(
key="ch2o_M10",
name="Formaldehyde",
translation_key="formaldehyde",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch2o_M10"),
),
AirQEntityDescription(
key="f2",
name="Fluorine",
translation_key="fluorine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("f2"),
),
AirQEntityDescription(
key="h2s",
name="H2S",
translation_key="hydrogen_sulfide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2s"),
),
AirQEntityDescription(
key="hcl",
name="HCl",
translation_key="hydrochloric_acid",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hcl"),
),
AirQEntityDescription(
key="hcn",
name="HCN",
translation_key="hydrogen_cyanide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hcn"),
),
AirQEntityDescription(
key="hf",
name="HF",
translation_key="hydrogen_fluoride",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hf"),
),
AirQEntityDescription(
key="health",
name="Health Index",
translation_key="health_index",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:heart-pulse",
@@ -196,7 +195,6 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="humidity",
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -204,7 +202,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="humidity_abs",
name="Absolute humidity",
translation_key="absolute_humidity",
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity_abs"),
@@ -212,28 +210,27 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="h2_M1000",
name="Hydrogen",
translation_key="hydrogen",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2_M1000"),
),
AirQEntityDescription(
key="h2o2",
name="Hydrogen peroxide",
translation_key="hydrogen_peroxide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2o2"),
),
AirQEntityDescription(
key="ch4_MIPEX",
name="Methane",
translation_key="methane",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch4_MIPEX"),
),
AirQEntityDescription(
key="n2o",
name="N2O",
device_class=SensorDeviceClass.NITROUS_OXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
@@ -241,7 +238,6 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="no_M250",
name="NO",
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
@@ -249,7 +245,6 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="no2",
name="NO2",
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
@@ -257,14 +252,14 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="acid_M100",
name="Organic acid",
translation_key="organic_acid",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("acid_M100"),
),
AirQEntityDescription(
key="oxygen",
name="Oxygen",
translation_key="oxygen",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("oxygen"),
@@ -272,7 +267,6 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="o3",
name="Ozone",
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
@@ -280,7 +274,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="performance",
name="Performance Index",
translation_key="performance_index",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:head-check",
@@ -288,14 +282,13 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="ph3",
name="PH3",
translation_key="hydrogen_phosphide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ph3"),
),
AirQEntityDescription(
key="pm1",
name="PM1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
@@ -304,7 +297,6 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="pm2_5",
name="PM2.5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
@@ -313,7 +305,6 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="pm10",
name="PM10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
@@ -322,7 +313,6 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="pressure",
name="Pressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA,
state_class=SensorStateClass.MEASUREMENT,
@@ -330,7 +320,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="pressure_rel",
name="Relative pressure",
translation_key="relative_pressure",
native_unit_of_measurement=UnitOfPressure.HPA,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pressure_rel"),
@@ -338,28 +328,27 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="c3h8_MIPEX",
name="Propane",
translation_key="propane",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c3h8_MIPEX"),
),
AirQEntityDescription(
key="refigerant",
name="Refrigerant",
translation_key="refigerant",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("refigerant"),
),
AirQEntityDescription(
key="sih4",
name="SiH4",
translation_key="silicon_hydride",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("sih4"),
),
AirQEntityDescription(
key="so2",
name="SO2",
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
@@ -367,7 +356,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="sound",
name="Noise",
translation_key="noise",
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("sound"),
@@ -375,7 +364,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="sound_max",
name="Noise (Maximum)",
translation_key="maximum_noise",
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("sound_max"),
@@ -383,7 +372,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="radon",
name="Radon",
translation_key="radon",
native_unit_of_measurement=ACTIVITY_BECQUEREL_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("radon"),
@@ -391,7 +380,6 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="temperature",
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
@@ -399,21 +387,22 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="tvoc",
name="VOC",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("tvoc"),
),
AirQEntityDescription(
key="tvoc_ionsc",
name="VOC (Industrial)",
translation_key="industrial_volatile_organic_compounds",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("tvoc_ionsc"),
),
AirQEntityDescription(
key="virus",
name="Virus Index",
translation_key="virus_index",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:virus-off",

View File

@@ -18,5 +18,117 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"sensor": {
"acetaldehyde": {
"name": "Acetaldehyde"
},
"ammonia": {
"name": "Ammonia"
},
"arsine": {
"name": "Arsine"
},
"bromine": {
"name": "Bromine"
},
"methanethiol": {
"name": "Methanethiol"
},
"chlorine": {
"name": "Chlorine"
},
"chlorine_dioxide": {
"name": "Chlorine dioxide"
},
"carbon_disulfide": {
"name": "Carbon disulfide"
},
"carbon_monoxide": {
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
},
"dew_point": {
"name": "Dew point"
},
"ethanol": {
"name": "Ethanol"
},
"ethylene": {
"name": "Ethylene"
},
"formaldehyde": {
"name": "Formaldehyde"
},
"fluorine": {
"name": "Fluorine"
},
"hydrogen_sulfide": {
"name": "Hydrogen sulfide"
},
"hydrochloric_acid": {
"name": "Hydrochloric acid"
},
"hydrogen_cyanide": {
"name": "Hydrogen cyanide"
},
"hydrogen_fluoride": {
"name": "Hydrogen fluoride"
},
"health_index": {
"name": "Health Index"
},
"absolute_humidity": {
"name": "Absolute humidity"
},
"hydrogen": {
"name": "Hydrogen"
},
"hydrogen_peroxide": {
"name": "Hydrogen peroxide"
},
"methane": {
"name": "Methane"
},
"organic_acid": {
"name": "Organic acid"
},
"oxygen": {
"name": "Oxygen"
},
"performance_index": {
"name": "Performance Index"
},
"hydrogen_phosphide": {
"name": "Hydrogen Phosphide"
},
"relative_pressure": {
"name": "Relative pressure"
},
"propane": {
"name": "Propane"
},
"refigerant": {
"name": "Refrigerant"
},
"silicon_hydride": {
"name": "Silicon Hydride"
},
"noise": {
"name": "Noise"
},
"maximum_noise": {
"name": "Noise (Maximum)"
},
"radon": {
"name": "Radon"
},
"industrial_volatile_organic_compounds": {
"name": "VOCs (Industrial)"
},
"virus_index": {
"name": "Virus Index"
}
}
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airthings",
"iot_class": "cloud_polling",
"loggers": ["airthings"],
"requirements": ["airthings_cloud==0.1.0"]
"requirements": ["airthings-cloud==0.1.0"]
}

View File

@@ -35,62 +35,56 @@ SENSORS: dict[str, SensorEntityDescription] = {
"radonShortTermAvg": SensorEntityDescription(
key="radonShortTermAvg",
native_unit_of_measurement="Bq/m³",
name="Radon",
translation_key="radon",
),
"temp": SensorEntityDescription(
key="temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
name="Temperature",
),
"humidity": SensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
name="Humidity",
),
"pressure": SensorEntityDescription(
key="pressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.MBAR,
name="Pressure",
),
"battery": SensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
name="Battery",
),
"co2": SensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
name="CO2",
),
"voc": SensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
name="VOC",
),
"light": SensorEntityDescription(
key="light",
native_unit_of_measurement=PERCENTAGE,
name="Light",
translation_key="light",
),
"virusRisk": SensorEntityDescription(
key="virusRisk",
name="Virus Risk",
translation_key="virus_risk",
),
"mold": SensorEntityDescription(
key="mold",
name="Mold",
translation_key="mold",
),
"rssi": SensorEntityDescription(
key="rssi",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
name="RSSI",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
@@ -98,13 +92,11 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="pm1",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
name="PM1",
),
"pm25": SensorEntityDescription(
key="pm25",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
name="PM25",
),
}
@@ -134,6 +126,7 @@ class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity):
"""Representation of a Airthings Sensor device."""
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_has_entity_name = True
def __init__(
self,
@@ -146,7 +139,6 @@ class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity):
self.entity_description = entity_description
self._attr_name = f"{airthings_device.name} {entity_description.name}"
self._attr_unique_id = f"{airthings_device.device_id}_{entity_description.key}"
self._id = airthings_device.device_id
self._attr_device_info = DeviceInfo(

View File

@@ -17,5 +17,21 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
},
"entity": {
"sensor": {
"radon": {
"name": "Radon"
},
"light": {
"name": "Light"
},
"virus_risk": {
"name": "Virus Risk"
},
"mold": {
"name": "Mold"
}
}
}
}

View File

@@ -39,26 +39,26 @@ _LOGGER = logging.getLogger(__name__)
SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
"radon_1day_avg": SensorEntityDescription(
key="radon_1day_avg",
translation_key="radon_1day_avg",
native_unit_of_measurement=VOLUME_BECQUEREL,
name="Radon 1-day average",
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:radioactive",
),
"radon_longterm_avg": SensorEntityDescription(
key="radon_longterm_avg",
translation_key="radon_longterm_avg",
native_unit_of_measurement=VOLUME_BECQUEREL,
name="Radon longterm average",
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:radioactive",
),
"radon_1day_level": SensorEntityDescription(
key="radon_1day_level",
name="Radon 1-day level",
translation_key="radon_1day_level",
icon="mdi:radioactive",
),
"radon_longterm_level": SensorEntityDescription(
key="radon_longterm_level",
name="Radon longterm level",
translation_key="radon_longterm_level",
icon="mdi:radioactive",
),
"temperature": SensorEntityDescription(
@@ -66,21 +66,18 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
name="Temperature",
),
"humidity": SensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
name="Humidity",
),
"pressure": SensorEntityDescription(
key="pressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
name="Pressure",
),
"battery": SensorEntityDescription(
key="battery",
@@ -88,20 +85,18 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
name="Battery",
),
"co2": SensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
name="co2",
),
"voc": SensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
name="VOC",
icon="mdi:cloud",
),
"illuminance": SensorEntityDescription(
@@ -109,7 +104,6 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
name="Illuminance",
),
}

View File

@@ -19,5 +19,21 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
"sensor": {
"radon_1day_avg": {
"name": "Radon 1-day average"
},
"radon_longterm_avg": {
"name": "Radon longterm average"
},
"radon_1day_level": {
"name": "Radon 1-day level"
},
"radon_longterm_level": {
"name": "Radon longterm level"
}
}
}
}

View File

@@ -17,7 +17,7 @@
"api_key": "[%key:common::config_flow::data::api_key%]",
"city": "City",
"country": "Country",
"state": "state"
"state": "State"
}
},
"reauth_confirm": {

View File

@@ -193,6 +193,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode."""
slave_raise = False
params = {}
if hvac_mode == HVACMode.OFF:
params[API_ON] = 0
@@ -202,12 +204,13 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
if self.get_airzone_value(AZD_MASTER):
params[API_MODE] = mode
else:
raise HomeAssistantError(
f"Mode can't be changed on slave zone {self.name}"
)
slave_raise = True
params[API_ON] = 1
await self._async_update_hvac_params(params)
if slave_raise:
raise HomeAssistantError(f"Mode can't be changed on slave zone {self.name}")
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
params = {}

View File

@@ -12,7 +12,10 @@ from homeassistant.helpers import aiohttp_client
from .const import DOMAIN
from .coordinator import AirzoneUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -0,0 +1,106 @@
"""Support for the Airzone Cloud binary sensors."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Final
from aioairzone_cloud.const import AZD_PROBLEMS, AZD_WARNINGS, AZD_ZONES
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneZoneEntity
@dataclass
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
"""A class that describes Airzone Cloud binary sensor entities."""
attributes: dict[str, str] | None = None
ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = (
AirzoneBinarySensorEntityDescription(
attributes={
"warnings": AZD_WARNINGS,
},
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
key=AZD_PROBLEMS,
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Add Airzone Cloud binary sensors from a config_entry."""
coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
binary_sensors: list[AirzoneBinarySensor] = []
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items():
for description in ZONE_BINARY_SENSOR_TYPES:
if description.key in zone_data:
binary_sensors.append(
AirzoneZoneBinarySensor(
coordinator,
description,
zone_id,
zone_data,
)
)
async_add_entities(binary_sensors)
class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
"""Define an Airzone Cloud binary sensor."""
entity_description: AirzoneBinarySensorEntityDescription
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator updates."""
self._async_update_attrs()
super()._handle_coordinator_update()
@callback
def _async_update_attrs(self) -> None:
"""Update binary sensor attributes."""
self._attr_is_on = self.get_airzone_value(self.entity_description.key)
if self.entity_description.attributes:
self._attr_extra_state_attributes = {
key: self.get_airzone_value(val)
for key, val in self.entity_description.attributes.items()
}
class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor):
"""Define an Airzone Cloud Zone binary sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
description: AirzoneBinarySensorEntityDescription,
zone_id: str,
zone_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, zone_id, zone_data)
self._attr_unique_id = f"{zone_id}_{description.key}"
self.entity_description = description
self._async_update_attrs()

View File

@@ -15,7 +15,6 @@ from aioairzone_cloud.const import (
AZD_ZONES,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -43,7 +42,6 @@ class AirzoneAidooEntity(AirzoneEntity):
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
entry: ConfigEntry,
aidoo_id: str,
aidoo_data: dict[str, Any],
) -> None:
@@ -73,7 +71,6 @@ class AirzoneWebServerEntity(AirzoneEntity):
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
entry: ConfigEntry,
ws_id: str,
ws_data: dict[str, Any],
) -> None:
@@ -86,7 +83,7 @@ class AirzoneWebServerEntity(AirzoneEntity):
connections={(dr.CONNECTION_NETWORK_MAC, ws_id)},
identifiers={(DOMAIN, ws_id)},
manufacturer=MANUFACTURER,
name=f"WebServer {ws_id}",
name=ws_data[AZD_NAME],
sw_version=ws_data[AZD_FIRMWARE],
)
@@ -104,7 +101,6 @@ class AirzoneZoneEntity(AirzoneEntity):
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
entry: ConfigEntry,
zone_id: str,
zone_data: dict[str, Any],
) -> None:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_polling",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.1.8"]
"requirements": ["aioairzone-cloud==0.2.0"]
}

View File

@@ -6,7 +6,6 @@ from typing import Any, Final
from aioairzone_cloud.const import (
AZD_AIDOOS,
AZD_HUMIDITY,
AZD_NAME,
AZD_TEMP,
AZD_WEBSERVERS,
AZD_WIFI_RSSI,
@@ -42,7 +41,6 @@ AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
SensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key=AZD_TEMP,
name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -53,9 +51,7 @@ WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_entity_name=True,
key=AZD_WIFI_RSSI,
name="RSSI",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -65,14 +61,12 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
SensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key=AZD_TEMP,
name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
device_class=SensorDeviceClass.HUMIDITY,
key=AZD_HUMIDITY,
name="Humidity",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -95,7 +89,6 @@ async def async_setup_entry(
AirzoneAidooSensor(
coordinator,
description,
entry,
aidoo_id,
aidoo_data,
)
@@ -109,7 +102,6 @@ async def async_setup_entry(
AirzoneWebServerSensor(
coordinator,
description,
entry,
ws_id,
ws_data,
)
@@ -123,7 +115,6 @@ async def async_setup_entry(
AirzoneZoneSensor(
coordinator,
description,
entry,
zone_id,
zone_data,
)
@@ -154,14 +145,13 @@ class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor):
self,
coordinator: AirzoneUpdateCoordinator,
description: SensorEntityDescription,
entry: ConfigEntry,
aidoo_id: str,
aidoo_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, entry, aidoo_id, aidoo_data)
super().__init__(coordinator, aidoo_id, aidoo_data)
self._attr_name = f"{aidoo_data[AZD_NAME]} {description.name}"
self._attr_has_entity_name = True
self._attr_unique_id = f"{aidoo_id}_{description.key}"
self.entity_description = description
@@ -175,13 +165,13 @@ class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
self,
coordinator: AirzoneUpdateCoordinator,
description: SensorEntityDescription,
entry: ConfigEntry,
ws_id: str,
ws_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, entry, ws_id, ws_data)
super().__init__(coordinator, ws_id, ws_data)
self._attr_has_entity_name = True
self._attr_unique_id = f"{ws_id}_{description.key}"
self.entity_description = description
@@ -195,14 +185,13 @@ class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor):
self,
coordinator: AirzoneUpdateCoordinator,
description: SensorEntityDescription,
entry: ConfigEntry,
zone_id: str,
zone_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, entry, zone_id, zone_data)
super().__init__(coordinator, zone_id, zone_data)
self._attr_name = f"{zone_data[AZD_NAME]} {description.name}"
self._attr_has_entity_name = True
self._attr_unique_id = f"{zone_id}_{description.key}"
self.entity_description = description

View File

@@ -40,26 +40,24 @@ class AladdinDevice(CoverEntity):
_attr_device_class = CoverDeviceClass.GARAGE
_attr_supported_features = SUPPORTED_FEATURES
_attr_has_entity_name = True
_attr_name = None
def __init__(
self, acc: AladdinConnectClient, device: DoorDevice, entry: ConfigEntry
) -> None:
"""Initialize the Aladdin Connect cover."""
self._acc = acc
self._entry_id = entry.entry_id
self._device_id = device["device_id"]
self._number = device["door_number"]
self._name = device["name"]
self._serial = device["serial"]
self._model = device["model"]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._device_id}-{self._number}")},
name=self._name,
name=device["name"],
manufacturer="Overhead Door",
model=self._model,
model=device["model"],
)
self._attr_has_entity_name = True
self._attr_unique_id = f"{self._device_id}-{self._number}"
async def async_added_to_hass(self) -> None:

View File

@@ -40,7 +40,6 @@ class AccSensorEntityDescription(
SENSORS: tuple[AccSensorEntityDescription, ...] = (
AccSensorEntityDescription(
key="battery_level",
name="Battery level",
device_class=SensorDeviceClass.BATTERY,
entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE,
@@ -49,7 +48,7 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = (
),
AccSensorEntityDescription(
key="rssi",
name="Wi-Fi RSSI",
translation_key="wifi_strength",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_registry_enabled_default=False,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
@@ -58,7 +57,7 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = (
),
AccSensorEntityDescription(
key="ble_strength",
name="BLE Strength",
translation_key="ble_strength",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_registry_enabled_default=False,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
@@ -89,8 +88,8 @@ async def async_setup_entry(
class AladdinConnectSensor(SensorEntity):
"""A sensor implementation for Aladdin Connect devices."""
_device: AladdinConnectSensor
entity_description: AccSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
@@ -101,24 +100,20 @@ class AladdinConnectSensor(SensorEntity):
"""Initialize a sensor for an Aladdin Connect device."""
self._device_id = device["device_id"]
self._number = device["door_number"]
self._name = device["name"]
self._model = device["model"]
self._acc = acc
self.entity_description = description
self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}"
self._attr_has_entity_name = True
if self._model == "01" and description.key in ("battery_level", "ble_strength"):
self._attr_entity_registry_enabled_default = True
@property
def device_info(self) -> DeviceInfo | None:
"""Device information for Aladdin Connect sensors."""
return DeviceInfo(
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._device_id}-{self._number}")},
name=self._name,
name=device["name"],
manufacturer="Overhead Door",
model=self._model,
model=device["model"],
)
if device["model"] == "01" and description.key in (
"battery_level",
"ble_strength",
):
self._attr_entity_registry_enabled_default = True
@property
def native_value(self) -> float | None:

View File

@@ -25,5 +25,15 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
"sensor": {
"wifi_strength": {
"name": "Wi-Fi RSSI"
},
"ble_strength": {
"name": "BLE Strength"
}
}
}
}

View File

@@ -5,6 +5,7 @@ from typing import Final
import voluptuous as vol
from homeassistant.components.device_automation import async_validate_entity_schema
from homeassistant.const import (
ATTR_CODE,
ATTR_ENTITY_ID,
@@ -44,15 +45,22 @@ ACTION_TYPES: Final[set[str]] = {
"trigger",
}
ACTION_SCHEMA: Final = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
_ACTION_SCHEMA: Final = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(ACTION_TYPES),
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
vol.Optional(CONF_CODE): cv.string,
}
)
async def async_validate_action_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return async_validate_entity_schema(hass, config, _ACTION_SCHEMA)
async def async_get_actions(
hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:
@@ -70,7 +78,7 @@ async def async_get_actions(
base_action: dict = {
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_ENTITY_ID: entry.id,
}
# Add actions for each entity that belongs to this integration
@@ -124,7 +132,9 @@ async def async_get_action_capabilities(
"""List action capabilities."""
# We need to refer to the state directly because ATTR_CODE_ARM_REQUIRED is not a
# capability attribute
state = hass.states.get(config[CONF_ENTITY_ID])
registry = er.async_get(hass)
entity_id = er.async_resolve_entity_id(registry, config[CONF_ENTITY_ID])
state = hass.states.get(entity_id) if entity_id else None
code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False
if config[CONF_TYPE] == "trigger" or (

View File

@@ -58,7 +58,7 @@ CONDITION_TYPES: Final[set[str]] = {
CONDITION_SCHEMA: Final = DEVICE_CONDITION_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES),
}
)
@@ -83,7 +83,7 @@ async def async_get_conditions(
CONF_CONDITION: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_ENTITY_ID: entry.id,
}
conditions += [
@@ -126,8 +126,11 @@ def async_condition_from_config(
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
registry = er.async_get(hass)
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])
def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
"""Test if an entity is a certain state."""
return condition.state(hass, config[ATTR_ENTITY_ID], state)
return condition.state(hass, entity_id, state)
return test_is_state

View File

@@ -46,7 +46,7 @@ TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | {
TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
@@ -72,7 +72,7 @@ async def async_get_triggers(
CONF_PLATFORM: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_ENTITY_ID: entry.id,
}
triggers += [

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/alpha_vantage",
"iot_class": "cloud_polling",
"loggers": ["alpha_vantage"],
"requirements": ["alpha_vantage==2.3.1"]
"requirements": ["alpha-vantage==2.3.1"]
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/ambiclimate",
"iot_class": "cloud_polling",
"loggers": ["ambiclimate"],
"requirements": ["ambiclimate==0.2.1"]
"requirements": ["Ambiclimate==0.2.1"]
}

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
import logging
from homeassistant.helpers.typing import UndefinedType
from .const import DOMAIN
@@ -14,7 +16,7 @@ def service_signal(service: str, *args: str) -> str:
def log_update_error(
logger: logging.Logger,
action: str,
name: str | None,
name: str | UndefinedType | None,
entity_type: str,
error: Exception,
level: int = logging.ERROR,

View File

@@ -21,12 +21,22 @@ from homeassistant.components.recorder import (
DOMAIN as RECORDER_DOMAIN,
get_instance as get_recorder_instance,
)
import homeassistant.config as conf_util
from homeassistant.config_entries import (
SOURCE_IGNORE,
)
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.storage import Store
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.loader import IntegrationNotFound, async_get_integrations
from homeassistant.loader import (
Integration,
IntegrationNotFound,
async_get_integrations,
)
from homeassistant.setup import async_get_loaded_integrations
from .const import (
@@ -206,8 +216,25 @@ class Analytics:
if self.preferences.get(ATTR_USAGE, False) or self.preferences.get(
ATTR_STATISTICS, False
):
ent_reg = er.async_get(self.hass)
try:
yaml_configuration = await conf_util.async_hass_config_yaml(self.hass)
except HomeAssistantError as err:
LOGGER.error(err)
return
configuration_set = set(yaml_configuration)
er_platforms = {
entity.platform
for entity in ent_reg.entities.values()
if not entity.disabled
}
domains = async_get_loaded_integrations(self.hass)
configured_integrations = await async_get_integrations(self.hass, domains)
enabled_domains = set(configured_integrations)
for integration in configured_integrations.values():
if isinstance(integration, IntegrationNotFound):
continue
@@ -215,7 +242,11 @@ class Analytics:
if isinstance(integration, BaseException):
raise integration
if integration.disabled:
if not self._async_should_report_integration(
integration=integration,
yaml_domains=configuration_set,
entity_registry_platforms=er_platforms,
):
continue
if not integration.is_built_in:
@@ -253,12 +284,12 @@ class Analytics:
if supervisor_info is not None:
payload[ATTR_ADDONS] = addons
if ENERGY_DOMAIN in integrations:
if ENERGY_DOMAIN in enabled_domains:
payload[ATTR_ENERGY] = {
ATTR_CONFIGURED: await energy_is_configured(self.hass)
}
if RECORDER_DOMAIN in integrations:
if RECORDER_DOMAIN in enabled_domains:
instance = get_recorder_instance(self.hass)
engine = instance.database_engine
if engine and engine.version is not None:
@@ -306,3 +337,34 @@ class Analytics:
LOGGER.error(
"Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err
)
@callback
def _async_should_report_integration(
self,
integration: Integration,
yaml_domains: set[str],
entity_registry_platforms: set[str],
) -> bool:
"""Return a bool to indicate if this integration should be reported."""
if integration.disabled:
return False
# Check if the integration is defined in YAML or in the entity registry
if (
integration.domain in yaml_domains
or integration.domain in entity_registry_platforms
):
return True
# Check if the integration provide a config flow
if not integration.config_flow:
return False
entries = self.hass.config_entries.async_entries(integration.domain)
# Filter out ignored and disabled entries
return any(
entry
for entry in entries
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
)

View File

@@ -296,7 +296,6 @@ class ADBDevice(MediaPlayerEntity):
self._process_config,
)
)
return
@property
def media_image_hash(self) -> str | None:

View File

@@ -16,6 +16,7 @@ from .const import DOMAIN
class AndroidTVRemoteBaseEntity(Entity):
"""Android TV Remote Base Entity."""
_attr_name = None
_attr_has_entity_name = True
_attr_should_poll = False

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl",
"iot_class": "local_polling",
"loggers": ["anel_pwrctrl"],
"requirements": ["anel_pwrctrl-homeassistant==0.0.1.dev2"]
"requirements": ["anel-pwrctrl-homeassistant==0.0.1.dev2"]
}

View File

@@ -80,6 +80,7 @@ class AnthemAVR(MediaPlayerEntity):
self._attr_name = f"zone {zone_number}"
self._attr_unique_id = f"{mac_address}_{zone_number}"
else:
self._attr_name = None
self._attr_unique_id = mac_address
self._attr_device_info = DeviceInfo(

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.12.0"],
"requirements": ["pyatv==0.13.2"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",

View File

@@ -282,7 +282,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
"""Send the play_media command to the media player."""
# If input (file) has a file format supported by pyatv, then stream it with
# RAOP. Otherwise try to play it with regular AirPlay.
if media_type == MediaType.APP:
if media_type in {MediaType.APP, MediaType.URL}:
await self.atv.apps.launch_app(media_id)
return

View File

@@ -49,6 +49,7 @@
},
"abort": {
"ipv6_not_supported": "IPv6 is not supported.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/aquostv",
"iot_class": "local_polling",
"loggers": ["sharp_aquos_rc"],
"requirements": ["sharp_aquos_rc==0.3.2"]
"requirements": ["sharp-aquos-rc==0.3.2"]
}

View File

@@ -1,6 +1,8 @@
"""Support for Aranet sensors."""
from __future__ import annotations
from dataclasses import dataclass
from aranet4.client import Aranet4Advertisement
from bleak.backends.device import BLEDevice
@@ -23,6 +25,7 @@ from homeassistant.const import (
ATTR_SW_VERSION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfPressure,
UnitOfTemperature,
UnitOfTime,
@@ -33,43 +36,55 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@dataclass
class AranetSensorEntityDescription(SensorEntityDescription):
"""Class to describe an Aranet sensor entity."""
# PassiveBluetoothDataUpdate does not support UNDEFINED
# Restrict the type to satisfy the type checker and catch attempts
# to use UNDEFINED in the entity descriptions.
name: str | None = None
SENSOR_DESCRIPTIONS = {
"temperature": SensorEntityDescription(
"temperature": AranetSensorEntityDescription(
key="temperature",
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
"humidity": SensorEntityDescription(
"humidity": AranetSensorEntityDescription(
key="humidity",
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
"pressure": SensorEntityDescription(
"pressure": AranetSensorEntityDescription(
key="pressure",
name="Pressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA,
state_class=SensorStateClass.MEASUREMENT,
),
"co2": SensorEntityDescription(
"co2": AranetSensorEntityDescription(
key="co2",
name="Carbon Dioxide",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
"battery": SensorEntityDescription(
"battery": AranetSensorEntityDescription(
key="battery",
name="Battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
"interval": SensorEntityDescription(
"interval": AranetSensorEntityDescription(
key="update_interval",
name="Update Interval",
device_class=SensorDeviceClass.DURATION,
@@ -77,6 +92,7 @@ SENSOR_DESCRIPTIONS = {
state_class=SensorStateClass.MEASUREMENT,
# The interval setting is not a generally useful entity for most users.
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
}

View File

@@ -3,7 +3,9 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation import (
DEVICE_TRIGGER_BASE_SCHEMA,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_DEVICE_ID,
@@ -22,7 +24,7 @@ from .const import DOMAIN, EVENT_TURN_ON
TRIGGER_TYPES = {"turn_on"}
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
}
)
@@ -43,7 +45,7 @@ async def async_get_triggers(
CONF_PLATFORM: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_ENTITY_ID: entry.id,
CONF_TYPE: "turn_on",
}
)
@@ -62,7 +64,8 @@ async def async_attach_trigger(
job = HassJob(action)
if config[CONF_TYPE] == "turn_on":
entity_id = config[CONF_ENTITY_ID]
registry = er.async_get(hass)
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])
@callback
def _handle_event(event: Event) -> None:
@@ -71,9 +74,10 @@ async def async_attach_trigger(
job,
{
"trigger": {
**trigger_data, # type: ignore[arg-type] # https://github.com/python/mypy/issues/9117
**trigger_data,
**config,
"description": f"{DOMAIN} - {entity_id}",
"entity_id": entity_id,
}
},
event.context,

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
"iot_class": "local_polling",
"loggers": ["arcam"],
"requirements": ["arcam-fmj==1.3.0"],
"requirements": ["arcam-fmj==1.4.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -57,6 +57,7 @@ async def async_pipeline_from_audio_stream(
pipeline_id: str | None = None,
conversation_id: str | None = None,
tts_audio_output: str | None = None,
device_id: str | None = None,
) -> None:
"""Create an audio pipeline from an audio stream.
@@ -64,6 +65,7 @@ async def async_pipeline_from_audio_stream(
"""
pipeline_input = PipelineInput(
conversation_id=conversation_id,
device_id=device_id,
stt_metadata=stt_metadata,
stt_stream=stt_stream,
run=PipelineRun(

View File

@@ -499,7 +499,7 @@ class PipelineRun:
self.intent_agent = agent_info.id
async def recognize_intent(
self, intent_input: str, conversation_id: str | None
self, intent_input: str, conversation_id: str | None, device_id: str | None
) -> str:
"""Run intent recognition portion of pipeline. Returns text to speak."""
if self.intent_agent is None:
@@ -512,6 +512,8 @@ class PipelineRun:
"engine": self.intent_agent,
"language": self.pipeline.conversation_language,
"intent_input": intent_input,
"conversation_id": conversation_id,
"device_id": device_id,
},
)
)
@@ -521,6 +523,7 @@ class PipelineRun:
hass=self.hass,
text=intent_input,
conversation_id=conversation_id,
device_id=device_id,
context=self.context,
language=self.pipeline.conversation_language,
agent_id=self.intent_agent,
@@ -655,6 +658,8 @@ class PipelineInput:
conversation_id: str | None = None
device_id: str | None = None
async def execute(self) -> None:
"""Run pipeline."""
self.run.start()
@@ -678,7 +683,9 @@ class PipelineInput:
if current_stage == PipelineStage.INTENT:
assert intent_input is not None
tts_input = await self.run.recognize_intent(
intent_input, self.conversation_id
intent_input,
self.conversation_id,
self.device_id,
)
current_stage = PipelineStage.TTS
@@ -730,17 +737,30 @@ class PipelineInput:
)
start_stage_index = PIPELINE_STAGE_ORDER.index(self.run.start_stage)
end_stage_index = PIPELINE_STAGE_ORDER.index(self.run.end_stage)
prepare_tasks = []
if start_stage_index <= PIPELINE_STAGE_ORDER.index(PipelineStage.STT):
if (
start_stage_index
<= PIPELINE_STAGE_ORDER.index(PipelineStage.STT)
<= end_stage_index
):
# self.stt_metadata can't be None or we'd raise above
prepare_tasks.append(self.run.prepare_speech_to_text(self.stt_metadata)) # type: ignore[arg-type]
if start_stage_index <= PIPELINE_STAGE_ORDER.index(PipelineStage.INTENT):
if (
start_stage_index
<= PIPELINE_STAGE_ORDER.index(PipelineStage.INTENT)
<= end_stage_index
):
prepare_tasks.append(self.run.prepare_recognize_intent())
if start_stage_index <= PIPELINE_STAGE_ORDER.index(PipelineStage.TTS):
if (
start_stage_index
<= PIPELINE_STAGE_ORDER.index(PipelineStage.TTS)
<= end_stage_index
):
prepare_tasks.append(self.run.prepare_text_to_speech())
if prepare_tasks:
@@ -944,6 +964,7 @@ class PipelineData:
pipeline_runs: dict[str, LimitedSizeDict[str, PipelineRunDebug]]
pipeline_store: PipelineStorageCollection
pipeline_devices: set[str] = field(default_factory=set, init=False)
@dataclass

View File

@@ -10,7 +10,8 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import collection, entity_registry as er, restore_state
from .const import DOMAIN
from .pipeline import PipelineStorageCollection
from .pipeline import PipelineData, PipelineStorageCollection
from .vad import VadSensitivity
OPTION_PREFERRED = "preferred"
@@ -38,6 +39,25 @@ def get_chosen_pipeline(
)
@callback
def get_vad_sensitivity(
hass: HomeAssistant, domain: str, unique_id_prefix: str
) -> VadSensitivity:
"""Get the chosen vad sensitivity for a domain."""
ent_reg = er.async_get(hass)
sensitivity_entity_id = ent_reg.async_get_entity_id(
Platform.SELECT, domain, f"{unique_id_prefix}-vad_sensitivity"
)
if sensitivity_entity_id is None:
return VadSensitivity.DEFAULT
state = hass.states.get(sensitivity_entity_id)
if state is None:
return VadSensitivity.DEFAULT
return VadSensitivity(state.state)
class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
"""Entity to represent a pipeline selector."""
@@ -60,15 +80,24 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
"""When entity is added to Home Assistant."""
await super().async_added_to_hass()
pipeline_store: PipelineStorageCollection = self.hass.data[
DOMAIN
].pipeline_store
pipeline_store.async_add_change_set_listener(self._pipelines_updated)
pipeline_data: PipelineData = self.hass.data[DOMAIN]
pipeline_store = pipeline_data.pipeline_store
self.async_on_remove(
pipeline_store.async_add_change_set_listener(self._pipelines_updated)
)
state = await self.async_get_last_state()
if state is not None and state.state in self.options:
self._attr_current_option = state.state
if self.registry_entry and (device_id := self.registry_entry.device_id):
pipeline_data.pipeline_devices.add(device_id)
self.async_on_remove(
lambda: pipeline_data.pipeline_devices.discard(
device_id # type: ignore[arg-type]
)
)
async def async_select_option(self, option: str) -> None:
"""Select an option."""
self._attr_current_option = option
@@ -93,3 +122,34 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
if self._attr_current_option not in options:
self._attr_current_option = OPTION_PREFERRED
class VadSensitivitySelect(SelectEntity, restore_state.RestoreEntity):
"""Entity to represent VAD sensitivity."""
entity_description = SelectEntityDescription(
key="vad_sensitivity",
translation_key="vad_sensitivity",
entity_category=EntityCategory.CONFIG,
)
_attr_should_poll = False
_attr_current_option = VadSensitivity.DEFAULT.value
_attr_options = [vs.value for vs in VadSensitivity]
def __init__(self, hass: HomeAssistant, unique_id_prefix: str) -> None:
"""Initialize a pipeline selector."""
self._attr_unique_id = f"{unique_id_prefix}-vad_sensitivity"
self.hass = hass
async def async_added_to_hass(self) -> None:
"""When entity is added to Home Assistant."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if state is not None and state.state in self.options:
self._attr_current_option = state.state
async def async_select_option(self, option: str) -> None:
"""Select an option."""
self._attr_current_option = option
self.async_write_ha_state()

View File

@@ -11,6 +11,14 @@
"state": {
"preferred": "Preferred"
}
},
"vad_sensitivity": {
"name": "Finished speaking detection",
"state": {
"default": "Default",
"aggressive": "Aggressive",
"relaxed": "Relaxed"
}
}
}
}

View File

@@ -1,11 +1,35 @@
"""Voice activity detection."""
from __future__ import annotations
from dataclasses import dataclass, field
import webrtcvad
from homeassistant.backports.enum import StrEnum
_SAMPLE_RATE = 16000
class VadSensitivity(StrEnum):
"""How quickly the end of a voice command is detected."""
DEFAULT = "default"
RELAXED = "relaxed"
AGGRESSIVE = "aggressive"
@staticmethod
def to_seconds(sensitivity: VadSensitivity | str) -> float:
"""Return seconds of silence for sensitivity level."""
sensitivity = VadSensitivity(sensitivity)
if sensitivity == VadSensitivity.RELAXED:
return 2.0
if sensitivity == VadSensitivity.AGGRESSIVE:
return 0.5
return 1.0
@dataclass
class VoiceCommandSegmenter:
"""Segments an audio stream into voice commands using webrtcvad."""
@@ -113,16 +137,15 @@ class VoiceCommandSegmenter:
self._reset_seconds_left -= self._seconds_per_chunk
if self._reset_seconds_left <= 0:
self._speech_seconds_left = self.speech_seconds
elif not is_speech:
self._reset_seconds_left = self.reset_seconds
self._silence_seconds_left -= self._seconds_per_chunk
if self._silence_seconds_left <= 0:
return False
else:
if not is_speech:
self._reset_seconds_left = self.reset_seconds
self._silence_seconds_left -= self._seconds_per_chunk
if self._silence_seconds_left <= 0:
return False
else:
# Reset if enough speech
self._reset_seconds_left -= self._seconds_per_chunk
if self._reset_seconds_left <= 0:
self._silence_seconds_left = self.silence_seconds
# Reset if enough speech
self._reset_seconds_left -= self._seconds_per_chunk
if self._reset_seconds_left <= 0:
self._silence_seconds_left = self.silence_seconds
return True

View File

@@ -56,6 +56,7 @@ def async_register_websocket_api(hass: HomeAssistant) -> None:
vol.Optional("input"): dict,
vol.Optional("pipeline"): str,
vol.Optional("conversation_id"): vol.Any(str, None),
vol.Optional("device_id"): vol.Any(str, None),
vol.Optional("timeout"): vol.Any(float, int),
},
),
@@ -105,6 +106,7 @@ async def websocket_run(
# Arguments to PipelineInput
input_args: dict[str, Any] = {
"conversation_id": msg.get("conversation_id"),
"device_id": msg.get("device_id"),
}
if start_stage == PipelineStage.STT:
@@ -280,7 +282,6 @@ def websocket_get_run(
)
@callback
@websocket_api.websocket_command(
{
vol.Required("type"): "assist_pipeline/language/list",

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/asterisk_mbox",
"iot_class": "local_push",
"loggers": ["asterisk_mbox"],
"requirements": ["asterisk_mbox==0.5.0"]
"requirements": ["asterisk-mbox==0.5.0"]
}

View File

@@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.enum import try_parse_enum
from . import DOMAIN, AtagEntity
@@ -52,14 +53,12 @@ class AtagThermostat(AtagEntity, ClimateEntity):
self._attr_temperature_unit = coordinator.data.climate.temp_unit
@property
def hvac_mode(self) -> str | None:
def hvac_mode(self) -> HVACMode | None:
"""Return hvac operation ie. heat, cool mode."""
if self.coordinator.data.climate.hvac_mode in HVAC_MODES:
return self.coordinator.data.climate.hvac_mode
return None
return try_parse_enum(HVACMode, self.coordinator.data.climate.hvac_mode)
@property
def hvac_action(self) -> str | None:
def hvac_action(self) -> HVACAction | None:
"""Return the current running hvac operation."""
is_active = self.coordinator.data.climate.status
return HVACAction.HEATING if is_active else HVACAction.IDLE

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/atome",
"iot_class": "cloud_polling",
"loggers": ["pyatome"],
"requirements": ["pyatome==0.1.1"]
"requirements": ["pyAtome==0.1.1"]
}

View File

@@ -5,7 +5,6 @@ from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import cast
from yalexs.activity import (
ACTION_DOORBELL_CALL_MISSED,
@@ -104,7 +103,16 @@ def _native_datetime() -> datetime:
@dataclass
class AugustRequiredKeysMixin:
class AugustBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes August binary_sensor entity."""
# AugustBinarySensor does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
@dataclass
class AugustDoorbellRequiredKeysMixin:
"""Mixin for required keys."""
value_fn: Callable[[AugustData, DoorbellDetail], bool]
@@ -112,41 +120,45 @@ class AugustRequiredKeysMixin:
@dataclass
class AugustBinarySensorEntityDescription(
BinarySensorEntityDescription, AugustRequiredKeysMixin
class AugustDoorbellBinarySensorEntityDescription(
BinarySensorEntityDescription, AugustDoorbellRequiredKeysMixin
):
"""Describes August binary_sensor entity."""
# AugustDoorbellBinarySensor does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
SENSOR_TYPE_DOOR = BinarySensorEntityDescription(
SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription(
key="door_open",
name="Open",
)
SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = (
AugustBinarySensorEntityDescription(
SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
AugustDoorbellBinarySensorEntityDescription(
key="doorbell_ding",
name="Ding",
device_class=BinarySensorDeviceClass.OCCUPANCY,
value_fn=_retrieve_ding_state,
is_time_based=True,
),
AugustBinarySensorEntityDescription(
AugustDoorbellBinarySensorEntityDescription(
key="doorbell_motion",
name="Motion",
device_class=BinarySensorDeviceClass.MOTION,
value_fn=_retrieve_motion_state,
is_time_based=True,
),
AugustBinarySensorEntityDescription(
AugustDoorbellBinarySensorEntityDescription(
key="doorbell_image_capture",
name="Image Capture",
icon="mdi:file-image",
value_fn=_retrieve_image_capture_state,
is_time_based=True,
),
AugustBinarySensorEntityDescription(
AugustDoorbellBinarySensorEntityDescription(
key="doorbell_online",
name="Online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
@@ -199,7 +211,10 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.DOOR
def __init__(
self, data: AugustData, device: Lock, description: BinarySensorEntityDescription
self,
data: AugustData,
device: Lock,
description: AugustBinarySensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(data, device)
@@ -207,9 +222,7 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
self._data = data
self._device = device
self._attr_name = f"{device.device_name} {description.name}"
self._attr_unique_id = (
f"{self._device_id}_{cast(str, description.name).lower()}"
)
self._attr_unique_id = f"{self._device_id}_{description.name.lower()}"
@callback
def _update_from_data(self):
@@ -243,13 +256,13 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
"""Representation of an August binary sensor."""
entity_description: AugustBinarySensorEntityDescription
entity_description: AugustDoorbellBinarySensorEntityDescription
def __init__(
self,
data: AugustData,
device: Doorbell,
description: AugustBinarySensorEntityDescription,
description: AugustDoorbellBinarySensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(data, device)
@@ -257,9 +270,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
self._check_for_off_update_listener = None
self._data = data
self._attr_name = f"{device.device_name} {description.name}"
self._attr_unique_id = (
f"{self._device_id}_{cast(str, description.name).lower()}"
)
self._attr_unique_id = f"{self._device_id}_{description.name.lower()}"
@callback
def _update_from_data(self):

View File

@@ -1,25 +1,15 @@
"""The aurora component."""
from datetime import timedelta
import logging
from aiohttp import ClientError
from auroranoaa import AuroraForecast
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import (
ATTRIBUTION,
AURORA_API,
CONF_THRESHOLD,
COORDINATOR,
@@ -27,6 +17,7 @@ from .const import (
DEFAULT_THRESHOLD,
DOMAIN,
)
from .coordinator import AuroraDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -79,71 +70,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class AuroraDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the NOAA Aurora API."""
def __init__(
self,
hass: HomeAssistant,
name: str,
polling_interval: int,
api: str,
latitude: float,
longitude: float,
threshold: float,
) -> None:
"""Initialize the data updater."""
super().__init__(
hass=hass,
logger=_LOGGER,
name=name,
update_interval=timedelta(minutes=polling_interval),
)
self.api = api
self.name = name
self.latitude = int(latitude)
self.longitude = int(longitude)
self.threshold = int(threshold)
async def _async_update_data(self):
"""Fetch the data from the NOAA Aurora Forecast."""
try:
return await self.api.get_forecast_data(self.longitude, self.latitude)
except ClientError as error:
raise UpdateFailed(f"Error updating from NOAA: {error}") from error
class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]):
"""Implementation of the base Aurora Entity."""
_attr_attribution = ATTRIBUTION
def __init__(
self,
coordinator: AuroraDataUpdateCoordinator,
name: str,
icon: str,
) -> None:
"""Initialize the Aurora Entity."""
super().__init__(coordinator=coordinator)
self._attr_name = name
self._attr_unique_id = f"{coordinator.latitude}_{coordinator.longitude}"
self._attr_icon = icon
@property
def device_info(self) -> DeviceInfo:
"""Define the device based on name."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, str(self.unique_id))},
manufacturer="NOAA",
model="Aurora Visibility Sensor",
name=self.coordinator.name,
)

View File

@@ -4,8 +4,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AuroraEntity
from .const import COORDINATOR, DOMAIN
from .entity import AuroraEntity
async def async_setup_entry(

View File

@@ -0,0 +1,52 @@
"""The aurora component."""
from datetime import timedelta
import logging
from aiohttp import ClientError
from auroranoaa import AuroraForecast
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
UpdateFailed,
)
_LOGGER = logging.getLogger(__name__)
class AuroraDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the NOAA Aurora API."""
def __init__(
self,
hass: HomeAssistant,
name: str,
polling_interval: int,
api: AuroraForecast,
latitude: float,
longitude: float,
threshold: float,
) -> None:
"""Initialize the data updater."""
super().__init__(
hass=hass,
logger=_LOGGER,
name=name,
update_interval=timedelta(minutes=polling_interval),
)
self.api = api
self.name = name
self.latitude = int(latitude)
self.longitude = int(longitude)
self.threshold = int(threshold)
async def _async_update_data(self):
"""Fetch the data from the NOAA Aurora Forecast."""
try:
return await self.api.get_forecast_data(self.longitude, self.latitude)
except ClientError as error:
raise UpdateFailed(f"Error updating from NOAA: {error}") from error

View File

@@ -0,0 +1,48 @@
"""The aurora component."""
import logging
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
)
from .const import (
ATTRIBUTION,
DOMAIN,
)
from .coordinator import AuroraDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]):
"""Implementation of the base Aurora Entity."""
_attr_attribution = ATTRIBUTION
def __init__(
self,
coordinator: AuroraDataUpdateCoordinator,
name: str,
icon: str,
) -> None:
"""Initialize the Aurora Entity."""
super().__init__(coordinator=coordinator)
self._attr_name = name
self._attr_unique_id = f"{coordinator.latitude}_{coordinator.longitude}"
self._attr_icon = icon
@property
def device_info(self) -> DeviceInfo:
"""Define the device based on name."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, str(self.unique_id))},
manufacturer="NOAA",
model="Aurora Visibility Sensor",
name=self.coordinator.name,
)

View File

@@ -5,8 +5,8 @@ from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AuroraEntity
from .const import COORDINATOR, DOMAIN
from .entity import AuroraEntity
async def async_setup_entry(

View File

@@ -47,10 +47,10 @@ class AuroraEntity(Entity):
@property
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""
return {
"identifiers": {(DOMAIN, self._data[ATTR_SERIAL_NUMBER])},
"manufacturer": MANUFACTURER,
"model": self._data[ATTR_MODEL],
"name": self._data.get(ATTR_DEVICE_NAME, DEFAULT_DEVICE_NAME),
"sw_version": self._data[ATTR_FIRMWARE],
}
return DeviceInfo(
identifiers={(DOMAIN, self._data[ATTR_SERIAL_NUMBER])},
manufacturer=MANUFACTURER,
model=self._data[ATTR_MODEL],
name=self._data.get(ATTR_DEVICE_NAME, DEFAULT_DEVICE_NAME),
sw_version=self._data[ATTR_FIRMWARE],
)

View File

@@ -34,7 +34,7 @@ SENSOR_TYPES = [
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
name="Power Output",
translation_key="power_output",
),
SensorEntityDescription(
key="temp",
@@ -42,14 +42,13 @@ SENSOR_TYPES = [
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
name="Temperature",
),
SensorEntityDescription(
key="totalenergy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
name="Total Energy",
translation_key="total_energy",
),
]
@@ -75,6 +74,8 @@ async def async_setup_entry(
class AuroraSensor(AuroraEntity, SensorEntity):
"""Representation of a Sensor on a Aurora ABB PowerOne Solar inverter."""
_attr_has_entity_name = True
def __init__(
self,
client: AuroraSerialClient,

View File

@@ -18,5 +18,15 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_serial_ports": "No com ports found. Need a valid RS485 device to communicate."
}
},
"entity": {
"sensor": {
"power_output": {
"name": "Power Output"
},
"total_energy": {
"name": "Total Energy"
}
}
}
}

View File

@@ -35,7 +35,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
# Internet Services sensors
SensorValueEntityDescription(
key="usedMb",
name="Data used",
translation_key="data_used",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
@@ -43,7 +43,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
),
SensorValueEntityDescription(
key="downloadedMb",
name="Downloaded",
translation_key="downloaded",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
@@ -51,7 +51,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
),
SensorValueEntityDescription(
key="uploadedMb",
name="Uploaded",
translation_key="uploaded",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
@@ -60,21 +60,21 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
# Mobile Phone Services sensors
SensorValueEntityDescription(
key="national",
name="National calls",
translation_key="national_calls",
state_class=SensorStateClass.TOTAL_INCREASING,
icon="mdi:phone",
value=lambda x: x.get("calls"),
),
SensorValueEntityDescription(
key="mobile",
name="Mobile calls",
translation_key="mobile_calls",
state_class=SensorStateClass.TOTAL_INCREASING,
icon="mdi:phone",
value=lambda x: x.get("calls"),
),
SensorValueEntityDescription(
key="international",
name="International calls",
translation_key="international_calls",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
icon="mdi:phone-plus",
@@ -82,14 +82,14 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
),
SensorValueEntityDescription(
key="sms",
name="SMS sent",
translation_key="sms_sent",
state_class=SensorStateClass.TOTAL_INCREASING,
icon="mdi:message-processing",
value=lambda x: x.get("calls"),
),
SensorValueEntityDescription(
key="internet",
name="Data used",
translation_key="data_used",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfInformation.KILOBYTES,
device_class=SensorDeviceClass.DATA_SIZE,
@@ -98,7 +98,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
),
SensorValueEntityDescription(
key="voicemail",
name="Voicemail calls",
translation_key="voicemail_calls",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
icon="mdi:phone",
@@ -106,7 +106,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
),
SensorValueEntityDescription(
key="other",
name="Other calls",
translation_key="other_calls",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
icon="mdi:phone",
@@ -115,13 +115,13 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
# Generic sensors
SensorValueEntityDescription(
key="daysTotal",
name="Billing cycle length",
translation_key="billing_cycle_length",
native_unit_of_measurement=UnitOfTime.DAYS,
icon="mdi:calendar-range",
),
SensorValueEntityDescription(
key="daysRemaining",
name="Billing cycle remaining",
translation_key="billing_cycle_remaining",
native_unit_of_measurement=UnitOfTime.DAYS,
icon="mdi:calendar-clock",
),

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