forked from home-assistant/core
Compare commits
5 Commits
2024.8.2
...
compensati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02c0cfd680 | ||
|
|
22bb68d610 | ||
|
|
42fe1d6097 | ||
|
|
ebb450db48 | ||
|
|
d73e12df93 |
8
.github/workflows/builder.yml
vendored
8
.github/workflows/builder.yml
vendored
@@ -190,7 +190,7 @@ jobs:
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -329,14 +329,14 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
37
.github/workflows/ci.yaml
vendored
37
.github/workflows/ci.yaml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
tests_glob: ${{ steps.info.outputs.tests_glob }}
|
||||
tests: ${{ steps.info.outputs.tests }}
|
||||
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
@@ -218,7 +218,7 @@ jobs:
|
||||
|
||||
pre-commit:
|
||||
name: Prepare pre-commit base
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
@@ -266,7 +266,7 @@ jobs:
|
||||
|
||||
lint-ruff-format:
|
||||
name: Check ruff-format
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
- pre-commit
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
|
||||
lint-ruff:
|
||||
name: Check ruff
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
- pre-commit
|
||||
@@ -345,7 +345,7 @@ jobs:
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
lint-other:
|
||||
name: Check other linters
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
- pre-commit
|
||||
@@ -437,7 +437,7 @@ jobs:
|
||||
|
||||
base:
|
||||
name: Prepare dependencies
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: info
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -514,7 +514,7 @@ jobs:
|
||||
|
||||
hassfest:
|
||||
name: Check hassfest
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
@@ -552,7 +552,7 @@ jobs:
|
||||
|
||||
gen-requirements-all:
|
||||
name: Check all requirements
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
@@ -584,7 +584,7 @@ jobs:
|
||||
|
||||
audit-licenses:
|
||||
name: Audit licenses
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -624,7 +624,7 @@ jobs:
|
||||
|
||||
pylint:
|
||||
name: Check pylint
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
if: |
|
||||
github.event.inputs.mypy-only != 'true'
|
||||
@@ -669,7 +669,7 @@ jobs:
|
||||
|
||||
pylint-tests:
|
||||
name: Check pylint on tests
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
if: |
|
||||
(github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true')
|
||||
@@ -714,7 +714,7 @@ jobs:
|
||||
|
||||
mypy:
|
||||
name: Check mypy
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
|| github.event.inputs.mypy-only == 'true'
|
||||
@@ -775,7 +775,7 @@ jobs:
|
||||
mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
|
||||
prepare-pytest-full:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
@@ -825,7 +825,7 @@ jobs:
|
||||
overwrite: true
|
||||
|
||||
pytest-full:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
@@ -904,7 +904,6 @@ jobs:
|
||||
cov_params+=(--cov-report=xml)
|
||||
fi
|
||||
|
||||
echo "Test group ${{ matrix.group }}: $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt)"
|
||||
python3 -b -X dev -m pytest \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
@@ -937,7 +936,7 @@ jobs:
|
||||
./script/check_dirty
|
||||
|
||||
pytest-mariadb:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
mariadb:
|
||||
image: ${{ matrix.mariadb-group }}
|
||||
@@ -1190,7 +1189,7 @@ jobs:
|
||||
coverage-full:
|
||||
name: Upload test coverage to Codecov (full suite)
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
- pytest-full
|
||||
@@ -1214,7 +1213,7 @@ jobs:
|
||||
version: v0.6.0
|
||||
|
||||
pytest-partial:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
@@ -1329,7 +1328,7 @@ jobs:
|
||||
coverage-partial:
|
||||
name: Upload test coverage to Codecov (partial suite)
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
- pytest-partial
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.25.15
|
||||
uses: github/codeql-action/init@v3.25.11
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.25.15
|
||||
uses: github/codeql-action/analyze@v3.25.11
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.5
|
||||
rev: v0.5.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -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
|
||||
@@ -120,7 +119,6 @@ homeassistant.components.bond.*
|
||||
homeassistant.components.braviatv.*
|
||||
homeassistant.components.brother.*
|
||||
homeassistant.components.browser.*
|
||||
homeassistant.components.bryant_evolution.*
|
||||
homeassistant.components.bthome.*
|
||||
homeassistant.components.button.*
|
||||
homeassistant.components.calendar.*
|
||||
@@ -168,7 +166,6 @@ homeassistant.components.ecowitt.*
|
||||
homeassistant.components.efergy.*
|
||||
homeassistant.components.electrasmart.*
|
||||
homeassistant.components.electric_kiwi.*
|
||||
homeassistant.components.elevenlabs.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.elkm1.*
|
||||
homeassistant.components.emulated_hue.*
|
||||
@@ -257,7 +254,6 @@ homeassistant.components.integration.*
|
||||
homeassistant.components.intent.*
|
||||
homeassistant.components.intent_script.*
|
||||
homeassistant.components.ios.*
|
||||
homeassistant.components.iotty.*
|
||||
homeassistant.components.ipp.*
|
||||
homeassistant.components.iqvia.*
|
||||
homeassistant.components.islamic_prayer_times.*
|
||||
@@ -282,7 +278,6 @@ homeassistant.components.lidarr.*
|
||||
homeassistant.components.lifx.*
|
||||
homeassistant.components.light.*
|
||||
homeassistant.components.linear_garage_door.*
|
||||
homeassistant.components.linkplay.*
|
||||
homeassistant.components.litejet.*
|
||||
homeassistant.components.litterrobot.*
|
||||
homeassistant.components.local_ip.*
|
||||
@@ -389,7 +384,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.*
|
||||
|
||||
30
CODEOWNERS
30
CODEOWNERS
@@ -197,8 +197,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/bluemaestro/ @bdraco
|
||||
/homeassistant/components/blueprint/ @home-assistant/core
|
||||
/tests/components/blueprint/ @home-assistant/core
|
||||
/homeassistant/components/bluesound/ @thrawnarn @LouisChrist
|
||||
/tests/components/bluesound/ @thrawnarn @LouisChrist
|
||||
/homeassistant/components/bluesound/ @thrawnarn
|
||||
/homeassistant/components/bluetooth/ @bdraco
|
||||
/tests/components/bluetooth/ @bdraco
|
||||
/homeassistant/components/bluetooth_adapters/ @bdraco
|
||||
@@ -221,8 +220,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/brottsplatskartan/ @gjohansson-ST
|
||||
/homeassistant/components/brunt/ @eavanvalkenburg
|
||||
/tests/components/brunt/ @eavanvalkenburg
|
||||
/homeassistant/components/bryant_evolution/ @danielsmyers
|
||||
/tests/components/bryant_evolution/ @danielsmyers
|
||||
/homeassistant/components/bsblan/ @liudger
|
||||
/tests/components/bsblan/ @liudger
|
||||
/homeassistant/components/bt_smarthub/ @typhoon2099
|
||||
@@ -376,8 +373,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/electrasmart/ @jafar-atili
|
||||
/homeassistant/components/electric_kiwi/ @mikey0000
|
||||
/tests/components/electric_kiwi/ @mikey0000
|
||||
/homeassistant/components/elevenlabs/ @sorgfresser
|
||||
/tests/components/elevenlabs/ @sorgfresser
|
||||
/homeassistant/components/elgato/ @frenck
|
||||
/tests/components/elgato/ @frenck
|
||||
/homeassistant/components/elkm1/ @gwww @bdraco
|
||||
@@ -389,7 +384,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/elvia/ @ludeeus
|
||||
/homeassistant/components/emby/ @mezz64
|
||||
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
||||
/tests/components/emoncms/ @borpin @alexandrecuer
|
||||
/homeassistant/components/emonitor/ @bdraco
|
||||
/tests/components/emonitor/ @bdraco
|
||||
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
||||
@@ -511,7 +505,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/generic_hygrostat/ @Shulyaka
|
||||
/tests/components/generic_hygrostat/ @Shulyaka
|
||||
/homeassistant/components/geniushub/ @manzanotti
|
||||
/tests/components/geniushub/ @manzanotti
|
||||
/homeassistant/components/geo_json_events/ @exxamalte
|
||||
/tests/components/geo_json_events/ @exxamalte
|
||||
/homeassistant/components/geo_location/ @home-assistant/core
|
||||
@@ -702,8 +695,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ios/ @robbiet480
|
||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
||||
/homeassistant/components/iotty/ @pburgio
|
||||
/tests/components/iotty/ @pburgio
|
||||
/homeassistant/components/iperf3/ @rohankapoorcom
|
||||
/homeassistant/components/ipma/ @dgomes
|
||||
/tests/components/ipma/ @dgomes
|
||||
@@ -712,8 +703,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/iqvia/ @bachya
|
||||
/tests/components/iqvia/ @bachya
|
||||
/homeassistant/components/irish_rail_transport/ @ttroy50
|
||||
/homeassistant/components/iron_os/ @tr4nt0r
|
||||
/tests/components/iron_os/ @tr4nt0r
|
||||
/homeassistant/components/isal/ @bdraco
|
||||
/tests/components/isal/ @bdraco
|
||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
@@ -754,8 +743,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
|
||||
@@ -802,8 +791,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/light/ @home-assistant/core
|
||||
/homeassistant/components/linear_garage_door/ @IceBotYT
|
||||
/tests/components/linear_garage_door/ @IceBotYT
|
||||
/homeassistant/components/linkplay/ @Velleman
|
||||
/tests/components/linkplay/ @Velleman
|
||||
/homeassistant/components/linux_battery/ @fabaff
|
||||
/homeassistant/components/litejet/ @joncar
|
||||
/tests/components/litejet/ @joncar
|
||||
@@ -846,8 +833,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lyric/ @timmo001
|
||||
/homeassistant/components/madvr/ @iloveicedgreentea
|
||||
/tests/components/madvr/ @iloveicedgreentea
|
||||
/homeassistant/components/mastodon/ @fabaff @andrew-codechimp
|
||||
/tests/components/mastodon/ @fabaff @andrew-codechimp
|
||||
/homeassistant/components/mastodon/ @fabaff
|
||||
/homeassistant/components/matrix/ @PaarthShah
|
||||
/tests/components/matrix/ @PaarthShah
|
||||
/homeassistant/components/matter/ @home-assistant/matter
|
||||
@@ -898,6 +884,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
|
||||
@@ -1053,8 +1041,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/otbr/ @home-assistant/core
|
||||
/homeassistant/components/ourgroceries/ @OnFreund
|
||||
/tests/components/ourgroceries/ @OnFreund
|
||||
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
||||
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
||||
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117
|
||||
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117
|
||||
/homeassistant/components/ovo_energy/ @timmo001
|
||||
/tests/components/ovo_energy/ @timmo001
|
||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||
@@ -1446,8 +1434,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
/tests/components/tesla_fleet/ @Bre77
|
||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||
/tests/components/tesla_wall_connector/ @einarhauks
|
||||
/homeassistant/components/teslemetry/ @Bre77
|
||||
|
||||
@@ -12,7 +12,7 @@ ENV \
|
||||
ARG QEMU_CPU
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.2.27
|
||||
RUN pip3 install uv==0.2.13
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -18,12 +18,9 @@ from homeassistant.const import (
|
||||
EVENT_THEMES_UPDATED,
|
||||
)
|
||||
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.category_registry import EVENT_CATEGORY_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.floor_registry import EVENT_FLOOR_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.label_registry import EVENT_LABEL_REGISTRY_UPDATED
|
||||
from homeassistant.util.event_type import EventType
|
||||
|
||||
# These are events that do not contain any sensitive data
|
||||
@@ -44,7 +41,4 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
|
||||
EVENT_SHOPPING_LIST_UPDATED,
|
||||
EVENT_STATE_CHANGED,
|
||||
EVENT_THEMES_UPDATED,
|
||||
EVENT_LABEL_REGISTRY_UPDATED,
|
||||
EVENT_CATEGORY_REGISTRY_UPDATED,
|
||||
EVENT_FLOOR_REGISTRY_UPDATED,
|
||||
}
|
||||
|
||||
@@ -223,10 +223,8 @@ CRITICAL_INTEGRATIONS = {
|
||||
SETUP_ORDER = (
|
||||
# Load logging and http deps as soon as possible
|
||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
|
||||
# Setup frontend
|
||||
("frontend", FRONTEND_INTEGRATIONS),
|
||||
# Setup recorder
|
||||
("recorder", RECORDER_INTEGRATIONS),
|
||||
# Setup frontend and recorder
|
||||
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
("debugger", DEBUGGER_INTEGRATIONS),
|
||||
)
|
||||
@@ -908,13 +906,7 @@ async def _async_resolve_domains_to_setup(
|
||||
await asyncio.gather(*resolve_dependencies_tasks)
|
||||
|
||||
for itg in integrations_to_process:
|
||||
try:
|
||||
all_deps = itg.all_dependencies
|
||||
except RuntimeError:
|
||||
# Integration.all_dependencies raises RuntimeError if
|
||||
# dependencies could not be resolved
|
||||
continue
|
||||
for dep in all_deps:
|
||||
for dep in itg.all_dependencies:
|
||||
if dep in domains_to_setup:
|
||||
continue
|
||||
domains_to_setup.add(dep)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "logitech",
|
||||
"name": "Logitech",
|
||||
"integrations": ["harmony", "squeezebox"]
|
||||
"integrations": ["harmony", "ue_smart_radio", "squeezebox"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "tesla",
|
||||
"name": "Tesla",
|
||||
"integrations": ["powerwall", "tesla_wall_connector", "tesla_fleet"]
|
||||
"integrations": ["powerwall", "tesla_wall_connector"]
|
||||
}
|
||||
|
||||
@@ -206,8 +206,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set the HVAC Mode and State."""
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.async_turn_off()
|
||||
return
|
||||
return await self.async_turn_off()
|
||||
if hvac_mode == HVACMode.HEAT_COOL and self.preset_mode != ADVANTAGE_AIR_MYAUTO:
|
||||
raise ServiceValidationError("Heat/Cool is not supported in this mode")
|
||||
await self.async_update_ac(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aemet_opendata"],
|
||||
"requirements": ["AEMET-OpenData==0.5.4"]
|
||||
"requirements": ["AEMET-OpenData==0.5.2"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from airgradient import AirGradientClient, get_model_name
|
||||
from airgradient import AirGradientClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
@@ -35,7 +35,7 @@ class AirGradientData:
|
||||
type AirGradientConfigEntry = ConfigEntry[AirGradientData]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Airgradient from a config entry."""
|
||||
|
||||
client = AirGradientClient(
|
||||
@@ -53,8 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
|
||||
manufacturer="AirGradient",
|
||||
model=get_model_name(measurement_coordinator.data.model),
|
||||
model_id=measurement_coordinator.data.model,
|
||||
model=measurement_coordinator.data.model,
|
||||
serial_number=measurement_coordinator.data.serial_number,
|
||||
sw_version=measurement_coordinator.data.firmware_version,
|
||||
)
|
||||
@@ -69,8 +68,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AirGradientConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from airgradient import (
|
||||
AirGradientClient,
|
||||
AirGradientError,
|
||||
AirGradientParseError,
|
||||
ConfigurationControl,
|
||||
)
|
||||
from airgradient import AirGradientClient, AirGradientError, ConfigurationControl
|
||||
from awesomeversion import AwesomeVersion
|
||||
from mashumaro import MissingField
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
@@ -87,10 +83,10 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.client = AirGradientClient(user_input[CONF_HOST], session=session)
|
||||
try:
|
||||
current_measures = await self.client.get_current_measures()
|
||||
except AirGradientParseError:
|
||||
return self.async_abort(reason="invalid_version")
|
||||
except AirGradientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except MissingField:
|
||||
return self.async_abort(reason="invalid_version")
|
||||
else:
|
||||
await self.async_set_unique_id(current_measures.serial_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
||||
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
"""Class to manage fetching AirGradient data."""
|
||||
|
||||
_update_interval: timedelta
|
||||
config_entry: AirGradientConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
||||
@@ -27,7 +28,7 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
hass,
|
||||
logger=LOGGER,
|
||||
name=f"AirGradient {client.host}",
|
||||
update_interval=timedelta(minutes=1),
|
||||
update_interval=self._update_interval,
|
||||
)
|
||||
self.client = client
|
||||
assert self.config_entry.unique_id
|
||||
@@ -46,6 +47,8 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
|
||||
"""Class to manage fetching AirGradient data."""
|
||||
|
||||
_update_interval = timedelta(minutes=1)
|
||||
|
||||
async def _update_data(self) -> Measures:
|
||||
return await self.client.get_current_measures()
|
||||
|
||||
@@ -53,5 +56,7 @@ class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
|
||||
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
|
||||
"""Class to manage fetching AirGradient data."""
|
||||
|
||||
_update_interval = timedelta(minutes=5)
|
||||
|
||||
async def _update_data(self) -> Config:
|
||||
return await self.client.get_config()
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.8.0"],
|
||||
"requirements": ["airgradient==0.6.1"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -156,8 +156,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
|
||||
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.async_turn_off()
|
||||
return
|
||||
return await self.async_turn_off()
|
||||
await self._airtouch.SetCoolingModeForAc(
|
||||
self._ac_number, HA_STATE_TO_AT[hvac_mode]
|
||||
)
|
||||
@@ -263,8 +262,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
||||
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.async_turn_off()
|
||||
return
|
||||
return await self.async_turn_off()
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
await self.async_turn_on()
|
||||
self._unit = self._airtouch.GetGroups()[self._group_number]
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE]
|
||||
|
||||
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
||||
|
||||
|
||||
@@ -121,7 +121,6 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity):
|
||||
"""Base class for Airtouch5 Climate Entities."""
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_translation_key = DOMAIN
|
||||
_attr_target_temperature_step = 1
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
"""Representation of the Damper for AirTouch 5 Devices."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
|
||||
from airtouch5py.packets.zone_control import (
|
||||
ZoneControlZone,
|
||||
ZoneSettingPower,
|
||||
ZoneSettingValue,
|
||||
)
|
||||
from airtouch5py.packets.zone_name import ZoneName
|
||||
from airtouch5py.packets.zone_status import ZoneStatusZone
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import Airtouch5ConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import Airtouch5Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: Airtouch5ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Airtouch 5 Cover entities."""
|
||||
client = config_entry.runtime_data
|
||||
|
||||
# Each zone has a cover for its open percentage
|
||||
async_add_entities(
|
||||
Airtouch5ZoneOpenPercentage(
|
||||
client, zone, client.latest_zone_status[zone.zone_number].has_sensor
|
||||
)
|
||||
for zone in client.zones
|
||||
)
|
||||
|
||||
|
||||
class Airtouch5ZoneOpenPercentage(CoverEntity, Airtouch5Entity):
|
||||
"""How open the damper is in each zone."""
|
||||
|
||||
_attr_device_class = CoverDeviceClass.DAMPER
|
||||
_attr_translation_key = "damper"
|
||||
|
||||
# Zones with temperature sensors shouldn't be manually controlled.
|
||||
# We allow it but warn the user in the integration documentation.
|
||||
_attr_supported_features = (
|
||||
CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, client: Airtouch5SimpleClient, zone_name: ZoneName, has_sensor: bool
|
||||
) -> None:
|
||||
"""Initialise the Cover Entity."""
|
||||
super().__init__(client)
|
||||
self._zone_name = zone_name
|
||||
|
||||
self._attr_unique_id = f"zone_{zone_name.zone_number}_open_percentage"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"zone_{zone_name.zone_number}")},
|
||||
name=zone_name.zone_name,
|
||||
manufacturer="Polyaire",
|
||||
model="AirTouch 5",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self, data: dict[int, ZoneStatusZone]) -> None:
|
||||
if self._zone_name.zone_number not in data:
|
||||
return
|
||||
status = data[self._zone_name.zone_number]
|
||||
|
||||
self._attr_current_cover_position = int(status.open_percentage * 100)
|
||||
if status.open_percentage == 0:
|
||||
self._attr_is_closed = True
|
||||
else:
|
||||
self._attr_is_closed = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add data updated listener after this object has been initialized."""
|
||||
await super().async_added_to_hass()
|
||||
self._client.zone_status_callbacks.append(self._async_update_attrs)
|
||||
self._async_update_attrs(self._client.latest_zone_status)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove data updated listener after this object has been initialized."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self._client.zone_status_callbacks.remove(self._async_update_attrs)
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the damper."""
|
||||
await self._set_cover_position(100)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close damper."""
|
||||
await self._set_cover_position(0)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Update the damper to a specific position."""
|
||||
|
||||
if (position := kwargs.get(ATTR_POSITION)) is None:
|
||||
_LOGGER.debug("Argument `position` is missing in set_cover_position")
|
||||
return
|
||||
await self._set_cover_position(position)
|
||||
|
||||
async def _set_cover_position(self, position_percent: float) -> None:
|
||||
power: ZoneSettingPower
|
||||
|
||||
if position_percent == 0:
|
||||
power = ZoneSettingPower.SET_TO_OFF
|
||||
else:
|
||||
power = ZoneSettingPower.SET_TO_ON
|
||||
|
||||
zcz = ZoneControlZone(
|
||||
self._zone_name.zone_number,
|
||||
ZoneSettingValue.SET_OPEN_PERCENTAGE,
|
||||
power,
|
||||
position_percent / 100.0,
|
||||
)
|
||||
|
||||
packet = self._client.data_packet_factory.zone_control([zcz])
|
||||
await self._client.send_packet(packet)
|
||||
@@ -6,12 +6,15 @@ from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class Airtouch5Entity(Entity):
|
||||
"""Base class for Airtouch5 entities."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = DOMAIN
|
||||
|
||||
def __init__(self, client: Airtouch5SimpleClient) -> None:
|
||||
"""Initialise the Entity."""
|
||||
|
||||
@@ -27,11 +27,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"damper": {
|
||||
"name": "[%key:component::cover::entity_component::damper::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -114,7 +114,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
try:
|
||||
await airzone.get_version()
|
||||
except (AirzoneError, TimeoutError) as err:
|
||||
except AirzoneError as err:
|
||||
raise AbortFlow("cannot_connect") from err
|
||||
|
||||
return await self.async_step_discovered_connection()
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.8.1"]
|
||||
"requirements": ["aioairzone==0.7.7"]
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -14,7 +14,6 @@ from aioairzone_cloud.const import (
|
||||
AZD_FLOOR_DEMAND,
|
||||
AZD_PROBLEMS,
|
||||
AZD_SYSTEMS,
|
||||
AZD_THERMOSTAT_BATTERY_LOW,
|
||||
AZD_WARNINGS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
@@ -89,10 +88,6 @@ ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]
|
||||
key=AZD_AQ_ACTIVE,
|
||||
translation_key="air_quality_active",
|
||||
),
|
||||
AirzoneBinarySensorEntityDescription(
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
key=AZD_THERMOSTAT_BATTERY_LOW,
|
||||
),
|
||||
AirzoneBinarySensorEntityDescription(
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
key=AZD_FLOOR_DEMAND,
|
||||
|
||||
@@ -13,12 +13,9 @@ from aioairzone_cloud.const import (
|
||||
AZD_GROUPS,
|
||||
AZD_HOT_WATERS,
|
||||
AZD_INSTALLATIONS,
|
||||
AZD_MODEL,
|
||||
AZD_NAME,
|
||||
AZD_SYSTEM_ID,
|
||||
AZD_SYSTEMS,
|
||||
AZD_THERMOSTAT_FW,
|
||||
AZD_THERMOSTAT_MODEL,
|
||||
AZD_WEBSERVER,
|
||||
AZD_WEBSERVERS,
|
||||
AZD_ZONES,
|
||||
@@ -72,7 +69,6 @@ class AirzoneAidooEntity(AirzoneEntity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, aidoo_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=aidoo_data[AZD_MODEL],
|
||||
name=aidoo_data[AZD_NAME],
|
||||
via_device=(DOMAIN, aidoo_data[AZD_WEBSERVER]),
|
||||
)
|
||||
@@ -115,7 +111,6 @@ class AirzoneGroupEntity(AirzoneEntity):
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, group_id)},
|
||||
model="Group",
|
||||
manufacturer=MANUFACTURER,
|
||||
name=group_data[AZD_NAME],
|
||||
)
|
||||
@@ -159,7 +154,6 @@ class AirzoneHotWaterEntity(AirzoneEntity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, dhw_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model="Hot Water",
|
||||
name=dhw_data[AZD_NAME],
|
||||
via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]),
|
||||
)
|
||||
@@ -201,7 +195,6 @@ class AirzoneInstallationEntity(AirzoneEntity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, inst_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model="Installation",
|
||||
name=inst_data[AZD_NAME],
|
||||
)
|
||||
|
||||
@@ -247,11 +240,9 @@ class AirzoneSystemEntity(AirzoneEntity):
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, system_id)},
|
||||
model=system_data.get(AZD_MODEL),
|
||||
manufacturer=MANUFACTURER,
|
||||
name=system_data[AZD_NAME],
|
||||
via_device=(DOMAIN, system_data[AZD_WEBSERVER]),
|
||||
sw_version=system_data.get(AZD_FIRMWARE),
|
||||
)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
@@ -279,7 +270,6 @@ class AirzoneWebServerEntity(AirzoneEntity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, ws_id)},
|
||||
identifiers={(DOMAIN, ws_id)},
|
||||
model="WebServer",
|
||||
manufacturer=MANUFACTURER,
|
||||
name=ws_data[AZD_NAME],
|
||||
sw_version=ws_data[AZD_FIRMWARE],
|
||||
@@ -310,11 +300,9 @@ class AirzoneZoneEntity(AirzoneEntity):
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, zone_id)},
|
||||
model=zone_data.get(AZD_THERMOSTAT_MODEL),
|
||||
manufacturer=MANUFACTURER,
|
||||
name=zone_data[AZD_NAME],
|
||||
via_device=(DOMAIN, self.system_id),
|
||||
sw_version=zone_data.get(AZD_THERMOSTAT_FW),
|
||||
)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"cpu_usage": {
|
||||
"default": "mdi:cpu-32-bit"
|
||||
},
|
||||
"free_memory": {
|
||||
"default": "mdi:memory"
|
||||
},
|
||||
"thermostat_coverage": {
|
||||
"default": "mdi:signal"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.6.2"]
|
||||
"requirements": ["aioairzone-cloud==0.5.4"]
|
||||
}
|
||||
|
||||
@@ -10,12 +10,8 @@ from aioairzone_cloud.const import (
|
||||
AZD_AQ_PM_1,
|
||||
AZD_AQ_PM_2P5,
|
||||
AZD_AQ_PM_10,
|
||||
AZD_CPU_USAGE,
|
||||
AZD_HUMIDITY,
|
||||
AZD_MEMORY_FREE,
|
||||
AZD_TEMP,
|
||||
AZD_THERMOSTAT_BATTERY,
|
||||
AZD_THERMOSTAT_COVERAGE,
|
||||
AZD_WEBSERVERS,
|
||||
AZD_WIFI_RSSI,
|
||||
AZD_ZONES,
|
||||
@@ -32,7 +28,6 @@ from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfInformation,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -57,22 +52,6 @@ AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
)
|
||||
|
||||
WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_CPU_USAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="cpu_usage",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_MEMORY_FREE,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="free_memory",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
@@ -119,20 +98,6 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
key=AZD_THERMOSTAT_BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_THERMOSTAT_COVERAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="thermostat_coverage",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -37,17 +37,6 @@
|
||||
"auto": "Auto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"cpu_usage": {
|
||||
"name": "CPU usage"
|
||||
},
|
||||
"free_memory": {
|
||||
"name": "Free memory"
|
||||
},
|
||||
"thermostat_coverage": {
|
||||
"name": "Signal percentage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1513,7 +1513,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))
|
||||
|
||||
@@ -5,6 +5,5 @@
|
||||
"codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"],
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push"
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from amcrest import AmcrestError
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
@@ -245,9 +244,7 @@ class AmcrestCam(Camera):
|
||||
websession = async_get_clientsession(self.hass)
|
||||
streaming_url = self._api.mjpeg_url(typeno=self._resolution)
|
||||
stream_coro = websession.get(
|
||||
streaming_url,
|
||||
auth=self._token,
|
||||
timeout=aiohttp.ClientTimeout(total=CAMERA_WEB_SESSION_TIMEOUT),
|
||||
streaming_url, auth=self._token, timeout=CAMERA_WEB_SESSION_TIMEOUT
|
||||
)
|
||||
|
||||
return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -4,6 +4,3 @@ from typing import Final
|
||||
|
||||
DOMAIN: Final = "apcupsd"
|
||||
CONNECTION_TIMEOUT: int = 10
|
||||
|
||||
# Field name of last self test retrieved from apcupsd.
|
||||
LASTSTEST: Final = "laststest"
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfApparentPower,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
@@ -26,7 +25,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, LASTSTEST
|
||||
from .const import DOMAIN
|
||||
from .coordinator import APCUPSdCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -157,8 +156,8 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
LASTSTEST: SensorEntityDescription(
|
||||
key=LASTSTEST,
|
||||
"laststest": SensorEntityDescription(
|
||||
key="laststest",
|
||||
translation_key="last_self_test",
|
||||
),
|
||||
"lastxfer": SensorEntityDescription(
|
||||
@@ -418,12 +417,7 @@ async def async_setup_entry(
|
||||
available_resources: set[str] = {k.lower() for k, _ in coordinator.data.items()}
|
||||
|
||||
entities = []
|
||||
|
||||
# "laststest" is a special sensor that only appears when the APC UPS daemon has done a
|
||||
# periodical (or manual) self test since last daemon restart. It might not be available
|
||||
# when we set up the integration, and we do not know if it would ever be available. Here we
|
||||
# add it anyway and mark it as unknown initially.
|
||||
for resource in available_resources | {LASTSTEST}:
|
||||
for resource in available_resources:
|
||||
if resource not in SENSORS:
|
||||
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
|
||||
continue
|
||||
@@ -479,14 +473,6 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update sensor attributes based on coordinator data."""
|
||||
key = self.entity_description.key.upper()
|
||||
# For most sensors the key will always be available for each refresh. However, some sensors
|
||||
# (e.g., "laststest") will only appear after certain event occurs (e.g., a self test is
|
||||
# performed) and may disappear again after certain event. So we mark the state as "unknown"
|
||||
# when it becomes unknown after such events.
|
||||
if key not in self.coordinator.data:
|
||||
self._attr_native_value = STATE_UNKNOWN
|
||||
return
|
||||
|
||||
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
|
||||
if not self.native_unit_of_measurement:
|
||||
self._attr_native_unit_of_measurement = inferred_unit
|
||||
|
||||
@@ -45,7 +45,7 @@ from homeassistant.exceptions import (
|
||||
TemplateError,
|
||||
Unauthorized,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, recorder, template
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.json import json_dumps, json_fragment
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -119,10 +119,7 @@ class APICoreStateView(HomeAssistantView):
|
||||
to check if Home Assistant is running.
|
||||
"""
|
||||
hass = request.app[KEY_HASS]
|
||||
migration = recorder.async_migration_in_progress(hass)
|
||||
live = recorder.async_migration_is_live(hass)
|
||||
recorder_state = {"migration_in_progress": migration, "migration_is_live": live}
|
||||
return self.json({"state": hass.state.value, "recorder_state": recorder_state})
|
||||
return self.json({"state": hass.state.value})
|
||||
|
||||
|
||||
class APIEventStream(HomeAssistantView):
|
||||
@@ -390,27 +387,6 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
)
|
||||
|
||||
context = self.context(request)
|
||||
if not hass.services.has_service(domain, service):
|
||||
raise HTTPBadRequest from ServiceNotFound(domain, service)
|
||||
|
||||
if response_requested := "return_response" in request.query:
|
||||
if (
|
||||
hass.services.supports_response(domain, service)
|
||||
is ha.SupportsResponse.NONE
|
||||
):
|
||||
return self.json_message(
|
||||
"Service does not support responses. Remove return_response from request.",
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
elif (
|
||||
hass.services.supports_response(domain, service) is ha.SupportsResponse.ONLY
|
||||
):
|
||||
return self.json_message(
|
||||
"Service call requires responses but caller did not ask for responses. "
|
||||
"Add ?return_response to query parameters.",
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
changed_states: list[json_fragment] = []
|
||||
|
||||
@ha.callback
|
||||
@@ -427,14 +403,13 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
|
||||
try:
|
||||
# shield the service call from cancellation on connection drop
|
||||
response = await shield(
|
||||
await shield(
|
||||
hass.services.async_call(
|
||||
domain,
|
||||
service,
|
||||
data, # type: ignore[arg-type]
|
||||
blocking=True,
|
||||
context=context,
|
||||
return_response=response_requested,
|
||||
)
|
||||
)
|
||||
except (vol.Invalid, ServiceNotFound) as ex:
|
||||
@@ -442,11 +417,6 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
finally:
|
||||
cancel_listen()
|
||||
|
||||
if response_requested:
|
||||
return self.json(
|
||||
{"changed_states": changed_states, "service_response": response}
|
||||
)
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ AUTH_EXCEPTIONS = (
|
||||
exceptions.NoCredentialsError,
|
||||
)
|
||||
CONNECTION_TIMEOUT_EXCEPTIONS = (
|
||||
OSError,
|
||||
asyncio.CancelledError,
|
||||
TimeoutError,
|
||||
exceptions.ConnectionLostError,
|
||||
|
||||
@@ -7,13 +7,12 @@ from dataclasses import dataclass
|
||||
from APsystemsEZ1 import APsystemsEZ1M
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
|
||||
from homeassistant.const import CONF_IP_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DEFAULT_PORT
|
||||
from .coordinator import ApSystemsDataCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -29,11 +28,7 @@ type ApSystemsConfigEntry = ConfigEntry[ApSystemsData]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
api = APsystemsEZ1M(
|
||||
ip_address=entry.data[CONF_IP_ADDRESS],
|
||||
port=entry.data.get(CONF_PORT, DEFAULT_PORT),
|
||||
timeout=8,
|
||||
)
|
||||
api = APsystemsEZ1M(ip_address=entry.data[CONF_IP_ADDRESS], timeout=8)
|
||||
coordinator = ApSystemsDataCoordinator(hass, api)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
assert entry.unique_id
|
||||
|
||||
@@ -7,16 +7,14 @@ from APsystemsEZ1 import APsystemsEZ1M
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT
|
||||
from homeassistant.const import CONF_IP_ADDRESS
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
vol.Required(CONF_IP_ADDRESS): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -34,11 +32,7 @@ class APsystemsLocalAPIFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass, False)
|
||||
api = APsystemsEZ1M(
|
||||
ip_address=user_input[CONF_IP_ADDRESS],
|
||||
port=user_input.get(CONF_PORT, DEFAULT_PORT),
|
||||
session=session,
|
||||
)
|
||||
api = APsystemsEZ1M(user_input[CONF_IP_ADDRESS], session=session)
|
||||
try:
|
||||
device_info = await api.get_device_info()
|
||||
except (TimeoutError, ClientConnectionError):
|
||||
|
||||
@@ -4,4 +4,3 @@ from logging import Logger, getLogger
|
||||
|
||||
LOGGER: Logger = getLogger(__package__)
|
||||
DOMAIN = "apsystems"
|
||||
DEFAULT_PORT = 8050
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"port": "The integration will default to 8050, if not set, which should be suitable for most installs"
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -20,43 +16,18 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"total_power": {
|
||||
"name": "Total power"
|
||||
},
|
||||
"total_power_p1": {
|
||||
"name": "Power of P1"
|
||||
},
|
||||
"total_power_p2": {
|
||||
"name": "Power of P2"
|
||||
},
|
||||
"lifetime_production": {
|
||||
"name": "Total lifetime production"
|
||||
},
|
||||
"lifetime_production_p1": {
|
||||
"name": "Lifetime production of P1"
|
||||
},
|
||||
"lifetime_production_p2": {
|
||||
"name": "Lifetime production of P2"
|
||||
},
|
||||
"today_production": {
|
||||
"name": "Production of today"
|
||||
},
|
||||
"today_production_p1": {
|
||||
"name": "Production of today from P1"
|
||||
},
|
||||
"today_production_p2": {
|
||||
"name": "Production of today from P2"
|
||||
}
|
||||
"total_power": { "name": "Total power" },
|
||||
"total_power_p1": { "name": "Power of P1" },
|
||||
"total_power_p2": { "name": "Power of P2" },
|
||||
"lifetime_production": { "name": "Total lifetime production" },
|
||||
"lifetime_production_p1": { "name": "Lifetime production of P1" },
|
||||
"lifetime_production_p2": { "name": "Lifetime production of P2" },
|
||||
"today_production": { "name": "Production of today" },
|
||||
"today_production_p1": { "name": "Production of today from P1" },
|
||||
"today_production_p2": { "name": "Production of today from P2" }
|
||||
},
|
||||
"number": {
|
||||
"max_output": {
|
||||
"name": "Max output"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"inverter_status": {
|
||||
"name": "Inverter status"
|
||||
}
|
||||
"max_output": { "name": "Max output" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
"""The power switch which can be toggled via the APsystems local API integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectionError
|
||||
from APsystemsEZ1 import Status
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ApSystemsConfigEntry, ApSystemsData
|
||||
from .entity import ApSystemsEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ApSystemsConfigEntry,
|
||||
add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
|
||||
add_entities([ApSystemsInverterSwitch(config_entry.runtime_data)], True)
|
||||
|
||||
|
||||
class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
|
||||
"""The switch class for APSystems switches."""
|
||||
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
_attr_translation_key = "inverter_status"
|
||||
|
||||
def __init__(self, data: ApSystemsData) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(data)
|
||||
self._api = data.coordinator.api
|
||||
self._attr_unique_id = f"{data.device_id}_inverter_status"
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update switch status and availability."""
|
||||
try:
|
||||
status = await self._api.get_device_power_status()
|
||||
except (TimeoutError, ClientConnectionError):
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._attr_is_on = status == Status.normal
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self._api.set_device_power_status(0)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self._api.set_device_power_status(1)
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaquacell"],
|
||||
"requirements": ["aioaquacell==0.2.0"]
|
||||
"requirements": ["aioaquacell==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -19,10 +19,7 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):
|
||||
super().__init__(coordinator)
|
||||
self._unit = unit
|
||||
|
||||
if self._unit.type == "Remote":
|
||||
self._device_model = "ASIN Pool"
|
||||
else:
|
||||
self._device_model = f"ASIN AQUA {self._unit.type}"
|
||||
self._device_model = f"ASIN AQUA {self._unit.type}"
|
||||
self._device_name = self._unit.name if self._unit.name else self._device_model
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaseko"],
|
||||
"requirements": ["aioaseko==0.2.0"]
|
||||
"requirements": ["aioaseko==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -16,10 +16,6 @@ from .const import (
|
||||
DATA_LAST_WAKE_UP,
|
||||
DOMAIN,
|
||||
EVENT_RECORDING,
|
||||
SAMPLE_CHANNELS,
|
||||
SAMPLE_RATE,
|
||||
SAMPLE_WIDTH,
|
||||
SAMPLES_PER_CHUNK,
|
||||
)
|
||||
from .error import PipelineNotFound
|
||||
from .pipeline import (
|
||||
@@ -57,10 +53,6 @@ __all__ = (
|
||||
"PipelineNotFound",
|
||||
"WakeWordSettings",
|
||||
"EVENT_RECORDING",
|
||||
"SAMPLES_PER_CHUNK",
|
||||
"SAMPLE_RATE",
|
||||
"SAMPLE_WIDTH",
|
||||
"SAMPLE_CHANNELS",
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
"""Audio enhancement for Assist."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pymicro_vad import MicroVad
|
||||
|
||||
from .const import BYTES_PER_CHUNK
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EnhancedAudioChunk:
|
||||
"""Enhanced audio chunk and metadata."""
|
||||
|
||||
audio: bytes
|
||||
"""Raw PCM audio @ 16Khz with 16-bit mono samples"""
|
||||
|
||||
timestamp_ms: int
|
||||
"""Timestamp relative to start of audio stream (milliseconds)"""
|
||||
|
||||
is_speech: bool | None
|
||||
"""True if audio chunk likely contains speech, False if not, None if unknown"""
|
||||
|
||||
|
||||
class AudioEnhancer(ABC):
|
||||
"""Base class for audio enhancement."""
|
||||
|
||||
def __init__(
|
||||
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
||||
) -> None:
|
||||
"""Initialize audio enhancer."""
|
||||
self.auto_gain = auto_gain
|
||||
self.noise_suppression = noise_suppression
|
||||
self.is_vad_enabled = is_vad_enabled
|
||||
|
||||
@abstractmethod
|
||||
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
||||
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||
|
||||
|
||||
class MicroVadEnhancer(AudioEnhancer):
|
||||
"""Audio enhancer that just runs microVAD."""
|
||||
|
||||
def __init__(
|
||||
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
||||
) -> None:
|
||||
"""Initialize audio enhancer."""
|
||||
super().__init__(auto_gain, noise_suppression, is_vad_enabled)
|
||||
|
||||
self.vad: MicroVad | None = None
|
||||
self.threshold = 0.5
|
||||
|
||||
if self.is_vad_enabled:
|
||||
self.vad = MicroVad()
|
||||
_LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold)
|
||||
|
||||
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
||||
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||
is_speech: bool | None = None
|
||||
|
||||
if self.vad is not None:
|
||||
# Run VAD
|
||||
assert len(audio) == BYTES_PER_CHUNK
|
||||
speech_prob = self.vad.Process10ms(audio)
|
||||
is_speech = speech_prob > self.threshold
|
||||
|
||||
return EnhancedAudioChunk(
|
||||
audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech
|
||||
)
|
||||
@@ -15,10 +15,3 @@ DATA_LAST_WAKE_UP = f"{DOMAIN}.last_wake_up"
|
||||
WAKE_WORD_COOLDOWN = 2 # seconds
|
||||
|
||||
EVENT_RECORDING = f"{DOMAIN}_recording"
|
||||
|
||||
SAMPLE_RATE = 16000 # hertz
|
||||
SAMPLE_WIDTH = 2 # bytes
|
||||
SAMPLE_CHANNELS = 1 # mono
|
||||
MS_PER_CHUNK = 10
|
||||
SAMPLES_PER_CHUNK = SAMPLE_RATE // (1000 // MS_PER_CHUNK) # 10 ms @ 16Khz
|
||||
BYTES_PER_CHUNK = SAMPLES_PER_CHUNK * SAMPLE_WIDTH * SAMPLE_CHANNELS # 16-bit
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"codeowners": ["@balloob", "@synesthesiam"],
|
||||
"dependencies": ["conversation", "stt", "tts", "wake_word"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_pipeline",
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["pymicro-vad==1.0.1"]
|
||||
"requirements": ["webrtc-noise-gain==1.2.3"]
|
||||
}
|
||||
|
||||
@@ -13,11 +13,14 @@ from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
from threading import Thread
|
||||
import time
|
||||
from typing import Any, Literal, cast
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, cast
|
||||
import wave
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from webrtc_noise_gain import AudioProcessor
|
||||
|
||||
from homeassistant.components import (
|
||||
conversation,
|
||||
media_source,
|
||||
@@ -49,19 +52,12 @@ from homeassistant.util import (
|
||||
)
|
||||
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
||||
|
||||
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadEnhancer
|
||||
from .const import (
|
||||
BYTES_PER_CHUNK,
|
||||
CONF_DEBUG_RECORDING_DIR,
|
||||
DATA_CONFIG,
|
||||
DATA_LAST_WAKE_UP,
|
||||
DATA_MIGRATIONS,
|
||||
DOMAIN,
|
||||
MS_PER_CHUNK,
|
||||
SAMPLE_CHANNELS,
|
||||
SAMPLE_RATE,
|
||||
SAMPLE_WIDTH,
|
||||
SAMPLES_PER_CHUNK,
|
||||
WAKE_WORD_COOLDOWN,
|
||||
)
|
||||
from .error import (
|
||||
@@ -115,6 +111,9 @@ STORED_PIPELINE_RUNS = 10
|
||||
|
||||
SAVE_DELAY = 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(
|
||||
@@ -504,8 +503,8 @@ class AudioSettings:
|
||||
is_vad_enabled: bool = True
|
||||
"""True if VAD is used to determine the end of the voice command."""
|
||||
|
||||
silence_seconds: float = 0.5
|
||||
"""Seconds of silence after voice command has ended."""
|
||||
is_chunking_enabled: bool = True
|
||||
"""True if audio is automatically split into 10 ms chunks (required for VAD, etc.)"""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Verify settings post-initialization."""
|
||||
@@ -515,6 +514,9 @@ class AudioSettings:
|
||||
if (self.auto_gain_dbfs < 0) or (self.auto_gain_dbfs > 31):
|
||||
raise ValueError("auto_gain_dbfs must be in [0, 31]")
|
||||
|
||||
if self.needs_processor and (not self.is_chunking_enabled):
|
||||
raise ValueError("Chunking must be enabled for audio processing")
|
||||
|
||||
@property
|
||||
def needs_processor(self) -> bool:
|
||||
"""True if an audio processor is needed."""
|
||||
@@ -525,6 +527,20 @@ class AudioSettings:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProcessedAudioChunk:
|
||||
"""Processed audio chunk and metadata."""
|
||||
|
||||
audio: bytes
|
||||
"""Raw PCM audio @ 16Khz with 16-bit mono samples"""
|
||||
|
||||
timestamp_ms: int
|
||||
"""Timestamp relative to start of audio stream (milliseconds)"""
|
||||
|
||||
is_speech: bool | None
|
||||
"""True if audio chunk likely contains speech, False if not, None if unknown"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineRun:
|
||||
"""Running context for a pipeline."""
|
||||
@@ -557,12 +573,10 @@ class PipelineRun:
|
||||
debug_recording_queue: Queue[str | bytes | None] | None = None
|
||||
"""Queue to communicate with debug recording thread"""
|
||||
|
||||
audio_enhancer: AudioEnhancer | None = None
|
||||
audio_processor: AudioProcessor | None = None
|
||||
"""VAD/noise suppression/auto gain"""
|
||||
|
||||
audio_chunking_buffer: AudioBuffer = field(
|
||||
default_factory=lambda: AudioBuffer(BYTES_PER_CHUNK)
|
||||
)
|
||||
audio_processor_buffer: AudioBuffer = field(init=False, repr=False)
|
||||
"""Buffer used when splitting audio into chunks for audio processing"""
|
||||
|
||||
_device_id: str | None = None
|
||||
@@ -587,12 +601,17 @@ class PipelineRun:
|
||||
pipeline_data.pipeline_runs.add_run(self)
|
||||
|
||||
# Initialize with audio settings
|
||||
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
|
||||
# Default audio enhancer
|
||||
self.audio_enhancer = MicroVadEnhancer(
|
||||
self.audio_processor_buffer = AudioBuffer(AUDIO_PROCESSOR_BYTES)
|
||||
if self.audio_settings.needs_processor:
|
||||
# Delay import of webrtc so HA start up is not crashing
|
||||
# on older architectures (armhf).
|
||||
#
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from webrtc_noise_gain import AudioProcessor
|
||||
|
||||
self.audio_processor = AudioProcessor(
|
||||
self.audio_settings.auto_gain_dbfs,
|
||||
self.audio_settings.noise_suppression_level,
|
||||
self.audio_settings.is_vad_enabled,
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
@@ -669,8 +688,8 @@ class PipelineRun:
|
||||
|
||||
async def wake_word_detection(
|
||||
self,
|
||||
stream: AsyncIterable[EnhancedAudioChunk],
|
||||
audio_chunks_for_stt: list[EnhancedAudioChunk],
|
||||
stream: AsyncIterable[ProcessedAudioChunk],
|
||||
audio_chunks_for_stt: list[ProcessedAudioChunk],
|
||||
) -> wake_word.DetectionResult | None:
|
||||
"""Run wake-word-detection portion of pipeline. Returns detection result."""
|
||||
metadata_dict = asdict(
|
||||
@@ -713,11 +732,10 @@ class PipelineRun:
|
||||
# Audio chunk buffer. This audio will be forwarded to speech-to-text
|
||||
# after wake-word-detection.
|
||||
num_audio_chunks_to_buffer = int(
|
||||
(wake_word_settings.audio_seconds_to_buffer * SAMPLE_RATE)
|
||||
/ SAMPLES_PER_CHUNK
|
||||
(wake_word_settings.audio_seconds_to_buffer * 16000)
|
||||
/ AUDIO_PROCESSOR_SAMPLES
|
||||
)
|
||||
|
||||
stt_audio_buffer: deque[EnhancedAudioChunk] | None = None
|
||||
stt_audio_buffer: deque[ProcessedAudioChunk] | None = None
|
||||
if num_audio_chunks_to_buffer > 0:
|
||||
stt_audio_buffer = deque(maxlen=num_audio_chunks_to_buffer)
|
||||
|
||||
@@ -779,7 +797,7 @@ class PipelineRun:
|
||||
# speech-to-text so the user does not have to pause before
|
||||
# speaking the voice command.
|
||||
audio_chunks_for_stt.extend(
|
||||
EnhancedAudioChunk(
|
||||
ProcessedAudioChunk(
|
||||
audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False
|
||||
)
|
||||
for chunk_ts in result.queued_audio
|
||||
@@ -801,17 +819,18 @@ class PipelineRun:
|
||||
|
||||
async def _wake_word_audio_stream(
|
||||
self,
|
||||
audio_stream: AsyncIterable[EnhancedAudioChunk],
|
||||
stt_audio_buffer: deque[EnhancedAudioChunk] | None,
|
||||
audio_stream: AsyncIterable[ProcessedAudioChunk],
|
||||
stt_audio_buffer: deque[ProcessedAudioChunk] | None,
|
||||
wake_word_vad: VoiceActivityTimeout | None,
|
||||
sample_rate: int = SAMPLE_RATE,
|
||||
sample_width: int = SAMPLE_WIDTH,
|
||||
sample_rate: int = 16000,
|
||||
sample_width: int = 2,
|
||||
) -> AsyncIterable[tuple[bytes, int]]:
|
||||
"""Yield audio chunks with timestamps (milliseconds since start of stream).
|
||||
|
||||
Adds audio to a ring buffer that will be forwarded to speech-to-text after
|
||||
detection. Times out if VAD detects enough silence.
|
||||
"""
|
||||
chunk_seconds = AUDIO_PROCESSOR_SAMPLES / sample_rate
|
||||
async for chunk in audio_stream:
|
||||
if self.abort_wake_word_detection:
|
||||
raise WakeWordDetectionAborted
|
||||
@@ -826,7 +845,6 @@ class PipelineRun:
|
||||
stt_audio_buffer.append(chunk)
|
||||
|
||||
if wake_word_vad is not None:
|
||||
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
|
||||
if not wake_word_vad.process(chunk_seconds, chunk.is_speech):
|
||||
raise WakeWordTimeoutError(
|
||||
code="wake-word-timeout", message="Wake word was not detected"
|
||||
@@ -863,7 +881,7 @@ class PipelineRun:
|
||||
async def speech_to_text(
|
||||
self,
|
||||
metadata: stt.SpeechMetadata,
|
||||
stream: AsyncIterable[EnhancedAudioChunk],
|
||||
stream: AsyncIterable[ProcessedAudioChunk],
|
||||
) -> str:
|
||||
"""Run speech-to-text portion of pipeline. Returns the spoken text."""
|
||||
# Create a background task to prepare the conversation agent
|
||||
@@ -898,9 +916,7 @@ class PipelineRun:
|
||||
# Transcribe audio stream
|
||||
stt_vad: VoiceCommandSegmenter | None = None
|
||||
if self.audio_settings.is_vad_enabled:
|
||||
stt_vad = VoiceCommandSegmenter(
|
||||
silence_seconds=self.audio_settings.silence_seconds
|
||||
)
|
||||
stt_vad = VoiceCommandSegmenter()
|
||||
|
||||
result = await self.stt_provider.async_process_audio_stream(
|
||||
metadata,
|
||||
@@ -941,18 +957,18 @@ class PipelineRun:
|
||||
|
||||
async def _speech_to_text_stream(
|
||||
self,
|
||||
audio_stream: AsyncIterable[EnhancedAudioChunk],
|
||||
audio_stream: AsyncIterable[ProcessedAudioChunk],
|
||||
stt_vad: VoiceCommandSegmenter | None,
|
||||
sample_rate: int = SAMPLE_RATE,
|
||||
sample_width: int = SAMPLE_WIDTH,
|
||||
sample_rate: int = 16000,
|
||||
sample_width: int = 2,
|
||||
) -> AsyncGenerator[bytes]:
|
||||
"""Yield audio chunks until VAD detects silence or speech-to-text completes."""
|
||||
chunk_seconds = AUDIO_PROCESSOR_SAMPLES / sample_rate
|
||||
sent_vad_start = False
|
||||
async for chunk in audio_stream:
|
||||
self._capture_chunk(chunk.audio)
|
||||
|
||||
if stt_vad is not None:
|
||||
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
|
||||
if not stt_vad.process(chunk_seconds, chunk.is_speech):
|
||||
# Silence detected at the end of voice command
|
||||
self.process_event(
|
||||
@@ -1056,8 +1072,8 @@ class PipelineRun:
|
||||
tts_options[tts.ATTR_PREFERRED_FORMAT] = self.tts_audio_output
|
||||
if self.tts_audio_output == "wav":
|
||||
# 16 Khz, 16-bit mono
|
||||
tts_options[tts.ATTR_PREFERRED_SAMPLE_RATE] = SAMPLE_RATE
|
||||
tts_options[tts.ATTR_PREFERRED_SAMPLE_CHANNELS] = SAMPLE_CHANNELS
|
||||
tts_options[tts.ATTR_PREFERRED_SAMPLE_RATE] = 16000
|
||||
tts_options[tts.ATTR_PREFERRED_SAMPLE_CHANNELS] = 1
|
||||
|
||||
try:
|
||||
options_supported = await tts.async_support_options(
|
||||
@@ -1202,31 +1218,53 @@ class PipelineRun:
|
||||
self.debug_recording_thread = None
|
||||
|
||||
async def process_volume_only(
|
||||
self, audio_stream: AsyncIterable[bytes]
|
||||
) -> AsyncGenerator[EnhancedAudioChunk]:
|
||||
self,
|
||||
audio_stream: AsyncIterable[bytes],
|
||||
sample_rate: int = 16000,
|
||||
sample_width: int = 2,
|
||||
) -> AsyncGenerator[ProcessedAudioChunk]:
|
||||
"""Apply volume transformation only (no VAD/audio enhancements) with optional chunking."""
|
||||
ms_per_sample = sample_rate // 1000
|
||||
ms_per_chunk = (AUDIO_PROCESSOR_SAMPLES // sample_width) // ms_per_sample
|
||||
timestamp_ms = 0
|
||||
|
||||
async for chunk in audio_stream:
|
||||
if self.audio_settings.volume_multiplier != 1.0:
|
||||
chunk = _multiply_volume(chunk, self.audio_settings.volume_multiplier)
|
||||
|
||||
for sub_chunk in chunk_samples(
|
||||
chunk, BYTES_PER_CHUNK, self.audio_chunking_buffer
|
||||
):
|
||||
yield EnhancedAudioChunk(
|
||||
audio=sub_chunk,
|
||||
if self.audio_settings.is_chunking_enabled:
|
||||
# 10 ms chunking
|
||||
for chunk_10ms in chunk_samples(
|
||||
chunk, AUDIO_PROCESSOR_BYTES, self.audio_processor_buffer
|
||||
):
|
||||
yield ProcessedAudioChunk(
|
||||
audio=chunk_10ms,
|
||||
timestamp_ms=timestamp_ms,
|
||||
is_speech=None, # no VAD
|
||||
)
|
||||
timestamp_ms += ms_per_chunk
|
||||
else:
|
||||
# No chunking
|
||||
yield ProcessedAudioChunk(
|
||||
audio=chunk,
|
||||
timestamp_ms=timestamp_ms,
|
||||
is_speech=None, # no VAD
|
||||
)
|
||||
timestamp_ms += MS_PER_CHUNK
|
||||
timestamp_ms += (len(chunk) // sample_width) // ms_per_sample
|
||||
|
||||
async def process_enhance_audio(
|
||||
self, audio_stream: AsyncIterable[bytes]
|
||||
) -> AsyncGenerator[EnhancedAudioChunk]:
|
||||
"""Split audio into chunks and apply VAD/noise suppression/auto gain/volume transformation."""
|
||||
assert self.audio_enhancer is not None
|
||||
self,
|
||||
audio_stream: AsyncIterable[bytes],
|
||||
sample_rate: int = 16000,
|
||||
sample_width: int = 2,
|
||||
) -> AsyncGenerator[ProcessedAudioChunk]:
|
||||
"""Split audio into 10 ms chunks and apply VAD/noise suppression/auto gain/volume transformation."""
|
||||
assert self.audio_processor is not None
|
||||
|
||||
ms_per_sample = sample_rate // 1000
|
||||
ms_per_chunk = (AUDIO_PROCESSOR_SAMPLES // sample_width) // ms_per_sample
|
||||
timestamp_ms = 0
|
||||
|
||||
async for dirty_samples in audio_stream:
|
||||
if self.audio_settings.volume_multiplier != 1.0:
|
||||
# Static gain
|
||||
@@ -1234,12 +1272,18 @@ class PipelineRun:
|
||||
dirty_samples, self.audio_settings.volume_multiplier
|
||||
)
|
||||
|
||||
# Split into chunks for audio enhancements/VAD
|
||||
for dirty_chunk in chunk_samples(
|
||||
dirty_samples, BYTES_PER_CHUNK, self.audio_chunking_buffer
|
||||
# Split into 10ms chunks for audio enhancements/VAD
|
||||
for dirty_10ms_chunk in chunk_samples(
|
||||
dirty_samples, AUDIO_PROCESSOR_BYTES, self.audio_processor_buffer
|
||||
):
|
||||
yield self.audio_enhancer.enhance_chunk(dirty_chunk, timestamp_ms)
|
||||
timestamp_ms += MS_PER_CHUNK
|
||||
ap_result = self.audio_processor.Process10ms(dirty_10ms_chunk)
|
||||
yield ProcessedAudioChunk(
|
||||
audio=ap_result.audio,
|
||||
timestamp_ms=timestamp_ms,
|
||||
is_speech=ap_result.is_speech,
|
||||
)
|
||||
|
||||
timestamp_ms += ms_per_chunk
|
||||
|
||||
|
||||
def _multiply_volume(chunk: bytes, volume_multiplier: float) -> bytes:
|
||||
@@ -1279,9 +1323,9 @@ def _pipeline_debug_recording_thread_proc(
|
||||
|
||||
wav_path = run_recording_dir / f"{message}.wav"
|
||||
wav_writer = wave.open(str(wav_path), "wb")
|
||||
wav_writer.setframerate(SAMPLE_RATE)
|
||||
wav_writer.setsampwidth(SAMPLE_WIDTH)
|
||||
wav_writer.setnchannels(SAMPLE_CHANNELS)
|
||||
wav_writer.setframerate(16000)
|
||||
wav_writer.setsampwidth(2)
|
||||
wav_writer.setnchannels(1)
|
||||
elif isinstance(message, bytes):
|
||||
# Chunk of 16-bit mono audio at 16Khz
|
||||
if wav_writer is not None:
|
||||
@@ -1324,8 +1368,8 @@ class PipelineInput:
|
||||
"""Run pipeline."""
|
||||
self.run.start(device_id=self.device_id)
|
||||
current_stage: PipelineStage | None = self.run.start_stage
|
||||
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
||||
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
||||
stt_audio_buffer: list[ProcessedAudioChunk] = []
|
||||
stt_processed_stream: AsyncIterable[ProcessedAudioChunk] | None = None
|
||||
|
||||
if self.stt_stream is not None:
|
||||
if self.run.audio_settings.needs_processor:
|
||||
@@ -1379,7 +1423,7 @@ class PipelineInput:
|
||||
# Send audio in the buffer first to speech-to-text, then move on to stt_stream.
|
||||
# This is basically an async itertools.chain.
|
||||
async def buffer_then_audio_stream() -> (
|
||||
AsyncGenerator[EnhancedAudioChunk]
|
||||
AsyncGenerator[ProcessedAudioChunk]
|
||||
):
|
||||
# Buffered audio
|
||||
for chunk in stt_audio_buffer:
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from typing import Final, cast
|
||||
|
||||
_SAMPLE_RATE: Final = 16000 # Hz
|
||||
_SAMPLE_WIDTH: Final = 2 # bytes
|
||||
@@ -34,6 +32,44 @@ class VadSensitivity(StrEnum):
|
||||
return 1.0
|
||||
|
||||
|
||||
class VoiceActivityDetector(ABC):
|
||||
"""Base class for voice activity detectors (VAD)."""
|
||||
|
||||
@abstractmethod
|
||||
def is_speech(self, chunk: bytes) -> bool:
|
||||
"""Return True if audio chunk contains speech."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def samples_per_chunk(self) -> int | None:
|
||||
"""Return number of samples per chunk or None if chunking is not required."""
|
||||
|
||||
|
||||
class WebRtcVad(VoiceActivityDetector):
|
||||
"""Voice activity detector based on webrtc."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize webrtcvad."""
|
||||
# Delay import of webrtc so HA start up is not crashing
|
||||
# on older architectures (armhf).
|
||||
#
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from webrtc_noise_gain import AudioProcessor
|
||||
|
||||
# Just VAD: no noise suppression or auto gain
|
||||
self._audio_processor = AudioProcessor(0, 0)
|
||||
|
||||
def is_speech(self, chunk: bytes) -> bool:
|
||||
"""Return True if audio chunk contains speech."""
|
||||
result = self._audio_processor.Process10ms(chunk)
|
||||
return cast(bool, result.is_speech)
|
||||
|
||||
@property
|
||||
def samples_per_chunk(self) -> int | None:
|
||||
"""Return 10 ms."""
|
||||
return int(0.01 * _SAMPLE_RATE) # 10 ms
|
||||
|
||||
|
||||
class AudioBuffer:
|
||||
"""Fixed-sized audio buffer with variable internal length."""
|
||||
|
||||
@@ -80,7 +116,7 @@ class VoiceCommandSegmenter:
|
||||
speech_seconds: float = 0.3
|
||||
"""Seconds of speech before voice command has started."""
|
||||
|
||||
silence_seconds: float = 1.0
|
||||
silence_seconds: float = 0.5
|
||||
"""Seconds of silence after voice command has ended."""
|
||||
|
||||
timeout_seconds: float = 15.0
|
||||
@@ -123,10 +159,6 @@ class VoiceCommandSegmenter:
|
||||
"""
|
||||
self._timeout_seconds_left -= chunk_seconds
|
||||
if self._timeout_seconds_left <= 0:
|
||||
_LOGGER.warning(
|
||||
"VAD end of speech detection timed out after %s seconds",
|
||||
self.timeout_seconds,
|
||||
)
|
||||
self.reset()
|
||||
return False
|
||||
|
||||
@@ -137,38 +169,29 @@ class VoiceCommandSegmenter:
|
||||
if self._speech_seconds_left <= 0:
|
||||
# Inside voice command
|
||||
self.in_command = True
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
_LOGGER.debug("Voice command started")
|
||||
else:
|
||||
# Reset if enough silence
|
||||
self._reset_seconds_left -= chunk_seconds
|
||||
if self._reset_seconds_left <= 0:
|
||||
self._speech_seconds_left = self.speech_seconds
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
elif not is_speech:
|
||||
# Silence in command
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
self._silence_seconds_left -= chunk_seconds
|
||||
if self._silence_seconds_left <= 0:
|
||||
# Command finished successfully
|
||||
self.reset()
|
||||
_LOGGER.debug("Voice command finished")
|
||||
return False
|
||||
else:
|
||||
# Speech in command.
|
||||
# Reset silence counter if enough speech.
|
||||
# Reset if enough speech
|
||||
self._reset_seconds_left -= chunk_seconds
|
||||
if self._reset_seconds_left <= 0:
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
|
||||
return True
|
||||
|
||||
def process_with_vad(
|
||||
self,
|
||||
chunk: bytes,
|
||||
vad_samples_per_chunk: int | None,
|
||||
vad_is_speech: Callable[[bytes], bool],
|
||||
vad: VoiceActivityDetector,
|
||||
leftover_chunk_buffer: AudioBuffer | None,
|
||||
) -> bool:
|
||||
"""Process an audio chunk using an external VAD.
|
||||
@@ -177,20 +200,20 @@ class VoiceCommandSegmenter:
|
||||
|
||||
Returns False when voice command is finished.
|
||||
"""
|
||||
if vad_samples_per_chunk is None:
|
||||
if vad.samples_per_chunk is None:
|
||||
# No chunking
|
||||
chunk_seconds = (len(chunk) // _SAMPLE_WIDTH) / _SAMPLE_RATE
|
||||
is_speech = vad_is_speech(chunk)
|
||||
is_speech = vad.is_speech(chunk)
|
||||
return self.process(chunk_seconds, is_speech)
|
||||
|
||||
if leftover_chunk_buffer is None:
|
||||
raise ValueError("leftover_chunk_buffer is required when vad uses chunking")
|
||||
|
||||
# With chunking
|
||||
seconds_per_chunk = vad_samples_per_chunk / _SAMPLE_RATE
|
||||
bytes_per_chunk = vad_samples_per_chunk * _SAMPLE_WIDTH
|
||||
seconds_per_chunk = vad.samples_per_chunk / _SAMPLE_RATE
|
||||
bytes_per_chunk = vad.samples_per_chunk * _SAMPLE_WIDTH
|
||||
for vad_chunk in chunk_samples(chunk, bytes_per_chunk, leftover_chunk_buffer):
|
||||
is_speech = vad_is_speech(vad_chunk)
|
||||
is_speech = vad.is_speech(vad_chunk)
|
||||
if not self.process(seconds_per_chunk, is_speech):
|
||||
return False
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@ from .const import (
|
||||
DEFAULT_WAKE_WORD_TIMEOUT,
|
||||
DOMAIN,
|
||||
EVENT_RECORDING,
|
||||
SAMPLE_CHANNELS,
|
||||
SAMPLE_RATE,
|
||||
SAMPLE_WIDTH,
|
||||
)
|
||||
from .error import PipelineNotFound
|
||||
from .pipeline import (
|
||||
@@ -95,6 +92,7 @@ def async_register_websocket_api(hass: HomeAssistant) -> None:
|
||||
vol.Optional("volume_multiplier"): float,
|
||||
# Advanced use cases/testing
|
||||
vol.Optional("no_vad"): bool,
|
||||
vol.Optional("no_chunking"): bool,
|
||||
}
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
@@ -172,14 +170,9 @@ async def websocket_run(
|
||||
|
||||
# Yield until we receive an empty chunk
|
||||
while chunk := await audio_queue.get():
|
||||
if incoming_sample_rate != SAMPLE_RATE:
|
||||
if incoming_sample_rate != 16000:
|
||||
chunk, state = audioop.ratecv(
|
||||
chunk,
|
||||
SAMPLE_WIDTH,
|
||||
SAMPLE_CHANNELS,
|
||||
incoming_sample_rate,
|
||||
SAMPLE_RATE,
|
||||
state,
|
||||
chunk, 2, 1, incoming_sample_rate, 16000, state
|
||||
)
|
||||
yield chunk
|
||||
|
||||
@@ -213,6 +206,7 @@ async def websocket_run(
|
||||
auto_gain_dbfs=msg_input.get("auto_gain_dbfs", 0),
|
||||
volume_multiplier=msg_input.get("volume_multiplier", 1.0),
|
||||
is_vad_enabled=not msg_input.get("no_vad", False),
|
||||
is_chunking_enabled=not msg_input.get("no_chunking", False),
|
||||
)
|
||||
elif start_stage == PipelineStage.INTENT:
|
||||
# Input to conversation agent
|
||||
@@ -430,9 +424,9 @@ def websocket_list_languages(
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"languages": (
|
||||
sorted(pipeline_languages) if pipeline_languages else pipeline_languages
|
||||
)
|
||||
"languages": sorted(pipeline_languages)
|
||||
if pipeline_languages
|
||||
else pipeline_languages
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
|
||||
SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="ding",
|
||||
translation_key="ding",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
value_fn=retrieve_ding_activity,
|
||||
is_time_based=True,
|
||||
|
||||
@@ -16,6 +16,8 @@ NOTIFICATION_TITLE = "August"
|
||||
|
||||
MANUFACTURER = "August Home Inc."
|
||||
|
||||
DEFAULT_AUGUST_CONFIG_FILE = ".august.conf"
|
||||
|
||||
DEFAULT_NAME = "August"
|
||||
DOMAIN = "august"
|
||||
|
||||
|
||||
@@ -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.2", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"ding": {
|
||||
"name": "Doorbell ding"
|
||||
},
|
||||
"image_capture": {
|
||||
"name": "Image capture"
|
||||
}
|
||||
|
||||
@@ -9,3 +9,5 @@ from typing import Final
|
||||
DOMAIN: Final = "autarco"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
SENSORS_SOLAR: Final = "solar"
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
from autarco import AccountSite, Autarco, Inverter, Solar
|
||||
from autarco import AccountSite, Autarco, Solar
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -17,7 +17,6 @@ class AutarcoData(NamedTuple):
|
||||
"""Class for defining data in dict."""
|
||||
|
||||
solar: Solar
|
||||
inverters: dict[str, Inverter]
|
||||
|
||||
|
||||
class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
|
||||
@@ -45,5 +44,4 @@ class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[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),
|
||||
)
|
||||
|
||||
@@ -27,16 +27,6 @@ async def async_get_config_entry_diagnostics(
|
||||
"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
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from autarco import Inverter, Solar
|
||||
from autarco import Solar
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -29,7 +29,7 @@ from .coordinator import AutarcoDataUpdateCoordinator
|
||||
class AutarcoSolarSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Autarco sensor entity."""
|
||||
|
||||
value_fn: Callable[[Solar], StateType]
|
||||
state: Callable[[Solar], StateType]
|
||||
|
||||
|
||||
SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
|
||||
@@ -39,21 +39,21 @@ SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda solar: solar.power_production,
|
||||
state=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,
|
||||
state=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,
|
||||
state=lambda solar: solar.energy_production_month,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_total",
|
||||
@@ -61,34 +61,7 @@ SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
|
||||
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,
|
||||
state=lambda solar: solar.energy_production_total,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -99,25 +72,14 @@ async def async_setup_entry(
|
||||
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(
|
||||
async_add_entities(
|
||||
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(
|
||||
@@ -149,41 +111,4 @@ class AutarcoSolarSensorEntity(
|
||||
@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]
|
||||
)
|
||||
return self.entity_description.state(self.coordinator.data.solar)
|
||||
|
||||
@@ -34,12 +34,6 @@
|
||||
},
|
||||
"energy_production_total": {
|
||||
"name": "Energy production total"
|
||||
},
|
||||
"out_ac_power": {
|
||||
"name": "Power AC output"
|
||||
},
|
||||
"out_ac_energy_total": {
|
||||
"name": "Energy AC output total"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ async def fetch_redirect_uris(hass: HomeAssistant, url: str) -> list[str]:
|
||||
try:
|
||||
async with (
|
||||
aiohttp.ClientSession() as session,
|
||||
session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp,
|
||||
session.get(url, timeout=5) as resp,
|
||||
):
|
||||
async for data in resp.content.iter_chunked(1024):
|
||||
parser.feed(data.decode())
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
},
|
||||
"issues": {
|
||||
"service_not_found": {
|
||||
"title": "{name} uses an unknown action",
|
||||
"title": "{name} uses an unknown service",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::automation::issues::service_not_found::title%]",
|
||||
"description": "The automation \"{name}\" (`{entity_id}`) has an unknown action: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this action is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove this action.\n\nClick on SUBMIT below to confirm you have fixed this automation."
|
||||
"description": "The automation \"{name}\" (`{entity_id}`) has an action that calls an unknown service: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this service is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove the action that calls this service.\n\nClick on SUBMIT below to confirm you have fixed this automation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aws",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiobotocore", "botocore"],
|
||||
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
|
||||
"requirements": ["aiobotocore==2.13.0"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import PLATFORMS
|
||||
from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
from .hub import AxisHub, get_axis_api
|
||||
|
||||
@@ -18,6 +18,8 @@ type AxisConfigEntry = ConfigEntry[AxisHub]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: AxisConfigEntry) -> bool:
|
||||
"""Set up the Axis integration."""
|
||||
hass.data.setdefault(AXIS_DOMAIN, {})
|
||||
|
||||
try:
|
||||
api = await get_axis_api(hass, config_entry.data)
|
||||
except CannotConnect as err:
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.core import Event, HomeAssistant, State
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.json import ExtendedJSONEncoder
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
@@ -203,7 +203,9 @@ class AzureDataExplorer:
|
||||
return None, dropped
|
||||
if (utcnow() - time_fired).seconds > DEFAULT_MAX_DELAY + self._send_interval:
|
||||
return None, dropped + 1
|
||||
if "\n" in state.state:
|
||||
return None, dropped + 1
|
||||
|
||||
json_event = json.dumps(obj=state, cls=ExtendedJSONEncoder)
|
||||
json_event = json.dumps(obj=state, cls=JSONEncoder)
|
||||
|
||||
return (json_event, dropped)
|
||||
|
||||
@@ -65,18 +65,13 @@ class AzureDataExplorerClient:
|
||||
)
|
||||
|
||||
if data[CONF_USE_QUEUED_CLIENT] is True:
|
||||
# Queued is the only option supported on free tier of ADX
|
||||
# Queded is the only option supported on free tear of ADX
|
||||
self.write_client = QueuedIngestClient(kcsb_ingest)
|
||||
else:
|
||||
self.write_client = ManagedStreamingIngestClient(kcsb_ingest)
|
||||
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest)
|
||||
|
||||
self.query_client = KustoClient(kcsb_query)
|
||||
|
||||
# Reduce the HTTP logging, the default INFO logging is too verbose.
|
||||
logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(
|
||||
logging.WARNING
|
||||
)
|
||||
|
||||
def test_connection(self) -> None:
|
||||
"""Test connection, will throw Exception if it cannot connect."""
|
||||
|
||||
@@ -85,7 +80,7 @@ class AzureDataExplorerClient:
|
||||
self.query_client.execute_query(self._database, query)
|
||||
|
||||
def ingest_data(self, adx_events: str) -> None:
|
||||
"""Send data to Azure Data Explorer."""
|
||||
"""Send data to Axure Data Explorer."""
|
||||
|
||||
bytes_stream = io.StringIO(adx_events)
|
||||
stream_descriptor = StreamDescriptor(bytes_stream)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/azure_data_explorer",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["azure"],
|
||||
"requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"]
|
||||
"requirements": ["azure-kusto-ingest==3.1.0", "azure-kusto-data[aio]==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -43,10 +43,7 @@ class BAFFan(BAFEntity, FanEntity):
|
||||
FanEntityFeature.SET_SPEED
|
||||
| FanEntityFeature.DIRECTION
|
||||
| FanEntityFeature.PRESET_MODE
|
||||
| FanEntityFeature.TURN_OFF
|
||||
| FanEntityFeature.TURN_ON
|
||||
)
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
_attr_preset_modes = [PRESET_MODE_AUTO]
|
||||
_attr_speed_count = SPEED_COUNT
|
||||
_attr_name = None
|
||||
|
||||
@@ -32,12 +32,7 @@ async def async_setup_entry(
|
||||
class BalboaPumpFanEntity(BalboaEntity, FanEntity):
|
||||
"""Representation of a Balboa Spa pump fan entity."""
|
||||
|
||||
_attr_supported_features = (
|
||||
FanEntityFeature.SET_SPEED
|
||||
| FanEntityFeature.TURN_OFF
|
||||
| FanEntityFeature.TURN_ON
|
||||
)
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
_attr_supported_features = FanEntityFeature.SET_SPEED
|
||||
_attr_translation_key = "pump"
|
||||
|
||||
def __init__(self, control: SpaControl) -> None:
|
||||
|
||||
@@ -135,15 +135,6 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
except AddressValueError:
|
||||
return self.async_abort(reason="ipv6_address")
|
||||
|
||||
# Check connection to ensure valid address is received
|
||||
self._client = MozartClient(self._host)
|
||||
|
||||
async with self._client:
|
||||
try:
|
||||
await self._client.get_beolink_self(_request_timeout=3)
|
||||
except (ClientConnectorError, TimeoutError):
|
||||
return self.async_abort(reason="invalid_address")
|
||||
|
||||
self._model = discovery_info.hostname[:-16].replace("-", " ")
|
||||
self._serial_number = discovery_info.properties[ATTR_SERIAL_NUMBER]
|
||||
self._beolink_jid = f"{discovery_info.properties[ATTR_TYPE_NUMBER]}.{discovery_info.properties[ATTR_ITEM_NUMBER]}.{self._serial_number}@products.bang-olufsen.com"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"name": "Bayesian",
|
||||
"codeowners": ["@HarvsG"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bayesian",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Support for Blinkstick lights."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# from blinkstick import blinkstick
|
||||
from blinkstick import blinkstick
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"domain": "blinksticklight",
|
||||
"name": "BlinkStick",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/blinksticklight",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["blinkstick"],
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -245,36 +245,14 @@ async def fetch_blueprint_from_website_url(
|
||||
return ImportedBlueprint(suggested_filename, raw_yaml, blueprint)
|
||||
|
||||
|
||||
async def fetch_blueprint_from_generic_url(
|
||||
hass: HomeAssistant, url: str
|
||||
) -> ImportedBlueprint:
|
||||
"""Get a blueprint from a generic website."""
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
resp = await session.get(url, raise_for_status=True)
|
||||
raw_yaml = await resp.text()
|
||||
data = yaml.parse_yaml(raw_yaml)
|
||||
|
||||
assert isinstance(data, dict)
|
||||
blueprint = Blueprint(data)
|
||||
|
||||
parsed_import_url = yarl.URL(url)
|
||||
suggested_filename = f"{parsed_import_url.host}/{parsed_import_url.parts[-1][:-5]}"
|
||||
return ImportedBlueprint(suggested_filename, raw_yaml, blueprint)
|
||||
|
||||
|
||||
FETCH_FUNCTIONS = (
|
||||
fetch_blueprint_from_community_post,
|
||||
fetch_blueprint_from_github_url,
|
||||
fetch_blueprint_from_github_gist_url,
|
||||
fetch_blueprint_from_website_url,
|
||||
fetch_blueprint_from_generic_url,
|
||||
)
|
||||
|
||||
|
||||
async def fetch_blueprint_from_url(hass: HomeAssistant, url: str) -> ImportedBlueprint:
|
||||
"""Get a blueprint from a url."""
|
||||
for func in FETCH_FUNCTIONS:
|
||||
for func in (
|
||||
fetch_blueprint_from_community_post,
|
||||
fetch_blueprint_from_github_url,
|
||||
fetch_blueprint_from_github_gist_url,
|
||||
fetch_blueprint_from_website_url,
|
||||
):
|
||||
with suppress(UnsupportedUrl):
|
||||
imported_bp = await func(hass, url)
|
||||
imported_bp.blueprint.update_metadata(source_url=url)
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
CONF_PATH,
|
||||
__version__,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import yaml
|
||||
|
||||
@@ -372,7 +372,7 @@ class DomainBlueprints:
|
||||
|
||||
shutil.copytree(
|
||||
integration.file_path / BLUEPRINT_FOLDER,
|
||||
self.blueprint_folder / HOMEASSISTANT_DOMAIN,
|
||||
self.blueprint_folder / HA_DOMAIN,
|
||||
)
|
||||
|
||||
await self.hass.async_add_executor_job(populate)
|
||||
|
||||
@@ -1,70 +1 @@
|
||||
"""The bluesound component."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import aiohttp
|
||||
from pyblu import Player, SyncStatus
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .media_player import setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BluesoundData:
|
||||
"""Bluesound data class."""
|
||||
|
||||
player: Player
|
||||
sync_status: SyncStatus
|
||||
|
||||
|
||||
type BluesoundConfigEntry = ConfigEntry[BluesoundData]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Bluesound."""
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = []
|
||||
setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: BluesoundConfigEntry
|
||||
) -> bool:
|
||||
"""Set up the Bluesound entry."""
|
||||
host = config_entry.data[CONF_HOST]
|
||||
port = config_entry.data[CONF_PORT]
|
||||
session = async_get_clientsession(hass)
|
||||
async with Player(host, port, session=session, default_timeout=10) as player:
|
||||
try:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except TimeoutError as ex:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timeout while connecting to {host}:{port}"
|
||||
) from ex
|
||||
except aiohttp.ClientError as ex:
|
||||
raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex
|
||||
|
||||
config_entry.runtime_data = BluesoundData(player, sync_status)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
"""Config flow for bluesound."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from pyblu import Player, SyncStatus
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .media_player import DEFAULT_PORT
|
||||
from .utils import format_unique_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BluesoundConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Bluesound config flow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._host: str | None = None
|
||||
self._port = DEFAULT_PORT
|
||||
self._sync_status: SyncStatus | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
async with Player(
|
||||
user_input[CONF_HOST], user_input[CONF_PORT], session=session
|
||||
) as player:
|
||||
try:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
format_unique_id(sync_status.mac, user_input[CONF_PORT])
|
||||
)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=sync_status.name,
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Optional(CONF_PORT, default=11000): int,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import bluesound config entry from configuration.yaml."""
|
||||
session = async_get_clientsession(self.hass)
|
||||
async with Player(
|
||||
import_data[CONF_HOST], import_data[CONF_PORT], session=session
|
||||
) as player:
|
||||
try:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
await self.async_set_unique_id(
|
||||
format_unique_id(sync_status.mac, import_data[CONF_PORT])
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=sync_status.name,
|
||||
data=import_data,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by zeroconf discovery."""
|
||||
if discovery_info.port is not None:
|
||||
self._port = discovery_info.port
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
try:
|
||||
async with Player(
|
||||
discovery_info.host, self._port, session=session
|
||||
) as player:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
await self.async_set_unique_id(format_unique_id(sync_status.mac, self._port))
|
||||
|
||||
self._host = discovery_info.host
|
||||
self._sync_status = sync_status
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self._host,
|
||||
}
|
||||
)
|
||||
|
||||
self.context.update(
|
||||
{
|
||||
"title_placeholders": {"name": sync_status.name},
|
||||
"configuration_url": f"http://{discovery_info.host}",
|
||||
}
|
||||
)
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Confirm the zeroconf setup."""
|
||||
assert self._sync_status is not None
|
||||
assert self._host is not None
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self._sync_status.name,
|
||||
data={
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
description_placeholders={
|
||||
"name": self._sync_status.name,
|
||||
"host": self._host,
|
||||
},
|
||||
)
|
||||
@@ -1,10 +1,7 @@
|
||||
"""Constants for the Bluesound HiFi wireless speakers and audio integrations component."""
|
||||
|
||||
DOMAIN = "bluesound"
|
||||
INTEGRATION_TITLE = "Bluesound"
|
||||
SERVICE_CLEAR_TIMER = "clear_sleep_timer"
|
||||
SERVICE_JOIN = "join"
|
||||
SERVICE_SET_TIMER = "set_sleep_timer"
|
||||
SERVICE_UNJOIN = "unjoin"
|
||||
ATTR_BLUESOUND_GROUP = "bluesound_group"
|
||||
ATTR_MASTER = "master"
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
{
|
||||
"domain": "bluesound",
|
||||
"name": "Bluesound",
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@thrawnarn", "@LouisChrist"],
|
||||
"config_flow": true,
|
||||
"codeowners": ["@thrawnarn"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==0.4.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
}
|
||||
]
|
||||
"requirements": ["xmltodict==0.13.0"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,4 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your Bluesound player",
|
||||
"port": "Port of your Bluesound player. This is usually 11000."
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Discover Bluesound player",
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"join": {
|
||||
"name": "Join",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
"""Utility functions for the Bluesound component."""
|
||||
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
|
||||
def format_unique_id(mac: str, port: int) -> str:
|
||||
"""Generate a unique ID based on the MAC address and port number."""
|
||||
return f"{format_mac(mac)}-{port}"
|
||||
@@ -16,9 +16,9 @@
|
||||
"requirements": [
|
||||
"bleak==0.22.2",
|
||||
"bleak-retry-connector==3.5.0",
|
||||
"bluetooth-adapters==0.19.4",
|
||||
"bluetooth-adapters==0.19.3",
|
||||
"bluetooth-auto-recovery==1.4.2",
|
||||
"bluetooth-data-tools==1.19.4",
|
||||
"bluetooth-data-tools==1.19.3",
|
||||
"dbus-fast==2.22.1",
|
||||
"habluetooth==3.1.3"
|
||||
]
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from fnmatch import translate
|
||||
from functools import lru_cache
|
||||
@@ -174,10 +173,10 @@ class BluetoothMatcherIndexBase[
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the matcher index."""
|
||||
self.local_name: defaultdict[str, list[_T]] = defaultdict(list)
|
||||
self.service_uuid: defaultdict[str, list[_T]] = defaultdict(list)
|
||||
self.service_data_uuid: defaultdict[str, list[_T]] = defaultdict(list)
|
||||
self.manufacturer_id: defaultdict[int, list[_T]] = defaultdict(list)
|
||||
self.local_name: dict[str, list[_T]] = {}
|
||||
self.service_uuid: dict[str, list[_T]] = {}
|
||||
self.service_data_uuid: dict[str, list[_T]] = {}
|
||||
self.manufacturer_id: dict[int, list[_T]] = {}
|
||||
self.service_uuid_set: set[str] = set()
|
||||
self.service_data_uuid_set: set[str] = set()
|
||||
self.manufacturer_id_set: set[int] = set()
|
||||
@@ -191,22 +190,26 @@ class BluetoothMatcherIndexBase[
|
||||
"""
|
||||
# Local name is the cheapest to match since its just a dict lookup
|
||||
if LOCAL_NAME in matcher:
|
||||
self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].append(
|
||||
matcher
|
||||
)
|
||||
self.local_name.setdefault(
|
||||
_local_name_to_index_key(matcher[LOCAL_NAME]), []
|
||||
).append(matcher)
|
||||
return True
|
||||
|
||||
# Manufacturer data is 2nd cheapest since its all ints
|
||||
if MANUFACTURER_ID in matcher:
|
||||
self.manufacturer_id[matcher[MANUFACTURER_ID]].append(matcher)
|
||||
self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append(
|
||||
matcher
|
||||
)
|
||||
return True
|
||||
|
||||
if SERVICE_UUID in matcher:
|
||||
self.service_uuid[matcher[SERVICE_UUID]].append(matcher)
|
||||
self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher)
|
||||
return True
|
||||
|
||||
if SERVICE_DATA_UUID in matcher:
|
||||
self.service_data_uuid[matcher[SERVICE_DATA_UUID]].append(matcher)
|
||||
self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append(
|
||||
matcher
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -257,38 +260,32 @@ class BluetoothMatcherIndexBase[
|
||||
if ble_device_matches(matcher, service_info)
|
||||
)
|
||||
|
||||
if (
|
||||
(service_data_uuid_set := self.service_data_uuid_set)
|
||||
and (service_data := service_info.service_data)
|
||||
and (matched_uuids := service_data_uuid_set.intersection(service_data))
|
||||
):
|
||||
if self.service_data_uuid_set and service_info.service_data:
|
||||
matches.extend(
|
||||
matcher
|
||||
for service_data_uuid in matched_uuids
|
||||
for service_data_uuid in self.service_data_uuid_set.intersection(
|
||||
service_info.service_data
|
||||
)
|
||||
for matcher in self.service_data_uuid[service_data_uuid]
|
||||
if ble_device_matches(matcher, service_info)
|
||||
)
|
||||
|
||||
if (
|
||||
(manufacturer_id_set := self.manufacturer_id_set)
|
||||
and (manufacturer_data := service_info.manufacturer_data)
|
||||
and (matched_ids := manufacturer_id_set.intersection(manufacturer_data))
|
||||
):
|
||||
if self.manufacturer_id_set and service_info.manufacturer_data:
|
||||
matches.extend(
|
||||
matcher
|
||||
for manufacturer_id in matched_ids
|
||||
for manufacturer_id in self.manufacturer_id_set.intersection(
|
||||
service_info.manufacturer_data
|
||||
)
|
||||
for matcher in self.manufacturer_id[manufacturer_id]
|
||||
if ble_device_matches(matcher, service_info)
|
||||
)
|
||||
|
||||
if (
|
||||
(service_uuid_set := self.service_uuid_set)
|
||||
and (service_uuids := service_info.service_uuids)
|
||||
and (matched_uuids := service_uuid_set.intersection(service_uuids))
|
||||
):
|
||||
if self.service_uuid_set and service_info.service_uuids:
|
||||
matches.extend(
|
||||
matcher
|
||||
for service_uuid in matched_uuids
|
||||
for service_uuid in self.service_uuid_set.intersection(
|
||||
service_info.service_uuids
|
||||
)
|
||||
for matcher in self.service_uuid[service_uuid]
|
||||
if ble_device_matches(matcher, service_info)
|
||||
)
|
||||
@@ -313,9 +310,7 @@ class BluetoothCallbackMatcherIndex(
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the matcher index."""
|
||||
super().__init__()
|
||||
self.address: defaultdict[str, list[BluetoothCallbackMatcherWithCallback]] = (
|
||||
defaultdict(list)
|
||||
)
|
||||
self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {}
|
||||
self.connectable: list[BluetoothCallbackMatcherWithCallback] = []
|
||||
|
||||
def add_callback_matcher(
|
||||
@@ -328,7 +323,7 @@ class BluetoothCallbackMatcherIndex(
|
||||
We put them in the bucket that they are most likely to match.
|
||||
"""
|
||||
if ADDRESS in matcher:
|
||||
self.address[matcher[ADDRESS]].append(matcher)
|
||||
self.address.setdefault(matcher[ADDRESS], []).append(matcher)
|
||||
return
|
||||
|
||||
if super().add(matcher):
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bimmer-connected[china]==0.16.1"]
|
||||
"requirements": ["bimmer-connected[china]==0.15.3"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user