Compare commits

..

5 Commits

Author SHA1 Message Date
Erik
903054db14 Refactor event handlers 2024-06-26 13:37:11 +02:00
Erik
423f4c5bcf Refactor event handlers 2024-06-26 13:25:52 +02:00
Erik
9cb8b5be54 Subscribe also to EVENT_STATE_CHANGED 2024-06-26 11:48:15 +02:00
Erik
2058777db6 Update tests 2024-06-26 11:27:02 +02:00
Erik
0da50c3f14 Modify integration sensor 2024-06-26 11:26:59 +02:00
2523 changed files with 76341 additions and 88288 deletions

View File

@@ -49,7 +49,6 @@ base_platforms: &base_platforms
- homeassistant/components/tts/**
- homeassistant/components/update/**
- homeassistant/components/vacuum/**
- homeassistant/components/valve/**
- homeassistant/components/water_heater/**
- homeassistant/components/weather/**

1740
.coveragerc Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -74,6 +74,7 @@ If the code communicates with devices, web services, or third-party tools:
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
- [ ] Untested files have been added to `.coveragerc`.
<!--
This project is very active and we have a high turnover of pull requests.

View File

@@ -32,7 +32,7 @@ jobs:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -69,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: translations
path: translations.tar.gz
@@ -116,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: translations
@@ -453,12 +453,12 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: translations

View File

@@ -36,7 +36,7 @@ env:
CACHE_VERSION: 9
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.8"
HA_SHORT_VERSION: "2024.7"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version
@@ -229,7 +229,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -274,7 +274,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -314,7 +314,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -353,7 +353,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -448,7 +448,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -522,17 +522,11 @@ jobs:
- info
- base
steps:
- name: Install additional OS dependencies
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update
sudo apt-get -y install \
libturbojpeg
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -564,7 +558,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -582,46 +576,6 @@ jobs:
. venv/bin/activate
python -m script.gen_requirements_all validate
audit-licenses:
name: Audit licenses
runs-on: ubuntu-22.04
needs:
- info
- base
if: |
needs.info.outputs.requirements == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.2
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run pip-licenses
run: |
. venv/bin/activate
pip-licenses --format=json --output-file=licenses.json
- name: Upload licenses
uses: actions/upload-artifact@v4.3.4
with:
name: licenses
path: licenses.json
- name: Process licenses
run: |
. venv/bin/activate
python -m script.licenses
pylint:
name: Check pylint
runs-on: ubuntu-22.04
@@ -637,7 +591,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -682,7 +636,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -726,7 +680,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -800,7 +754,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -818,7 +772,7 @@ jobs:
. venv/bin/activate
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
- name: Upload pytest_buckets
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest_buckets
path: pytest_buckets.txt
@@ -863,7 +817,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -882,7 +836,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: pytest_buckets
- name: Compile English translations
@@ -917,14 +871,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -981,7 +935,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1042,7 +996,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1050,7 +1004,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1106,7 +1060,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1168,7 +1122,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1176,7 +1130,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1200,7 +1154,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1251,7 +1205,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1309,14 +1263,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -1337,7 +1291,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
pattern: coverage-*
- name: Upload coverage to Codecov

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.25.12
uses: github/codeql-action/init@v3.25.10
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.25.12
uses: github/codeql-action/analyze@v3.25.10
with:
category: "/language:python"

View File

@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}

View File

@@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -82,14 +82,14 @@ jobs:
) > .env_file
- name: Upload env_file
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: env_file
path: ./.env_file
overwrite: true
- name: Upload requirements_diff
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -101,7 +101,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -121,17 +121,17 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Download env_file
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: requirements_diff
- name: Build wheels
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -159,17 +159,17 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Download env_file
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: requirements_all_wheels
@@ -203,7 +203,7 @@ jobs:
sed -i "/numpy/d" homeassistant/package_constraints.txt
- name: Build wheels (old cython)
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -218,7 +218,7 @@ jobs:
pip: "'cython<3'"
- name: Build wheels (part 1)
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -232,7 +232,7 @@ jobs:
requirements: "requirements_all.txtaa"
- name: Build wheels (part 2)
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -246,7 +246,7 @@ jobs:
requirements: "requirements_all.txtab"
- name: Build wheels (part 3)
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.2
rev: v0.4.9
hooks:
- id: ruff
args:
@@ -83,7 +83,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
- id: hassfest-metadata
name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata

View File

@@ -21,7 +21,6 @@ homeassistant.helpers.entity_platform
homeassistant.helpers.entity_values
homeassistant.helpers.event
homeassistant.helpers.reload
homeassistant.helpers.script
homeassistant.helpers.script_variables
homeassistant.helpers.singleton
homeassistant.helpers.sun
@@ -98,7 +97,6 @@ homeassistant.components.assist_pipeline.*
homeassistant.components.asterisk_cdr.*
homeassistant.components.asterisk_mbox.*
homeassistant.components.asuswrt.*
homeassistant.components.autarco.*
homeassistant.components.auth.*
homeassistant.components.automation.*
homeassistant.components.awair.*
@@ -289,7 +287,6 @@ homeassistant.components.logger.*
homeassistant.components.london_underground.*
homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.madvr.*
homeassistant.components.mailbox.*
homeassistant.components.map.*
homeassistant.components.mastodon.*
@@ -373,7 +370,6 @@ homeassistant.components.rhasspy.*
homeassistant.components.ridwell.*
homeassistant.components.ring.*
homeassistant.components.rituals_perfume_genie.*
homeassistant.components.roborock.*
homeassistant.components.roku.*
homeassistant.components.romy.*
homeassistant.components.rpi_power.*
@@ -385,7 +381,6 @@ homeassistant.components.samsungtv.*
homeassistant.components.scene.*
homeassistant.components.schedule.*
homeassistant.components.scrape.*
homeassistant.components.script.*
homeassistant.components.search.*
homeassistant.components.select.*
homeassistant.components.sensibo.*

42
.vscode/launch.json vendored
View File

@@ -6,52 +6,38 @@
"configurations": [
{
"name": "Home Assistant",
"type": "debugpy",
"type": "python",
"request": "launch",
"module": "homeassistant",
"justMyCode": false,
"args": [
"--debug",
"-c",
"config"
],
"args": ["--debug", "-c", "config"],
"preLaunchTask": "Compile English translations"
},
{
"name": "Home Assistant (skip pip)",
"type": "debugpy",
"type": "python",
"request": "launch",
"module": "homeassistant",
"justMyCode": false,
"args": [
"--debug",
"-c",
"config",
"--skip-pip"
],
"args": ["--debug", "-c", "config", "--skip-pip"],
"preLaunchTask": "Compile English translations"
},
{
"name": "Home Assistant: Changed tests",
"type": "debugpy",
"type": "python",
"request": "launch",
"module": "pytest",
"justMyCode": false,
"args": [
"--timeout=10",
"--picked"
],
"args": ["--timeout=10", "--picked"],
},
{
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
// See https://www.home-assistant.io/integrations/debugpy/
"name": "Home Assistant: Attach Local",
"type": "debugpy",
"type": "python",
"request": "attach",
"connect": {
"port": 5678,
"host": "localhost"
},
"port": 5678,
"host": "localhost",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
@@ -63,12 +49,10 @@
// Debug by attaching to remote Home Assistant server using Remote Python Debugger.
// See https://www.home-assistant.io/integrations/debugpy/
"name": "Home Assistant: Attach Remote",
"type": "debugpy",
"type": "python",
"request": "attach",
"connect": {
"port": 5678,
"host": "homeassistant.local"
},
"port": 5678,
"host": "homeassistant.local",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
@@ -77,4 +61,4 @@
]
}
]
}
}

1
.vscode/tasks.json vendored
View File

@@ -76,7 +76,6 @@
"detail": "Generate code coverage report for a given integration.",
"type": "shell",
"command": "python3 -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
"dependsOn": ["Compile English translations"],
"group": {
"kind": "test",
"isDefault": true

View File

@@ -80,6 +80,8 @@ build.json @home-assistant/supervisor
/tests/components/airzone/ @Noltari
/homeassistant/components/airzone_cloud/ @Noltari
/tests/components/airzone_cloud/ @Noltari
/homeassistant/components/aladdin_connect/ @swcloudgenie
/tests/components/aladdin_connect/ @swcloudgenie
/homeassistant/components/alarm_control_panel/ @home-assistant/core
/tests/components/alarm_control_panel/ @home-assistant/core
/homeassistant/components/alert/ @home-assistant/core @frenck
@@ -155,8 +157,6 @@ build.json @home-assistant/supervisor
/tests/components/aurora_abb_powerone/ @davet2001
/homeassistant/components/aussie_broadband/ @nickw444 @Bre77
/tests/components/aussie_broadband/ @nickw444 @Bre77
/homeassistant/components/autarco/ @klaasnicolaas
/tests/components/autarco/ @klaasnicolaas
/homeassistant/components/auth/ @home-assistant/core
/tests/components/auth/ @home-assistant/core
/homeassistant/components/automation/ @home-assistant/core
@@ -239,8 +239,6 @@ build.json @home-assistant/supervisor
/tests/components/ccm15/ @ocalvo
/homeassistant/components/cert_expiry/ @jjlawren
/tests/components/cert_expiry/ @jjlawren
/homeassistant/components/chacon_dio/ @cnico
/tests/components/chacon_dio/ @cnico
/homeassistant/components/cisco_ios/ @fbradyirl
/homeassistant/components/cisco_mobility_express/ @fbradyirl
/homeassistant/components/cisco_webex_teams/ @fbradyirl
@@ -362,8 +360,8 @@ build.json @home-assistant/supervisor
/tests/components/ecoforest/ @pjanuario
/homeassistant/components/econet/ @w1ll1am23
/tests/components/econet/ @w1ll1am23
/homeassistant/components/ecovacs/ @mib1185 @edenhaus @Augar
/tests/components/ecovacs/ @mib1185 @edenhaus @Augar
/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar
/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar
/homeassistant/components/ecowitt/ @pvizeli
/tests/components/ecowitt/ @pvizeli
/homeassistant/components/efergy/ @tkdrob
@@ -400,8 +398,8 @@ build.json @home-assistant/supervisor
/tests/components/enigma2/ @autinerd
/homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
/tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
/homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie
@@ -433,8 +431,6 @@ build.json @home-assistant/supervisor
/tests/components/fan/ @home-assistant/core
/homeassistant/components/fastdotcom/ @rohankapoorcom @erwindouna
/tests/components/fastdotcom/ @rohankapoorcom @erwindouna
/homeassistant/components/feedreader/ @mib1185
/tests/components/feedreader/ @mib1185
/homeassistant/components/fibaro/ @rappenze
/tests/components/fibaro/ @rappenze
/homeassistant/components/file/ @fabaff
@@ -707,8 +703,6 @@ build.json @home-assistant/supervisor
/tests/components/isal/ @bdraco
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
/homeassistant/components/israel_rail/ @shaiu
/tests/components/israel_rail/ @shaiu
/homeassistant/components/iss/ @DurgNomis-drol
/tests/components/iss/ @DurgNomis-drol
/homeassistant/components/ista_ecotrend/ @tr4nt0r
@@ -743,8 +737,8 @@ build.json @home-assistant/supervisor
/tests/components/kitchen_sink/ @home-assistant/core
/homeassistant/components/kmtronic/ @dgomes
/tests/components/kmtronic/ @dgomes
/homeassistant/components/knocki/ @joostlek @jgatto1 @JakeBosh
/tests/components/knocki/ @joostlek @jgatto1 @JakeBosh
/homeassistant/components/knocki/ @joostlek @jgatto1
/tests/components/knocki/ @joostlek @jgatto1
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
/tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund
@@ -785,8 +779,6 @@ build.json @home-assistant/supervisor
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/lifx/ @Djelibeybi
/tests/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core
/homeassistant/components/linear_garage_door/ @IceBotYT
@@ -831,15 +823,13 @@ build.json @home-assistant/supervisor
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
/homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001
/homeassistant/components/madvr/ @iloveicedgreentea
/tests/components/madvr/ @iloveicedgreentea
/homeassistant/components/mastodon/ @fabaff
/homeassistant/components/matrix/ @PaarthShah
/tests/components/matrix/ @PaarthShah
/homeassistant/components/matter/ @home-assistant/matter
/tests/components/matter/ @home-assistant/matter
/homeassistant/components/mealie/ @joostlek @andrew-codechimp
/tests/components/mealie/ @joostlek @andrew-codechimp
/homeassistant/components/mealie/ @joostlek
/tests/components/mealie/ @joostlek
/homeassistant/components/meater/ @Sotolotl @emontnemery
/tests/components/meater/ @Sotolotl @emontnemery
/homeassistant/components/medcom_ble/ @elafargue
@@ -884,6 +874,8 @@ build.json @home-assistant/supervisor
/tests/components/moat/ @bdraco
/homeassistant/components/mobile_app/ @home-assistant/core
/tests/components/mobile_app/ @home-assistant/core
/homeassistant/components/modbus/ @janiversen
/tests/components/modbus/ @janiversen
/homeassistant/components/modem_callerid/ @tkdrob
/tests/components/modem_callerid/ @tkdrob
/homeassistant/components/modern_forms/ @wonderslug
@@ -995,6 +987,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/ollama/ @synesthesiam
/tests/components/ollama/ @synesthesiam
/homeassistant/components/ombi/ @larssont
/homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu
/tests/components/omnilogic/ @oliver84 @djtimca @gentoosu
/homeassistant/components/onboarding/ @home-assistant/core
/tests/components/onboarding/ @home-assistant/core
/homeassistant/components/oncue/ @bdraco @peterager
@@ -1210,8 +1204,6 @@ build.json @home-assistant/supervisor
/tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/homeassistant/components/russound_rio/ @noahhusby
/tests/components/russound_rio/ @noahhusby
/homeassistant/components/ruuvi_gateway/ @akx
/tests/components/ruuvi_gateway/ @akx
/homeassistant/components/ruuvitag_ble/ @akx
@@ -1279,8 +1271,6 @@ build.json @home-assistant/supervisor
/tests/components/sighthound/ @robmarkcole
/homeassistant/components/signal_messenger/ @bbernhard
/tests/components/signal_messenger/ @bbernhard
/homeassistant/components/simplefin/ @scottg489 @jeeftor
/tests/components/simplefin/ @scottg489 @jeeftor
/homeassistant/components/simplepush/ @engrbm87
/tests/components/simplepush/ @engrbm87
/homeassistant/components/simplisafe/ @bachya
@@ -1470,8 +1460,8 @@ build.json @home-assistant/supervisor
/tests/components/tomorrowio/ @raman325 @lymanepp
/homeassistant/components/totalconnect/ @austinmroczek
/tests/components/totalconnect/ @austinmroczek
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
/tests/components/tplink/ @rytilahti @bdraco @sdb9696
/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696
/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696
/homeassistant/components/tplink_omada/ @MarkGodwin
/tests/components/tplink_omada/ @MarkGodwin
/homeassistant/components/traccar/ @ludeeus

View File

@@ -4,7 +4,7 @@ coverage:
status:
project:
default:
target: auto
target: 90
threshold: 0.09
required:
target: auto

View File

@@ -363,15 +363,15 @@ class AuthManager:
local_only: bool | None = None,
) -> None:
"""Update a user."""
kwargs: dict[str, Any] = {
attr_name: value
for attr_name, value in (
("name", name),
("group_ids", group_ids),
("local_only", local_only),
)
if value is not None
}
kwargs: dict[str, Any] = {}
for attr_name, value in (
("name", name),
("group_ids", group_ids),
("local_only", local_only),
):
if value is not None:
kwargs[attr_name] = value
await self._store.async_update_user(user, **kwargs)
if is_active is not None:

View File

@@ -105,18 +105,14 @@ class AuthStore:
"perm_lookup": self._perm_lookup,
}
kwargs.update(
{
attr_name: value
for attr_name, value in (
("is_owner", is_owner),
("is_active", is_active),
("local_only", local_only),
("system_generated", system_generated),
)
if value is not None
}
)
for attr_name, value in (
("is_owner", is_owner),
("is_active", is_active),
("local_only", local_only),
("system_generated", system_generated),
):
if value is not None:
kwargs[attr_name] = value
new_user = models.User(**kwargs)

View File

@@ -55,6 +55,13 @@ class InvalidUser(HomeAssistantError):
Will not be raised when validating authentication.
"""
class InvalidUsername(InvalidUser):
"""Raised when invalid username is specified.
Will not be raised when validating authentication.
"""
def __init__(
self,
*args: object,
@@ -70,13 +77,6 @@ class InvalidUser(HomeAssistantError):
)
class InvalidUsername(InvalidUser):
"""Raised when invalid username is specified.
Will not be raised when validating authentication.
"""
class Data:
"""Hold the user data."""
@@ -216,7 +216,7 @@ class Data:
break
if index is None:
raise InvalidUser(translation_key="user_not_found")
raise InvalidUser
self.users.pop(index)
@@ -232,7 +232,7 @@ class Data:
user["password"] = self.hash_password(new_password, True).decode()
break
else:
raise InvalidUser(translation_key="user_not_found")
raise InvalidUser
@callback
def _validate_new_username(self, new_username: str) -> None:
@@ -275,7 +275,7 @@ class Data:
self._async_check_for_not_normalized_usernames(self._data)
break
else:
raise InvalidUser(translation_key="user_not_found")
raise InvalidUser
async def async_save(self) -> None:
"""Save data."""

View File

@@ -8,7 +8,7 @@ import contextlib
from functools import partial
from itertools import chain
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
import logging.handlers
import mimetypes
from operator import contains, itemgetter
import os
@@ -88,7 +88,7 @@ from .helpers import (
)
from .helpers.dispatcher import async_dispatcher_send_internal
from .helpers.storage import get_internal_store_manager
from .helpers.system_info import async_get_system_info, is_official_image
from .helpers.system_info import async_get_system_info
from .helpers.typing import ConfigType
from .setup import (
# _setup_started is marked as protected to make it clear
@@ -104,7 +104,7 @@ from .setup import (
from .util.async_ import create_eager_task
from .util.hass_dict import HassKey
from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
from .util.package import async_get_user_site, is_virtual_env
with contextlib.suppress(ImportError):
# Ensure anyio backend is imported to avoid it being imported in the event loop
@@ -257,12 +257,12 @@ async def async_setup_hass(
) -> core.HomeAssistant | None:
"""Set up Home Assistant."""
async def create_hass() -> core.HomeAssistant:
def create_hass() -> core.HomeAssistant:
"""Create the hass object and do basic setup."""
hass = core.HomeAssistant(runtime_config.config_dir)
loader.async_setup(hass)
await async_enable_logging(
async_enable_logging(
hass,
runtime_config.verbose,
runtime_config.log_rotate_days,
@@ -287,7 +287,7 @@ async def async_setup_hass(
async with hass.timeout.async_timeout(10):
await hass.async_stop()
hass = await create_hass()
hass = create_hass()
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
_LOGGER.warning(
@@ -326,13 +326,13 @@ async def async_setup_hass(
if config_dict is None:
recovery_mode = True
await stop_hass(hass)
hass = await create_hass()
hass = create_hass()
elif not basic_setup_success:
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
recovery_mode = True
await stop_hass(hass)
hass = await create_hass()
hass = create_hass()
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
_LOGGER.warning(
@@ -345,7 +345,7 @@ async def async_setup_hass(
recovery_mode = True
await stop_hass(hass)
hass = await create_hass()
hass = create_hass()
if old_logging:
hass.data[DATA_LOGGING] = old_logging
@@ -407,10 +407,6 @@ def _init_blocking_io_modules_in_executor() -> None:
# Initialize the mimetypes module to avoid blocking calls
# to the filesystem to load the mime.types file.
mimetypes.init()
# Initialize is_official_image and is_docker_env to avoid blocking calls
# to the filesystem.
is_official_image()
is_docker_env()
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
@@ -527,7 +523,8 @@ async def async_from_config_dict(
return hass
async def async_enable_logging(
@core.callback
def async_enable_logging(
hass: core.HomeAssistant,
verbose: bool = False,
log_rotate_days: int | None = None,
@@ -610,9 +607,23 @@ async def async_enable_logging(
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
err_handler: (
logging.handlers.RotatingFileHandler
| logging.handlers.TimedRotatingFileHandler
)
if log_rotate_days:
err_handler = logging.handlers.TimedRotatingFileHandler(
err_log_path, when="midnight", backupCount=log_rotate_days
)
else:
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
err_log_path, backupCount=1
)
try:
err_handler.doRollover()
except OSError as err:
_LOGGER.error("Error rolling over log file: %s", err)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
@@ -629,29 +640,7 @@ async def async_enable_logging(
async_activate_log_queue_handler(hass)
def _create_log_file(
err_log_path: str, log_rotate_days: int | None
) -> RotatingFileHandler | TimedRotatingFileHandler:
"""Create log file and do roll over."""
err_handler: RotatingFileHandler | TimedRotatingFileHandler
if log_rotate_days:
err_handler = TimedRotatingFileHandler(
err_log_path, when="midnight", backupCount=log_rotate_days
)
else:
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
err_log_path, backupCount=1
)
try:
err_handler.doRollover()
except OSError as err:
_LOGGER.error("Error rolling over log file: %s", err)
return err_handler
class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler):
class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler):
"""RotatingFileHandler that does not check if it should roll over on every log."""
def shouldRollover(self, record: logging.LogRecord) -> bool:

View File

@@ -9,5 +9,5 @@
},
"iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==5.2.1"]
"requirements": ["jaraco.abode==5.1.2"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["adguardhome"],
"requirements": ["adguardhome==0.7.0"]
"requirements": ["adguardhome==0.6.3"]
}

View File

@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components.cover import (
ATTR_POSITION,
DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA,
PLATFORM_SCHEMA,
CoverEntity,
CoverEntityFeature,
)
@@ -36,7 +36,7 @@ CONF_ADS_VAR_OPEN = "adsvar_open"
CONF_ADS_VAR_CLOSE = "adsvar_close"
CONF_ADS_VAR_STOP = "adsvar_stop"
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,

View File

@@ -9,7 +9,7 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
PLATFORM_SCHEMA,
ColorMode,
LightEntity,
)
@@ -29,7 +29,7 @@ from . import (
)
DEFAULT_NAME = "ADS Light"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,

View File

@@ -4,10 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorEntity,
)
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
@@ -25,7 +22,7 @@ from . import (
)
DEFAULT_NAME = "ADS sensor"
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,

View File

@@ -9,7 +9,10 @@ from typing import Final, final
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, StateType
@@ -18,11 +21,6 @@ from .const import DOMAIN
_LOGGER: Final = logging.getLogger(__name__)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
SCAN_INTERVAL: Final = timedelta(seconds=30)
ATTR_AQI: Final = "air_quality_index"
ATTR_CO2: Final = "carbon_dioxide"
ATTR_CO: Final = "carbon_monoxide"
@@ -35,6 +33,10 @@ ATTR_PM_10: Final = "particulate_matter_10"
ATTR_PM_2_5: Final = "particulate_matter_2_5"
ATTR_SO2: Final = "sulphur_dioxide"
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
SCAN_INTERVAL: Final = timedelta(seconds=30)
PROP_TO_ATTR: Final[dict[str, str]] = {
"air_quality_index": ATTR_AQI,
"carbon_dioxide": ATTR_CO2,

View File

@@ -20,7 +20,6 @@ PLATFORMS: list[Platform] = [
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@@ -8,34 +8,6 @@
"default": "mdi:lightbulb-on-outline"
}
},
"number": {
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"display_brightness": {
"default": "mdi:brightness-percent"
}
},
"select": {
"configuration_control": {
"default": "mdi:cloud-cog"
},
"display_temperature_unit": {
"default": "mdi:thermometer-lines"
},
"led_bar_mode": {
"default": "mdi:led-strip"
},
"nox_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"voc_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
}
},
"sensor": {
"total_volatile_organic_component_index": {
"default": "mdi:molecule"
@@ -45,32 +17,6 @@
},
"pm003_count": {
"default": "mdi:blur"
},
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"display_brightness": {
"default": "mdi:brightness-percent"
},
"display_temperature_unit": {
"default": "mdi:thermometer-lines"
},
"led_bar_mode": {
"default": "mdi:led-strip"
},
"nox_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"voc_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
}
},
"switch": {
"post_data_to_airgradient": {
"default": "mdi:cogs"
}
}
}

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/airgradient",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airgradient==0.6.1"],
"requirements": ["airgradient==0.6.0"],
"zeroconf": ["_airgradient._tcp.local."]
}

View File

@@ -79,63 +79,6 @@ LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = (
),
)
LEARNING_TIME_OFFSET_OPTIONS = [
"12",
"60",
"120",
"360",
"720",
]
ABC_DAYS = [
"1",
"8",
"30",
"90",
"180",
"0",
]
def _get_value(value: int, values: list[str]) -> str | None:
str_value = str(value)
return str_value if str_value in values else None
CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = (
AirGradientSelectEntityDescription(
key="nox_index_learning_time_offset",
translation_key="nox_index_learning_time_offset",
options=LEARNING_TIME_OFFSET_OPTIONS,
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: _get_value(
config.nox_learning_offset, LEARNING_TIME_OFFSET_OPTIONS
),
set_value_fn=lambda client, value: client.set_nox_learning_offset(int(value)),
),
AirGradientSelectEntityDescription(
key="voc_index_learning_time_offset",
translation_key="voc_index_learning_time_offset",
options=LEARNING_TIME_OFFSET_OPTIONS,
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: _get_value(
config.tvoc_learning_offset, LEARNING_TIME_OFFSET_OPTIONS
),
set_value_fn=lambda client, value: client.set_tvoc_learning_offset(int(value)),
),
AirGradientSelectEntityDescription(
key="co2_automatic_baseline_calibration",
translation_key="co2_automatic_baseline_calibration",
options=ABC_DAYS,
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: _get_value(
config.co2_automatic_baseline_calibration_days, ABC_DAYS
),
set_value_fn=lambda client,
value: client.set_co2_automatic_baseline_calibration(int(value)),
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -161,10 +104,7 @@ async def async_setup_entry(
coordinator.data.configuration_control is ConfigurationControl.LOCAL
and not added_entities
):
entities: list[AirGradientSelect] = [
AirGradientSelect(coordinator, description)
for description in CONTROL_ENTITIES
]
entities: list[AirGradientSelect] = []
if "I" in model:
entities.extend(
AirGradientSelect(coordinator, description)
@@ -183,9 +123,7 @@ async def async_setup_entry(
and added_entities
):
entity_registry = er.async_get(hass)
for entity_description in (
DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES + CONTROL_ENTITIES
):
for entity_description in DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES:
unique_id = f"{coordinator.serial_number}-{entity_description.key}"
if entity_id := entity_registry.async_get_entity_id(
SELECT_DOMAIN, DOMAIN, unique_id

View File

@@ -16,7 +16,6 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1."
},
"error": {
@@ -70,37 +69,6 @@
"co2": "Carbon dioxide",
"pm": "Particulate matter"
}
},
"nox_index_learning_time_offset": {
"name": "NOx index learning offset",
"state": {
"12": "12 hours",
"60": "60 hours",
"120": "120 hours",
"360": "360 hours",
"720": "720 hours"
}
},
"voc_index_learning_time_offset": {
"name": "VOC index learning offset",
"state": {
"12": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::12%]",
"60": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::60%]",
"120": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::120%]",
"360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]",
"720": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::720%]"
}
},
"co2_automatic_baseline_calibration": {
"name": "CO2 automatic baseline duration",
"state": {
"1": "1 day",
"8": "8 days",
"30": "30 days",
"90": "90 days",
"180": "180 days",
"0": "[%key:common::state::off%]"
}
}
},
"sensor": {
@@ -130,10 +98,10 @@
"name": "Carbon dioxide automatic baseline calibration"
},
"nox_learning_offset": {
"name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]"
"name": "NOx learning offset"
},
"tvoc_learning_offset": {
"name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]"
"name": "VOC learning offset"
},
"led_bar_mode": {
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
@@ -156,11 +124,6 @@
"display_brightness": {
"name": "[%key:component::airgradient::entity::number::display_brightness::name%]"
}
},
"switch": {
"post_data_to_airgradient": {
"name": "Post data to Airgradient"
}
}
}
}

View File

@@ -1,110 +0,0 @@
"""Support for AirGradient switch entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from airgradient import AirGradientClient, Config
from airgradient.models import ConfigurationControl
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry
from .const import DOMAIN
from .coordinator import AirGradientConfigCoordinator
from .entity import AirGradientEntity
@dataclass(frozen=True, kw_only=True)
class AirGradientSwitchEntityDescription(SwitchEntityDescription):
"""Describes AirGradient switch entity."""
value_fn: Callable[[Config], bool]
set_value_fn: Callable[[AirGradientClient, bool], Awaitable[None]]
POST_DATA_TO_AIRGRADIENT = AirGradientSwitchEntityDescription(
key="post_data_to_airgradient",
translation_key="post_data_to_airgradient",
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: config.post_data_to_airgradient,
set_value_fn=lambda client, value: client.enable_sharing_data(enable=value),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirGradientConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AirGradient switch entities based on a config entry."""
coordinator = entry.runtime_data.config
added_entities = False
@callback
def _async_check_entities() -> None:
nonlocal added_entities
if (
coordinator.data.configuration_control is ConfigurationControl.LOCAL
and not added_entities
):
async_add_entities(
[AirGradientSwitch(coordinator, POST_DATA_TO_AIRGRADIENT)]
)
added_entities = True
elif (
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
and added_entities
):
entity_registry = er.async_get(hass)
unique_id = f"{coordinator.serial_number}-{POST_DATA_TO_AIRGRADIENT.key}"
if entity_id := entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, unique_id
):
entity_registry.async_remove(entity_id)
added_entities = False
coordinator.async_add_listener(_async_check_entities)
_async_check_entities()
class AirGradientSwitch(AirGradientEntity, SwitchEntity):
"""Defines an AirGradient switch entity."""
entity_description: AirGradientSwitchEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__(
self,
coordinator: AirGradientConfigCoordinator,
description: AirGradientSwitchEntityDescription,
) -> None:
"""Initialize AirGradient switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
@property
def is_on(self) -> bool:
"""Return the state of the switch."""
return self.entity_description.value_fn(self.coordinator.data)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.entity_description.set_value_fn(self.coordinator.client, True)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.entity_description.set_value_fn(self.coordinator.client, False)
await self.coordinator.async_request_refresh()

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
"iot_class": "local_push",
"loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.2.10"]
"requirements": ["airtouch5py==0.2.8"]
}

View File

@@ -82,54 +82,33 @@ async def async_setup_entry(
"""Add Airzone binary sensors from a config_entry."""
coordinator = entry.runtime_data
added_systems: set[str] = set()
added_zones: set[str] = set()
binary_sensors: list[AirzoneBinarySensor] = [
AirzoneSystemBinarySensor(
coordinator,
description,
entry,
system_id,
system_data,
)
for system_id, system_data in coordinator.data[AZD_SYSTEMS].items()
for description in SYSTEM_BINARY_SENSOR_TYPES
if description.key in system_data
]
def _async_entity_listener() -> None:
"""Handle additions of binary sensors."""
binary_sensors.extend(
AirzoneZoneBinarySensor(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
for description in ZONE_BINARY_SENSOR_TYPES
if description.key in zone_data
)
entities: list[AirzoneBinarySensor] = []
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
received_systems = set(systems_data)
new_systems = received_systems - added_systems
if new_systems:
entities.extend(
AirzoneSystemBinarySensor(
coordinator,
description,
entry,
system_id,
systems_data.get(system_id),
)
for system_id in new_systems
for description in SYSTEM_BINARY_SENSOR_TYPES
if description.key in systems_data.get(system_id)
)
added_systems.update(new_systems)
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities.extend(
AirzoneZoneBinarySensor(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_BINARY_SENSOR_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(binary_sensors)
class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):

View File

@@ -102,31 +102,17 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone climate from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of climate."""
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneClimate(
coordinator,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
)
added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(
AirzoneClimate(
coordinator,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
)
class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.8.0"]
"requirements": ["aioairzone==0.7.7"]
}

View File

@@ -83,34 +83,21 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone select from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of select."""
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for description in ZONE_SELECT_TYPES
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
if description.key in zone_data
)
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):

View File

@@ -85,37 +85,21 @@ async def async_setup_entry(
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of sensors."""
entities: list[AirzoneSensor] = []
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities.extend(
AirzoneZoneSensor(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SENSOR_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
async_add_entities(entities)
entities: list[AirzoneSensor] = []
sensors: list[AirzoneSensor] = [
AirzoneZoneSensor(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
for description in ZONE_SENSOR_TYPES
if description.key in zone_data
]
if AZD_HOT_WATER in coordinator.data:
entities.extend(
sensors.extend(
AirzoneHotWaterSensor(
coordinator,
description,
@@ -126,7 +110,7 @@ async def async_setup_entry(
)
if AZD_WEBSERVER in coordinator.data:
entities.extend(
sensors.extend(
AirzoneWebServerSensor(
coordinator,
description,
@@ -136,10 +120,7 @@ async def async_setup_entry(
if description.key in coordinator.data[AZD_WEBSERVER]
)
async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(sensors)
class AirzoneSensor(AirzoneEntity, SensorEntity):

View File

@@ -61,7 +61,7 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone Water Heater from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
if AZD_HOT_WATER in coordinator.data:
async_add_entities([AirzoneWaterHeater(coordinator, entry)])

View File

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

View File

@@ -2,37 +2,93 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from genie_partner_sdk.client import AladdinConnectClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)
DOMAIN = "aladdin_connect"
from .api import AsyncConfigEntryAuth
from .const import DOMAIN
from .coordinator import AladdinConnectCoordinator
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
"""Set up Aladdin Connect from a config entry."""
ir.async_create_issue(
hass,
DOMAIN,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"entries": "/config/integrations/integration/aladdin_connect",
},
)
async def async_setup_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Set up Aladdin Connect Genie from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session)
coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth))
await coordinator.async_setup()
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async_remove_stale_devices(hass, entry)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Unload a config entry."""
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
) -> bool:
"""Migrate old config."""
if config_entry.version < 2:
config_entry.async_start_reauth(hass)
hass.config_entries.async_update_entry(
config_entry,
version=2,
minor_version=1,
)
return True
def async_remove_stale_devices(
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
) -> None:
"""Remove stale devices from device registry."""
device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors}
for device_entry in device_entries:
device_id: str | None = None
for identifier in device_entry.identifiers:
if identifier[0] == DOMAIN:
device_id = identifier[1]
break
if device_id is None or device_id not in all_device_ids:
# If device_id is None an invalid device entry was found for this config entry.
# If the device_id is not in existing device ids it's a stale device entry.
# Remove config entry from this device entry in either case.
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=config_entry.entry_id
)

View File

@@ -0,0 +1,32 @@
"""API for Aladdin Connect Genie bound to Home Assistant OAuth."""
from typing import cast
from aiohttp import ClientSession
from genie_partner_sdk.auth import Auth
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1"
API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3"
class AsyncConfigEntryAuth(Auth): # type: ignore[misc]
"""Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry."""
def __init__(
self,
websession: ClientSession,
oauth_session: OAuth2Session,
) -> None:
"""Initialize Aladdin Connect Genie auth."""
super().__init__(
websession, API_URL, oauth_session.token["access_token"], API_KEY
)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
await self._oauth_session.async_ensure_token_valid()
return cast(str, self._oauth_session.token["access_token"])

View File

@@ -0,0 +1,14 @@
"""application_credentials platform the Aladdin Connect Genie integration."""
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
"""Return authorization server."""
return AuthorizationServer(
authorize_url=OAUTH2_AUTHORIZE,
token_url=OAUTH2_TOKEN,
)

View File

@@ -1,11 +1,70 @@
"""Config flow for Aladdin Connect integration."""
"""Config flow for Aladdin Connect Genie."""
from homeassistant.config_entries import ConfigFlow
from collections.abc import Mapping
import logging
from typing import Any
from . import DOMAIN
import jwt
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from .const import DOMAIN
class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Aladdin Connect."""
class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""
VERSION = 1
DOMAIN = DOMAIN
VERSION = 2
MINOR_VERSION = 1
reauth_entry: ConfigEntry | None = None
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon API auth error or upgrade from v1 to v2."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user()
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an oauth config entry or update existing entry for reauth."""
token_payload = jwt.decode(
data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False}
)
if not self.reauth_entry:
await self.async_set_unique_id(token_payload["sub"])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=token_payload["username"],
data=data,
)
if self.reauth_entry.unique_id == token_payload["username"]:
return self.async_update_reload_and_abort(
self.reauth_entry,
data=data,
unique_id=token_payload["sub"],
)
if self.reauth_entry.unique_id == token_payload["sub"]:
return self.async_update_reload_and_abort(self.reauth_entry, data=data)
return self.async_abort(reason="wrong_account")
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)

View File

@@ -0,0 +1,6 @@
"""Constants for the Aladdin Connect Genie integration."""
DOMAIN = "aladdin_connect"
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"

View File

@@ -0,0 +1,38 @@
"""Define an object to coordinate fetching Aladdin Connect data."""
from datetime import timedelta
import logging
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AladdinConnectCoordinator(DataUpdateCoordinator[None]):
"""Aladdin Connect Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None:
"""Initialize."""
super().__init__(
hass,
logger=_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=15),
)
self.acc = acc
self.doors: list[GarageDoor] = []
async def async_setup(self) -> None:
"""Fetch initial data."""
self.doors = await self.acc.get_doors()
async def _async_update_data(self) -> None:
"""Fetch data from API endpoint."""
for door in self.doors:
await self.acc.update_door(door.device_id, door.door_number)

View File

@@ -0,0 +1,84 @@
"""Cover Entity for Genie Garage Door."""
from typing import Any
from genie_partner_sdk.model import GarageDoor
from homeassistant.components.cover import (
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
from .entity import AladdinConnectEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AladdinConnectConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Aladdin Connect platform."""
coordinator = config_entry.runtime_data
async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors)
class AladdinDevice(AladdinConnectEntity, CoverEntity):
"""Representation of Aladdin Connect cover."""
_attr_device_class = CoverDeviceClass.GARAGE
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
_attr_name = None
def __init__(
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
) -> None:
"""Initialize the Aladdin Connect cover."""
super().__init__(coordinator, device)
self._attr_unique_id = device.unique_id
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
await self.coordinator.acc.open_door(
self._device.device_id, self._device.door_number
)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Issue close command to cover."""
await self.coordinator.acc.close_door(
self._device.device_id, self._device.door_number
)
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
value = self.coordinator.acc.get_door_status(
self._device.device_id, self._device.door_number
)
if value is None:
return None
return bool(value == "closed")
@property
def is_closing(self) -> bool | None:
"""Update is closing attribute."""
value = self.coordinator.acc.get_door_status(
self._device.device_id, self._device.door_number
)
if value is None:
return None
return bool(value == "closing")
@property
def is_opening(self) -> bool | None:
"""Update is opening attribute."""
value = self.coordinator.acc.get_door_status(
self._device.device_id, self._device.door_number
)
if value is None:
return None
return bool(value == "opening")

View File

@@ -0,0 +1,27 @@
"""Defines a base Aladdin Connect entity."""
from genie_partner_sdk.model import GarageDoor
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AladdinConnectCoordinator
class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
"""Defines a base Aladdin Connect entity."""
_attr_has_entity_name = True
def __init__(
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._device = device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
name=device.name,
manufacturer="Overhead Door",
)

View File

@@ -1,9 +1,10 @@
{
"domain": "aladdin_connect",
"name": "Aladdin Connect",
"codeowners": [],
"codeowners": ["@swcloudgenie"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"integration_type": "system",
"iot_class": "cloud_polling",
"requirements": []
"requirements": ["genie-partner-sdk==1.0.2"]
}

View File

@@ -0,0 +1,80 @@
"""Support for Aladdin Connect Garage Door sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
from .entity import AladdinConnectEntity
@dataclass(frozen=True, kw_only=True)
class AccSensorEntityDescription(SensorEntityDescription):
"""Describes AladdinConnect sensor entity."""
value_fn: Callable[[AladdinConnectClient, str, int], float | None]
SENSORS: tuple[AccSensorEntityDescription, ...] = (
AccSensorEntityDescription(
key="battery_level",
device_class=SensorDeviceClass.BATTERY,
entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=AladdinConnectClient.get_battery_status,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Aladdin Connect sensor devices."""
coordinator = entry.runtime_data
async_add_entities(
AladdinConnectSensor(coordinator, door, description)
for description in SENSORS
for door in coordinator.doors
)
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
"""A sensor implementation for Aladdin Connect devices."""
entity_description: AccSensorEntityDescription
def __init__(
self,
coordinator: AladdinConnectCoordinator,
device: GarageDoor,
description: AccSensorEntityDescription,
) -> None:
"""Initialize a sensor for an Aladdin Connect device."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{device.unique_id}-{description.key}"
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(
self.coordinator.acc, self._device.device_id, self._device.door_number
)

View File

@@ -1,8 +1,29 @@
{
"issues": {
"integration_removed": {
"title": "The Aladdin Connect integration has been removed",
"description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})."
"config": {
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Aladdin Connect needs to re-authenticate your account"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
}
}

View File

@@ -52,10 +52,8 @@ from .const import ( # noqa: F401
_LOGGER: Final = logging.getLogger(__name__)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
SCAN_INTERVAL: Final = timedelta(seconds=30)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
CONF_DEFAULT_CODE = "default_code"
@@ -63,6 +61,8 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema(
{vol.Optional(ATTR_CODE): cv.string}
)
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
# mypy: disallow-any-generics

View File

@@ -2,10 +2,11 @@
from __future__ import annotations
from collections.abc import Generator
import logging
from typing import Any
from typing_extensions import Generator
from homeassistant.components import (
button,
climate,
@@ -18,7 +19,6 @@ from homeassistant.components import (
light,
media_player,
number,
remote,
timer,
vacuum,
valve,
@@ -439,8 +439,6 @@ class AlexaPowerController(AlexaCapability):
is_on = self.entity.state == fan.STATE_ON
elif self.entity.domain == humidifier.DOMAIN:
is_on = self.entity.state == humidifier.STATE_ON
elif self.entity.domain == remote.DOMAIN:
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
elif self.entity.domain == vacuum.DOMAIN:
is_on = self.entity.state == vacuum.STATE_CLEANING
elif self.entity.domain == timer.DOMAIN:
@@ -1438,12 +1436,6 @@ class AlexaModeController(AlexaCapability):
if mode in modes:
return f"{humidifier.ATTR_MODE}.{mode}"
# Remote Activity
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
activity = self.entity.attributes.get(remote.ATTR_CURRENT_ACTIVITY, None)
if activity in self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST, []):
return f"{remote.ATTR_ACTIVITY}.{activity}"
# Water heater operation mode
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = self.entity.attributes.get(
@@ -1558,24 +1550,6 @@ class AlexaModeController(AlexaCapability):
)
return self._resource.serialize_capability_resources()
# Remote Resource
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
# Use the mode controller for a remote because the input controller
# only allows a preset of names as an input.
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
for activity in activities:
self._resource.add_mode(
f"{remote.ATTR_ACTIVITY}.{activity}", [activity]
)
# Remotes with a single activity completely break Alexa discovery, add a
# fake activity to the mode controller (see issue #53832).
if len(activities) == 1:
self._resource.add_mode(
f"{remote.ATTR_ACTIVITY}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
)
return self._resource.serialize_capability_resources()
# Cover Position Resources
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
self._resource = AlexaModeResource(

View File

@@ -88,7 +88,7 @@ API_THERMOSTAT_MODES_CUSTOM = {
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
# AlexaModeController does not like a single mode for the fan preset or humidifier mode,
# we add PRESET_MODE_NA if a fan / humidifier / remote has only one preset_mode
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
PRESET_MODE_NA = "-"
STORAGE_ACCESS_TOKEN = "access_token"

View File

@@ -2,10 +2,12 @@
from __future__ import annotations
from collections.abc import Generator, Iterable
from collections.abc import Iterable
import logging
from typing import TYPE_CHECKING, Any
from typing_extensions import Generator
from homeassistant.components import (
alarm_control_panel,
alert,
@@ -27,7 +29,6 @@ from homeassistant.components import (
lock,
media_player,
number,
remote,
scene,
script,
sensor,
@@ -197,10 +198,6 @@ class DisplayCategory:
# Indicates a device that prints.
PRINTER = "PRINTER"
# Indicates a decive that support stateless events,
# such as remote switches and smart buttons.
REMOTE = "REMOTE"
# Indicates a network router.
ROUTER = "ROUTER"
@@ -650,24 +647,6 @@ class FanCapabilities(AlexaEntity):
yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(remote.DOMAIN)
class RemoteCapabilities(AlexaEntity):
"""Class to represent Remote capabilities."""
def default_display_categories(self) -> list[str]:
"""Return the display categories for this entity."""
return [DisplayCategory.REMOTE]
def interfaces(self) -> Generator[AlexaCapability]:
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
yield AlexaModeController(
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
)
yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(humidifier.DOMAIN)
class HumidifierCapabilities(AlexaEntity):
"""Class to represent Humidifier capabilities."""

View File

@@ -21,7 +21,6 @@ from homeassistant.components import (
light,
media_player,
number,
remote,
timer,
vacuum,
valve,
@@ -186,8 +185,6 @@ async def async_api_turn_on(
service = fan.SERVICE_TURN_ON
elif domain == humidifier.DOMAIN:
service = humidifier.SERVICE_TURN_ON
elif domain == remote.DOMAIN:
service = remote.SERVICE_TURN_ON
elif domain == vacuum.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if (
@@ -237,8 +234,6 @@ async def async_api_turn_off(
service = climate.SERVICE_TURN_OFF
elif domain == fan.DOMAIN:
service = fan.SERVICE_TURN_OFF
elif domain == remote.DOMAIN:
service = remote.SERVICE_TURN_OFF
elif domain == humidifier.DOMAIN:
service = humidifier.SERVICE_TURN_OFF
elif domain == vacuum.DOMAIN:
@@ -1205,17 +1200,6 @@ async def async_api_set_mode(
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
raise AlexaInvalidValueError(msg)
# Remote Activity
if instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
activity = mode.split(".")[1]
activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
if activity != PRESET_MODE_NA and activities and activity in activities:
service = remote.SERVICE_TURN_ON
data[remote.ATTR_ACTIVITY] = activity
else:
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
raise AlexaInvalidValueError(msg)
# Water heater operation mode
elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = mode.split(".")[1]
@@ -1513,7 +1497,7 @@ async def async_api_adjust_range(
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
service = SERVICE_SET_COVER_POSITION
if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)):
if not (current := entity.attributes.get(cover.ATTR_POSITION)):
msg = f"Unable to determine {entity.entity_id} current position"
raise AlexaInvalidValueError(msg)
position = response_value = min(100, max(0, range_delta + current))

View File

@@ -10,10 +10,7 @@ from alpha_vantage.timeseries import TimeSeries
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorEntity,
)
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_API_KEY, CONF_CURRENCY, CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
@@ -62,7 +59,7 @@ CURRENCY_SCHEMA = vol.Schema(
}
)
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_FOREIGN_EXCHANGE): vol.All(cv.ensure_list, [CURRENCY_SCHEMA]),

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/amazon_polly",
"iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"],
"requirements": ["boto3==1.34.131"]
"requirements": ["boto3==1.34.51"]
}

View File

@@ -71,18 +71,6 @@ class AmberPriceSpikeBinarySensor(AmberPriceGridSensor):
}
class AmberDemandWindowBinarySensor(AmberPriceGridSensor):
"""Sensor to show whether demand window is active."""
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
grid = self.coordinator.data["grid"]
if "demand_window" in grid:
return grid["demand_window"] # type: ignore[no-any-return]
return None
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
@@ -95,14 +83,6 @@ async def async_setup_entry(
key="price_spike",
name=f"{entry.title} - Price Spike",
)
demand_window_description = BinarySensorEntityDescription(
key="demand_window",
name=f"{entry.title} - Demand Window",
translation_key="demand_window",
)
async_add_entities(
[
AmberPriceSpikeBinarySensor(coordinator, price_spike_description),
AmberDemandWindowBinarySensor(coordinator, demand_window_description),
]
[AmberPriceSpikeBinarySensor(coordinator, price_spike_description)]
)

View File

@@ -111,9 +111,6 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
]
result["grid"]["renewables"] = round(general[0].renewables)
result["grid"]["price_spike"] = general[0].spike_status.value
tariff_information = general[0].tariff_information
if tariff_information:
result["grid"]["demand_window"] = tariff_information.demand_window
controlled_load = [
interval for interval in current if is_controlled_load(interval)

View File

@@ -13,14 +13,6 @@
"renewables": {
"default": "mdi:solar-power"
}
},
"binary_sensor": {
"demand_window": {
"default": "mdi:meter-electric",
"state": {
"off": "mdi:meter-electric-outline"
}
}
}
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
"iot_class": "cloud_polling",
"loggers": ["amberelectric"],
"requirements": ["amberelectric==1.1.1"]
"requirements": ["amberelectric==1.1.0"]
}

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["python_homeassistant_analytics"],
"requirements": ["python-homeassistant-analytics==0.7.0"],
"requirements": ["python-homeassistant-analytics==0.6.0"],
"single_config_entry": true
}

View File

@@ -101,7 +101,7 @@
},
"learn_sendevent": {
"name": "Learn sendevent",
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of performing this action."
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service."
}
},
"exceptions": {

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/anova",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.17.0"]
"requirements": ["anova-wifi==0.12.0"]
}

View File

@@ -16,6 +16,8 @@ from homeassistant.const import (
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import Event, EventStateChangedData, HomeAssistant
import homeassistant.helpers.config_validation as cv
@@ -119,7 +121,7 @@ class KafkaManager:
state = event.data["new_state"]
if (
state is None
or state.state == ""
or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE)
or not self._entities_filter(state.entity_id)
):
return None
@@ -139,8 +141,7 @@ class KafkaManager:
async def write(self, event: Event[EventStateChangedData]) -> None:
"""Write a binary payload to Kafka."""
key = event.data["entity_id"].encode("utf-8")
payload = self._encode_event(event)
if payload:
await self._producer.send_and_wait(self._topic, payload, key)
await self._producer.send_and_wait(self._topic, payload)

View File

@@ -68,8 +68,4 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
"""Returns true if the UPS is online."""
# Check if ONLINE bit is set in STATFLAG.
key = self.entity_description.key.upper()
# The daemon could either report just a hex ("0x05000008"), or a hex with a "Status Flag"
# suffix ("0x05000008 Status Flag") in older versions.
# Here we trim the suffix if it exists to support both.
flag = self.coordinator.data[key].removesuffix(" Status Flag")
return int(flag, 16) & _VALUE_ONLINE_MASK != 0
return int(self.coordinator.data[key], 16) & _VALUE_ONLINE_MASK != 0

View File

@@ -17,7 +17,6 @@ from .coordinator import AprilaireCoordinator
PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.HUMIDIFIER,
Platform.SELECT,
Platform.SENSOR,
]

View File

@@ -62,7 +62,7 @@ class AprilaireConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return self.async_create_entry(title="AprilAire", data=user_input)
return self.async_create_entry(title="Aprilaire", data=user_input)
return self.async_show_form(
step_id="user",

View File

@@ -1,194 +0,0 @@
"""The Aprilaire humidifier component."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, cast
from pyaprilaire.const import Attribute
from homeassistant.components.humidifier import (
HumidifierAction,
HumidifierDeviceClass,
HumidifierEntity,
HumidifierEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DOMAIN
from .coordinator import AprilaireCoordinator
from .entity import BaseAprilaireEntity
HUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = {
0: HumidifierAction.IDLE,
1: HumidifierAction.IDLE,
2: HumidifierAction.HUMIDIFYING,
3: HumidifierAction.OFF,
}
DEHUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = {
0: HumidifierAction.IDLE,
1: HumidifierAction.IDLE,
2: HumidifierAction.DRYING,
3: HumidifierAction.DRYING,
4: HumidifierAction.OFF,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Aprilaire humidifier devices."""
coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id]
assert config_entry.unique_id is not None
descriptions: list[AprilaireHumidifierDescription] = []
if coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) in (0, 1, 2):
descriptions.append(
AprilaireHumidifierDescription(
key="humidifier",
translation_key="humidifier",
device_class=HumidifierDeviceClass.HUMIDIFIER,
action_key=Attribute.HUMIDIFICATION_STATUS,
action_map=HUMIDIFIER_ACTION_MAP,
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT,
min_humidity=10,
max_humidity=50,
default_humidity=30,
set_humidity_fn=coordinator.client.set_humidification_setpoint,
)
)
if coordinator.data.get(Attribute.DEHUMIDIFICATION_AVAILABLE) in (0, 1):
descriptions.append(
AprilaireHumidifierDescription(
key="dehumidifier",
translation_key="dehumidifier",
device_class=HumidifierDeviceClass.DEHUMIDIFIER,
action_key=Attribute.DEHUMIDIFICATION_STATUS,
action_map=DEHUMIDIFIER_ACTION_MAP,
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT,
min_humidity=40,
max_humidity=90,
default_humidity=60,
set_humidity_fn=coordinator.client.set_dehumidification_setpoint,
)
)
async_add_entities(
AprilaireHumidifierEntity(coordinator, description, config_entry.unique_id)
for description in descriptions
)
@dataclass(frozen=True, kw_only=True)
class AprilaireHumidifierDescription(HumidifierEntityDescription):
"""Class describing Aprilaire humidifier entities."""
action_key: str
action_map: dict[StateType, HumidifierAction]
current_humidity_key: str
target_humidity_key: str
min_humidity: int
max_humidity: int
default_humidity: int
set_humidity_fn: Callable[[int], Awaitable]
class AprilaireHumidifierEntity(BaseAprilaireEntity, HumidifierEntity):
"""Base humidity entity for Aprilaire."""
entity_description: AprilaireHumidifierDescription
last_target_humidity: int | None = None
def __init__(
self,
coordinator: AprilaireCoordinator,
description: AprilaireHumidifierDescription,
unique_id: str,
) -> None:
"""Initialize a select for an Aprilaire device."""
self.entity_description = description
super().__init__(coordinator, unique_id)
@property
def action(self) -> HumidifierAction | None:
"""Get the current action."""
action = self.coordinator.data.get(self.entity_description.action_key)
return self.entity_description.action_map.get(action, HumidifierAction.OFF)
@property
def is_on(self) -> bool:
"""Get whether the humidifier is on."""
return self.target_humidity is not None and self.target_humidity > 0
@property
def current_humidity(self) -> float | None:
"""Get the current humidity."""
return cast(
float,
self.coordinator.data.get(self.entity_description.current_humidity_key),
)
@property
def target_humidity(self) -> float | None:
"""Get the target humidity."""
target_humidity = cast(
float,
self.coordinator.data.get(self.entity_description.target_humidity_key),
)
if target_humidity is not None and target_humidity > 0:
self.last_target_humidity = int(target_humidity)
return target_humidity
@property
def min_humidity(self) -> float:
"""Return the minimum humidity."""
return self.entity_description.min_humidity
@property
def max_humidity(self) -> float:
"""Return the maximum humidity."""
return self.entity_description.max_humidity
async def async_set_humidity(self, humidity: int) -> None:
"""Set the humidity."""
await self.entity_description.set_humidity_fn(humidity)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if self.last_target_humidity is None or self.last_target_humidity == 0:
target_humidity = self.entity_description.default_humidity
else:
target_humidity = self.last_target_humidity
await self.entity_description.set_humidity_fn(target_humidity)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self.entity_description.set_humidity_fn(0)

View File

@@ -1,11 +1,11 @@
{
"domain": "aprilaire",
"name": "AprilAire",
"name": "Aprilaire",
"codeowners": ["@chamberlain2007"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aprilaire",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.7.4"]
"requirements": ["pyaprilaire==0.7.0"]
}

View File

@@ -24,14 +24,6 @@
"name": "Thermostat"
}
},
"humidifier": {
"humidifier": {
"name": "[%key:component::humidifier::title%]"
},
"dehumidifier": {
"name": "[%key:component::humidifier::entity_component::dehumidifier::name%]"
}
},
"select": {
"air_cleaning_event": {
"name": "Air cleaning event",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["apsystems-ez1==1.3.3"]
"requirements": ["apsystems-ez1==1.3.1"]
}

View File

@@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["aioaquacell"],
"requirements": ["aioaquacell==0.2.0"]
"requirements": ["aioaquacell==0.1.7"]
}

View File

@@ -7,7 +7,7 @@ from dataclasses import dataclass
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
@@ -104,7 +104,7 @@ SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = (
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All(
cv.ensure_list, [vol.In(SENSOR_KEYS)]

View File

@@ -10,7 +10,7 @@ import sharp_aquos_rc
import voluptuous as vol
from homeassistant.components.media_player import (
PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
PLATFORM_SCHEMA,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
@@ -37,7 +37,7 @@ DEFAULT_PASSWORD = "password"
DEFAULT_TIMEOUT = 0.5
DEFAULT_RETRIES = 2
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,

View File

@@ -13,17 +13,17 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
DOMAIN_DATA_ENTRIES,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
)
type ArcamFmjConfigEntry = ConfigEntry[Client]
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@@ -31,21 +31,34 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
hass.data[DOMAIN_DATA_ENTRIES] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up config entry."""
entry.runtime_data = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
entries = hass.data[DOMAIN_DATA_ENTRIES]
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
entries[entry.entry_id] = client
entry.async_create_background_task(
hass, _run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
hass, _run_client(hass, client, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Cleanup before removing config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN_DATA_ENTRIES].pop(entry.entry_id)
return unload_ok
async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> None:

View File

@@ -10,11 +10,18 @@ from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES
def get_entry_client(hass: HomeAssistant, entry: ConfigEntry) -> Client:
"""Retrieve client associated with a config entry."""
client: Client = hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id]
return client
class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):

View File

@@ -11,3 +11,5 @@ EVENT_TURN_ON = "arcam_fmj.turn_on"
DEFAULT_PORT = 50000
DEFAULT_NAME = "Arcam FMJ"
DEFAULT_SCAN_INTERVAL = 5
DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries"

View File

@@ -19,6 +19,7 @@ from homeassistant.components.media_player import (
MediaType,
)
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -26,7 +27,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ArcamFmjConfigEntry
from .config_flow import get_entry_client
from .const import (
DOMAIN,
EVENT_TURN_ON,
@@ -40,12 +41,12 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ArcamFmjConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the configuration entry."""
client = config_entry.runtime_data
client = get_entry_client(hass, config_entry)
async_add_entities(
[

View File

@@ -9,10 +9,7 @@ import logging
import requests
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorEntity,
)
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_VARIABLES,
CONF_NAME,
@@ -44,7 +41,7 @@ PIN_VARIABLE_SCHEMA = vol.Schema(
}
)
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arve",
"iot_class": "cloud_polling",
"requirements": ["asyncarve==0.1.1"]
"requirements": ["asyncarve==0.0.9"]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import array
import asyncio
from collections import defaultdict, deque
from collections.abc import AsyncGenerator, AsyncIterable, Callable
from collections.abc import AsyncIterable, Callable, Iterable
from dataclasses import asdict, dataclass, field
from enum import StrEnum
import logging
@@ -16,6 +16,7 @@ import time
from typing import TYPE_CHECKING, Any, Final, Literal, cast
import wave
from typing_extensions import AsyncGenerator
import voluptuous as vol
if TYPE_CHECKING:
@@ -115,13 +116,10 @@ AUDIO_PROCESSOR_SAMPLES: Final = 160 # 10 ms @ 16 Khz
AUDIO_PROCESSOR_BYTES: Final = AUDIO_PROCESSOR_SAMPLES * 2 # 16-bit samples
@callback
def _async_resolve_default_pipeline_settings(
async def _async_resolve_default_pipeline_settings(
hass: HomeAssistant,
*,
conversation_engine_id: str | None = None,
stt_engine_id: str | None = None,
tts_engine_id: str | None = None,
stt_engine_id: str | None,
tts_engine_id: str | None,
pipeline_name: str,
) -> dict[str, str | None]:
"""Resolve settings for a default pipeline.
@@ -139,13 +137,12 @@ def _async_resolve_default_pipeline_settings(
wake_word_entity = None
wake_word_id = None
if conversation_engine_id is None:
conversation_engine_id = conversation.HOME_ASSISTANT_AGENT
# Find a matching language supported by the Home Assistant conversation agent
conversation_languages = language_util.matches(
hass.config.language,
conversation.async_get_conversation_languages(hass, conversation_engine_id),
await conversation.async_get_conversation_languages(
hass, conversation.HOME_ASSISTANT_AGENT
),
country=hass.config.country,
)
if conversation_languages:
@@ -204,7 +201,7 @@ def _async_resolve_default_pipeline_settings(
tts_engine_id = None
return {
"conversation_engine": conversation_engine_id,
"conversation_engine": conversation.HOME_ASSISTANT_AGENT,
"conversation_language": conversation_language,
"language": hass.config.language,
"name": pipeline_name,
@@ -226,8 +223,8 @@ async def _async_create_default_pipeline(
The default pipeline will use the homeassistant conversation agent and the
default stt / tts engines.
"""
pipeline_settings = _async_resolve_default_pipeline_settings(
hass, pipeline_name="Home Assistant"
pipeline_settings = await _async_resolve_default_pipeline_settings(
hass, stt_engine_id=None, tts_engine_id=None, pipeline_name="Home Assistant"
)
return await pipeline_store.async_create_item(pipeline_settings)
@@ -245,11 +242,8 @@ async def async_create_default_pipeline(
"""
pipeline_data: PipelineData = hass.data[DOMAIN]
pipeline_store = pipeline_data.pipeline_store
pipeline_settings = _async_resolve_default_pipeline_settings(
hass,
stt_engine_id=stt_engine_id,
tts_engine_id=tts_engine_id,
pipeline_name=pipeline_name,
pipeline_settings = await _async_resolve_default_pipeline_settings(
hass, stt_engine_id, tts_engine_id, pipeline_name=pipeline_name
)
if (
pipeline_settings["stt_engine"] != stt_engine_id
@@ -259,22 +253,6 @@ async def async_create_default_pipeline(
return await pipeline_store.async_create_item(pipeline_settings)
@callback
def _async_get_pipeline_from_conversation_entity(
hass: HomeAssistant, entity_id: str
) -> Pipeline:
"""Get a pipeline by conversation entity ID."""
entity = hass.states.get(entity_id)
settings = _async_resolve_default_pipeline_settings(
hass,
pipeline_name=entity.name if entity else entity_id,
conversation_engine_id=entity_id,
)
settings["id"] = entity_id
return Pipeline.from_json(settings)
@callback
def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> Pipeline:
"""Get a pipeline by id or the preferred pipeline."""
@@ -284,9 +262,6 @@ def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> P
# A pipeline was not specified, use the preferred one
pipeline_id = pipeline_data.pipeline_store.async_get_preferred_item()
if pipeline_id.startswith("conversation."):
return _async_get_pipeline_from_conversation_entity(hass, pipeline_id)
pipeline = pipeline_data.pipeline_store.data.get(pipeline_id)
# If invalid pipeline ID was specified
@@ -299,11 +274,11 @@ def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> P
@callback
def async_get_pipelines(hass: HomeAssistant) -> list[Pipeline]:
def async_get_pipelines(hass: HomeAssistant) -> Iterable[Pipeline]:
"""Get all pipelines."""
pipeline_data: PipelineData = hass.data[DOMAIN]
return list(pipeline_data.pipeline_store.data.values())
return pipeline_data.pipeline_store.data.values()
async def async_update_pipeline(
@@ -329,25 +304,21 @@ async def async_update_pipeline(
updates.pop("id")
# Refactor this once we bump to Python 3.12
# and have https://peps.python.org/pep-0692/
updates.update(
{
key: val
for key, val in (
("conversation_engine", conversation_engine),
("conversation_language", conversation_language),
("language", language),
("name", name),
("stt_engine", stt_engine),
("stt_language", stt_language),
("tts_engine", tts_engine),
("tts_language", tts_language),
("tts_voice", tts_voice),
("wake_word_entity", wake_word_entity),
("wake_word_id", wake_word_id),
)
if val is not UNDEFINED
}
)
for key, val in (
("conversation_engine", conversation_engine),
("conversation_language", conversation_language),
("language", language),
("name", name),
("stt_engine", stt_engine),
("stt_language", stt_language),
("tts_engine", tts_engine),
("tts_language", tts_language),
("tts_voice", tts_voice),
("wake_word_entity", wake_word_entity),
("wake_word_id", wake_word_id),
):
if val is not UNDEFINED:
updates[key] = val
await pipeline_data.pipeline_store.async_update_item(pipeline.id, updates)
@@ -884,15 +855,6 @@ class PipelineRun:
stream: AsyncIterable[ProcessedAudioChunk],
) -> str:
"""Run speech-to-text portion of pipeline. Returns the spoken text."""
# Create a background task to prepare the conversation agent
if self.end_stage >= PipelineStage.INTENT:
self.hass.async_create_background_task(
conversation.async_prepare_agent(
self.hass, self.intent_agent, self.language
),
f"prepare conversation agent {self.intent_agent}",
)
if isinstance(self.stt_provider, stt.Provider):
engine = self.stt_provider.name
else:
@@ -995,6 +957,8 @@ class PipelineRun:
"""Prepare recognizing an intent."""
agent_info = conversation.async_get_agent_info(
self.hass,
# If no conversation engine is set, use the Home Assistant agent
# (the conversation integration default is currently the last one set)
self.pipeline.conversation_engine or conversation.HOME_ASSISTANT_AGENT,
)
@@ -1689,12 +1653,6 @@ class PipelineStorageCollectionWebsocket(
if item_id is None:
item_id = self.storage_collection.async_get_preferred_item()
if item_id.startswith("conversation.") and hass.states.get(item_id):
connection.send_result(
msg["id"], _async_get_pipeline_from_conversation_entity(hass, item_id)
)
return
if item_id not in self.storage_collection.data:
connection.send_error(
msg["id"],
@@ -1713,7 +1671,7 @@ class PipelineStorageCollectionWebsocket(
connection.send_result(
msg["id"],
{
"pipelines": async_get_pipelines(hass),
"pipelines": self.storage_collection.async_items(),
"preferred_pipeline": self.storage_collection.async_get_preferred_item(),
},
)

View File

@@ -5,12 +5,13 @@ import asyncio
# Suppressing disable=deprecated-module is needed for Python 3.11
import audioop # pylint: disable=deprecated-module
import base64
from collections.abc import AsyncGenerator, Callable
from collections.abc import Callable
import contextlib
import logging
import math
from typing import Any, Final
from typing_extensions import AsyncGenerator
import voluptuous as vol
from homeassistant.components import conversation, stt, tts, websocket_api
@@ -378,8 +379,8 @@ def websocket_get_run(
vol.Required("type"): "assist_pipeline/language/list",
}
)
@callback
def websocket_list_languages(
@websocket_api.async_response
async def websocket_list_languages(
hass: HomeAssistant,
connection: websocket_api.connection.ActiveConnection,
msg: dict[str, Any],
@@ -389,7 +390,7 @@ def websocket_list_languages(
This will return a list of languages which are supported by at least one stt, tts
and conversation engine respectively.
"""
conv_language_tags = conversation.async_get_conversation_languages(hass)
conv_language_tags = await conversation.async_get_conversation_languages(hass)
stt_language_tags = stt.async_get_speech_to_text_languages(hass)
tts_language_tags = tts.async_get_text_to_speech_languages(hass)
pipeline_languages: set[str] | None = None

View File

@@ -9,7 +9,7 @@ from pyatome.client import AtomeClient, PyAtomeError
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
@@ -49,7 +49,7 @@ WEEKLY_TYPE = "week"
MONTHLY_TYPE = "month"
YEARLY_TYPE = "year"
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,

View File

@@ -2,10 +2,10 @@
from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from path import Path
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig

View File

@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
from functools import partial
import logging
from yalexs.activity import Activity, ActivityType
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
from yalexs.doorbell import DoorbellDetail
from yalexs.lock import LockDetail, LockDoorStatus
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
@@ -26,25 +26,67 @@ from homeassistant.helpers.event import async_call_later
from . import AugustConfigEntry, AugustData
from .entity import AugustDescriptionEntity
from .util import (
retrieve_ding_activity,
retrieve_doorbell_motion_activity,
retrieve_online_state,
retrieve_time_based_activity,
)
_LOGGER = logging.getLogger(__name__)
TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds())
TIME_TO_RECHECK_DETECTION = timedelta(
seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3
)
def _retrieve_online_state(
data: AugustData, detail: DoorbellDetail | LockDetail
) -> bool:
"""Get the latest state of the sensor."""
# The doorbell will go into standby mode when there is no motion
# for a short while. It will wake by itself when needed so we need
# to consider is available or we will not report motion or dings
if isinstance(detail, DoorbellDetail):
return detail.is_online or detail.is_standby
return detail.bridge_is_online
def _retrieve_time_based_state(
activities: set[ActivityType], data: AugustData, detail: DoorbellDetail
) -> bool:
"""Get the latest state of the sensor."""
stream = data.activity_stream
if latest := stream.get_latest_device_activity(detail.device_id, activities):
return _activity_time_based_state(latest)
return False
_RING_ACTIVITIES = {ActivityType.DOORBELL_DING}
def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) -> bool:
stream = data.activity_stream
latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES)
if latest is None or (
data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED
):
return False
return _activity_time_based_state(latest)
def _activity_time_based_state(latest: Activity) -> bool:
"""Get the latest state of the sensor."""
start = latest.activity_start_time
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
return start <= _native_datetime() <= end
def _native_datetime() -> datetime:
"""Return time in the format august uses without timezone."""
return datetime.now()
@dataclass(frozen=True, kw_only=True)
class AugustDoorbellBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes August binary_sensor entity."""
value_fn: Callable[[AugustData, DoorbellDetail | LockDetail], Activity | None]
value_fn: Callable[[AugustData, DoorbellDetail], bool]
is_time_based: bool
@@ -57,14 +99,14 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
AugustDoorbellBinarySensorEntityDescription(
key="motion",
device_class=BinarySensorDeviceClass.MOTION,
value_fn=retrieve_doorbell_motion_activity,
value_fn=partial(_retrieve_time_based_state, {ActivityType.DOORBELL_MOTION}),
is_time_based=True,
),
AugustDoorbellBinarySensorEntityDescription(
key="image capture",
translation_key="image_capture",
value_fn=partial(
retrieve_time_based_activity, {ActivityType.DOORBELL_IMAGE_CAPTURE}
_retrieve_time_based_state, {ActivityType.DOORBELL_IMAGE_CAPTURE}
),
is_time_based=True,
),
@@ -72,7 +114,7 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=retrieve_online_state,
value_fn=_retrieve_online_state,
is_time_based=False,
),
)
@@ -82,7 +124,7 @@ SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] =
AugustDoorbellBinarySensorEntityDescription(
key="ding",
device_class=BinarySensorDeviceClass.OCCUPANCY,
value_fn=retrieve_ding_activity,
value_fn=_retrieve_ding_state,
is_time_based=True,
),
)
@@ -147,12 +189,10 @@ class AugustDoorbellBinarySensor(AugustDescriptionEntity, BinarySensorEntity):
def _update_from_data(self) -> None:
"""Get the latest state of the sensor."""
self._cancel_any_pending_updates()
self._attr_is_on = bool(
self.entity_description.value_fn(self._data, self._detail)
)
self._attr_is_on = self.entity_description.value_fn(self._data, self._detail)
if self.entity_description.is_time_based:
self._attr_available = retrieve_online_state(self._data, self._detail)
self._attr_available = _retrieve_online_state(self._data, self._detail)
self._schedule_update_to_recheck_turn_off_sensor()
else:
self._attr_available = True

View File

@@ -42,7 +42,6 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CAMERA,
Platform.EVENT,
Platform.LOCK,
Platform.SENSOR,
]

View File

@@ -1,104 +0,0 @@
"""Support for august events."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from yalexs.activity import Activity
from yalexs.doorbell import DoorbellDetail
from yalexs.lock import LockDetail
from homeassistant.components.event import (
EventDeviceClass,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AugustConfigEntry, AugustData
from .entity import AugustDescriptionEntity
from .util import (
retrieve_ding_activity,
retrieve_doorbell_motion_activity,
retrieve_online_state,
)
@dataclass(kw_only=True, frozen=True)
class AugustEventEntityDescription(EventEntityDescription):
"""Describe august event entities."""
value_fn: Callable[[AugustData, DoorbellDetail | LockDetail], Activity | None]
TYPES_VIDEO_DOORBELL: tuple[AugustEventEntityDescription, ...] = (
AugustEventEntityDescription(
key="motion",
translation_key="motion",
device_class=EventDeviceClass.MOTION,
event_types=["motion"],
value_fn=retrieve_doorbell_motion_activity,
),
)
TYPES_DOORBELL: tuple[AugustEventEntityDescription, ...] = (
AugustEventEntityDescription(
key="doorbell",
translation_key="doorbell",
device_class=EventDeviceClass.DOORBELL,
event_types=["ring"],
value_fn=retrieve_ding_activity,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AugustConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the august event platform."""
data = config_entry.runtime_data
entities: list[AugustEventEntity] = []
for lock in data.locks:
detail = data.get_device_detail(lock.device_id)
if detail.doorbell:
entities.extend(
AugustEventEntity(data, lock, description)
for description in TYPES_DOORBELL
)
for doorbell in data.doorbells:
entities.extend(
AugustEventEntity(data, doorbell, description)
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
)
async_add_entities(entities)
class AugustEventEntity(AugustDescriptionEntity, EventEntity):
"""An august event entity."""
entity_description: AugustEventEntityDescription
_attr_has_entity_name = True
_last_activity: Activity | None = None
@callback
def _update_from_data(self) -> None:
"""Update from data."""
self._attr_available = retrieve_online_state(self._data, self._detail)
current_activity = self.entity_description.value_fn(self._data, self._detail)
if not current_activity or current_activity == self._last_activity:
return
self._last_activity = current_activity
event_types = self.entity_description.event_types
if TYPE_CHECKING:
assert event_types is not None
self._trigger_event(event_type=event_types[0])
self.async_write_ha_state()

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==6.4.3", "yalexs-ble==2.4.3"]
"requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"]
}

View File

@@ -58,26 +58,6 @@
"operator": {
"name": "Operator"
}
},
"event": {
"doorbell": {
"state_attributes": {
"event_type": {
"state": {
"ring": "Ring"
}
}
}
},
"motion": {
"state_attributes": {
"event_type": {
"state": {
"motion": "Motion"
}
}
}
}
}
}
}

View File

@@ -1,24 +1,12 @@
"""August util functions."""
from __future__ import annotations
from datetime import datetime, timedelta
from functools import partial
import socket
import aiohttp
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
from yalexs.doorbell import DoorbellDetail
from yalexs.lock import LockDetail
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
from . import AugustData
TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds())
@callback
def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSession:
@@ -34,60 +22,3 @@ def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSess
# we can allow IPv6 again
#
return aiohttp_client.async_create_clientsession(hass, family=socket.AF_INET)
def retrieve_time_based_activity(
activities: set[ActivityType], data: AugustData, detail: DoorbellDetail | LockDetail
) -> Activity | None:
"""Get the latest state of the sensor."""
stream = data.activity_stream
if latest := stream.get_latest_device_activity(detail.device_id, activities):
return _activity_time_based(latest)
return False
_RING_ACTIVITIES = {ActivityType.DOORBELL_DING}
def retrieve_ding_activity(
data: AugustData, detail: DoorbellDetail | LockDetail
) -> Activity | None:
"""Get the ring/ding state."""
stream = data.activity_stream
latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES)
if latest is None or (
data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED
):
return None
return _activity_time_based(latest)
retrieve_doorbell_motion_activity = partial(
retrieve_time_based_activity, {ActivityType.DOORBELL_MOTION}
)
def _activity_time_based(latest: Activity) -> Activity | None:
"""Get the latest state of the sensor."""
start = latest.activity_start_time
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
if start <= _native_datetime() <= end:
return latest
return None
def _native_datetime() -> datetime:
"""Return time in the format august uses without timezone."""
return datetime.now()
def retrieve_online_state(
data: AugustData, detail: DoorbellDetail | LockDetail
) -> bool:
"""Get the latest state of the sensor."""
# The doorbell will go into standby mode when there is no motion
# for a short while. It will wake by itself when needed so we need
# to consider is available or we will not report motion or dings
if isinstance(detail, DoorbellDetail):
return detail.is_online or detail.is_standby
return detail.bridge_is_online

View File

@@ -1,49 +0,0 @@
"""The Autarco integration."""
from __future__ import annotations
import asyncio
from autarco import Autarco
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import AutarcoDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
type AutarcoConfigEntry = ConfigEntry[list[AutarcoDataUpdateCoordinator]]
async def async_setup_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool:
"""Set up Autarco from a config entry."""
client = Autarco(
email=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
)
account_sites = await client.get_account()
coordinators: list[AutarcoDataUpdateCoordinator] = [
AutarcoDataUpdateCoordinator(hass, client, site) for site in account_sites
]
await asyncio.gather(
*[
coordinator.async_config_entry_first_refresh()
for coordinator in coordinators
]
)
entry.runtime_data = coordinators
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,57 +0,0 @@
"""Config flow for Autarco integration."""
from __future__ import annotations
from typing import Any
from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)
class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Autarco."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
client = Autarco(
email=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
session=async_get_clientsession(self.hass),
)
try:
await client.get_account()
except AutarcoAuthenticationError:
errors["base"] = "invalid_auth"
except AutarcoConnectionError:
errors["base"] = "cannot_connect"
else:
return self.async_create_entry(
title=user_input[CONF_EMAIL],
data={
CONF_EMAIL: user_input[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=DATA_SCHEMA,
)

View File

@@ -1,11 +0,0 @@
"""Constants for the Autarco integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Final
DOMAIN: Final = "autarco"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=5)

View File

@@ -1,49 +0,0 @@
"""Coordinator for Autarco integration."""
from __future__ import annotations
from typing import NamedTuple
from autarco import AccountSite, Autarco, Inverter, Solar
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
class AutarcoData(NamedTuple):
"""Class for defining data in dict."""
solar: Solar
inverters: dict[str, Inverter]
class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
"""Class to manage fetching Autarco data from the API."""
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
client: Autarco,
site: AccountSite,
) -> None:
"""Initialize global Autarco data updater."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.client = client
self.site = site
async def _async_update_data(self) -> AutarcoData:
"""Fetch data from Autarco API."""
return AutarcoData(
solar=await self.client.get_solar(self.site.public_key),
inverters=await self.client.get_inverters(self.site.public_key),
)

View File

@@ -1,43 +0,0 @@
"""Support for the Autarco diagnostics."""
from __future__ import annotations
from typing import Any
from homeassistant.core import HomeAssistant
from . import AutarcoConfigEntry, AutarcoDataUpdateCoordinator
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: AutarcoConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
autarco_data: list[AutarcoDataUpdateCoordinator] = config_entry.runtime_data
return {
"sites_data": [
{
"id": coordinator.site.site_id,
"name": coordinator.site.system_name,
"health": coordinator.site.health,
"solar": {
"power_production": coordinator.data.solar.power_production,
"energy_production_today": coordinator.data.solar.energy_production_today,
"energy_production_month": coordinator.data.solar.energy_production_month,
"energy_production_total": coordinator.data.solar.energy_production_total,
},
"inverters": [
{
"serial_number": inverter.serial_number,
"out_ac_power": inverter.out_ac_power,
"out_ac_energy_total": inverter.out_ac_energy_total,
"grid_turned_off": inverter.grid_turned_off,
"health": inverter.health,
}
for inverter in coordinator.data.inverters.values()
],
}
for coordinator in autarco_data
],
}

View File

@@ -1,9 +0,0 @@
{
"domain": "autarco",
"name": "Autarco",
"codeowners": ["@klaasnicolaas"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/autarco",
"iot_class": "cloud_polling",
"requirements": ["autarco==2.0.0"]
}

View File

@@ -1,189 +0,0 @@
"""Support for Autarco sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from autarco import Inverter, Solar
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AutarcoConfigEntry
from .const import DOMAIN
from .coordinator import AutarcoDataUpdateCoordinator
@dataclass(frozen=True, kw_only=True)
class AutarcoSolarSensorEntityDescription(SensorEntityDescription):
"""Describes an Autarco sensor entity."""
value_fn: Callable[[Solar], StateType]
SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
AutarcoSolarSensorEntityDescription(
key="power_production",
translation_key="power_production",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda solar: solar.power_production,
),
AutarcoSolarSensorEntityDescription(
key="energy_production_today",
translation_key="energy_production_today",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
value_fn=lambda solar: solar.energy_production_today,
),
AutarcoSolarSensorEntityDescription(
key="energy_production_month",
translation_key="energy_production_month",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
value_fn=lambda solar: solar.energy_production_month,
),
AutarcoSolarSensorEntityDescription(
key="energy_production_total",
translation_key="energy_production_total",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda solar: solar.energy_production_total,
),
)
@dataclass(frozen=True, kw_only=True)
class AutarcoInverterSensorEntityDescription(SensorEntityDescription):
"""Describes an Autarco inverter sensor entity."""
value_fn: Callable[[Inverter], StateType]
SENSORS_INVERTER: tuple[AutarcoInverterSensorEntityDescription, ...] = (
AutarcoInverterSensorEntityDescription(
key="out_ac_power",
translation_key="out_ac_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda inverter: inverter.out_ac_power,
),
AutarcoInverterSensorEntityDescription(
key="out_ac_energy_total",
translation_key="out_ac_energy_total",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda inverter: inverter.out_ac_energy_total,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AutarcoConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Autarco sensors based on a config entry."""
entities: list[SensorEntity] = []
for coordinator in entry.runtime_data:
entities.extend(
AutarcoSolarSensorEntity(
coordinator=coordinator,
description=description,
)
for description in SENSORS_SOLAR
)
entities.extend(
AutarcoInverterSensorEntity(
coordinator=coordinator,
description=description,
serial_number=inverter,
)
for description in SENSORS_INVERTER
for inverter in coordinator.data.inverters
)
async_add_entities(entities)
class AutarcoSolarSensorEntity(
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
"""Defines an Autarco solar sensor."""
entity_description: AutarcoSolarSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
*,
coordinator: AutarcoDataUpdateCoordinator,
description: AutarcoSolarSensorEntityDescription,
) -> None:
"""Initialize Autarco sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.site.site_id}_solar_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{coordinator.site.site_id}_solar")},
entry_type=DeviceEntryType.SERVICE,
manufacturer="Autarco",
name="Solar",
)
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data.solar)
class AutarcoInverterSensorEntity(
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
"""Defines an Autarco inverter sensor."""
entity_description: AutarcoInverterSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
*,
coordinator: AutarcoDataUpdateCoordinator,
description: AutarcoInverterSensorEntityDescription,
serial_number: str,
) -> None:
"""Initialize Autarco sensor."""
super().__init__(coordinator)
self.entity_description = description
self._serial_number = serial_number
self._attr_unique_id = f"{serial_number}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=f"Inverter {serial_number}",
manufacturer="Autarco",
model="Inverter",
serial_number=serial_number,
)
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(
self.coordinator.data.inverters[self._serial_number]
)

View File

@@ -1,46 +0,0 @@
{
"config": {
"step": {
"user": {
"description": "Connect to your Autarco account to get information about your solar panels.",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "The email address of your Autarco account.",
"password": "The password of your Autarco account."
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"sensor": {
"power_production": {
"name": "Power production"
},
"energy_production_today": {
"name": "Energy production today"
},
"energy_production_month": {
"name": "Energy production month"
},
"energy_production_total": {
"name": "Energy production total"
},
"out_ac_power": {
"name": "Power AC output"
},
"out_ac_energy_total": {
"name": "Energy AC output total"
}
}
}
}

View File

@@ -215,7 +215,7 @@ def _prepare_result_json(
data = result.copy()
if (schema := data["data_schema"]) is None:
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
data["data_schema"] = []
else:
data["data_schema"] = voluptuous_serialize.convert(schema)

View File

@@ -156,7 +156,7 @@ def _prepare_result_json(
data = result.copy()
if (schema := data["data_schema"]) is None:
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
data["data_schema"] = []
else:
data["data_schema"] = voluptuous_serialize.convert(schema)

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