This commit is contained in:
Franck Nijhof 2023-08-02 15:01:08 +02:00 committed by GitHub
commit 111510b11a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2391 changed files with 55598 additions and 21525 deletions

View File

@ -24,6 +24,7 @@ base_platforms: &base_platforms
- homeassistant/components/datetime/** - homeassistant/components/datetime/**
- homeassistant/components/device_tracker/** - homeassistant/components/device_tracker/**
- homeassistant/components/diagnostics/** - homeassistant/components/diagnostics/**
- homeassistant/components/event/**
- homeassistant/components/fan/** - homeassistant/components/fan/**
- homeassistant/components/geo_location/** - homeassistant/components/geo_location/**
- homeassistant/components/humidifier/** - homeassistant/components/humidifier/**

View File

@ -82,6 +82,7 @@ omit =
homeassistant/components/arwn/sensor.py homeassistant/components/arwn/sensor.py
homeassistant/components/aseko_pool_live/__init__.py homeassistant/components/aseko_pool_live/__init__.py
homeassistant/components/aseko_pool_live/binary_sensor.py homeassistant/components/aseko_pool_live/binary_sensor.py
homeassistant/components/aseko_pool_live/coordinator.py
homeassistant/components/aseko_pool_live/entity.py homeassistant/components/aseko_pool_live/entity.py
homeassistant/components/aseko_pool_live/sensor.py homeassistant/components/aseko_pool_live/sensor.py
homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_cdr/mailbox.py
@ -229,6 +230,10 @@ omit =
homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dublin_bus_transport/sensor.py
homeassistant/components/dunehd/__init__.py homeassistant/components/dunehd/__init__.py
homeassistant/components/dunehd/media_player.py homeassistant/components/dunehd/media_player.py
homeassistant/components/duotecno/__init__.py
homeassistant/components/duotecno/entity.py
homeassistant/components/duotecno/switch.py
homeassistant/components/duotecno/cover.py
homeassistant/components/dwd_weather_warnings/const.py homeassistant/components/dwd_weather_warnings/const.py
homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/coordinator.py
homeassistant/components/dwd_weather_warnings/sensor.py homeassistant/components/dwd_weather_warnings/sensor.py
@ -260,6 +265,11 @@ omit =
homeassistant/components/eight_sleep/__init__.py homeassistant/components/eight_sleep/__init__.py
homeassistant/components/eight_sleep/binary_sensor.py homeassistant/components/eight_sleep/binary_sensor.py
homeassistant/components/eight_sleep/sensor.py homeassistant/components/eight_sleep/sensor.py
homeassistant/components/electric_kiwi/__init__.py
homeassistant/components/electric_kiwi/api.py
homeassistant/components/electric_kiwi/oauth2.py
homeassistant/components/electric_kiwi/sensor.py
homeassistant/components/electric_kiwi/coordinator.py
homeassistant/components/eliqonline/sensor.py homeassistant/components/eliqonline/sensor.py
homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/__init__.py
homeassistant/components/elkm1/alarm_control_panel.py homeassistant/components/elkm1/alarm_control_panel.py
@ -304,11 +314,8 @@ omit =
homeassistant/components/escea/__init__.py homeassistant/components/escea/__init__.py
homeassistant/components/escea/climate.py homeassistant/components/escea/climate.py
homeassistant/components/escea/discovery.py homeassistant/components/escea/discovery.py
homeassistant/components/esphome/__init__.py
homeassistant/components/esphome/bluetooth/* homeassistant/components/esphome/bluetooth/*
homeassistant/components/esphome/camera.py homeassistant/components/esphome/manager.py
homeassistant/components/esphome/domain_data.py
homeassistant/components/esphome/entry_data.py
homeassistant/components/etherscan/sensor.py homeassistant/components/etherscan/sensor.py
homeassistant/components/eufy/* homeassistant/components/eufy/*
homeassistant/components/eufylife_ble/__init__.py homeassistant/components/eufylife_ble/__init__.py
@ -316,12 +323,16 @@ omit =
homeassistant/components/everlights/light.py homeassistant/components/everlights/light.py
homeassistant/components/evohome/* homeassistant/components/evohome/*
homeassistant/components/ezviz/__init__.py homeassistant/components/ezviz/__init__.py
homeassistant/components/ezviz/alarm_control_panel.py
homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/binary_sensor.py
homeassistant/components/ezviz/button.py
homeassistant/components/ezviz/camera.py homeassistant/components/ezviz/camera.py
homeassistant/components/ezviz/image.py
homeassistant/components/ezviz/light.py homeassistant/components/ezviz/light.py
homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/coordinator.py
homeassistant/components/ezviz/number.py homeassistant/components/ezviz/number.py
homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/entity.py
homeassistant/components/ezviz/select.py
homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/sensor.py
homeassistant/components/ezviz/switch.py homeassistant/components/ezviz/switch.py
homeassistant/components/ezviz/update.py homeassistant/components/ezviz/update.py
@ -594,6 +605,7 @@ omit =
homeassistant/components/keymitt_ble/entity.py homeassistant/components/keymitt_ble/entity.py
homeassistant/components/keymitt_ble/switch.py homeassistant/components/keymitt_ble/switch.py
homeassistant/components/keymitt_ble/coordinator.py homeassistant/components/keymitt_ble/coordinator.py
homeassistant/components/kitchen_sink/weather.py
homeassistant/components/kiwi/lock.py homeassistant/components/kiwi/lock.py
homeassistant/components/kodi/__init__.py homeassistant/components/kodi/__init__.py
homeassistant/components/kodi/browse_media.py homeassistant/components/kodi/browse_media.py
@ -651,6 +663,7 @@ omit =
homeassistant/components/lookin/light.py homeassistant/components/lookin/light.py
homeassistant/components/lookin/media_player.py homeassistant/components/lookin/media_player.py
homeassistant/components/lookin/sensor.py homeassistant/components/lookin/sensor.py
homeassistant/components/loqed/sensor.py
homeassistant/components/luci/device_tracker.py homeassistant/components/luci/device_tracker.py
homeassistant/components/luftdaten/sensor.py homeassistant/components/luftdaten/sensor.py
homeassistant/components/lupusec/* homeassistant/components/lupusec/*
@ -698,13 +711,14 @@ omit =
homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/sensor.py
homeassistant/components/metoffice/weather.py homeassistant/components/metoffice/weather.py
homeassistant/components/microsoft/tts.py homeassistant/components/microsoft/tts.py
homeassistant/components/miflora/sensor.py
homeassistant/components/mikrotik/hub.py homeassistant/components/mikrotik/hub.py
homeassistant/components/mill/climate.py homeassistant/components/mill/climate.py
homeassistant/components/mill/sensor.py homeassistant/components/mill/sensor.py
homeassistant/components/minecraft_server/__init__.py homeassistant/components/minecraft_server/__init__.py
homeassistant/components/minecraft_server/binary_sensor.py
homeassistant/components/minecraft_server/entity.py
homeassistant/components/minecraft_server/sensor.py
homeassistant/components/minio/minio_helper.py homeassistant/components/minio/minio_helper.py
homeassistant/components/mitemp_bt/sensor.py
homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/camera.py
homeassistant/components/mjpeg/util.py homeassistant/components/mjpeg/util.py
homeassistant/components/mochad/__init__.py homeassistant/components/mochad/__init__.py
@ -755,7 +769,6 @@ omit =
homeassistant/components/neato/switch.py homeassistant/components/neato/switch.py
homeassistant/components/neato/vacuum.py homeassistant/components/neato/vacuum.py
homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/nest/legacy/*
homeassistant/components/netdata/sensor.py homeassistant/components/netdata/sensor.py
homeassistant/components/netgear/__init__.py homeassistant/components/netgear/__init__.py
homeassistant/components/netgear/button.py homeassistant/components/netgear/button.py
@ -858,6 +871,9 @@ omit =
homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/sensor.py
homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/openweathermap/weather_update_coordinator.py
homeassistant/components/opnsense/__init__.py homeassistant/components/opnsense/__init__.py
homeassistant/components/opower/__init__.py
homeassistant/components/opower/coordinator.py
homeassistant/components/opower/sensor.py
homeassistant/components/opnsense/device_tracker.py homeassistant/components/opnsense/device_tracker.py
homeassistant/components/opple/light.py homeassistant/components/opple/light.py
homeassistant/components/oru/* homeassistant/components/oru/*
@ -989,6 +1005,7 @@ omit =
homeassistant/components/reolink/light.py homeassistant/components/reolink/light.py
homeassistant/components/reolink/number.py homeassistant/components/reolink/number.py
homeassistant/components/reolink/select.py homeassistant/components/reolink/select.py
homeassistant/components/reolink/sensor.py
homeassistant/components/reolink/siren.py homeassistant/components/reolink/siren.py
homeassistant/components/reolink/switch.py homeassistant/components/reolink/switch.py
homeassistant/components/reolink/update.py homeassistant/components/reolink/update.py
@ -1314,6 +1331,7 @@ omit =
homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/sensor.py
homeassistant/components/tradfri/switch.py homeassistant/components/tradfri/switch.py
homeassistant/components/trafikverket_train/__init__.py homeassistant/components/trafikverket_train/__init__.py
homeassistant/components/trafikverket_train/coordinator.py
homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/__init__.py
homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/coordinator.py

View File

@ -7,6 +7,8 @@
"containerEnv": { "DEVCONTAINER": "1" }, "containerEnv": { "DEVCONTAINER": "1" },
"appPort": ["8123:8123"], "appPort": ["8123:8123"],
"runArgs": ["-e", "GIT_EDITOR=code --wait"], "runArgs": ["-e", "GIT_EDITOR=code --wait"],
"customizations": {
"vscode": {
"extensions": [ "extensions": [
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode", "visualstudioexptteam.vscodeintellicode",
@ -45,4 +47,6 @@
"!include_dir_merge_named scalar" "!include_dir_merge_named scalar"
] ]
} }
}
}
} }

View File

@ -59,15 +59,15 @@ body:
attributes: attributes:
label: Integration causing the issue label: Integration causing the issue
description: > description: >
The name of the integration. For example: Automation, Philips Hue The name of the integration, for example Automation or Philips Hue.
- type: input - type: input
id: integration_link id: integration_link
attributes: attributes:
label: Link to integration documentation on our website label: Link to integration documentation on our website
placeholder: "https://www.home-assistant.io/integrations/..." placeholder: "https://www.home-assistant.io/integrations/..."
description: | description: |
Providing a link [to the documentation][docs] helps us categorize the Providing a link [to the documentation][docs] helps us categorize the issue and might speed up the
issue, while also providing a useful reference for others. investigation by automatically informing a contributor, while also providing a useful reference for others.
[docs]: https://www.home-assistant.io/integrations [docs]: https://www.home-assistant.io/integrations

View File

@ -29,7 +29,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -59,7 +59,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -124,7 +124,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -32,9 +32,9 @@ env:
CACHE_VERSION: 5 CACHE_VERSION: 5
PIP_CACHE_VERSION: 4 PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 4 MYPY_CACHE_VERSION: 4
HA_SHORT_VERSION: 2023.7 HA_SHORT_VERSION: 2023.8
DEFAULT_PYTHON: "3.10" DEFAULT_PYTHON: "3.11"
ALL_PYTHON_VERSIONS: "['3.10', '3.11']" ALL_PYTHON_VERSIONS: "['3.11']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support # 10.6 is the current long-term-support
@ -209,7 +209,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -253,7 +253,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -299,7 +299,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -348,7 +348,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -443,7 +443,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -492,9 +492,9 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install --cache-dir=$PIP_CACHE -U "pip>=21.3.1,<23.2" setuptools wheel PIP_CACHE_DIR=$PIP_CACHE pip install -U "pip>=21.3.1" setuptools wheel
pip install --cache-dir=$PIP_CACHE -r requirements_all.txt PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_all.txt
pip install --cache-dir=$PIP_CACHE -r requirements_test.txt PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_test.txt
pip install -e . --config-settings editable_mode=compat pip install -e . --config-settings editable_mode=compat
hassfest: hassfest:
@ -511,7 +511,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -543,7 +543,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -576,7 +576,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -620,7 +620,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -702,7 +702,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -827,7 +827,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -934,7 +934,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1019,6 +1019,7 @@ jobs:
with: | with: |
fail_ci_if_error: true fail_ci_if_error: true
flags: full-suite flags: full-suite
token: ${{ env.CODECOV_TOKEN }}
attempt_limit: 5 attempt_limit: 5
attempt_delay: 30000 attempt_delay: 30000
- name: Upload coverage to Codecov (partial coverage) - name: Upload coverage to Codecov (partial coverage)
@ -1028,5 +1029,6 @@ jobs:
action: codecov/codecov-action@v3.1.3 action: codecov/codecov-action@v3.1.3
with: | with: |
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ env.CODECOV_TOKEN }}
attempt_limit: 5 attempt_limit: 5
attempt_delay: 30000 attempt_delay: 30000

View File

@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.6.1 uses: actions/setup-python@v4.7.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -1,12 +1,12 @@
repos: repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.272 rev: v0.0.280
hooks: hooks:
- id: ruff - id: ruff
args: args:
- --fix - --fix
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.3.0 rev: 23.7.0
hooks: hooks:
- id: black - id: black
args: args:
@ -17,8 +17,8 @@ repos:
hooks: hooks:
- id: codespell - id: codespell
args: args:
- --ignore-words-list=additionals,alle,alot,ba,bre,bund,currenty,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar - --ignore-words-list=additionals,alle,alot,bund,currenty,datas,farenheit,falsy,fo,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,withing,zar
- --skip="./.*,*.csv,*.json" - --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2 - --quiet-level=2
exclude_types: [csv, json] exclude_types: [csv, json]
exclude: ^tests/fixtures/|homeassistant/generated/ exclude: ^tests/fixtures/|homeassistant/generated/
@ -35,7 +35,7 @@ repos:
- --branch=master - --branch=master
- --branch=rc - --branch=rc
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.28.0 rev: v1.32.0
hooks: hooks:
- id: yamllint - id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier

View File

@ -108,11 +108,13 @@ homeassistant.components.dsmr.*
homeassistant.components.dunehd.* homeassistant.components.dunehd.*
homeassistant.components.efergy.* homeassistant.components.efergy.*
homeassistant.components.electrasmart.* homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.elkm1.* homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.* homeassistant.components.emulated_hue.*
homeassistant.components.energy.* homeassistant.components.energy.*
homeassistant.components.esphome.* homeassistant.components.esphome.*
homeassistant.components.event.*
homeassistant.components.evil_genius_labs.* homeassistant.components.evil_genius_labs.*
homeassistant.components.fan.* homeassistant.components.fan.*
homeassistant.components.fastdotcom.* homeassistant.components.fastdotcom.*

View File

@ -195,8 +195,8 @@ build.json @home-assistant/supervisor
/tests/components/camera/ @home-assistant/core /tests/components/camera/ @home-assistant/core
/homeassistant/components/cast/ @emontnemery /homeassistant/components/cast/ @emontnemery
/tests/components/cast/ @emontnemery /tests/components/cast/ @emontnemery
/homeassistant/components/cert_expiry/ @Cereal2nd @jjlawren /homeassistant/components/cert_expiry/ @jjlawren
/tests/components/cert_expiry/ @Cereal2nd @jjlawren /tests/components/cert_expiry/ @jjlawren
/homeassistant/components/circuit/ @braam /homeassistant/components/circuit/ @braam
/homeassistant/components/cisco_ios/ @fbradyirl /homeassistant/components/cisco_ios/ @fbradyirl
/homeassistant/components/cisco_mobility_express/ @fbradyirl /homeassistant/components/cisco_mobility_express/ @fbradyirl
@ -277,8 +277,6 @@ build.json @home-assistant/supervisor
/tests/components/discord/ @tkdrob /tests/components/discord/ @tkdrob
/homeassistant/components/discovergy/ @jpbede /homeassistant/components/discovergy/ @jpbede
/tests/components/discovergy/ @jpbede /tests/components/discovergy/ @jpbede
/homeassistant/components/discovery/ @home-assistant/core
/tests/components/discovery/ @home-assistant/core
/homeassistant/components/dlink/ @tkdrob /homeassistant/components/dlink/ @tkdrob
/tests/components/dlink/ @tkdrob /tests/components/dlink/ @tkdrob
/homeassistant/components/dlna_dmr/ @StevenLooman @chishm /homeassistant/components/dlna_dmr/ @StevenLooman @chishm
@ -299,6 +297,8 @@ build.json @home-assistant/supervisor
/tests/components/dsmr_reader/ @depl0y @glodenox /tests/components/dsmr_reader/ @depl0y @glodenox
/homeassistant/components/dunehd/ @bieniu /homeassistant/components/dunehd/ @bieniu
/tests/components/dunehd/ @bieniu /tests/components/dunehd/ @bieniu
/homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo /tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo
/homeassistant/components/dynalite/ @ziv1234 /homeassistant/components/dynalite/ @ziv1234
@ -321,6 +321,8 @@ build.json @home-assistant/supervisor
/tests/components/eight_sleep/ @mezz64 @raman325 /tests/components/eight_sleep/ @mezz64 @raman325
/homeassistant/components/electrasmart/ @jafar-atili /homeassistant/components/electrasmart/ @jafar-atili
/tests/components/electrasmart/ @jafar-atili /tests/components/electrasmart/ @jafar-atili
/homeassistant/components/electric_kiwi/ @mikey0000
/tests/components/electric_kiwi/ @mikey0000
/homeassistant/components/elgato/ @frenck /homeassistant/components/elgato/ @frenck
/tests/components/elgato/ @frenck /tests/components/elgato/ @frenck
/homeassistant/components/elkm1/ @gwww @bdraco /homeassistant/components/elkm1/ @gwww @bdraco
@ -360,6 +362,8 @@ build.json @home-assistant/supervisor
/tests/components/esphome/ @OttoWinter @jesserockz @bdraco /tests/components/esphome/ @OttoWinter @jesserockz @bdraco
/homeassistant/components/eufylife_ble/ @bdr99 /homeassistant/components/eufylife_ble/ @bdr99
/tests/components/eufylife_ble/ @bdr99 /tests/components/eufylife_ble/ @bdr99
/homeassistant/components/event/ @home-assistant/core
/tests/components/event/ @home-assistant/core
/homeassistant/components/evil_genius_labs/ @balloob /homeassistant/components/evil_genius_labs/ @balloob
/tests/components/evil_genius_labs/ @balloob /tests/components/evil_genius_labs/ @balloob
/homeassistant/components/evohome/ @zxdavb /homeassistant/components/evohome/ @zxdavb
@ -425,6 +429,8 @@ build.json @home-assistant/supervisor
/tests/components/fully_kiosk/ @cgarwood /tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/garages_amsterdam/ @klaasnicolaas /homeassistant/components/garages_amsterdam/ @klaasnicolaas
/tests/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gardena_bluetooth/ @elupus
/tests/components/gardena_bluetooth/ @elupus
/homeassistant/components/gdacs/ @exxamalte /homeassistant/components/gdacs/ @exxamalte
/tests/components/gdacs/ @exxamalte /tests/components/gdacs/ @exxamalte
/homeassistant/components/generic/ @davet2001 /homeassistant/components/generic/ @davet2001
@ -745,7 +751,6 @@ build.json @home-assistant/supervisor
/tests/components/meteoclimatic/ @adrianmo /tests/components/meteoclimatic/ @adrianmo
/homeassistant/components/metoffice/ @MrHarcombe @avee87 /homeassistant/components/metoffice/ @MrHarcombe @avee87
/tests/components/metoffice/ @MrHarcombe @avee87 /tests/components/metoffice/ @MrHarcombe @avee87
/homeassistant/components/miflora/ @danielhiversen @basnijholt
/homeassistant/components/mikrotik/ @engrbm87 /homeassistant/components/mikrotik/ @engrbm87
/tests/components/mikrotik/ @engrbm87 /tests/components/mikrotik/ @engrbm87
/homeassistant/components/mill/ @danielhiversen /homeassistant/components/mill/ @danielhiversen
@ -889,6 +894,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/openhome/ @bazwilliams /homeassistant/components/openhome/ @bazwilliams
/tests/components/openhome/ @bazwilliams /tests/components/openhome/ @bazwilliams
/homeassistant/components/opensky/ @joostlek /homeassistant/components/opensky/ @joostlek
/tests/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23 /homeassistant/components/opentherm_gw/ @mvn23
/tests/components/opentherm_gw/ @mvn23 /tests/components/opentherm_gw/ @mvn23
/homeassistant/components/openuv/ @bachya /homeassistant/components/openuv/ @bachya
@ -897,6 +903,8 @@ build.json @home-assistant/supervisor
/tests/components/openweathermap/ @fabaff @freekode @nzapponi /tests/components/openweathermap/ @fabaff @freekode @nzapponi
/homeassistant/components/opnsense/ @mtreinish /homeassistant/components/opnsense/ @mtreinish
/tests/components/opnsense/ @mtreinish /tests/components/opnsense/ @mtreinish
/homeassistant/components/opower/ @tronikos
/tests/components/opower/ @tronikos
/homeassistant/components/oralb/ @bdraco @Lash-L /homeassistant/components/oralb/ @bdraco @Lash-L
/tests/components/oralb/ @bdraco @Lash-L /tests/components/oralb/ @bdraco @Lash-L
/homeassistant/components/oru/ @bvlaicu /homeassistant/components/oru/ @bvlaicu
@ -914,6 +922,8 @@ build.json @home-assistant/supervisor
/tests/components/panel_iframe/ @home-assistant/frontend /tests/components/panel_iframe/ @home-assistant/frontend
/homeassistant/components/peco/ @IceBotYT /homeassistant/components/peco/ @IceBotYT
/tests/components/peco/ @IceBotYT /tests/components/peco/ @IceBotYT
/homeassistant/components/pegel_online/ @mib1185
/tests/components/pegel_online/ @mib1185
/homeassistant/components/persistent_notification/ @home-assistant/core /homeassistant/components/persistent_notification/ @home-assistant/core
/tests/components/persistent_notification/ @home-assistant/core /tests/components/persistent_notification/ @home-assistant/core
/homeassistant/components/philips_js/ @elupus /homeassistant/components/philips_js/ @elupus
@ -1000,8 +1010,8 @@ build.json @home-assistant/supervisor
/tests/components/rapt_ble/ @sairon /tests/components/rapt_ble/ @sairon
/homeassistant/components/raspberry_pi/ @home-assistant/core /homeassistant/components/raspberry_pi/ @home-assistant/core
/tests/components/raspberry_pi/ @home-assistant/core /tests/components/raspberry_pi/ @home-assistant/core
/homeassistant/components/rdw/ @frenck /homeassistant/components/rdw/ @frenck @joostlek
/tests/components/rdw/ @frenck /tests/components/rdw/ @frenck @joostlek
/homeassistant/components/recollect_waste/ @bachya /homeassistant/components/recollect_waste/ @bachya
/tests/components/recollect_waste/ @bachya /tests/components/recollect_waste/ @bachya
/homeassistant/components/recorder/ @home-assistant/core /homeassistant/components/recorder/ @home-assistant/core

View File

@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant image: ghcr.io/home-assistant/{arch}-homeassistant
build_from: build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.1 aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.07.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.1 armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.07.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.1 armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.07.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.1 amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.07.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.1 i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.07.0
codenotary: codenotary:
signer: notary@home-assistant.io signer: notary@home-assistant.io
base_image: notary@home-assistant.io base_image: notary@home-assistant.io

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -1,32 +1,15 @@
"""Enum backports from standard lib.""" """Enum backports from standard lib.
This file contained the backport of the StrEnum of Python 3.11.
Since we have dropped support for Python 3.10, we can remove this backport.
This file is kept for now to avoid breaking custom components that might
import it.
"""
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
from typing import Any
from typing_extensions import Self __all__ = [
"StrEnum",
]
class StrEnum(str, Enum):
"""Partial backport of Python 3.11's StrEnum for our basic use cases."""
def __new__(cls, value: str, *args: Any, **kwargs: Any) -> Self:
"""Create a new StrEnum instance."""
if not isinstance(value, str):
raise TypeError(f"{value!r} is not a string")
return super().__new__(cls, value, *args, **kwargs)
def __str__(self) -> str:
"""Return self.value."""
return str(self.value)
@staticmethod
def _generate_next_value_(
name: str, start: int, count: int, last_values: list[Any]
) -> Any:
"""Make `auto()` explicitly unsupported.
We may revisit this when it's very clear that Python 3.11's
`StrEnum.auto()` behavior will no longer change.
"""
raise TypeError("auto() is not supported by this implementation")

View File

@ -3,27 +3,24 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from types import GenericAlias from types import GenericAlias
from typing import Any, Generic, TypeVar, overload from typing import Any, Generic, Self, TypeVar, overload
from typing_extensions import Self
_T = TypeVar("_T") _T = TypeVar("_T")
_R = TypeVar("_R")
class cached_property(Generic[_T, _R]): # pylint: disable=invalid-name class cached_property(Generic[_T]): # pylint: disable=invalid-name
"""Backport of Python 3.12's cached_property. """Backport of Python 3.12's cached_property.
Includes https://github.com/python/cpython/pull/101890/files Includes https://github.com/python/cpython/pull/101890/files
""" """
def __init__(self, func: Callable[[_T], _R]) -> None: def __init__(self, func: Callable[[Any], _T]) -> None:
"""Initialize.""" """Initialize."""
self.func = func self.func: Callable[[Any], _T] = func
self.attrname: Any = None self.attrname: str | None = None
self.__doc__ = func.__doc__ self.__doc__ = func.__doc__
def __set_name__(self, owner: type[_T], name: str) -> None: def __set_name__(self, owner: type[Any], name: str) -> None:
"""Set name.""" """Set name."""
if self.attrname is None: if self.attrname is None:
self.attrname = name self.attrname = name
@ -34,14 +31,16 @@ class cached_property(Generic[_T, _R]): # pylint: disable=invalid-name
) )
@overload @overload
def __get__(self, instance: None, owner: type[_T]) -> Self: def __get__(self, instance: None, owner: type[Any] | None = None) -> Self:
... ...
@overload @overload
def __get__(self, instance: _T, owner: type[_T]) -> _R: def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T:
... ...
def __get__(self, instance: _T | None, owner: type[_T] | None = None) -> _R | Self: def __get__(
self, instance: Any | None, owner: type[Any] | None = None
) -> _T | Self:
"""Get.""" """Get."""
if instance is None: if instance is None:
return self return self

View File

@ -1,5 +1,5 @@
{ {
"domain": "u_tec", "domain": "u_tec",
"name": "U-tec", "name": "U-tec",
"integrations": ["ultraloq"] "iot_standards": ["zwave"]
} }

View File

@ -1,10 +1,6 @@
capture_image: capture_image:
name: Capture image
description: Request a new image capture from a camera device.
fields: fields:
entity_id: entity_id:
name: Entity
description: Entity id of the camera to request an image.
required: true required: true
selector: selector:
entity: entity:
@ -12,31 +8,21 @@ capture_image:
domain: camera domain: camera
change_setting: change_setting:
name: Change setting
description: Change an Abode system setting.
fields: fields:
setting: setting:
name: Setting
description: Setting to change.
required: true required: true
example: beeper_mute example: beeper_mute
selector: selector:
text: text:
value: value:
name: Value
description: Value of the setting.
required: true required: true
example: "1" example: "1"
selector: selector:
text: text:
trigger_automation: trigger_automation:
name: Trigger automation
description: Trigger an Abode automation.
fields: fields:
entity_id: entity_id:
name: Entity
description: Entity id of the automation to trigger.
required: true required: true
selector: selector:
entity: entity:

View File

@ -15,7 +15,7 @@
} }
}, },
"reauth_confirm": { "reauth_confirm": {
"title": "Fill in your Abode login information", "title": "[%key:component::abode::config::step::user::title%]",
"data": { "data": {
"username": "[%key:common::config_flow::data::email%]", "username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
@ -31,5 +31,41 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
},
"services": {
"capture_image": {
"name": "Capture image",
"description": "Request a new image capture from a camera device.",
"fields": {
"entity_id": {
"name": "Entity",
"description": "Entity id of the camera to request an image."
}
}
},
"change_setting": {
"name": "Change setting",
"description": "Change an Abode system setting.",
"fields": {
"setting": {
"name": "Setting",
"description": "Setting to change."
},
"value": {
"name": "Value",
"description": "Value of the setting."
}
}
},
"trigger_automation": {
"name": "Trigger automation",
"description": "Trigger an Abode automation.",
"fields": {
"entity_id": {
"name": "Entity",
"description": "Entity id of the automation to trigger."
}
}
}
} }
} }

View File

@ -3,7 +3,7 @@
"name": "AccuWeather", "name": "AccuWeather",
"codeowners": ["@bieniu"], "codeowners": ["@bieniu"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/accuweather/", "documentation": "https://www.home-assistant.io/integrations/accuweather",
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["accuweather"], "loggers": ["accuweather"],

View File

@ -25,7 +25,6 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherDataUpdateCoordinator from . import AccuWeatherDataUpdateCoordinator
@ -50,7 +49,7 @@ PARALLEL_UPDATES = 1
class AccuWeatherSensorDescriptionMixin: class AccuWeatherSensorDescriptionMixin:
"""Mixin for AccuWeather sensor.""" """Mixin for AccuWeather sensor."""
value_fn: Callable[[dict[str, Any]], StateType] value_fn: Callable[[dict[str, Any]], str | int | float | None]
@dataclass @dataclass
@ -59,7 +58,7 @@ class AccuWeatherSensorDescription(
): ):
"""Class describing AccuWeather sensor entities.""" """Class describing AccuWeather sensor entities."""
attr_fn: Callable[[dict[str, Any]], dict[str, StateType]] = lambda _: {} attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
@ -428,7 +427,7 @@ class AccuWeatherSensor(
self.forecast_day = forecast_day self.forecast_day = forecast_day
@property @property
def native_value(self) -> StateType: def native_value(self) -> str | int | float | None:
"""Return the state.""" """Return the state."""
return self.entity_description.value_fn(self._sensor_data) return self.entity_description.value_fn(self._sensor_data)

View File

@ -14,6 +14,7 @@ from homeassistant.components.weather import (
ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_UV_INDEX,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
Forecast, Forecast,
WeatherEntity, WeatherEntity,
@ -147,6 +148,11 @@ class AccuWeatherEntity(
"""Return the visibility.""" """Return the visibility."""
return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE]) return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE])
@property
def uv_index(self) -> float:
"""Return the UV index."""
return cast(float, self.coordinator.data["UVIndex"])
@property @property
def forecast(self) -> list[Forecast] | None: def forecast(self) -> list[Forecast] | None:
"""Return the forecast array.""" """Return the forecast array."""
@ -172,6 +178,7 @@ class AccuWeatherEntity(
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item["WindGustDay"][ATTR_SPEED][ ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item["WindGustDay"][ATTR_SPEED][
ATTR_VALUE ATTR_VALUE
], ],
ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE],
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
ATTR_FORECAST_CONDITION: [ ATTR_FORECAST_CONDITION: [
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import telnetlib import telnetlib # pylint: disable=deprecated-module
from typing import Final from typing import Final
import voluptuous as vol import voluptuous as vol

View File

@ -1,65 +1,43 @@
add_url: add_url:
name: Add url
description: Add a new filter subscription to AdGuard Home.
fields: fields:
name: name:
name: Name
description: The name of the filter subscription.
required: true required: true
example: Example example: Example
selector: selector:
text: text:
url: url:
name: Url
description: The filter URL to subscribe to, containing the filter rules.
required: true required: true
example: https://www.example.com/filter/1.txt example: https://www.example.com/filter/1.txt
selector: selector:
text: text:
remove_url: remove_url:
name: Remove url
description: Removes a filter subscription from AdGuard Home.
fields: fields:
url: url:
name: Url
description: The filter subscription URL to remove.
required: true required: true
example: https://www.example.com/filter/1.txt example: https://www.example.com/filter/1.txt
selector: selector:
text: text:
enable_url: enable_url:
name: Enable url
description: Enables a filter subscription in AdGuard Home.
fields: fields:
url: url:
name: Url
description: The filter subscription URL to enable.
required: true required: true
example: https://www.example.com/filter/1.txt example: https://www.example.com/filter/1.txt
selector: selector:
text: text:
disable_url: disable_url:
name: Disable url
description: Disables a filter subscription in AdGuard Home.
fields: fields:
url: url:
name: Url
description: The filter subscription URL to disable.
required: true required: true
example: https://www.example.com/filter/1.txt example: https://www.example.com/filter/1.txt
selector: selector:
text: text:
refresh: refresh:
name: Refresh
description: Refresh all filter subscriptions in AdGuard Home.
fields: fields:
force: force:
name: Force
description: Force update (bypasses AdGuard Home throttling). "true" to force, or "false" to omit for a regular refresh.
default: false default: false
selector: selector:
boolean: boolean:

View File

@ -72,5 +72,61 @@
"name": "Query log" "name": "Query log"
} }
} }
},
"services": {
"add_url": {
"name": "Add URL",
"description": "Add a new filter subscription to AdGuard Home.",
"fields": {
"name": {
"name": "[%key:common::config_flow::data::name%]",
"description": "The name of the filter subscription."
},
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter URL to subscribe to, containing the filter rules."
}
}
},
"remove_url": {
"name": "Remove URL",
"description": "Removes a filter subscription from AdGuard Home.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to remove."
}
}
},
"enable_url": {
"name": "Enable URL",
"description": "Enables a filter subscription in AdGuard Home.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to enable."
}
}
},
"disable_url": {
"name": "Disable URL",
"description": "Disables a filter subscription in AdGuard Home.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to disable."
}
}
},
"refresh": {
"name": "Refresh",
"description": "Refresh all filter subscriptions in AdGuard Home.",
"fields": {
"force": {
"name": "Force",
"description": "Force update (bypasses AdGuard Home throttling). \"true\" to force, or \"false\" to omit for a regular refresh."
}
}
}
} }
} }

View File

@ -1,19 +1,13 @@
# Describes the format for available ADS services # Describes the format for available ADS services
write_data_by_name: write_data_by_name:
name: Write data by name
description: Write a value to the connected ADS device.
fields: fields:
adsvar: adsvar:
name: ADS variable
description: The name of the variable to write to.
required: true required: true
example: ".global_var" example: ".global_var"
selector: selector:
text: text:
adstype: adstype:
name: ADS type
description: The data type of the variable to write to.
required: true required: true
selector: selector:
select: select:
@ -25,8 +19,6 @@ write_data_by_name:
- "udint" - "udint"
- "uint" - "uint"
value: value:
name: Value
description: The value to write to the variable.
required: true required: true
selector: selector:
number: number:

View File

@ -0,0 +1,22 @@
{
"services": {
"write_data_by_name": {
"name": "Write data by name",
"description": "Write a value to the connected ADS device.",
"fields": {
"adsvar": {
"name": "ADS variable",
"description": "The name of the variable to write to."
},
"adstype": {
"name": "ADS type",
"description": "The data type of the variable to write to."
},
"value": {
"name": "Value",
"description": "The value to write to the variable."
}
}
}
}
}

View File

@ -1,14 +1,10 @@
set_time_to: set_time_to:
name: Set Time To
description: Control timers to turn the system on or off after a set number of minutes
target: target:
entity: entity:
integration: advantage_air integration: advantage_air
domain: sensor domain: sensor
fields: fields:
minutes: minutes:
name: Minutes
description: Minutes until action
required: true required: true
selector: selector:
number: number:

View File

@ -13,7 +13,19 @@
"port": "[%key:common::config_flow::data::port%]" "port": "[%key:common::config_flow::data::port%]"
}, },
"description": "Connect to the API of your Advantage Air wall mounted tablet.", "description": "Connect to the API of your Advantage Air wall mounted tablet.",
"title": "Connect" "title": "[%key:common::action::connect%]"
}
}
},
"services": {
"set_time_to": {
"name": "Set time to",
"description": "Controls timers to turn the system on or off after a set number of minutes.",
"fields": {
"minutes": {
"name": "Minutes",
"description": "Minutes until action."
}
} }
} }
} }

View File

@ -1,43 +1,29 @@
# Describes the format for available aftership services # Describes the format for available aftership services
add_tracking: add_tracking:
name: Add tracking
description: Add new tracking number to Aftership.
fields: fields:
tracking_number: tracking_number:
name: Tracking number
description: Tracking number for the new tracking
required: true required: true
example: "123456789" example: "123456789"
selector: selector:
text: text:
title: title:
name: Title
description: A custom title for the new tracking
example: "Laptop" example: "Laptop"
selector: selector:
text: text:
slug: slug:
name: Slug
description: Slug (carrier) of the new tracking
example: "USPS" example: "USPS"
selector: selector:
text: text:
remove_tracking: remove_tracking:
name: Remove tracking
description: Remove a tracking number from Aftership.
fields: fields:
tracking_number: tracking_number:
name: Tracking number
description: Tracking number of the tracking to remove
required: true required: true
example: "123456789" example: "123456789"
selector: selector:
text: text:
slug: slug:
name: Slug
description: Slug (carrier) of the tracking to remove
example: "USPS" example: "USPS"
selector: selector:
text: text:

View File

@ -0,0 +1,36 @@
{
"services": {
"add_tracking": {
"name": "Add tracking",
"description": "Adds a new tracking number to Aftership.",
"fields": {
"tracking_number": {
"name": "Tracking number",
"description": "Tracking number for the new tracking."
},
"title": {
"name": "Title",
"description": "A custom title for the new tracking."
},
"slug": {
"name": "Slug",
"description": "Slug (carrier) of the new tracking."
}
}
},
"remove_tracking": {
"name": "Remove tracking",
"description": "Removes a tracking number from Aftership.",
"fields": {
"tracking_number": {
"name": "[%key:component::aftership::services::add_tracking::fields::tracking_number::name%]",
"description": "Tracking number of the tracking to remove."
},
"slug": {
"name": "[%key:component::aftership::services::add_tracking::fields::slug::name%]",
"description": "Slug (carrier) of the tracking to remove."
}
}
}
}
}

View File

@ -47,14 +47,16 @@ class AgentBaseStation(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT | AlarmControlPanelEntityFeature.ARM_NIGHT
) )
_attr_has_entity_name = True
_attr_name = None
def __init__(self, client): def __init__(self, client):
"""Initialize the alarm control panel.""" """Initialize the alarm control panel."""
self._client = client self._client = client
self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}"
self._attr_unique_id = f"{client.unique}_CP" self._attr_unique_id = f"{client.unique}_CP"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, client.unique)}, identifiers={(AGENT_DOMAIN, client.unique)},
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
manufacturer="Agent", manufacturer="Agent",
model=CONST_ALARM_CONTROL_PANEL_NAME, model=CONST_ALARM_CONTROL_PANEL_NAME,
sw_version=client.version, sw_version=client.version,

View File

@ -72,12 +72,13 @@ class AgentCamera(MjpegCamera):
_attr_attribution = ATTRIBUTION _attr_attribution = ATTRIBUTION
_attr_should_poll = True # Cameras default to False _attr_should_poll = True # Cameras default to False
_attr_supported_features = CameraEntityFeature.ON_OFF _attr_supported_features = CameraEntityFeature.ON_OFF
_attr_has_entity_name = True
_attr_name = None
def __init__(self, device): def __init__(self, device):
"""Initialize as a subclass of MjpegCamera.""" """Initialize as a subclass of MjpegCamera."""
self.device = device self.device = device
self._removed = False self._removed = False
self._attr_name = f"{device.client.name} {device.name}"
self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
super().__init__( super().__init__(
name=device.name, name=device.name,
@ -88,7 +89,7 @@ class AgentCamera(MjpegCamera):
identifiers={(AGENT_DOMAIN, self.unique_id)}, identifiers={(AGENT_DOMAIN, self.unique_id)},
manufacturer="Agent", manufacturer="Agent",
model="Camera", model="Camera",
name=self.name, name=f"{device.client.name} {device.name}",
sw_version=device.client.version, sw_version=device.client.version,
) )

View File

@ -3,7 +3,7 @@
"name": "Agent DVR", "name": "Agent DVR",
"codeowners": ["@ispysoftware"], "codeowners": ["@ispysoftware"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/agent_dvr/", "documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["agent"], "loggers": ["agent"],
"requirements": ["agent-py==0.0.23"] "requirements": ["agent-py==0.0.23"]

View File

@ -1,38 +1,28 @@
start_recording: start_recording:
name: Start recording
description: Enable continuous recording.
target: target:
entity: entity:
integration: agent_dvr integration: agent_dvr
domain: camera domain: camera
stop_recording: stop_recording:
name: Stop recording
description: Disable continuous recording.
target: target:
entity: entity:
integration: agent_dvr integration: agent_dvr
domain: camera domain: camera
enable_alerts: enable_alerts:
name: Enable alerts
description: Enable alerts
target: target:
entity: entity:
integration: agent_dvr integration: agent_dvr
domain: camera domain: camera
disable_alerts: disable_alerts:
name: Disable alerts
description: Disable alerts
target: target:
entity: entity:
integration: agent_dvr integration: agent_dvr
domain: camera domain: camera
snapshot: snapshot:
name: Snapshot
description: Take a photo
target: target:
entity: entity:
integration: agent_dvr integration: agent_dvr

View File

@ -16,5 +16,27 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
} }
},
"services": {
"start_recording": {
"name": "Start recording",
"description": "Enables continuous recording."
},
"stop_recording": {
"name": "Stop recording",
"description": "Disables continuous recording."
},
"enable_alerts": {
"name": "Enable alerts",
"description": "Enables alerts."
},
"disable_alerts": {
"name": "Disable alerts",
"description": "Disables alerts."
},
"snapshot": {
"name": "Snapshot",
"description": "Takes a photo."
}
} }
} }

View File

@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
str(longitude), str(longitude),
), ),
): ):
device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type] device_entry = device_registry.async_get_device(identifiers={old_ids}) # type: ignore[arg-type]
if device_entry and entry.entry_id in device_entry.config_entries: if device_entry and entry.entry_id in device_entry.config_entries:
new_ids = (DOMAIN, f"{latitude}-{longitude}") new_ids = (DOMAIN, f"{latitude}-{longitude}")
device_registry.async_update_device( device_registry.async_update_device(

View File

@ -17,7 +17,7 @@
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]", "already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"wrong_location": "No Airly measuring stations in this area." "wrong_location": "[%key:component::airly::config::error::wrong_location%]"
} }
}, },
"system_health": { "system_health": {

View File

@ -2,7 +2,7 @@
import logging import logging
from pyairnow import WebServiceAPI from pyairnow import WebServiceAPI
from pyairnow.errors import AirNowError, InvalidKeyError from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, exceptions
@ -35,6 +35,8 @@ async def validate_input(hass: core.HomeAssistant, data):
raise InvalidAuth from exc raise InvalidAuth from exc
except AirNowError as exc: except AirNowError as exc:
raise CannotConnect from exc raise CannotConnect from exc
except EmptyResponseError as exc:
raise InvalidLocation from exc
if not test_data: if not test_data:
raise InvalidLocation raise InvalidLocation

View File

@ -14,7 +14,7 @@
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_location": "No results found for that location", "invalid_location": "No results found for that location, try changing the location or station radius.",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {

View File

@ -3,7 +3,20 @@
"name": "Airthings BLE", "name": "Airthings BLE",
"bluetooth": [ "bluetooth": [
{ {
"manufacturer_id": 820 "manufacturer_id": 820,
"service_uuid": "b42e1f6e-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e4a8e-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e1c08-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba"
} }
], ],
"codeowners": ["@vincegio"], "codeowners": ["@vincegio"],

View File

@ -84,6 +84,9 @@ async def async_setup_entry(
class AirtouchAC(CoordinatorEntity, ClimateEntity): class AirtouchAC(CoordinatorEntity, ClimateEntity):
"""Representation of an AirTouch 4 ac.""" """Representation of an AirTouch 4 ac."""
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = ( _attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
) )
@ -107,7 +110,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
"""Return device info for this device.""" """Return device info for this device."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
name=self.name, name=f"AC {self._ac_number}",
manufacturer="Airtouch", manufacturer="Airtouch",
model="Airtouch 4", model="Airtouch 4",
) )
@ -122,11 +125,6 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
"""Return the current temperature.""" """Return the current temperature."""
return self._unit.Temperature return self._unit.Temperature
@property
def name(self):
"""Return the name of the climate device."""
return f"AC {self._ac_number}"
@property @property
def fan_mode(self): def fan_mode(self):
"""Return fan mode of the AC this group belongs to.""" """Return fan mode of the AC this group belongs to."""
@ -200,6 +198,8 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
class AirtouchGroup(CoordinatorEntity, ClimateEntity): class AirtouchGroup(CoordinatorEntity, ClimateEntity):
"""Representation of an AirTouch 4 group.""" """Representation of an AirTouch 4 group."""
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = AT_GROUP_MODES _attr_hvac_modes = AT_GROUP_MODES
@ -224,7 +224,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
identifiers={(DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer="Airtouch", manufacturer="Airtouch",
model="Airtouch 4", model="Airtouch 4",
name=self.name, name=self._unit.GroupName,
) )
@property @property
@ -242,11 +242,6 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
"""Return Max Temperature for AC of this group.""" """Return Max Temperature for AC of this group."""
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint
@property
def name(self):
"""Return the name of the climate device."""
return self._unit.GroupName
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""

View File

@ -11,7 +11,7 @@
} }
}, },
"geography_by_name": { "geography_by_name": {
"title": "Configure a Geography", "title": "[%key:component::airvisual::config::step::geography_by_coords::title%]",
"description": "Use the AirVisual cloud API to monitor a city/state/country.", "description": "Use the AirVisual cloud API to monitor a city/state/country.",
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]", "api_key": "[%key:common::config_flow::data::api_key%]",
@ -45,7 +45,7 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "Configure AirVisual", "title": "[%key:component::airvisual::config::step::user::title%]",
"data": { "data": {
"show_on_map": "Show monitored geography on the map" "show_on_map": "Show monitored geography on the map"
} }

View File

@ -60,6 +60,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Get data from the device.""" """Get data from the device."""
try: try:
data = await node.async_get_latest_measurements() data = await node.async_get_latest_measurements()
data["history"] = {}
if data["settings"].get("follow_mode") == "device":
history = await node.async_get_history(include_trends=False)
data["history"] = history.get("measurements", [])[-1]
except InvalidAuthenticationError as err: except InvalidAuthenticationError as err:
raise ConfigEntryAuthFailed("Invalid Samba password") from err raise ConfigEntryAuthFailed("Invalid Samba password") from err
except NodeConnectionError as err: except NodeConnectionError as err:

View File

@ -30,7 +30,9 @@ from .const import DOMAIN
class AirVisualProMeasurementKeyMixin: class AirVisualProMeasurementKeyMixin:
"""Define an entity description mixin to include a measurement key.""" """Define an entity description mixin to include a measurement key."""
value_fn: Callable[[dict[str, Any], dict[str, Any], dict[str, Any]], float | int] value_fn: Callable[
[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]], float | int
]
@dataclass @dataclass
@ -43,75 +45,81 @@ class AirVisualProMeasurementDescription(
SENSOR_DESCRIPTIONS = ( SENSOR_DESCRIPTIONS = (
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="air_quality_index", key="air_quality_index",
name="Air quality index",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements[ value_fn=lambda settings, status, measurements, history: measurements[
async_get_aqi_locale(settings) async_get_aqi_locale(settings)
], ],
), ),
AirVisualProMeasurementDescription(
key="outdoor_air_quality_index",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: int(
history.get(
f'Outdoor {"AQI(US)" if settings["is_aqi_usa"] else "AQI(CN)"}', -1
)
),
translation_key="outdoor_air_quality_index",
),
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="battery_level", key="battery_level",
name="Battery",
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value_fn=lambda settings, status, measurements: status["battery"], value_fn=lambda settings, status, measurements, history: status["battery"],
), ),
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="carbon_dioxide", key="carbon_dioxide",
name="C02",
device_class=SensorDeviceClass.CO2, device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["co2"], value_fn=lambda settings, status, measurements, history: measurements["co2"],
), ),
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="humidity", key="humidity",
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value_fn=lambda settings, status, measurements: measurements["humidity"], value_fn=lambda settings, status, measurements, history: measurements[
"humidity"
],
), ),
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="particulate_matter_0_1", key="particulate_matter_0_1",
name="PM 0.1", translation_key="pm01",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["pm0_1"], value_fn=lambda settings, status, measurements, history: measurements["pm0_1"],
), ),
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="particulate_matter_1_0", key="particulate_matter_1_0",
name="PM 1.0", device_class=SensorDeviceClass.PM1,
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["pm1_0"], value_fn=lambda settings, status, measurements, history: measurements["pm1_0"],
), ),
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="particulate_matter_2_5", key="particulate_matter_2_5",
name="PM 2.5",
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["pm2_5"], value_fn=lambda settings, status, measurements, history: measurements["pm2_5"],
), ),
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="temperature", key="temperature",
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["temperature_C"], value_fn=lambda settings, status, measurements, history: measurements[
"temperature_C"
],
), ),
AirVisualProMeasurementDescription( AirVisualProMeasurementDescription(
key="voc", key="voc",
name="VOC",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["voc"], value_fn=lambda settings, status, measurements, history: measurements["voc"],
), ),
) )
@ -150,4 +158,5 @@ class AirVisualProSensor(AirVisualProEntity, SensorEntity):
self.coordinator.data["settings"], self.coordinator.data["settings"],
self.coordinator.data["status"], self.coordinator.data["status"],
self.coordinator.data["measurements"], self.coordinator.data["measurements"],
self.coordinator.data["history"],
) )

View File

@ -24,5 +24,15 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
},
"entity": {
"sensor": {
"pm01": {
"name": "PM0.1"
},
"outdoor_air_quality_index": {
"name": "Outdoor air quality index"
}
}
} }
} }

View File

@ -4,7 +4,14 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Final from typing import Any, Final
from aioairzone_cloud.const import AZD_PROBLEMS, AZD_WARNINGS, AZD_ZONES from aioairzone_cloud.const import (
AZD_ACTIVE,
AZD_AIDOOS,
AZD_ERRORS,
AZD_PROBLEMS,
AZD_WARNINGS,
AZD_ZONES,
)
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
@ -18,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AirzoneUpdateCoordinator from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneZoneEntity from .entity import AirzoneAidooEntity, AirzoneEntity, AirzoneZoneEntity
@dataclass @dataclass
@ -28,7 +35,27 @@ class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
attributes: dict[str, str] | None = None attributes: dict[str, str] | None = None
AIDOO_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = (
AirzoneBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.RUNNING,
key=AZD_ACTIVE,
),
AirzoneBinarySensorEntityDescription(
attributes={
"errors": AZD_ERRORS,
"warnings": AZD_WARNINGS,
},
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
key=AZD_PROBLEMS,
),
)
ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = (
AirzoneBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.RUNNING,
key=AZD_ACTIVE,
),
AirzoneBinarySensorEntityDescription( AirzoneBinarySensorEntityDescription(
attributes={ attributes={
"warnings": AZD_WARNINGS, "warnings": AZD_WARNINGS,
@ -48,6 +75,18 @@ async def async_setup_entry(
binary_sensors: list[AirzoneBinarySensor] = [] binary_sensors: list[AirzoneBinarySensor] = []
for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items():
for description in AIDOO_BINARY_SENSOR_TYPES:
if description.key in aidoo_data:
binary_sensors.append(
AirzoneAidooBinarySensor(
coordinator,
description,
aidoo_id,
aidoo_data,
)
)
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items():
for description in ZONE_BINARY_SENSOR_TYPES: for description in ZONE_BINARY_SENSOR_TYPES:
if description.key in zone_data: if description.key in zone_data:
@ -85,6 +124,27 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
} }
class AirzoneAidooBinarySensor(AirzoneAidooEntity, AirzoneBinarySensor):
"""Define an Airzone Cloud Aidoo binary sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
description: AirzoneBinarySensorEntityDescription,
aidoo_id: str,
aidoo_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, aidoo_id, aidoo_data)
self._attr_unique_id = f"{aidoo_id}_{description.key}"
self.entity_description = description
self._async_update_attrs()
class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor): class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor):
"""Define an Airzone Cloud Zone binary sensor.""" """Define an Airzone Cloud Zone binary sensor."""

View File

@ -7,6 +7,7 @@ from typing import Any
from aioairzone_cloud.const import ( from aioairzone_cloud.const import (
API_CITY, API_CITY,
API_GROUP_ID, API_GROUP_ID,
API_GROUPS,
API_LOCATION_ID, API_LOCATION_ID,
API_OLD_ID, API_OLD_ID,
API_PIN, API_PIN,
@ -29,7 +30,6 @@ from .coordinator import AirzoneUpdateCoordinator
TO_REDACT_API = [ TO_REDACT_API = [
API_CITY, API_CITY,
API_GROUP_ID,
API_LOCATION_ID, API_LOCATION_ID,
API_OLD_ID, API_OLD_ID,
API_PIN, API_PIN,
@ -58,11 +58,17 @@ def gather_ids(api_data: dict[str, Any]) -> dict[str, Any]:
ids[dev_id] = f"device{dev_idx}" ids[dev_id] = f"device{dev_idx}"
dev_idx += 1 dev_idx += 1
group_idx = 1
inst_idx = 1 inst_idx = 1
for inst_id in api_data[RAW_INSTALLATIONS]: for inst_id, inst_data in api_data[RAW_INSTALLATIONS].items():
if inst_id not in ids: if inst_id not in ids:
ids[inst_id] = f"installation{inst_idx}" ids[inst_id] = f"installation{inst_idx}"
inst_idx += 1 inst_idx += 1
for group in inst_data[API_GROUPS]:
group_id = group[API_GROUP_ID]
if group_id not in ids:
ids[group_id] = f"group{group_idx}"
group_idx += 1
ws_idx = 1 ws_idx = 1
for ws_id in api_data[RAW_WEBSERVERS]: for ws_id in api_data[RAW_WEBSERVERS]:

View File

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

View File

@ -141,6 +141,8 @@ class AirzoneSensor(AirzoneEntity, SensorEntity):
class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor): class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor):
"""Define an Airzone Cloud Aidoo sensor.""" """Define an Airzone Cloud Aidoo sensor."""
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: AirzoneUpdateCoordinator, coordinator: AirzoneUpdateCoordinator,
@ -151,7 +153,6 @@ class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor):
"""Initialize.""" """Initialize."""
super().__init__(coordinator, aidoo_id, aidoo_data) super().__init__(coordinator, aidoo_id, aidoo_data)
self._attr_has_entity_name = True
self._attr_unique_id = f"{aidoo_id}_{description.key}" self._attr_unique_id = f"{aidoo_id}_{description.key}"
self.entity_description = description self.entity_description = description
@ -161,6 +162,8 @@ class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor):
class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor): class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
"""Define an Airzone Cloud WebServer sensor.""" """Define an Airzone Cloud WebServer sensor."""
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: AirzoneUpdateCoordinator, coordinator: AirzoneUpdateCoordinator,
@ -171,7 +174,6 @@ class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
"""Initialize.""" """Initialize."""
super().__init__(coordinator, ws_id, ws_data) super().__init__(coordinator, ws_id, ws_data)
self._attr_has_entity_name = True
self._attr_unique_id = f"{ws_id}_{description.key}" self._attr_unique_id = f"{ws_id}_{description.key}"
self.entity_description = description self.entity_description = description
@ -181,6 +183,8 @@ class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor): class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor):
"""Define an Airzone Cloud Zone sensor.""" """Define an Airzone Cloud Zone sensor."""
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: AirzoneUpdateCoordinator, coordinator: AirzoneUpdateCoordinator,
@ -191,7 +195,6 @@ class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor):
"""Initialize.""" """Initialize."""
super().__init__(coordinator, zone_id, zone_data) super().__init__(coordinator, zone_id, zone_data)
self._attr_has_entity_name = True
self._attr_unique_id = f"{zone_id}_{description.key}" self._attr_unique_id = f"{zone_id}_{description.key}"
self.entity_description = description self.entity_description = description

View File

@ -1,9 +1,7 @@
"""Provides the constants needed for component.""" """Provides the constants needed for component."""
from enum import IntFlag from enum import IntFlag, StrEnum
from typing import Final from typing import Final
from homeassistant.backports.enum import StrEnum
DOMAIN: Final = "alarm_control_panel" DOMAIN: Final = "alarm_control_panel"
ATTR_CHANGED_BY: Final = "changed_by" ATTR_CHANGED_BY: Final = "changed_by"

View File

@ -1,99 +1,83 @@
# Describes the format for available alarm control panel services # Describes the format for available alarm control panel services
alarm_disarm: alarm_disarm:
name: Disarm
description: Send the alarm the command for disarm.
target: target:
entity: entity:
domain: alarm_control_panel domain: alarm_control_panel
fields: fields:
code: code:
name: Code
description: An optional code to disarm the alarm control panel with.
example: "1234" example: "1234"
selector: selector:
text: text:
alarm_arm_custom_bypass: alarm_arm_custom_bypass:
name: Arm with custom bypass
description: Send arm custom bypass command.
target: target:
entity: entity:
domain: alarm_control_panel domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
fields: fields:
code: code:
name: Code
description: An optional code to arm custom bypass the alarm control panel with.
example: "1234" example: "1234"
selector: selector:
text: text:
alarm_arm_home: alarm_arm_home:
name: Arm home
description: Send the alarm the command for arm home.
target: target:
entity: entity:
domain: alarm_control_panel domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
fields: fields:
code: code:
name: Code
description: An optional code to arm home the alarm control panel with.
example: "1234" example: "1234"
selector: selector:
text: text:
alarm_arm_away: alarm_arm_away:
name: Arm away
description: Send the alarm the command for arm away.
target: target:
entity: entity:
domain: alarm_control_panel domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
fields: fields:
code: code:
name: Code
description: An optional code to arm away the alarm control panel with.
example: "1234" example: "1234"
selector: selector:
text: text:
alarm_arm_night: alarm_arm_night:
name: Arm night
description: Send the alarm the command for arm night.
target: target:
entity: entity:
domain: alarm_control_panel domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
fields: fields:
code: code:
name: Code
description: An optional code to arm night the alarm control panel with.
example: "1234" example: "1234"
selector: selector:
text: text:
alarm_arm_vacation: alarm_arm_vacation:
name: Arm vacation
description: Send the alarm the command for arm vacation.
target: target:
entity: entity:
domain: alarm_control_panel domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
fields: fields:
code: code:
name: Code
description: An optional code to arm vacation the alarm control panel with.
example: "1234" example: "1234"
selector: selector:
text: text:
alarm_trigger: alarm_trigger:
name: Trigger
description: Send the alarm the command for trigger.
target: target:
entity: entity:
domain: alarm_control_panel domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.TRIGGER
fields: fields:
code: code:
name: Code
description: An optional code to trigger the alarm control panel with.
example: "1234" example: "1234"
selector: selector:
text: text:

View File

@ -63,10 +63,76 @@
} }
} }
}, },
"issues": { "services": {
"platform_integration_no_support": { "alarm_disarm": {
"title": "[%key:common::issues::platform_integration_no_support_title%]", "name": "Disarm",
"description": "[%key:common::issues::platform_integration_no_support_description%]" "description": "Disarms the alarm.",
"fields": {
"code": {
"name": "Code",
"description": "Code to disarm the alarm."
}
}
},
"alarm_arm_custom_bypass": {
"name": "Arm with custom bypass",
"description": "Arms the alarm while allowing to bypass a custom area.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "Code to arm the alarm."
}
}
},
"alarm_arm_home": {
"name": "Arm home",
"description": "Sets the alarm to: _armed, but someone is home_.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
},
"alarm_arm_away": {
"name": "Arm away",
"description": "Sets the alarm to: _armed, no one home_.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
},
"alarm_arm_night": {
"name": "Arm night",
"description": "Sets the alarm to: _armed for the night_.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
},
"alarm_arm_vacation": {
"name": "Arm vacation",
"description": "Sets the alarm to: _armed for vacation_.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
},
"alarm_trigger": {
"name": "Trigger",
"description": "Enables an external alarm trigger.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
} }
} }
} }

View File

@ -1,30 +1,22 @@
alarm_keypress: alarm_keypress:
name: Key press
description: Send custom keypresses to the alarm.
target: target:
entity: entity:
integration: alarmdecoder integration: alarmdecoder
domain: alarm_control_panel domain: alarm_control_panel
fields: fields:
keypress: keypress:
name: Key press
description: "String to send to the alarm panel."
required: true required: true
example: "*71" example: "*71"
selector: selector:
text: text:
alarm_toggle_chime: alarm_toggle_chime:
name: Toggle Chime
description: Send the alarm the toggle chime command.
target: target:
entity: entity:
integration: alarmdecoder integration: alarmdecoder
domain: alarm_control_panel domain: alarm_control_panel
fields: fields:
code: code:
name: Code
description: A code to toggle the alarm control panel chime with.
required: true required: true
example: 1234 example: 1234
selector: selector:

View File

@ -20,7 +20,9 @@
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}, },
"create_entry": { "default": "Successfully connected to AlarmDecoder." }, "create_entry": {
"default": "Successfully connected to AlarmDecoder."
},
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
} }
@ -35,7 +37,7 @@
} }
}, },
"arm_settings": { "arm_settings": {
"title": "Configure AlarmDecoder", "title": "[%key:component::alarmdecoder::options::step::init::title%]",
"data": { "data": {
"auto_bypass": "Auto Bypass on Arm", "auto_bypass": "Auto Bypass on Arm",
"code_arm_required": "Code Required for Arming", "code_arm_required": "Code Required for Arming",
@ -43,14 +45,14 @@
} }
}, },
"zone_select": { "zone_select": {
"title": "Configure AlarmDecoder", "title": "[%key:component::alarmdecoder::options::step::init::title%]",
"description": "Enter the zone number you'd like to to add, edit, or remove.", "description": "Enter the zone number you'd like to to add, edit, or remove.",
"data": { "data": {
"zone_number": "Zone Number" "zone_number": "Zone Number"
} }
}, },
"zone_details": { "zone_details": {
"title": "Configure AlarmDecoder", "title": "[%key:component::alarmdecoder::options::step::init::title%]",
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.",
"data": { "data": {
"zone_name": "Zone Name", "zone_name": "Zone Name",
@ -68,5 +70,27 @@
"loop_rfid": "RF Loop cannot be used without RF Serial.", "loop_rfid": "RF Loop cannot be used without RF Serial.",
"loop_range": "RF Loop must be an integer between 1 and 4." "loop_range": "RF Loop must be an integer between 1 and 4."
} }
},
"services": {
"alarm_keypress": {
"name": "Key press",
"description": "Sends custom keypresses to the alarm.",
"fields": {
"keypress": {
"name": "[%key:component::alarmdecoder::services::alarm_keypress::name%]",
"description": "String to send to the alarm panel."
}
}
},
"alarm_toggle_chime": {
"name": "Toggle chime",
"description": "Sends the alarm the toggle chime command.",
"fields": {
"code": {
"name": "Code",
"description": "Code to toggle the alarm control panel chime with."
}
}
}
} }
} }

View File

@ -25,16 +25,17 @@ from homeassistant.const import (
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
) )
from homeassistant.core import Event, HassJob, HomeAssistant from homeassistant.core import HassJob, HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
EventStateChangedData,
async_track_point_in_time, async_track_point_in_time,
async_track_state_change_event, async_track_state_change_event,
) )
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType, EventType
from homeassistant.util.dt import now from homeassistant.util.dt import now
from .const import ( from .const import (
@ -196,11 +197,13 @@ class Alert(Entity):
return STATE_ON return STATE_ON
return STATE_IDLE return STATE_IDLE
async def watched_entity_change(self, event: Event) -> None: async def watched_entity_change(
self, event: EventType[EventStateChangedData]
) -> None:
"""Determine if the alert should start or stop.""" """Determine if the alert should start or stop."""
if (to_state := event.data.get("new_state")) is None: if (to_state := event.data["new_state"]) is None:
return return
LOGGER.debug("Watched entity (%s) has changed", event.data.get("entity_id")) LOGGER.debug("Watched entity (%s) has changed", event.data["entity_id"])
if to_state.state == self._alert_state and not self._firing: if to_state.state == self._alert_state and not self._firing:
await self.begin_alerting() await self.begin_alerting()
if to_state.state != self._alert_state and self._firing: if to_state.state != self._alert_state and self._firing:

View File

@ -1,20 +1,14 @@
toggle: toggle:
name: Toggle
description: Toggle alert's notifications.
target: target:
entity: entity:
domain: alert domain: alert
turn_off: turn_off:
name: Turn off
description: Silence alert's notifications.
target: target:
entity: entity:
domain: alert domain: alert
turn_on: turn_on:
name: Turn on
description: Reset alert's notifications.
target: target:
entity: entity:
domain: alert domain: alert

View File

@ -9,5 +9,19 @@
"on": "[%key:common::state::active%]" "on": "[%key:common::state::active%]"
} }
} }
},
"services": {
"toggle": {
"name": "[%key:common::action::toggle%]",
"description": "Toggles alert's notifications."
},
"turn_off": {
"name": "[%key:common::action::turn_off%]",
"description": "Silences alert's notifications."
},
"turn_on": {
"name": "[%key:common::action::turn_on%]",
"description": "Resets alert's notifications."
}
} }
} }

View File

@ -14,6 +14,7 @@ from homeassistant.components import (
camera, camera,
climate, climate,
cover, cover,
event,
fan, fan,
group, group,
humidifier, humidifier,
@ -527,6 +528,26 @@ class CoverCapabilities(AlexaEntity):
yield Alexa(self.entity) yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(event.DOMAIN)
class EventCapabilities(AlexaEntity):
"""Class to represent doorbel event capabilities."""
def default_display_categories(self) -> list[str] | None:
"""Return the display categories for this entity."""
attrs = self.entity.attributes
device_class: event.EventDeviceClass | None = attrs.get(ATTR_DEVICE_CLASS)
if device_class == event.EventDeviceClass.DOORBELL:
return [DisplayCategory.DOORBELL]
return None
def interfaces(self) -> Generator[AlexaCapability, None, None]:
"""Yield the supported interfaces."""
if self.default_display_categories() is not None:
yield AlexaDoorbellEventSource(self.entity)
yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(light.DOMAIN) @ENTITY_ADAPTERS.register(light.DOMAIN)
class LightCapabilities(AlexaEntity): class LightCapabilities(AlexaEntity):
"""Class to represent Light capabilities.""" """Class to represent Light capabilities."""

View File

@ -857,21 +857,47 @@ async def async_api_adjust_target_temp(
temp_delta = temperature_from_object( temp_delta = temperature_from_object(
hass, directive.payload["targetSetpointDelta"], interval=True hass, directive.payload["targetSetpointDelta"], interval=True
) )
response = directive.response()
current_target_temp_high = entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
current_target_temp_low = entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
if current_target_temp_high and current_target_temp_low:
target_temp_high = float(current_target_temp_high) + temp_delta
if target_temp_high < min_temp or target_temp_high > max_temp:
raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp)
target_temp_low = float(current_target_temp_low) + temp_delta
if target_temp_low < min_temp or target_temp_low > max_temp:
raise AlexaTempRangeError(hass, target_temp_low, min_temp, max_temp)
data = {
ATTR_ENTITY_ID: entity.entity_id,
climate.ATTR_TARGET_TEMP_HIGH: target_temp_high,
climate.ATTR_TARGET_TEMP_LOW: target_temp_low,
}
response.add_context_property(
{
"name": "upperSetpoint",
"namespace": "Alexa.ThermostatController",
"value": {"value": target_temp_high, "scale": API_TEMP_UNITS[unit]},
}
)
response.add_context_property(
{
"name": "lowerSetpoint",
"namespace": "Alexa.ThermostatController",
"value": {"value": target_temp_low, "scale": API_TEMP_UNITS[unit]},
}
)
else:
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp: if target_temp < min_temp or target_temp > max_temp:
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp} data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp}
response = directive.response()
await hass.services.async_call(
entity.domain,
climate.SERVICE_SET_TEMPERATURE,
data,
blocking=False,
context=context,
)
response.add_context_property( response.add_context_property(
{ {
"name": "targetSetpoint", "name": "targetSetpoint",
@ -880,6 +906,14 @@ async def async_api_adjust_target_temp(
} }
) )
await hass.services.async_call(
entity.domain,
climate.SERVICE_SET_TEMPERATURE,
data,
blocking=False,
context=context,
)
return response return response

View File

@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, cast
import aiohttp import aiohttp
import async_timeout import async_timeout
from homeassistant.components import event
from homeassistant.const import MATCH_ALL, STATE_ON from homeassistant.const import MATCH_ALL, STATE_ON
from homeassistant.core import HomeAssistant, State, callback from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -91,8 +92,10 @@ async def async_enable_proactive_mode(hass, smart_home_config):
return return
if should_doorbell: if should_doorbell:
if new_state.state == STATE_ON and ( if (
old_state is None or old_state.state != STATE_ON new_state.domain == event.DOMAIN
or new_state.state == STATE_ON
and (old_state is None or old_state.state != STATE_ON)
): ):
await async_send_doorbell_event_message( await async_send_doorbell_event_message(
hass, smart_home_config, alexa_changed_entity hass, smart_home_config, alexa_changed_entity

View File

@ -4,9 +4,10 @@ from amberelectric import Configuration
from amberelectric.api import amber_api from amberelectric.api import amber_api
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, PLATFORMS from .const import CONF_SITE_ID, DOMAIN, PLATFORMS
from .coordinator import AmberUpdateCoordinator from .coordinator import AmberUpdateCoordinator

View File

@ -3,7 +3,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"api_token": "API Token", "api_token": "[%key:common::config_flow::data::api_token%]",
"site_id": "Site ID" "site_id": "Site ID"
}, },
"description": "Go to {api_url} to generate an API key" "description": "Go to {api_url} to generate an API key"

View File

@ -154,17 +154,18 @@ class AmbiclimateEntity(ClimateEntity):
_attr_target_temperature_step = 1 _attr_target_temperature_step = 1
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_has_entity_name = True
_attr_name = None
def __init__(self, heater, store): def __init__(self, heater, store):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self._heater = heater self._heater = heater
self._store = store self._store = store
self._attr_unique_id = heater.device_id self._attr_unique_id = heater.device_id
self._attr_name = heater.name
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer="Ambiclimate", manufacturer="Ambiclimate",
name=self.name, name=heater.name,
) )
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:

View File

@ -1,53 +1,34 @@
# Describes the format for available services for ambiclimate # Describes the format for available services for ambiclimate
set_comfort_mode: set_comfort_mode:
name: Set comfort mode
description: >
Enable comfort mode on your AC.
fields: fields:
name: name:
description: >
String with device name.
required: true required: true
example: Bedroom example: Bedroom
selector: selector:
text: text:
send_comfort_feedback: send_comfort_feedback:
name: Send comfort feedback
description: >
Send feedback for comfort mode.
fields: fields:
name: name:
description: >
String with device name.
required: true required: true
example: Bedroom example: Bedroom
selector: selector:
text: text:
value: value:
description: >
Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing
required: true required: true
example: bit_warm example: bit_warm
selector: selector:
text: text:
set_temperature_mode: set_temperature_mode:
name: Set temperature mode
description: >
Enable temperature mode on your AC.
fields: fields:
name: name:
description: >
String with device name.
required: true required: true
example: Bedroom example: Bedroom
selector: selector:
text: text:
value: value:
description: >
Target value in celsius
required: true required: true
example: 22 example: 22
selector: selector:

View File

@ -18,5 +18,45 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"access_token": "Unknown error generating an access token." "access_token": "Unknown error generating an access token."
} }
},
"services": {
"set_comfort_mode": {
"name": "Set comfort mode",
"description": "Enables comfort mode on your AC.",
"fields": {
"name": {
"name": "Device name",
"description": "String with device name."
}
}
},
"send_comfort_feedback": {
"name": "Send comfort feedback",
"description": "Sends feedback for comfort mode.",
"fields": {
"name": {
"name": "[%key:component::ambiclimate::services::set_comfort_mode::fields::name::name%]",
"description": "[%key:component::ambiclimate::services::set_comfort_mode::fields::name::description%]"
},
"value": {
"name": "Comfort value",
"description": "Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing\n."
}
}
},
"set_temperature_mode": {
"name": "Set temperature mode",
"description": "Enables temperature mode on your AC.",
"fields": {
"name": {
"name": "[%key:component::ambiclimate::services::set_comfort_mode::fields::name::name%]",
"description": "[%key:component::ambiclimate::services::set_comfort_mode::fields::name::description%]"
},
"value": {
"name": "Temperature",
"description": "Target value in celsius."
}
}
}
} }
} }

View File

@ -210,8 +210,9 @@ class AmcrestChecker(ApiWrapper):
self, *args: Any, **kwargs: Any self, *args: Any, **kwargs: Any
) -> AsyncIterator[httpx.Response]: ) -> AsyncIterator[httpx.Response]:
"""amcrest.ApiWrapper.command wrapper to catch errors.""" """amcrest.ApiWrapper.command wrapper to catch errors."""
async with self._async_command_wrapper(): async with self._async_command_wrapper(), super().async_stream_command(
async with super().async_stream_command(*args, **kwargs) as ret: *args, **kwargs
) as ret:
yield ret yield ret
@asynccontextmanager @asynccontextmanager

View File

@ -1,82 +1,53 @@
enable_recording: enable_recording:
name: Enable recording
description: Enable continuous recording to camera storage.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
disable_recording: disable_recording:
name: Disable recording
description: Disable continuous recording to camera storage.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
enable_audio: enable_audio:
name: Enable audio
description: Enable audio stream.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
disable_audio: disable_audio:
name: Disable audio
description: Disable audio stream.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
enable_motion_recording: enable_motion_recording:
name: Enable motion recording
description: Enable recording a clip to camera storage when motion is detected.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
disable_motion_recording: disable_motion_recording:
name: Disable motion recording
description: Disable recording a clip to camera storage when motion is detected.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
goto_preset: goto_preset:
name: Go to preset
description: Move camera to PTZ preset.
fields: fields:
entity_id: entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
selector: selector:
entity: entity:
integration: amcrest integration: amcrest
domain: camera domain: camera
preset: preset:
name: Preset
description: Preset number.
required: true required: true
selector: selector:
number: number:
@ -84,18 +55,12 @@ goto_preset:
max: 1000 max: 1000
set_color_bw: set_color_bw:
name: Set color
description: Set camera color mode.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
color_bw: color_bw:
name: Color
description: Color mode.
selector: selector:
select: select:
options: options:
@ -104,40 +69,26 @@ set_color_bw:
- "color" - "color"
start_tour: start_tour:
name: Start tour
description: Start camera's PTZ tour function.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
stop_tour: stop_tour:
name: Stop tour
description: Stop camera's PTZ tour function.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
ptz_control: ptz_control:
name: PTZ control
description: Move (Pan/Tilt) and/or Zoom a PTZ camera.
fields: fields:
entity_id: entity_id:
name: Entity
description: "Name of the camera, or 'all' for all cameras."
example: "camera.house_front" example: "camera.house_front"
selector: selector:
text: text:
movement: movement:
name: Movement
description: "Direction to move the camera."
required: true required: true
selector: selector:
select: select:
@ -153,8 +104,6 @@ ptz_control:
- "zoom_in" - "zoom_in"
- "zoom_out" - "zoom_out"
travel_time: travel_time:
name: Travel time
description: "Travel time in fractional seconds: from 0 to 1."
default: .2 default: .2
selector: selector:
number: number:

View File

@ -0,0 +1,130 @@
{
"services": {
"enable_recording": {
"name": "Enable recording",
"description": "Enables continuous recording to camera storage.",
"fields": {
"entity_id": {
"name": "Entity",
"description": "Name(s) of the cameras, or 'all' for all cameras."
}
}
},
"disable_recording": {
"name": "Disable recording",
"description": "Disables continuous recording to camera storage.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
}
}
},
"enable_audio": {
"name": "Enable audio",
"description": "Enables audio stream.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
}
}
},
"disable_audio": {
"name": "Disable audio",
"description": "Disables audio stream.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
}
}
},
"enable_motion_recording": {
"name": "Enables motion recording",
"description": "Enables recording a clip to camera storage when motion is detected.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
}
}
},
"disable_motion_recording": {
"name": "Disables motion recording",
"description": "Disable recording a clip to camera storage when motion is detected.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
}
}
},
"goto_preset": {
"name": "Go to preset",
"description": "Moves camera to PTZ preset.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
},
"preset": {
"name": "Preset",
"description": "Preset number."
}
}
},
"set_color_bw": {
"name": "Set color",
"description": "Sets camera color mode.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
},
"color_bw": {
"name": "Color",
"description": "Color mode."
}
}
},
"start_tour": {
"name": "Start tour",
"description": "Starts camera's PTZ tour function.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
}
}
},
"stop_tour": {
"name": "Stop tour",
"description": "Stops camera's PTZ tour function.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
}
}
},
"ptz_control": {
"name": "PTZ control",
"description": "Moves (pan/tilt) and/or zoom a PTZ camera.",
"fields": {
"entity_id": {
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]"
},
"movement": {
"name": "Movement",
"description": "Direction to move the camera."
},
"travel_time": {
"name": "Travel time",
"description": "Travel time in fractional seconds: from 0 to 1."
}
}
}
}
}

View File

@ -17,11 +17,5 @@
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
} }
},
"issues": {
"deprecated_yaml": {
"title": "The Android IP Webcam YAML configuration is being removed",
"description": "Configuring Android IP Webcam using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Android IP Webcam YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
} }
} }

View File

@ -2,13 +2,15 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine from collections.abc import Awaitable, Callable, Coroutine
from datetime import datetime from datetime import timedelta
import functools import functools
import hashlib
import logging import logging
from typing import Any, Concatenate, ParamSpec, TypeVar from typing import Any, Concatenate, ParamSpec, TypeVar
from androidtv.constants import APPS, KEYS from androidtv.constants import APPS, KEYS
from androidtv.exceptions import LockNotAcquiredException from androidtv.exceptions import LockNotAcquiredException
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
import voluptuous as vol import voluptuous as vol
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
@ -34,6 +36,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle
from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac
from .const import ( from .const import (
@ -64,6 +67,8 @@ ATTR_DEVICE_PATH = "device_path"
ATTR_HDMI_INPUT = "hdmi_input" ATTR_HDMI_INPUT = "hdmi_input"
ATTR_LOCAL_PATH = "local_path" ATTR_LOCAL_PATH = "local_path"
MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
SERVICE_ADB_COMMAND = "adb_command" SERVICE_ADB_COMMAND = "adb_command"
SERVICE_DOWNLOAD = "download" SERVICE_DOWNLOAD = "download"
SERVICE_LEARN_SENDEVENT = "learn_sendevent" SERVICE_LEARN_SENDEVENT = "learn_sendevent"
@ -88,13 +93,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Android Debug Bridge entity.""" """Set up the Android Debug Bridge entity."""
aftv = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV] aftv: AndroidTVAsync | FireTVAsync = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV]
device_class = aftv.DEVICE_CLASS device_class = aftv.DEVICE_CLASS
device_type = ( device_type = (
PREFIX_ANDROIDTV if device_class == DEVICE_ANDROIDTV else PREFIX_FIRETV PREFIX_ANDROIDTV if device_class == DEVICE_ANDROIDTV else PREFIX_FIRETV
) )
# CONF_NAME may be present in entry.data for configuration imported from YAML # CONF_NAME may be present in entry.data for configuration imported from YAML
device_name = entry.data.get(CONF_NAME) or f"{device_type} {entry.data[CONF_HOST]}" device_name: str = entry.data.get(
CONF_NAME, f"{device_type} {entry.data[CONF_HOST]}"
)
device_args = [ device_args = [
aftv, aftv,
@ -171,8 +178,11 @@ def adb_decorator(
except LockNotAcquiredException: except LockNotAcquiredException:
# If the ADB lock could not be acquired, skip this command # If the ADB lock could not be acquired, skip this command
_LOGGER.info( _LOGGER.info(
"ADB command not executed because the connection is currently" (
" in use" "ADB command %s not executed because the connection is"
" currently in use"
),
func.__name__,
) )
return None return None
except self.exceptions as err: except self.exceptions as err:
@ -204,23 +214,27 @@ class ADBDevice(MediaPlayerEntity):
"""Representation of an Android or Fire TV device.""" """Representation of an Android or Fire TV device."""
_attr_device_class = MediaPlayerDeviceClass.TV _attr_device_class = MediaPlayerDeviceClass.TV
_attr_has_entity_name = True
_attr_name = None
def __init__( def __init__(
self, self,
aftv, aftv: AndroidTVAsync | FireTVAsync,
name, name: str,
dev_type, dev_type: str,
unique_id, unique_id: str,
entry_id, entry_id: str,
entry_data, entry_data: dict[str, Any],
): ) -> None:
"""Initialize the Android / Fire TV device.""" """Initialize the Android / Fire TV device."""
self.aftv = aftv self.aftv = aftv
self._attr_name = name
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
self._entry_id = entry_id self._entry_id = entry_id
self._entry_data = entry_data self._entry_data = entry_data
self._media_image: tuple[bytes | None, str | None] = None, None
self._attr_media_image_hash = None
info = aftv.device_properties info = aftv.device_properties
model = info.get(ATTR_MODEL) model = info.get(ATTR_MODEL)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
@ -235,13 +249,13 @@ class ADBDevice(MediaPlayerEntity):
if mac := get_androidtv_mac(info): if mac := get_androidtv_mac(info):
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)} self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
self._app_id_to_name = {} self._app_id_to_name: dict[str, str] = {}
self._app_name_to_id = {} self._app_name_to_id: dict[str, str] = {}
self._get_sources = DEFAULT_GET_SOURCES self._get_sources = DEFAULT_GET_SOURCES
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
self._screencap = DEFAULT_SCREENCAP self._screencap = DEFAULT_SCREENCAP
self.turn_on_command = None self.turn_on_command: str | None = None
self.turn_off_command = None self.turn_off_command: str | None = None
# ADB exceptions to catch # ADB exceptions to catch
if not aftv.adb_server_ip: if not aftv.adb_server_ip:
@ -260,7 +274,7 @@ class ADBDevice(MediaPlayerEntity):
# The number of consecutive failed connect attempts # The number of consecutive failed connect attempts
self._failed_connect_count = 0 self._failed_connect_count = 0
def _process_config(self): def _process_config(self) -> None:
"""Load the config options.""" """Load the config options."""
_LOGGER.debug("Loading configuration options") _LOGGER.debug("Loading configuration options")
options = self._entry_data[ANDROID_DEV_OPT] options = self._entry_data[ANDROID_DEV_OPT]
@ -297,34 +311,39 @@ class ADBDevice(MediaPlayerEntity):
) )
) )
@property
def media_image_hash(self) -> str | None:
"""Hash value for media image."""
return f"{datetime.now().timestamp()}" if self._screencap else None
@adb_decorator() @adb_decorator()
async def _adb_screencap(self): async def _adb_screencap(self) -> bytes | None:
"""Take a screen capture from the device.""" """Take a screen capture from the device."""
return await self.aftv.adb_screencap() return await self.aftv.adb_screencap()
async def async_get_media_image(self) -> tuple[bytes | None, str | None]: async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
"""Fetch current playing image.""" """Take a screen capture from the device when enabled."""
if ( if (
not self._screencap not self._screencap
or self.state in {MediaPlayerState.OFF, None} or self.state in {MediaPlayerState.OFF, None}
or not self.available or not self.available
): ):
return None, None self._media_image = None, None
self._attr_media_image_hash = None
else:
force: bool = prev_app_id is not None
if force:
force = prev_app_id != self._attr_app_id
await self._adb_get_screencap(no_throttle=force)
media_data = await self._adb_screencap() @Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
if media_data: async def _adb_get_screencap(self, **kwargs) -> None:
return media_data, "image/png" """Take a screen capture from the device every 60 seconds."""
if media_data := await self._adb_screencap():
self._media_image = media_data, "image/png"
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
else:
self._media_image = None, None
self._attr_media_image_hash = None
# If an exception occurred and the device is no longer available, write the state async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
if not self.available: """Fetch current playing image."""
self.async_write_ha_state() return self._media_image
return None, None
@adb_decorator() @adb_decorator()
async def async_media_play(self) -> None: async def async_media_play(self) -> None:
@ -382,7 +401,7 @@ class ADBDevice(MediaPlayerEntity):
await self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) await self.aftv.stop_app(self._app_name_to_id.get(source_, source_))
@adb_decorator() @adb_decorator()
async def adb_command(self, command): async def adb_command(self, command: str) -> None:
"""Send an ADB command to an Android / Fire TV device.""" """Send an ADB command to an Android / Fire TV device."""
if key := KEYS.get(command): if key := KEYS.get(command):
await self.aftv.adb_shell(f"input keyevent {key}") await self.aftv.adb_shell(f"input keyevent {key}")
@ -407,7 +426,7 @@ class ADBDevice(MediaPlayerEntity):
return return
@adb_decorator() @adb_decorator()
async def learn_sendevent(self): async def learn_sendevent(self) -> None:
"""Translate a key press on a remote to ADB 'sendevent' commands.""" """Translate a key press on a remote to ADB 'sendevent' commands."""
output = await self.aftv.learn_sendevent() output = await self.aftv.learn_sendevent()
if output: if output:
@ -426,7 +445,7 @@ class ADBDevice(MediaPlayerEntity):
_LOGGER.info("%s", msg) _LOGGER.info("%s", msg)
@adb_decorator() @adb_decorator()
async def service_download(self, device_path, local_path): async def service_download(self, device_path: str, local_path: str) -> None:
"""Download a file from your Android / Fire TV device to your Home Assistant instance.""" """Download a file from your Android / Fire TV device to your Home Assistant instance."""
if not self.hass.config.is_allowed_path(local_path): if not self.hass.config.is_allowed_path(local_path):
_LOGGER.warning("'%s' is not secure to load data from!", local_path) _LOGGER.warning("'%s' is not secure to load data from!", local_path)
@ -435,7 +454,7 @@ class ADBDevice(MediaPlayerEntity):
await self.aftv.adb_pull(local_path, device_path) await self.aftv.adb_pull(local_path, device_path)
@adb_decorator() @adb_decorator()
async def service_upload(self, device_path, local_path): async def service_upload(self, device_path: str, local_path: str) -> None:
"""Upload a file from your Home Assistant instance to an Android / Fire TV device.""" """Upload a file from your Home Assistant instance to an Android / Fire TV device."""
if not self.hass.config.is_allowed_path(local_path): if not self.hass.config.is_allowed_path(local_path):
_LOGGER.warning("'%s' is not secure to load data from!", local_path) _LOGGER.warning("'%s' is not secure to load data from!", local_path)
@ -460,6 +479,7 @@ class AndroidTVDevice(ADBDevice):
| MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_STEP
) )
aftv: AndroidTVAsync
@adb_decorator(override_available=True) @adb_decorator(override_available=True)
async def async_update(self) -> None: async def async_update(self) -> None:
@ -477,6 +497,7 @@ class AndroidTVDevice(ADBDevice):
if not self.available: if not self.available:
return return
prev_app_id = self._attr_app_id
# Get the updated state and attributes. # Get the updated state and attributes.
( (
state, state,
@ -492,7 +513,7 @@ class AndroidTVDevice(ADBDevice):
if self._attr_state is None: if self._attr_state is None:
self._attr_available = False self._attr_available = False
if running_apps: if running_apps and self._attr_app_id:
self._attr_source = self._attr_app_name = self._app_id_to_name.get( self._attr_source = self._attr_app_name = self._app_id_to_name.get(
self._attr_app_id, self._attr_app_id self._attr_app_id, self._attr_app_id
) )
@ -506,6 +527,8 @@ class AndroidTVDevice(ADBDevice):
else: else:
self._attr_source_list = None self._attr_source_list = None
await self._async_get_screencap(prev_app_id)
@adb_decorator() @adb_decorator()
async def async_media_stop(self) -> None: async def async_media_stop(self) -> None:
"""Send stop command.""" """Send stop command."""
@ -549,6 +572,7 @@ class FireTVDevice(ADBDevice):
| MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.STOP
) )
aftv: FireTVAsync
@adb_decorator(override_available=True) @adb_decorator(override_available=True)
async def async_update(self) -> None: async def async_update(self) -> None:
@ -566,6 +590,7 @@ class FireTVDevice(ADBDevice):
if not self.available: if not self.available:
return return
prev_app_id = self._attr_app_id
# Get the `state`, `current_app`, `running_apps` and `hdmi_input`. # Get the `state`, `current_app`, `running_apps` and `hdmi_input`.
( (
state, state,
@ -578,7 +603,7 @@ class FireTVDevice(ADBDevice):
if self._attr_state is None: if self._attr_state is None:
self._attr_available = False self._attr_available = False
if running_apps: if running_apps and self._attr_app_id:
self._attr_source = self._app_id_to_name.get( self._attr_source = self._app_id_to_name.get(
self._attr_app_id, self._attr_app_id self._attr_app_id, self._attr_app_id
) )
@ -592,6 +617,8 @@ class FireTVDevice(ADBDevice):
else: else:
self._attr_source_list = None self._attr_source_list = None
await self._async_get_screencap(prev_app_id)
@adb_decorator() @adb_decorator()
async def async_media_stop(self) -> None: async def async_media_stop(self) -> None:
"""Send stop (back) command.""" """Send stop (back) command."""

View File

@ -1,67 +1,49 @@
# Describes the format for available Android and Fire TV services # Describes the format for available Android and Fire TV services
adb_command: adb_command:
name: ADB command
description: Send an ADB command to an Android / Fire TV device.
target: target:
entity: entity:
integration: androidtv integration: androidtv
domain: media_player domain: media_player
fields: fields:
command: command:
name: Command
description: Either a key command or an ADB shell command.
required: true required: true
example: "HOME" example: "HOME"
selector: selector:
text: text:
download: download:
name: Download
description: Download a file from your Android / Fire TV device to your Home Assistant instance.
target: target:
entity: entity:
integration: androidtv integration: androidtv
domain: media_player domain: media_player
fields: fields:
device_path: device_path:
name: Device path
description: The filepath on the Android / Fire TV device.
required: true required: true
example: "/storage/emulated/0/Download/example.txt" example: "/storage/emulated/0/Download/example.txt"
selector: selector:
text: text:
local_path: local_path:
name: Local path
description: The filepath on your Home Assistant instance.
required: true required: true
example: "/config/www/example.txt" example: "/config/www/example.txt"
selector: selector:
text: text:
upload: upload:
name: Upload
description: Upload a file from your Home Assistant instance to an Android / Fire TV device.
target: target:
entity: entity:
integration: androidtv integration: androidtv
domain: media_player domain: media_player
fields: fields:
device_path: device_path:
name: Device path
description: The filepath on the Android / Fire TV device.
required: true required: true
example: "/storage/emulated/0/Download/example.txt" example: "/storage/emulated/0/Download/example.txt"
selector: selector:
text: text:
local_path: local_path:
name: Local path
description: The filepath on your Home Assistant instance.
required: true required: true
example: "/config/www/example.txt" example: "/config/www/example.txt"
selector: selector:
text: text:
learn_sendevent: learn_sendevent:
name: Learn sendevent
description: Translate 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.
target: target:
entity: entity:
integration: androidtv integration: androidtv

View File

@ -50,7 +50,7 @@
"title": "Configure Android state detection rules", "title": "Configure Android state detection rules",
"description": "Configure detection rule for application id {rule_id}", "description": "Configure detection rule for application id {rule_id}",
"data": { "data": {
"rule_id": "Application ID", "rule_id": "[%key:component::androidtv::options::step::apps::data::app_id%]",
"rule_values": "List of state detection rules (see documentation)", "rule_values": "List of state detection rules (see documentation)",
"rule_delete": "Check to delete this rule" "rule_delete": "Check to delete this rule"
} }
@ -59,5 +59,49 @@
"error": { "error": {
"invalid_det_rules": "Invalid state detection rules" "invalid_det_rules": "Invalid state detection rules"
} }
},
"services": {
"adb_command": {
"name": "ADB command",
"description": "Sends an ADB command to an Android / Fire TV device.",
"fields": {
"command": {
"name": "Command",
"description": "Either a key command or an ADB shell command."
}
}
},
"download": {
"name": "Download",
"description": "Downloads a file from your Android / Fire TV device to your Home Assistant instance.",
"fields": {
"device_path": {
"name": "Device path",
"description": "The filepath on the Android / Fire TV device."
},
"local_path": {
"name": "Local path",
"description": "The filepath on your Home Assistant instance."
}
}
},
"upload": {
"name": "Upload",
"description": "Uploads a file from your Home Assistant instance to an Android / Fire TV device.",
"fields": {
"device_path": {
"name": "[%key:component::androidtv::services::download::fields::device_path::name%]",
"description": "[%key:component::androidtv::services::download::fields::device_path::description%]"
},
"local_path": {
"name": "[%key:component::androidtv::services::download::fields::local_path::name%]",
"description": "[%key:component::androidtv::services::download::fields::local_path::description%]"
}
}
},
"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 calling this service."
}
} }
} }

View File

@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import DOMAIN from .const import DOMAIN
from .helpers import create_api from .helpers import create_api, get_enable_ime
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,7 +27,7 @@ PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Android TV Remote from a config entry.""" """Set up Android TV Remote from a config entry."""
api = create_api(hass, entry.data[CONF_HOST]) api = create_api(hass, entry.data[CONF_HOST], get_enable_ime(entry))
@callback @callback
def is_available_updated(is_available: bool) -> None: def is_available_updated(is_available: bool) -> None:
@ -76,6 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload( entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
) )
entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True
@ -87,3 +88,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api.disconnect() api.disconnect()
return unload_ok return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -15,11 +15,12 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
from .const import DOMAIN from .const import CONF_ENABLE_IME, DOMAIN
from .helpers import create_api from .helpers import create_api, get_enable_ime
STEP_USER_DATA_SCHEMA = vol.Schema( STEP_USER_DATA_SCHEMA = vol.Schema(
{ {
@ -55,7 +56,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
self.host = user_input["host"] self.host = user_input["host"]
assert self.host assert self.host
api = create_api(self.hass, self.host) api = create_api(self.hass, self.host, enable_ime=False)
try: try:
self.name, self.mac = await api.async_get_name_and_mac() self.name, self.mac = await api.async_get_name_and_mac()
assert self.mac assert self.mac
@ -75,7 +76,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_start_pair(self) -> FlowResult: async def _async_start_pair(self) -> FlowResult:
"""Start pairing with the Android TV. Navigate to the pair flow to enter the PIN shown on screen.""" """Start pairing with the Android TV. Navigate to the pair flow to enter the PIN shown on screen."""
assert self.host assert self.host
self.api = create_api(self.hass, self.host) self.api = create_api(self.hass, self.host, enable_ime=False)
await self.api.async_generate_cert_if_missing() await self.api.async_generate_cert_if_missing()
await self.api.async_start_pairing() await self.api.async_start_pairing()
return await self.async_step_pair() return await self.async_step_pair()
@ -186,3 +187,38 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={CONF_NAME: self.name}, description_placeholders={CONF_NAME: self.name},
errors=errors, errors=errors,
) )
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Android TV Remote options flow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_ENABLE_IME,
default=get_enable_ime(self.config_entry),
): bool,
}
),
)

View File

@ -4,3 +4,6 @@ from __future__ import annotations
from typing import Final from typing import Final
DOMAIN: Final = "androidtv_remote" DOMAIN: Final = "androidtv_remote"
CONF_ENABLE_IME: Final = "enable_ime"
CONF_ENABLE_IME_DEFAULT_VALUE: Final = True

View File

@ -3,11 +3,14 @@ from __future__ import annotations
from androidtvremote2 import AndroidTVRemote from androidtvremote2 import AndroidTVRemote
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.storage import STORAGE_DIR
from .const import CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE
def create_api(hass: HomeAssistant, host: str) -> AndroidTVRemote:
def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRemote:
"""Create an AndroidTVRemote instance.""" """Create an AndroidTVRemote instance."""
return AndroidTVRemote( return AndroidTVRemote(
client_name="Home Assistant", client_name="Home Assistant",
@ -15,4 +18,10 @@ def create_api(hass: HomeAssistant, host: str) -> AndroidTVRemote:
keyfile=hass.config.path(STORAGE_DIR, "androidtv_remote_key.pem"), keyfile=hass.config.path(STORAGE_DIR, "androidtv_remote_key.pem"),
host=host, host=host,
loop=hass.loop, loop=hass.loop,
enable_ime=enable_ime,
) )
def get_enable_ime(entry: ConfigEntry) -> bool:
"""Get value of enable_ime option or its default value."""
return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE)

View File

@ -8,6 +8,6 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["androidtvremote2"], "loggers": ["androidtvremote2"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["androidtvremote2==0.0.9"], "requirements": ["androidtvremote2==0.0.13"],
"zeroconf": ["_androidtvremote2._tcp.local."] "zeroconf": ["_androidtvremote2._tcp.local."]
} }

View File

@ -34,5 +34,14 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
},
"options": {
"step": {
"init": {
"data": {
"enable_ime": "Enable IME. Needed for getting the current app. Disable for devices that show 'Use keyboard on mobile device screen' instead of the on screen keyboard."
}
}
}
} }
} }

View File

@ -29,7 +29,7 @@
"name": "State" "name": "State"
}, },
"mode": { "mode": {
"name": "Mode" "name": "[%key:common::config_flow::data::mode%]"
}, },
"target_temperature": { "target_temperature": {
"name": "Target temperature" "name": "Target temperature"

View File

@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.util import Throttle from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -79,16 +80,6 @@ class APCUPSdData:
return self.status[model_key] return self.status[model_key]
return None return None
@property
def sw_version(self) -> str | None:
"""Return the software version of the APCUPSd, if available."""
return self.status.get("VERSION")
@property
def hw_version(self) -> str | None:
"""Return the firmware version of the UPS, if available."""
return self.status.get("FIRMWARE")
@property @property
def serial_no(self) -> str | None: def serial_no(self) -> str | None:
"""Return the unique serial number of the UPS, if available.""" """Return the unique serial number of the UPS, if available."""
@ -99,6 +90,21 @@ class APCUPSdData:
"""Return the STATFLAG indicating the status of the UPS, if available.""" """Return the STATFLAG indicating the status of the UPS, if available."""
return self.status.get("STATFLAG") return self.status.get("STATFLAG")
@property
def device_info(self) -> DeviceInfo | None:
"""Return the DeviceInfo of this APC UPS for the sensors, if serial number is available."""
if self.serial_no is None:
return None
return DeviceInfo(
identifiers={(DOMAIN, self.serial_no)},
model=self.model,
manufacturer="APC",
name=self.name if self.name is not None else "APC UPS",
hw_version=self.status.get("FIRMWARE"),
sw_version=self.status.get("VERSION"),
)
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs: Any) -> None: def update(self, **kwargs: Any) -> None:
"""Fetch the latest status from APCUPSd. """Fetch the latest status from APCUPSd.

View File

@ -9,7 +9,6 @@ from homeassistant.components.binary_sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, VALUE_ONLINE, APCUPSdData from . import DOMAIN, VALUE_ONLINE, APCUPSdData
@ -53,13 +52,8 @@ class OnlineStatus(BinarySensorEntity):
# Set up unique id and device info if serial number is available. # Set up unique id and device info if serial number is available.
if (serial_no := data_service.serial_no) is not None: if (serial_no := data_service.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}" self._attr_unique_id = f"{serial_no}_{description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = data_service.device_info
identifiers={(DOMAIN, serial_no)},
model=data_service.model,
manufacturer="APC",
hw_version=data_service.hw_version,
sw_version=data_service.sw_version,
)
self.entity_description = description self.entity_description = description
self._data_service = data_service self._data_service = data_service

View File

@ -21,7 +21,6 @@ from homeassistant.const import (
UnitOfTime, UnitOfTime,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, APCUPSdData from . import DOMAIN, APCUPSdData
@ -496,13 +495,7 @@ class APCUPSdSensor(SensorEntity):
# Set up unique id and device info if serial number is available. # Set up unique id and device info if serial number is available.
if (serial_no := data_service.serial_no) is not None: if (serial_no := data_service.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}" self._attr_unique_id = f"{serial_no}_{description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = data_service.device_info
identifiers={(DOMAIN, serial_no)},
model=data_service.model,
manufacturer="APC",
hw_version=data_service.hw_version,
sw_version=data_service.sw_version,
)
self.entity_description = description self.entity_description = description
self._data_service = data_service self._data_service = data_service

View File

@ -16,11 +16,5 @@
"description": "Enter the host and port on which the apcupsd NIS is being served." "description": "Enter the host and port on which the apcupsd NIS is being served."
} }
} }
},
"issues": {
"deprecated_yaml": {
"title": "The APC UPS Daemon YAML configuration is being removed",
"description": "Configuring APC UPS Daemon using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the APC UPS Daemon YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
} }
} }

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
URL_API, URL_API,
URL_API_COMPONENTS, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_CONFIG,
URL_API_CORE_STATE,
URL_API_ERROR_LOG, URL_API_ERROR_LOG,
URL_API_EVENTS, URL_API_EVENTS,
URL_API_SERVICES, URL_API_SERVICES,
@ -55,6 +56,7 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Register the API with the HTTP interface.""" """Register the API with the HTTP interface."""
hass.http.register_view(APIStatusView) hass.http.register_view(APIStatusView)
hass.http.register_view(APICoreStateView)
hass.http.register_view(APIEventStream) hass.http.register_view(APIEventStream)
hass.http.register_view(APIConfigView) hass.http.register_view(APIConfigView)
hass.http.register_view(APIStatesView) hass.http.register_view(APIStatesView)
@ -84,6 +86,24 @@ class APIStatusView(HomeAssistantView):
return self.json_message("API running.") return self.json_message("API running.")
class APICoreStateView(HomeAssistantView):
"""View to handle core state requests."""
url = URL_API_CORE_STATE
name = "api:core:state"
@ha.callback
def get(self, request: web.Request) -> web.Response:
"""Retrieve the current core state.
This API is intended to be a fast and lightweight way to check if the
Home Assistant core is running. Its primary use case is for supervisor
to check if Home Assistant is running.
"""
hass: HomeAssistant = request.app["hass"]
return self.json({"state": hass.state.value})
class APIEventStream(HomeAssistantView): class APIEventStream(HomeAssistantView):
"""View to handle EventStream requests.""" """View to handle EventStream requests."""

View File

@ -88,14 +88,18 @@ class AppleTVEntity(Entity):
"""Device that sends commands to an Apple TV.""" """Device that sends commands to an Apple TV."""
_attr_should_poll = False _attr_should_poll = False
_attr_has_entity_name = True
_attr_name = None
def __init__(self, name, identifier, manager): def __init__(self, name, identifier, manager):
"""Initialize device.""" """Initialize device."""
self.atv = None self.atv = None
self.manager = manager self.manager = manager
self._attr_name = name
self._attr_unique_id = identifier self._attr_unique_id = identifier
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, identifier)}) self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, identifier)},
name=name,
)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Handle when an entity is about to be added to Home Assistant.""" """Handle when an entity is about to be added to Home Assistant."""

View File

@ -6,7 +6,7 @@
"title": "Set up a new Apple TV", "title": "Set up a new Apple TV",
"description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.", "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.",
"data": { "data": {
"device_input": "Device" "device_input": "[%key:common::config_flow::data::device%]"
} }
}, },
"reconfigure": { "reconfigure": {

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/apprise", "documentation": "https://www.home-assistant.io/integrations/apprise",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["apprise"], "loggers": ["apprise"],
"requirements": ["apprise==1.4.0"] "requirements": ["apprise==1.4.5"]
} }

View File

@ -1,19 +1,18 @@
"""The Aseko Pool Live integration.""" """The Aseko Pool Live integration."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
import logging import logging
from aioaseko import APIUnavailable, MobileAccount, Unit, Variable from aioaseko import APIUnavailable, MobileAccount
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -49,28 +48,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]):
"""Class to manage fetching Aseko unit data from single endpoint."""
def __init__(self, hass: HomeAssistant, unit: Unit) -> None:
"""Initialize global Aseko unit data updater."""
self._unit = unit
if self._unit.name:
name = self._unit.name
else:
name = f"{self._unit.type}-{self._unit.serial_number}"
super().__init__(
hass,
_LOGGER,
name=name,
update_interval=timedelta(minutes=2),
)
async def _async_update_data(self) -> dict[str, Variable]:
"""Fetch unit data."""
await self._unit.get_state()
return {variable.type: variable for variable in self._unit.variables}

View File

@ -15,8 +15,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AsekoDataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
from .entity import AsekoEntity from .entity import AsekoEntity
@ -31,7 +31,7 @@ class AsekoBinarySensorDescriptionMixin:
class AsekoBinarySensorEntityDescription( class AsekoBinarySensorEntityDescription(
BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin
): ):
"""Describes a Aseko binary sensor entity.""" """Describes an Aseko binary sensor entity."""
UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (

View File

@ -0,0 +1,37 @@
"""The Aseko Pool Live integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from aioaseko import Unit, Variable
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]):
"""Class to manage fetching Aseko unit data from single endpoint."""
def __init__(self, hass: HomeAssistant, unit: Unit) -> None:
"""Initialize global Aseko unit data updater."""
self._unit = unit
if self._unit.name:
name = self._unit.name
else:
name = f"{self._unit.type}-{self._unit.serial_number}"
super().__init__(
hass,
_LOGGER,
name=name,
update_interval=timedelta(minutes=2),
)
async def _async_update_data(self) -> dict[str, Variable]:
"""Fetch unit data."""
await self._unit.get_state()
return {variable.type: variable for variable in self._unit.variables}

View File

@ -4,8 +4,8 @@ from aioaseko import Unit
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AsekoDataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]): class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):

View File

@ -12,8 +12,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AsekoDataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
from .entity import AsekoEntity from .entity import AsekoEntity
@ -36,7 +36,7 @@ async def async_setup_entry(
class VariableSensorEntity(AsekoEntity, SensorEntity): class VariableSensorEntity(AsekoEntity, SensorEntity):
"""Representation of a unit variable sensor entity.""" """Representation of a unit variable sensor entity."""
attr_state_class = SensorStateClass.MEASUREMENT _attr_state_class = SensorStateClass.MEASUREMENT
def __init__( def __init__(
self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator

View File

@ -4,12 +4,12 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import AsyncIterable, Callable, Iterable from collections.abc import AsyncIterable, Callable, Iterable
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from enum import StrEnum
import logging import logging
from typing import Any, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.enum import StrEnum
from homeassistant.components import conversation, media_source, stt, tts, websocket_api from homeassistant.components import conversation, media_source, stt, tts, websocket_api
from homeassistant.components.tts.media_source import ( from homeassistant.components.tts.media_source import (
generate_media_source_id as tts_generate_media_source_id, generate_media_source_id as tts_generate_media_source_id,

View File

@ -2,11 +2,10 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import StrEnum
import webrtcvad import webrtcvad
from homeassistant.backports.enum import StrEnum
_SAMPLE_RATE = 16000 _SAMPLE_RATE = 16000

View File

@ -111,7 +111,7 @@ async def websocket_run(
if start_stage == PipelineStage.STT: if start_stage == PipelineStage.STT:
# Audio pipeline that will receive audio as binary websocket messages # Audio pipeline that will receive audio as binary websocket messages
audio_queue: "asyncio.Queue[bytes]" = asyncio.Queue() audio_queue: asyncio.Queue[bytes] = asyncio.Queue()
incoming_sample_rate = msg["input"]["sample_rate"] incoming_sample_rate = msg["input"]["sample_rate"]
async def stt_stream() -> AsyncGenerator[bytes, None]: async def stt_stream() -> AsyncGenerator[bytes, None]:

View File

@ -0,0 +1,273 @@
"""aioasuswrt and pyasuswrt bridge classes."""
from __future__ import annotations
from abc import ABC, abstractmethod
from collections import namedtuple
import logging
from typing import Any, cast
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
from homeassistant.const import (
CONF_HOST,
CONF_MODE,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import (
CONF_DNSMASQ,
CONF_INTERFACE,
CONF_REQUIRE_IP,
CONF_SSH_KEY,
DEFAULT_DNSMASQ,
DEFAULT_INTERFACE,
KEY_METHOD,
KEY_SENSORS,
PROTOCOL_TELNET,
SENSORS_BYTES,
SENSORS_LOAD_AVG,
SENSORS_RATES,
SENSORS_TEMPERATURES,
)
SENSORS_TYPE_BYTES = "sensors_bytes"
SENSORS_TYPE_COUNT = "sensors_count"
SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
SENSORS_TYPE_RATES = "sensors_rates"
SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"])
_LOGGER = logging.getLogger(__name__)
def _get_dict(keys: list, values: list) -> dict[str, Any]:
"""Create a dict from a list of keys and values."""
return dict(zip(keys, values))
class AsusWrtBridge(ABC):
"""The Base Bridge abstract class."""
@staticmethod
def get_bridge(
hass: HomeAssistant, conf: dict[str, Any], options: dict[str, Any] | None = None
) -> AsusWrtBridge:
"""Get Bridge instance."""
return AsusWrtLegacyBridge(conf, options)
def __init__(self, host: str) -> None:
"""Initialize Bridge."""
self._host = host
self._firmware: str | None = None
self._label_mac: str | None = None
self._model: str | None = None
@property
def host(self) -> str:
"""Return hostname."""
return self._host
@property
def firmware(self) -> str | None:
"""Return firmware information."""
return self._firmware
@property
def label_mac(self) -> str | None:
"""Return label mac information."""
return self._label_mac
@property
def model(self) -> str | None:
"""Return model information."""
return self._model
@property
@abstractmethod
def is_connected(self) -> bool:
"""Get connected status."""
@abstractmethod
async def async_connect(self) -> None:
"""Connect to the device."""
@abstractmethod
async def async_disconnect(self) -> None:
"""Disconnect to the device."""
@abstractmethod
async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
"""Get list of connected devices."""
@abstractmethod
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
"""Return a dictionary of available sensors for this bridge."""
class AsusWrtLegacyBridge(AsusWrtBridge):
"""The Bridge that use legacy library."""
def __init__(
self, conf: dict[str, Any], options: dict[str, Any] | None = None
) -> None:
"""Initialize Bridge."""
super().__init__(conf[CONF_HOST])
self._protocol: str = conf[CONF_PROTOCOL]
self._api: AsusWrtLegacy = self._get_api(conf, options)
@staticmethod
def _get_api(
conf: dict[str, Any], options: dict[str, Any] | None = None
) -> AsusWrtLegacy:
"""Get the AsusWrtLegacy API."""
opt = options or {}
return AsusWrtLegacy(
conf[CONF_HOST],
conf.get(CONF_PORT),
conf[CONF_PROTOCOL] == PROTOCOL_TELNET,
conf[CONF_USERNAME],
conf.get(CONF_PASSWORD, ""),
conf.get(CONF_SSH_KEY, ""),
conf[CONF_MODE],
opt.get(CONF_REQUIRE_IP, True),
interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE),
dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ),
)
@property
def is_connected(self) -> bool:
"""Get connected status."""
return cast(bool, self._api.is_connected)
async def async_connect(self) -> None:
"""Connect to the device."""
await self._api.connection.async_connect()
# get main router properties
if self._label_mac is None:
await self._get_label_mac()
if self._firmware is None:
await self._get_firmware()
if self._model is None:
await self._get_model()
async def async_disconnect(self) -> None:
"""Disconnect to the device."""
if self._api is not None and self._protocol == PROTOCOL_TELNET:
self._api.connection.disconnect()
async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
"""Get list of connected devices."""
try:
api_devices = await self._api.async_get_connected_devices()
except OSError as exc:
raise UpdateFailed(exc) from exc
return {
format_mac(mac): WrtDevice(dev.ip, dev.name, None)
for mac, dev in api_devices.items()
}
async def _get_nvram_info(self, info_type: str) -> dict[str, Any]:
"""Get AsusWrt router info from nvram."""
info = {}
try:
info = await self._api.async_get_nvram(info_type)
except OSError as exc:
_LOGGER.warning(
"Error calling method async_get_nvram(%s): %s", info_type, exc
)
return info
async def _get_label_mac(self) -> None:
"""Get label mac information."""
label_mac = await self._get_nvram_info("LABEL_MAC")
if label_mac and "label_mac" in label_mac:
self._label_mac = format_mac(label_mac["label_mac"])
async def _get_firmware(self) -> None:
"""Get firmware information."""
firmware = await self._get_nvram_info("FIRMWARE")
if firmware and "firmver" in firmware:
firmver: str = firmware["firmver"]
if "buildno" in firmware:
firmver += f" (build {firmware['buildno']})"
self._firmware = firmver
async def _get_model(self) -> None:
"""Get model information."""
model = await self._get_nvram_info("MODEL")
if model and "model" in model:
self._model = model["model"]
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
"""Return a dictionary of available sensors for this bridge."""
sensors_temperatures = await self._get_available_temperature_sensors()
sensors_types = {
SENSORS_TYPE_BYTES: {
KEY_SENSORS: SENSORS_BYTES,
KEY_METHOD: self._get_bytes,
},
SENSORS_TYPE_LOAD_AVG: {
KEY_SENSORS: SENSORS_LOAD_AVG,
KEY_METHOD: self._get_load_avg,
},
SENSORS_TYPE_RATES: {
KEY_SENSORS: SENSORS_RATES,
KEY_METHOD: self._get_rates,
},
SENSORS_TYPE_TEMPERATURES: {
KEY_SENSORS: sensors_temperatures,
KEY_METHOD: self._get_temperatures,
},
}
return sensors_types
async def _get_available_temperature_sensors(self) -> list[str]:
"""Check which temperature information is available on the router."""
availability = await self._api.async_find_temperature_commands()
return [SENSORS_TEMPERATURES[i] for i in range(3) if availability[i]]
async def _get_bytes(self) -> dict[str, Any]:
"""Fetch byte information from the router."""
try:
datas = await self._api.async_get_bytes_total()
except (IndexError, OSError, ValueError) as exc:
raise UpdateFailed(exc) from exc
return _get_dict(SENSORS_BYTES, datas)
async def _get_rates(self) -> dict[str, Any]:
"""Fetch rates information from the router."""
try:
rates = await self._api.async_get_current_transfer_rates()
except (IndexError, OSError, ValueError) as exc:
raise UpdateFailed(exc) from exc
return _get_dict(SENSORS_RATES, rates)
async def _get_load_avg(self) -> dict[str, Any]:
"""Fetch load average information from the router."""
try:
avg = await self._api.async_get_loadavg()
except (IndexError, OSError, ValueError) as exc:
raise UpdateFailed(exc) from exc
return _get_dict(SENSORS_LOAD_AVG, avg)
async def _get_temperatures(self) -> dict[str, Any]:
"""Fetch temperatures information from the router."""
try:
temperatures: dict[str, Any] = await self._api.async_get_temperature()
except (OSError, ValueError) as exc:
raise UpdateFailed(exc) from exc
return temperatures

View File

@ -25,13 +25,13 @@ from homeassistant.const import (
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.schema_config_entry_flow import ( from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler, SchemaCommonFlowHandler,
SchemaFlowFormStep, SchemaFlowFormStep,
SchemaOptionsFlowHandler, SchemaOptionsFlowHandler,
) )
from .bridge import AsusWrtBridge
from .const import ( from .const import (
CONF_DNSMASQ, CONF_DNSMASQ,
CONF_INTERFACE, CONF_INTERFACE,
@ -47,7 +47,6 @@ from .const import (
PROTOCOL_SSH, PROTOCOL_SSH,
PROTOCOL_TELNET, PROTOCOL_TELNET,
) )
from .router import get_api, get_nvram_info
LABEL_MAC = "LABEL_MAC" LABEL_MAC = "LABEL_MAC"
@ -143,16 +142,15 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors or {}, errors=errors or {},
) )
@staticmethod
async def _async_check_connection( async def _async_check_connection(
user_input: dict[str, Any] self, user_input: dict[str, Any]
) -> tuple[str, str | None]: ) -> tuple[str, str | None]:
"""Attempt to connect the AsusWrt router.""" """Attempt to connect the AsusWrt router."""
host: str = user_input[CONF_HOST] host: str = user_input[CONF_HOST]
api = get_api(user_input) api = AsusWrtBridge.get_bridge(self.hass, user_input)
try: try:
await api.connection.async_connect() await api.async_connect()
except OSError: except OSError:
_LOGGER.error("Error connecting to the AsusWrt router at %s", host) _LOGGER.error("Error connecting to the AsusWrt router at %s", host)
@ -168,14 +166,9 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.error("Error connecting to the AsusWrt router at %s", host) _LOGGER.error("Error connecting to the AsusWrt router at %s", host)
return RESULT_CONN_ERROR, None return RESULT_CONN_ERROR, None
label_mac = await get_nvram_info(api, LABEL_MAC) unique_id = api.label_mac
conf_protocol = user_input[CONF_PROTOCOL] await api.async_disconnect()
if conf_protocol == PROTOCOL_TELNET:
api.connection.disconnect()
unique_id = None
if label_mac and "label_mac" in label_mac:
unique_id = format_mac(label_mac["label_mac"])
return RESULT_SUCCESS, unique_id return RESULT_SUCCESS, unique_id
async def async_step_user( async def async_step_user(

View File

@ -13,6 +13,10 @@ DEFAULT_DNSMASQ = "/var/lib/misc"
DEFAULT_INTERFACE = "eth0" DEFAULT_INTERFACE = "eth0"
DEFAULT_TRACK_UNKNOWN = False DEFAULT_TRACK_UNKNOWN = False
KEY_COORDINATOR = "coordinator"
KEY_METHOD = "method"
KEY_SENSORS = "sensors"
MODE_AP = "ap" MODE_AP = "ap"
MODE_ROUTER = "router" MODE_ROUTER = "router"

View File

@ -6,22 +6,12 @@ from datetime import datetime, timedelta
import logging import logging
from typing import Any from typing import Any
from aioasuswrt.asuswrt import AsusWrt, Device as WrtDevice
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
CONF_CONSIDER_HOME, CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME, DEFAULT_CONSIDER_HOME,
DOMAIN as TRACKER_DOMAIN, DOMAIN as TRACKER_DOMAIN,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_MODE,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
CONF_USERNAME,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -30,57 +20,37 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util, slugify
from .bridge import AsusWrtBridge, WrtDevice
from .const import ( from .const import (
CONF_DNSMASQ, CONF_DNSMASQ,
CONF_INTERFACE, CONF_INTERFACE,
CONF_REQUIRE_IP, CONF_REQUIRE_IP,
CONF_SSH_KEY,
CONF_TRACK_UNKNOWN, CONF_TRACK_UNKNOWN,
DEFAULT_DNSMASQ, DEFAULT_DNSMASQ,
DEFAULT_INTERFACE, DEFAULT_INTERFACE,
DEFAULT_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN,
DOMAIN, DOMAIN,
PROTOCOL_TELNET, KEY_COORDINATOR,
SENSORS_BYTES, KEY_METHOD,
KEY_SENSORS,
SENSORS_CONNECTED_DEVICE, SENSORS_CONNECTED_DEVICE,
SENSORS_LOAD_AVG,
SENSORS_RATES,
SENSORS_TEMPERATURES,
) )
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
DEFAULT_NAME = "Asuswrt"
KEY_COORDINATOR = "coordinator"
KEY_SENSORS = "sensors"
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
SENSORS_TYPE_BYTES = "sensors_bytes"
SENSORS_TYPE_COUNT = "sensors_count" SENSORS_TYPE_COUNT = "sensors_count"
SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
SENSORS_TYPE_RATES = "sensors_rates"
SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _get_dict(keys: list, values: list) -> dict[str, Any]:
"""Create a dict from a list of keys and values."""
ret_dict: dict[str, Any] = dict.fromkeys(keys)
for index, key in enumerate(ret_dict):
ret_dict[key] = values[index]
return ret_dict
class AsusWrtSensorDataHandler: class AsusWrtSensorDataHandler:
"""Data handler for AsusWrt sensor.""" """Data handler for AsusWrt sensor."""
def __init__(self, hass: HomeAssistant, api: AsusWrt) -> None: def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
"""Initialize a AsusWrt sensor data handler.""" """Initialize a AsusWrt sensor data handler."""
self._hass = hass self._hass = hass
self._api = api self._api = api
@ -90,42 +60,6 @@ class AsusWrtSensorDataHandler:
"""Return number of connected devices.""" """Return number of connected devices."""
return {SENSORS_CONNECTED_DEVICE[0]: self._connected_devices} return {SENSORS_CONNECTED_DEVICE[0]: self._connected_devices}
async def _get_bytes(self) -> dict[str, Any]:
"""Fetch byte information from the router."""
try:
datas = await self._api.async_get_bytes_total()
except (OSError, ValueError) as exc:
raise UpdateFailed(exc) from exc
return _get_dict(SENSORS_BYTES, datas)
async def _get_rates(self) -> dict[str, Any]:
"""Fetch rates information from the router."""
try:
rates = await self._api.async_get_current_transfer_rates()
except (OSError, ValueError) as exc:
raise UpdateFailed(exc) from exc
return _get_dict(SENSORS_RATES, rates)
async def _get_load_avg(self) -> dict[str, Any]:
"""Fetch load average information from the router."""
try:
avg = await self._api.async_get_loadavg()
except (OSError, ValueError) as exc:
raise UpdateFailed(exc) from exc
return _get_dict(SENSORS_LOAD_AVG, avg)
async def _get_temperatures(self) -> dict[str, Any]:
"""Fetch temperatures information from the router."""
try:
temperatures: dict[str, Any] = await self._api.async_get_temperature()
except (OSError, ValueError) as exc:
raise UpdateFailed(exc) from exc
return temperatures
def update_device_count(self, conn_devices: int) -> bool: def update_device_count(self, conn_devices: int) -> bool:
"""Update connected devices attribute.""" """Update connected devices attribute."""
if self._connected_devices == conn_devices: if self._connected_devices == conn_devices:
@ -134,19 +68,17 @@ class AsusWrtSensorDataHandler:
return True return True
async def get_coordinator( async def get_coordinator(
self, sensor_type: str, should_poll: bool = True self,
sensor_type: str,
update_method: Callable[[], Any] | None = None,
) -> DataUpdateCoordinator: ) -> DataUpdateCoordinator:
"""Get the coordinator for a specific sensor type.""" """Get the coordinator for a specific sensor type."""
should_poll = True
if sensor_type == SENSORS_TYPE_COUNT: if sensor_type == SENSORS_TYPE_COUNT:
should_poll = False
method = self._get_connected_devices method = self._get_connected_devices
elif sensor_type == SENSORS_TYPE_BYTES: elif update_method is not None:
method = self._get_bytes method = update_method
elif sensor_type == SENSORS_TYPE_LOAD_AVG:
method = self._get_load_avg
elif sensor_type == SENSORS_TYPE_RATES:
method = self._get_rates
elif sensor_type == SENSORS_TYPE_TEMPERATURES:
method = self._get_temperatures
else: else:
raise RuntimeError(f"Invalid sensor type: {sensor_type}") raise RuntimeError(f"Invalid sensor type: {sensor_type}")
@ -226,12 +158,6 @@ class AsusWrtRouter:
self.hass = hass self.hass = hass
self._entry = entry self._entry = entry
self._api: AsusWrt = None
self._protocol: str = entry.data[CONF_PROTOCOL]
self._host: str = entry.data[CONF_HOST]
self._model: str = "Asus Router"
self._sw_v: str | None = None
self._devices: dict[str, AsusWrtDevInfo] = {} self._devices: dict[str, AsusWrtDevInfo] = {}
self._connected_devices: int = 0 self._connected_devices: int = 0
self._connect_error: bool = False self._connect_error: bool = False
@ -248,26 +174,57 @@ class AsusWrtRouter:
} }
self._options.update(entry.options) self._options.update(entry.options)
self._api: AsusWrtBridge = AsusWrtBridge.get_bridge(
self.hass, dict(self._entry.data), self._options
)
def _migrate_entities_unique_id(self) -> None:
"""Migrate router entities to new unique id format."""
_ENTITY_MIGRATION_ID = {
"sensor_connected_device": "Devices Connected",
"sensor_rx_bytes": "Download",
"sensor_tx_bytes": "Upload",
"sensor_rx_rates": "Download Speed",
"sensor_tx_rates": "Upload Speed",
"sensor_load_avg1": "Load Avg (1m)",
"sensor_load_avg5": "Load Avg (5m)",
"sensor_load_avg15": "Load Avg (15m)",
"2.4GHz": "2.4GHz Temperature",
"5.0GHz": "5GHz Temperature",
"CPU": "CPU Temperature",
}
entity_reg = er.async_get(self.hass)
router_entries = er.async_entries_for_config_entry(
entity_reg, self._entry.entry_id
)
migrate_entities: dict[str, str] = {}
for entry in router_entries:
if entry.domain == TRACKER_DOMAIN:
continue
old_unique_id = entry.unique_id
if not old_unique_id.startswith(DOMAIN):
continue
for new_id, old_id in _ENTITY_MIGRATION_ID.items():
if old_unique_id.endswith(old_id):
migrate_entities[entry.entity_id] = slugify(
f"{self.unique_id}_{new_id}"
)
break
for entity_id, unique_id in migrate_entities.items():
entity_reg.async_update_entity(entity_id, new_unique_id=unique_id)
async def setup(self) -> None: async def setup(self) -> None:
"""Set up a AsusWrt router.""" """Set up a AsusWrt router."""
self._api = get_api(dict(self._entry.data), self._options)
try: try:
await self._api.connection.async_connect() await self._api.async_connect()
except OSError as exp: except OSError as exc:
raise ConfigEntryNotReady from exp raise ConfigEntryNotReady from exc
if not self._api.is_connected: if not self._api.is_connected:
raise ConfigEntryNotReady raise ConfigEntryNotReady
# System
model = await get_nvram_info(self._api, "MODEL")
if model and "model" in model:
self._model = model["model"]
firmware = await get_nvram_info(self._api, "FIRMWARE")
if firmware and "firmver" in firmware and "buildno" in firmware:
self._sw_v = f"{firmware['firmver']} (build {firmware['buildno']})"
# Load tracked entities from registry # Load tracked entities from registry
entity_reg = er.async_get(self.hass) entity_reg = er.async_get(self.hass)
track_entries = er.async_entries_for_config_entry( track_entries = er.async_entries_for_config_entry(
@ -295,6 +252,9 @@ class AsusWrtRouter:
self._devices[device_mac] = AsusWrtDevInfo(device_mac, entry.original_name) self._devices[device_mac] = AsusWrtDevInfo(device_mac, entry.original_name)
# Migrate entities to new unique id format
self._migrate_entities_unique_id()
# Update devices # Update devices
await self.update_devices() await self.update_devices()
@ -312,24 +272,24 @@ class AsusWrtRouter:
async def update_devices(self) -> None: async def update_devices(self) -> None:
"""Update AsusWrt devices tracker.""" """Update AsusWrt devices tracker."""
new_device = False new_device = False
_LOGGER.debug("Checking devices for ASUS router %s", self._host) _LOGGER.debug("Checking devices for ASUS router %s", self.host)
try: try:
api_devices = await self._api.async_get_connected_devices() wrt_devices = await self._api.async_get_connected_devices()
except OSError as exc: except UpdateFailed as exc:
if not self._connect_error: if not self._connect_error:
self._connect_error = True self._connect_error = True
_LOGGER.error( _LOGGER.error(
"Error connecting to ASUS router %s for device update: %s", "Error connecting to ASUS router %s for device update: %s",
self._host, self.host,
exc, exc,
) )
return return
if self._connect_error: if self._connect_error:
self._connect_error = False self._connect_error = False
_LOGGER.info("Reconnected to ASUS router %s", self._host) _LOGGER.info("Reconnected to ASUS router %s", self.host)
self._connected_devices = len(api_devices) self._connected_devices = len(wrt_devices)
consider_home: int = self._options.get( consider_home: int = self._options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
) )
@ -337,7 +297,6 @@ class AsusWrtRouter:
CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN
) )
wrt_devices = {format_mac(mac): dev for mac, dev in api_devices.items()}
for device_mac, device in self._devices.items(): for device_mac, device in self._devices.items():
dev_info = wrt_devices.pop(device_mac, None) dev_info = wrt_devices.pop(device_mac, None)
device.update(dev_info, consider_home) device.update(dev_info, consider_home)
@ -363,19 +322,14 @@ class AsusWrtRouter:
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api) self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
self._sensors_data_handler.update_device_count(self._connected_devices) self._sensors_data_handler.update_device_count(self._connected_devices)
sensors_types: dict[str, list[str]] = { sensors_types = await self._api.async_get_available_sensors()
SENSORS_TYPE_BYTES: SENSORS_BYTES, sensors_types[SENSORS_TYPE_COUNT] = {KEY_SENSORS: SENSORS_CONNECTED_DEVICE}
SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE,
SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG,
SENSORS_TYPE_RATES: SENSORS_RATES,
SENSORS_TYPE_TEMPERATURES: await self._get_available_temperature_sensors(),
}
for sensor_type, sensor_names in sensors_types.items(): for sensor_type, sensor_def in sensors_types.items():
if not sensor_names: if not (sensor_names := sensor_def.get(KEY_SENSORS)):
continue continue
coordinator = await self._sensors_data_handler.get_coordinator( coordinator = await self._sensors_data_handler.get_coordinator(
sensor_type, sensor_type != SENSORS_TYPE_COUNT sensor_type, update_method=sensor_def.get(KEY_METHOD)
) )
self._sensors_coordinator[sensor_type] = { self._sensors_coordinator[sensor_type] = {
KEY_COORDINATOR: coordinator, KEY_COORDINATOR: coordinator,
@ -392,31 +346,10 @@ class AsusWrtRouter:
if self._sensors_data_handler.update_device_count(self._connected_devices): if self._sensors_data_handler.update_device_count(self._connected_devices):
await coordinator.async_refresh() await coordinator.async_refresh()
async def _get_available_temperature_sensors(self) -> list[str]:
"""Check which temperature information is available on the router."""
try:
availability = await self._api.async_find_temperature_commands()
available_sensors = [
SENSORS_TEMPERATURES[i] for i in range(3) if availability[i]
]
except Exception as exc: # pylint: disable=broad-except
_LOGGER.debug(
(
"Failed checking temperature sensor availability for ASUS router"
" %s. Exception: %s"
),
self._host,
exc,
)
return []
return available_sensors
async def close(self) -> None: async def close(self) -> None:
"""Close the connection.""" """Close the connection."""
if self._api is not None and self._protocol == PROTOCOL_TELNET: if self._api is not None:
self._api.connection.disconnect() await self._api.async_disconnect()
self._api = None
for func in self._on_close: for func in self._on_close:
func() func()
@ -443,14 +376,17 @@ class AsusWrtRouter:
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device information.""" """Return the device information."""
return DeviceInfo( info = DeviceInfo(
identifiers={(DOMAIN, self.unique_id or "AsusWRT")}, identifiers={(DOMAIN, self._entry.unique_id or "AsusWRT")},
name=self._host, name=self.host,
model=self._model, model=self._api.model or "Asus Router",
manufacturer="Asus", manufacturer="Asus",
sw_version=self._sw_v, configuration_url=f"http://{self.host}",
configuration_url=f"http://{self._host}",
) )
if self._api.firmware:
info["sw_version"] = self._api.firmware
return info
@property @property
def signal_device_new(self) -> str: def signal_device_new(self) -> str:
@ -465,17 +401,12 @@ class AsusWrtRouter:
@property @property
def host(self) -> str: def host(self) -> str:
"""Return router hostname.""" """Return router hostname."""
return self._host return self._api.host
@property @property
def unique_id(self) -> str | None: def unique_id(self) -> str:
"""Return router unique id.""" """Return router unique id."""
return self._entry.unique_id return self._entry.unique_id or self._entry.entry_id
@property
def name(self) -> str:
"""Return router name."""
return self._host if self.unique_id else DEFAULT_NAME
@property @property
def devices(self) -> dict[str, AsusWrtDevInfo]: def devices(self) -> dict[str, AsusWrtDevInfo]:
@ -486,32 +417,3 @@ class AsusWrtRouter:
def sensors_coordinator(self) -> dict[str, Any]: def sensors_coordinator(self) -> dict[str, Any]:
"""Return sensors coordinators.""" """Return sensors coordinators."""
return self._sensors_coordinator return self._sensors_coordinator
async def get_nvram_info(api: AsusWrt, info_type: str) -> dict[str, Any]:
"""Get AsusWrt router info from nvram."""
info = {}
try:
info = await api.async_get_nvram(info_type)
except OSError as exc:
_LOGGER.warning("Error calling method async_get_nvram(%s): %s", info_type, exc)
return info
def get_api(conf: dict[str, Any], options: dict[str, Any] | None = None) -> AsusWrt:
"""Get the AsusWrt API."""
opt = options or {}
return AsusWrt(
conf[CONF_HOST],
conf.get(CONF_PORT),
conf[CONF_PROTOCOL] == PROTOCOL_TELNET,
conf[CONF_USERNAME],
conf.get(CONF_PASSWORD, ""),
conf.get(CONF_SSH_KEY, ""),
conf[CONF_MODE],
opt.get(CONF_REQUIRE_IP, True),
interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE),
dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ),
)

View File

@ -22,17 +22,20 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
) )
from homeassistant.util import slugify
from .const import ( from .const import (
DATA_ASUSWRT, DATA_ASUSWRT,
DOMAIN, DOMAIN,
KEY_COORDINATOR,
KEY_SENSORS,
SENSORS_BYTES, SENSORS_BYTES,
SENSORS_CONNECTED_DEVICE, SENSORS_CONNECTED_DEVICE,
SENSORS_LOAD_AVG, SENSORS_LOAD_AVG,
SENSORS_RATES, SENSORS_RATES,
SENSORS_TEMPERATURES, SENSORS_TEMPERATURES,
) )
from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter from .router import AsusWrtRouter
@dataclass @dataclass
@ -47,14 +50,14 @@ UNIT_DEVICES = "Devices"
CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_CONNECTED_DEVICE[0], key=SENSORS_CONNECTED_DEVICE[0],
name="Devices Connected", translation_key="devices_connected",
icon="mdi:router-network", icon="mdi:router-network",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UNIT_DEVICES, native_unit_of_measurement=UNIT_DEVICES,
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_RATES[0], key=SENSORS_RATES[0],
name="Download Speed", translation_key="download_speed",
icon="mdi:download-network", icon="mdi:download-network",
device_class=SensorDeviceClass.DATA_RATE, device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
@ -65,7 +68,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_RATES[1], key=SENSORS_RATES[1],
name="Upload Speed", translation_key="upload_speed",
icon="mdi:upload-network", icon="mdi:upload-network",
device_class=SensorDeviceClass.DATA_RATE, device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
@ -76,7 +79,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_BYTES[0], key=SENSORS_BYTES[0],
name="Download", translation_key="download",
icon="mdi:download", icon="mdi:download",
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfInformation.GIGABYTES, native_unit_of_measurement=UnitOfInformation.GIGABYTES,
@ -87,7 +90,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_BYTES[1], key=SENSORS_BYTES[1],
name="Upload", translation_key="upload",
icon="mdi:upload", icon="mdi:upload",
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfInformation.GIGABYTES, native_unit_of_measurement=UnitOfInformation.GIGABYTES,
@ -98,7 +101,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_LOAD_AVG[0], key=SENSORS_LOAD_AVG[0],
name="Load Avg (1m)", translation_key="load_avg_1m",
icon="mdi:cpu-32-bit", icon="mdi:cpu-32-bit",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
@ -107,7 +110,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_LOAD_AVG[1], key=SENSORS_LOAD_AVG[1],
name="Load Avg (5m)", translation_key="load_avg_5m",
icon="mdi:cpu-32-bit", icon="mdi:cpu-32-bit",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
@ -116,7 +119,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_LOAD_AVG[2], key=SENSORS_LOAD_AVG[2],
name="Load Avg (15m)", translation_key="load_avg_15m",
icon="mdi:cpu-32-bit", icon="mdi:cpu-32-bit",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
@ -125,7 +128,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_TEMPERATURES[0], key=SENSORS_TEMPERATURES[0],
name="2.4GHz Temperature", translation_key="24ghz_temperature",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -135,7 +138,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_TEMPERATURES[1], key=SENSORS_TEMPERATURES[1],
name="5GHz Temperature", translation_key="5ghz_temperature",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -145,7 +148,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
), ),
AsusWrtSensorEntityDescription( AsusWrtSensorEntityDescription(
key=SENSORS_TEMPERATURES[2], key=SENSORS_TEMPERATURES[2],
name="CPU Temperature", translation_key="cpu_temperature",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -180,6 +183,9 @@ async def async_setup_entry(
class AsusWrtSensor(CoordinatorEntity, SensorEntity): class AsusWrtSensor(CoordinatorEntity, SensorEntity):
"""Representation of a AsusWrt sensor.""" """Representation of a AsusWrt sensor."""
entity_description: AsusWrtSensorEntityDescription
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator,
@ -188,13 +194,9 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity):
) -> None: ) -> None:
"""Initialize a AsusWrt sensor.""" """Initialize a AsusWrt sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description: AsusWrtSensorEntityDescription = description self.entity_description = description
self._attr_name = f"{router.name} {description.name}" self._attr_unique_id = slugify(f"{router.unique_id}_{description.key}")
if router.unique_id:
self._attr_unique_id = f"{DOMAIN} {router.unique_id} {description.name}"
else:
self._attr_unique_id = f"{DOMAIN} {self.name}"
self._attr_device_info = router.device_info self._attr_device_info = router.device_info
self._attr_extra_state_attributes = {"hostname": router.host} self._attr_extra_state_attributes = {"hostname": router.host}

View File

@ -36,11 +36,48 @@
"data": { "data": {
"consider_home": "Seconds to wait before considering a device away", "consider_home": "Seconds to wait before considering a device away",
"track_unknown": "Track unknown / unnamed devices", "track_unknown": "Track unknown / unnamed devices",
"interface": "The interface that you want statistics from (e.g. eth0,eth1 etc)", "interface": "The interface that you want statistics from (e.g. eth0, eth1 etc)",
"dnsmasq": "The location in the router of the dnsmasq.leases files", "dnsmasq": "The location in the router of the dnsmasq.leases files",
"require_ip": "Devices must have IP (for access point mode)" "require_ip": "Devices must have IP (for access point mode)"
} }
} }
} }
},
"entity": {
"sensor": {
"devices_connected": {
"name": "Devices connected"
},
"download_speed": {
"name": "Download speed"
},
"upload_speed": {
"name": "Upload speed"
},
"download": {
"name": "Download"
},
"upload": {
"name": "Upload"
},
"load_avg_1m": {
"name": "Average load (1m)"
},
"load_avg_5m": {
"name": "Average load (5m)"
},
"load_avg_15m": {
"name": "Average load (15m)"
},
"24ghz_temperature": {
"name": "2.4GHz Temperature"
},
"5ghz_temperature": {
"name": "5GHz Temperature"
},
"cpu_temperature": {
"name": "CPU Temperature"
}
}
} }
} }

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