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/device_tracker/**
- homeassistant/components/diagnostics/**
- homeassistant/components/event/**
- homeassistant/components/fan/**
- homeassistant/components/geo_location/**
- homeassistant/components/humidifier/**

View File

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

View File

@ -7,42 +7,46 @@
"containerEnv": { "DEVCONTAINER": "1" },
"appPort": ["8123:8123"],
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
"extensions": [
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github"
],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.blackPath": "/usr/local/bin/black",
"python.linting.pycodestylePath": "/usr/local/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/bin/pydocstyle",
"python.linting.mypyPath": "/usr/local/bin/mypy",
"python.linting.pylintPath": "/usr/local/bin/pylint",
"python.formatting.provider": "black",
"python.testing.pytestArgs": ["--no-cov"],
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/usr/bin/zsh"
"customizations": {
"vscode": {
"extensions": [
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github"
],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.blackPath": "/usr/local/bin/black",
"python.linting.pycodestylePath": "/usr/local/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/bin/pydocstyle",
"python.linting.mypyPath": "/usr/local/bin/mypy",
"python.linting.pylintPath": "/usr/local/bin/pylint",
"python.formatting.provider": "black",
"python.testing.pytestArgs": ["--no-cov"],
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/usr/bin/zsh"
}
},
"terminal.integrated.defaultProfile.linux": "zsh",
"yaml.customTags": [
"!input scalar",
"!secret scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
"!include_dir_merge_list scalar",
"!include_dir_merge_named scalar"
]
}
},
"terminal.integrated.defaultProfile.linux": "zsh",
"yaml.customTags": [
"!input scalar",
"!secret scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
"!include_dir_merge_list scalar",
"!include_dir_merge_named scalar"
]
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.272
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.280
hooks:
- id: ruff
args:
- --fix
- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.7.0
hooks:
- id: black
args:
@ -17,8 +17,8 @@ repos:
hooks:
- id: codespell
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
- --skip="./.*,*.csv,*.json"
- --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,*.ambr"
- --quiet-level=2
exclude_types: [csv, json]
exclude: ^tests/fixtures/|homeassistant/generated/
@ -35,7 +35,7 @@ repos:
- --branch=master
- --branch=rc
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.28.0
rev: v1.32.0
hooks:
- id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier

View File

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

View File

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

View File

@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.1
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.07.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.07.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.07.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.07.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.07.0
codenotary:
signer: 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 enum import Enum
from typing import Any
from enum import StrEnum
from typing_extensions import Self
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")
__all__ = [
"StrEnum",
]

View File

@ -3,27 +3,24 @@ from __future__ import annotations
from collections.abc import Callable
from types import GenericAlias
from typing import Any, Generic, TypeVar, overload
from typing_extensions import Self
from typing import Any, Generic, Self, TypeVar, overload
_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.
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."""
self.func = func
self.attrname: Any = None
self.func: Callable[[Any], _T] = func
self.attrname: str | None = None
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."""
if self.attrname is None:
self.attrname = name
@ -34,14 +31,16 @@ class cached_property(Generic[_T, _R]): # pylint: disable=invalid-name
)
@overload
def __get__(self, instance: None, owner: type[_T]) -> Self:
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self:
...
@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."""
if instance is None:
return self

View File

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

View File

@ -1,10 +1,6 @@
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.
required: true
selector:
entity:
@ -12,31 +8,21 @@ capture_image:
domain: camera
change_setting:
name: Change setting
description: Change an Abode system setting.
fields:
setting:
name: Setting
description: Setting to change.
required: true
example: beeper_mute
selector:
text:
value:
name: Value
description: Value of the setting.
required: true
example: "1"
selector:
text:
trigger_automation:
name: Trigger automation
description: Trigger an Abode automation.
fields:
entity_id:
name: Entity
description: Entity id of the automation to trigger.
required: true
selector:
entity:

View File

@ -15,7 +15,7 @@
}
},
"reauth_confirm": {
"title": "Fill in your Abode login information",
"title": "[%key:component::abode::config::step::user::title%]",
"data": {
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
@ -31,5 +31,41 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"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",
"codeowners": ["@bieniu"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
"documentation": "https://www.home-assistant.io/integrations/accuweather",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],

View File

@ -25,7 +25,6 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherDataUpdateCoordinator
@ -50,7 +49,7 @@ PARALLEL_UPDATES = 1
class AccuWeatherSensorDescriptionMixin:
"""Mixin for AccuWeather sensor."""
value_fn: Callable[[dict[str, Any]], StateType]
value_fn: Callable[[dict[str, Any]], str | int | float | None]
@dataclass
@ -59,7 +58,7 @@ class AccuWeatherSensorDescription(
):
"""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, ...] = (
@ -428,7 +427,7 @@ class AccuWeatherSensor(
self.forecast_day = forecast_day
@property
def native_value(self) -> StateType:
def native_value(self) -> str | int | float | None:
"""Return the state."""
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_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
ATTR_FORECAST_UV_INDEX,
ATTR_FORECAST_WIND_BEARING,
Forecast,
WeatherEntity,
@ -147,6 +148,11 @@ class AccuWeatherEntity(
"""Return the visibility."""
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
def forecast(self) -> list[Forecast] | None:
"""Return the forecast array."""
@ -172,6 +178,7 @@ class AccuWeatherEntity(
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item["WindGustDay"][ATTR_SPEED][
ATTR_VALUE
],
ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE],
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
ATTR_FORECAST_CONDITION: [
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v

View File

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

View File

@ -1,65 +1,43 @@
add_url:
name: Add url
description: Add a new filter subscription to AdGuard Home.
fields:
name:
name: Name
description: The name of the filter subscription.
required: true
example: Example
selector:
text:
url:
name: Url
description: The filter URL to subscribe to, containing the filter rules.
required: true
example: https://www.example.com/filter/1.txt
selector:
text:
remove_url:
name: Remove url
description: Removes a filter subscription from AdGuard Home.
fields:
url:
name: Url
description: The filter subscription URL to remove.
required: true
example: https://www.example.com/filter/1.txt
selector:
text:
enable_url:
name: Enable url
description: Enables a filter subscription in AdGuard Home.
fields:
url:
name: Url
description: The filter subscription URL to enable.
required: true
example: https://www.example.com/filter/1.txt
selector:
text:
disable_url:
name: Disable url
description: Disables a filter subscription in AdGuard Home.
fields:
url:
name: Url
description: The filter subscription URL to disable.
required: true
example: https://www.example.com/filter/1.txt
selector:
text:
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.
default: false
selector:
boolean:

View File

@ -72,5 +72,61 @@
"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
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.
required: true
example: ".global_var"
selector:
text:
adstype:
name: ADS type
description: The data type of the variable to write to.
required: true
selector:
select:
@ -25,8 +19,6 @@ write_data_by_name:
- "udint"
- "uint"
value:
name: Value
description: The value to write to the variable.
required: true
selector:
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:
name: Set Time To
description: Control timers to turn the system on or off after a set number of minutes
target:
entity:
integration: advantage_air
domain: sensor
fields:
minutes:
name: Minutes
description: Minutes until action
required: true
selector:
number:

View File

@ -13,7 +13,19 @@
"port": "[%key:common::config_flow::data::port%]"
},
"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
add_tracking:
name: Add tracking
description: Add new tracking number to Aftership.
fields:
tracking_number:
name: Tracking number
description: Tracking number for the new tracking
required: true
example: "123456789"
selector:
text:
title:
name: Title
description: A custom title for the new tracking
example: "Laptop"
selector:
text:
slug:
name: Slug
description: Slug (carrier) of the new tracking
example: "USPS"
selector:
text:
remove_tracking:
name: Remove tracking
description: Remove a tracking number from Aftership.
fields:
tracking_number:
name: Tracking number
description: Tracking number of the tracking to remove
required: true
example: "123456789"
selector:
text:
slug:
name: Slug
description: Slug (carrier) of the tracking to remove
example: "USPS"
selector:
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_NIGHT
)
_attr_has_entity_name = True
_attr_name = None
def __init__(self, client):
"""Initialize the alarm control panel."""
self._client = client
self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}"
self._attr_unique_id = f"{client.unique}_CP"
self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, client.unique)},
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
manufacturer="Agent",
model=CONST_ALARM_CONTROL_PANEL_NAME,
sw_version=client.version,

View File

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

View File

@ -3,7 +3,7 @@
"name": "Agent DVR",
"codeowners": ["@ispysoftware"],
"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",
"loggers": ["agent"],
"requirements": ["agent-py==0.0.23"]

View File

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

View File

@ -16,5 +16,27 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"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),
),
):
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:
new_ids = (DOMAIN, f"{latitude}-{longitude}")
device_registry.async_update_device(

View File

@ -17,7 +17,7 @@
},
"abort": {
"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": {

View File

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

View File

@ -14,7 +14,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"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%]"
},
"abort": {

View File

@ -3,7 +3,20 @@
"name": "Airthings BLE",
"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"],

View File

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

View File

@ -11,7 +11,7 @@
}
},
"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.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
@ -45,7 +45,7 @@
"options": {
"step": {
"init": {
"title": "Configure AirVisual",
"title": "[%key:component::airvisual::config::step::user::title%]",
"data": {
"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."""
try:
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:
raise ConfigEntryAuthFailed("Invalid Samba password") from err
except NodeConnectionError as err:

View File

@ -30,7 +30,9 @@ from .const import DOMAIN
class AirVisualProMeasurementKeyMixin:
"""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
@ -43,75 +45,81 @@ class AirVisualProMeasurementDescription(
SENSOR_DESCRIPTIONS = (
AirVisualProMeasurementDescription(
key="air_quality_index",
name="Air quality index",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements[
value_fn=lambda settings, status, measurements, history: measurements[
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(
key="battery_level",
name="Battery",
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda settings, status, measurements: status["battery"],
value_fn=lambda settings, status, measurements, history: status["battery"],
),
AirVisualProMeasurementDescription(
key="carbon_dioxide",
name="C02",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["co2"],
value_fn=lambda settings, status, measurements, history: measurements["co2"],
),
AirVisualProMeasurementDescription(
key="humidity",
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda settings, status, measurements: measurements["humidity"],
value_fn=lambda settings, status, measurements, history: measurements[
"humidity"
],
),
AirVisualProMeasurementDescription(
key="particulate_matter_0_1",
name="PM 0.1",
device_class=SensorDeviceClass.PM1,
translation_key="pm01",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["pm0_1"],
value_fn=lambda settings, status, measurements, history: measurements["pm0_1"],
),
AirVisualProMeasurementDescription(
key="particulate_matter_1_0",
name="PM 1.0",
device_class=SensorDeviceClass.PM10,
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["pm1_0"],
value_fn=lambda settings, status, measurements, history: measurements["pm1_0"],
),
AirVisualProMeasurementDescription(
key="particulate_matter_2_5",
name="PM 2.5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["pm2_5"],
value_fn=lambda settings, status, measurements, history: measurements["pm2_5"],
),
AirVisualProMeasurementDescription(
key="temperature",
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["temperature_C"],
value_fn=lambda settings, status, measurements, history: measurements[
"temperature_C"
],
),
AirVisualProMeasurementDescription(
key="voc",
name="VOC",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
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["status"],
self.coordinator.data["measurements"],
self.coordinator.data["history"],
)

View File

@ -24,5 +24,15 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"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 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 (
BinarySensorDeviceClass,
@ -18,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneZoneEntity
from .entity import AirzoneAidooEntity, AirzoneEntity, AirzoneZoneEntity
@dataclass
@ -28,7 +35,27 @@ class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
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, ...]] = (
AirzoneBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.RUNNING,
key=AZD_ACTIVE,
),
AirzoneBinarySensorEntityDescription(
attributes={
"warnings": AZD_WARNINGS,
@ -48,6 +75,18 @@ async def async_setup_entry(
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 description in ZONE_BINARY_SENSOR_TYPES:
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):
"""Define an Airzone Cloud Zone binary sensor."""

View File

@ -7,6 +7,7 @@ from typing import Any
from aioairzone_cloud.const import (
API_CITY,
API_GROUP_ID,
API_GROUPS,
API_LOCATION_ID,
API_OLD_ID,
API_PIN,
@ -29,7 +30,6 @@ from .coordinator import AirzoneUpdateCoordinator
TO_REDACT_API = [
API_CITY,
API_GROUP_ID,
API_LOCATION_ID,
API_OLD_ID,
API_PIN,
@ -58,11 +58,17 @@ def gather_ids(api_data: dict[str, Any]) -> dict[str, Any]:
ids[dev_id] = f"device{dev_idx}"
dev_idx += 1
group_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:
ids[inst_id] = f"installation{inst_idx}"
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
for ws_id in api_data[RAW_WEBSERVERS]:

View File

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

View File

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

View File

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

View File

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

View File

@ -63,10 +63,76 @@
}
}
},
"issues": {
"platform_integration_no_support": {
"title": "[%key:common::issues::platform_integration_no_support_title%]",
"description": "[%key:common::issues::platform_integration_no_support_description%]"
"services": {
"alarm_disarm": {
"name": "Disarm",
"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:
name: Key press
description: Send custom keypresses to the alarm.
target:
entity:
integration: alarmdecoder
domain: alarm_control_panel
fields:
keypress:
name: Key press
description: "String to send to the alarm panel."
required: true
example: "*71"
selector:
text:
alarm_toggle_chime:
name: Toggle Chime
description: Send the alarm the toggle chime command.
target:
entity:
integration: alarmdecoder
domain: alarm_control_panel
fields:
code:
name: Code
description: A code to toggle the alarm control panel chime with.
required: true
example: 1234
selector:

View File

@ -20,7 +20,9 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"create_entry": { "default": "Successfully connected to AlarmDecoder." },
"create_entry": {
"default": "Successfully connected to AlarmDecoder."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
@ -35,7 +37,7 @@
}
},
"arm_settings": {
"title": "Configure AlarmDecoder",
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
"data": {
"auto_bypass": "Auto Bypass on Arm",
"code_arm_required": "Code Required for Arming",
@ -43,14 +45,14 @@
}
},
"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.",
"data": {
"zone_number": "Zone Number"
}
},
"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.",
"data": {
"zone_name": "Zone Name",
@ -68,5 +70,27 @@
"loop_rfid": "RF Loop cannot be used without RF Serial.",
"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_ON,
)
from homeassistant.core import Event, HassJob, HomeAssistant
from homeassistant.core import HassJob, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import (
EventStateChangedData,
async_track_point_in_time,
async_track_state_change_event,
)
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 .const import (
@ -196,11 +197,13 @@ class Alert(Entity):
return STATE_ON
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."""
if (to_state := event.data.get("new_state")) is None:
if (to_state := event.data["new_state"]) is None:
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:
await self.begin_alerting()
if to_state.state != self._alert_state and self._firing:

View File

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

View File

@ -9,5 +9,19 @@
"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,
climate,
cover,
event,
fan,
group,
humidifier,
@ -527,6 +528,26 @@ class CoverCapabilities(AlexaEntity):
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)
class LightCapabilities(AlexaEntity):
"""Class to represent Light capabilities."""

View File

@ -857,14 +857,55 @@ async def async_api_adjust_target_temp(
temp_delta = temperature_from_object(
hass, directive.payload["targetSetpointDelta"], interval=True
)
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp}
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
if target_temp < min_temp or target_temp > max_temp:
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp}
response.add_context_property(
{
"name": "targetSetpoint",
"namespace": "Alexa.ThermostatController",
"value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]},
}
)
await hass.services.async_call(
entity.domain,
climate.SERVICE_SET_TEMPERATURE,
@ -872,13 +913,6 @@ async def async_api_adjust_target_temp(
blocking=False,
context=context,
)
response.add_context_property(
{
"name": "targetSetpoint",
"namespace": "Alexa.ThermostatController",
"value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]},
}
)
return response

View File

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

View File

@ -4,9 +4,10 @@ from amberelectric import Configuration
from amberelectric.api import amber_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN
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

View File

@ -3,7 +3,7 @@
"step": {
"user": {
"data": {
"api_token": "API Token",
"api_token": "[%key:common::config_flow::data::api_token%]",
"site_id": "Site ID"
},
"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_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_has_entity_name = True
_attr_name = None
def __init__(self, heater, store):
"""Initialize the thermostat."""
self._heater = heater
self._store = store
self._attr_unique_id = heater.device_id
self._attr_name = heater.name
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
manufacturer="Ambiclimate",
name=self.name,
name=heater.name,
)
async def async_set_temperature(self, **kwargs: Any) -> None:

View File

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

View File

@ -18,5 +18,45 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"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,9 +210,10 @@ class AmcrestChecker(ApiWrapper):
self, *args: Any, **kwargs: Any
) -> AsyncIterator[httpx.Response]:
"""amcrest.ApiWrapper.command wrapper to catch errors."""
async with self._async_command_wrapper():
async with super().async_stream_command(*args, **kwargs) as ret:
yield ret
async with self._async_command_wrapper(), super().async_stream_command(
*args, **kwargs
) as ret:
yield ret
@asynccontextmanager
async def _async_command_wrapper(self) -> AsyncIterator[None]:

View File

@ -1,82 +1,53 @@
enable_recording:
name: Enable recording
description: Enable continuous recording to camera storage.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
disable_recording:
name: Disable recording
description: Disable continuous recording to camera storage.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
enable_audio:
name: Enable audio
description: Enable audio stream.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
disable_audio:
name: Disable audio
description: Disable audio stream.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
enable_motion_recording:
name: Enable motion recording
description: Enable recording a clip to camera storage when motion is detected.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
disable_motion_recording:
name: Disable motion recording
description: Disable recording a clip to camera storage when motion is detected.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
goto_preset:
name: Go to preset
description: Move camera to PTZ preset.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
selector:
entity:
integration: amcrest
domain: camera
preset:
name: Preset
description: Preset number.
required: true
selector:
number:
@ -84,18 +55,12 @@ goto_preset:
max: 1000
set_color_bw:
name: Set color
description: Set camera color mode.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
color_bw:
name: Color
description: Color mode.
selector:
select:
options:
@ -104,40 +69,26 @@ set_color_bw:
- "color"
start_tour:
name: Start tour
description: Start camera's PTZ tour function.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
stop_tour:
name: Stop tour
description: Stop camera's PTZ tour function.
fields:
entity_id:
name: Entity
description: "Name(s) of the cameras, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
ptz_control:
name: PTZ control
description: Move (Pan/Tilt) and/or Zoom a PTZ camera.
fields:
entity_id:
name: Entity
description: "Name of the camera, or 'all' for all cameras."
example: "camera.house_front"
selector:
text:
movement:
name: Movement
description: "Direction to move the camera."
required: true
selector:
select:
@ -153,8 +104,6 @@ ptz_control:
- "zoom_in"
- "zoom_out"
travel_time:
name: Travel time
description: "Travel time in fractional seconds: from 0 to 1."
default: .2
selector:
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": {
"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 collections.abc import Awaitable, Callable, Coroutine
from datetime import datetime
from datetime import timedelta
import functools
import hashlib
import logging
from typing import Any, Concatenate, ParamSpec, TypeVar
from androidtv.constants import APPS, KEYS
from androidtv.exceptions import LockNotAcquiredException
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
import voluptuous as vol
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.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle
from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac
from .const import (
@ -64,6 +67,8 @@ ATTR_DEVICE_PATH = "device_path"
ATTR_HDMI_INPUT = "hdmi_input"
ATTR_LOCAL_PATH = "local_path"
MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
SERVICE_ADB_COMMAND = "adb_command"
SERVICE_DOWNLOAD = "download"
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
@ -88,13 +93,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""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_type = (
PREFIX_ANDROIDTV if device_class == DEVICE_ANDROIDTV else PREFIX_FIRETV
)
# 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 = [
aftv,
@ -171,8 +178,11 @@ def adb_decorator(
except LockNotAcquiredException:
# If the ADB lock could not be acquired, skip this command
_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
except self.exceptions as err:
@ -204,23 +214,27 @@ class ADBDevice(MediaPlayerEntity):
"""Representation of an Android or Fire TV device."""
_attr_device_class = MediaPlayerDeviceClass.TV
_attr_has_entity_name = True
_attr_name = None
def __init__(
self,
aftv,
name,
dev_type,
unique_id,
entry_id,
entry_data,
):
aftv: AndroidTVAsync | FireTVAsync,
name: str,
dev_type: str,
unique_id: str,
entry_id: str,
entry_data: dict[str, Any],
) -> None:
"""Initialize the Android / Fire TV device."""
self.aftv = aftv
self._attr_name = name
self._attr_unique_id = unique_id
self._entry_id = entry_id
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
model = info.get(ATTR_MODEL)
self._attr_device_info = DeviceInfo(
@ -235,13 +249,13 @@ class ADBDevice(MediaPlayerEntity):
if mac := get_androidtv_mac(info):
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
self._app_id_to_name = {}
self._app_name_to_id = {}
self._app_id_to_name: dict[str, str] = {}
self._app_name_to_id: dict[str, str] = {}
self._get_sources = DEFAULT_GET_SOURCES
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
self._screencap = DEFAULT_SCREENCAP
self.turn_on_command = None
self.turn_off_command = None
self.turn_on_command: str | None = None
self.turn_off_command: str | None = None
# ADB exceptions to catch
if not aftv.adb_server_ip:
@ -260,7 +274,7 @@ class ADBDevice(MediaPlayerEntity):
# The number of consecutive failed connect attempts
self._failed_connect_count = 0
def _process_config(self):
def _process_config(self) -> None:
"""Load the config options."""
_LOGGER.debug("Loading configuration options")
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()
async def _adb_screencap(self):
async def _adb_screencap(self) -> bytes | None:
"""Take a screen capture from the device."""
return await self.aftv.adb_screencap()
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
"""Fetch current playing image."""
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
"""Take a screen capture from the device when enabled."""
if (
not self._screencap
or self.state in {MediaPlayerState.OFF, None}
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()
if media_data:
return media_data, "image/png"
@Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
async def _adb_get_screencap(self, **kwargs) -> None:
"""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
if not self.available:
self.async_write_ha_state()
return None, None
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
"""Fetch current playing image."""
return self._media_image
@adb_decorator()
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_))
@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."""
if key := KEYS.get(command):
await self.aftv.adb_shell(f"input keyevent {key}")
@ -407,7 +426,7 @@ class ADBDevice(MediaPlayerEntity):
return
@adb_decorator()
async def learn_sendevent(self):
async def learn_sendevent(self) -> None:
"""Translate a key press on a remote to ADB 'sendevent' commands."""
output = await self.aftv.learn_sendevent()
if output:
@ -426,7 +445,7 @@ class ADBDevice(MediaPlayerEntity):
_LOGGER.info("%s", msg)
@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."""
if not self.hass.config.is_allowed_path(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)
@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."""
if not self.hass.config.is_allowed_path(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_STEP
)
aftv: AndroidTVAsync
@adb_decorator(override_available=True)
async def async_update(self) -> None:
@ -477,6 +497,7 @@ class AndroidTVDevice(ADBDevice):
if not self.available:
return
prev_app_id = self._attr_app_id
# Get the updated state and attributes.
(
state,
@ -492,7 +513,7 @@ class AndroidTVDevice(ADBDevice):
if self._attr_state is None:
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_app_id, self._attr_app_id
)
@ -506,6 +527,8 @@ class AndroidTVDevice(ADBDevice):
else:
self._attr_source_list = None
await self._async_get_screencap(prev_app_id)
@adb_decorator()
async def async_media_stop(self) -> None:
"""Send stop command."""
@ -549,6 +572,7 @@ class FireTVDevice(ADBDevice):
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.STOP
)
aftv: FireTVAsync
@adb_decorator(override_available=True)
async def async_update(self) -> None:
@ -566,6 +590,7 @@ class FireTVDevice(ADBDevice):
if not self.available:
return
prev_app_id = self._attr_app_id
# Get the `state`, `current_app`, `running_apps` and `hdmi_input`.
(
state,
@ -578,7 +603,7 @@ class FireTVDevice(ADBDevice):
if self._attr_state is None:
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_app_id, self._attr_app_id
)
@ -592,6 +617,8 @@ class FireTVDevice(ADBDevice):
else:
self._attr_source_list = None
await self._async_get_screencap(prev_app_id)
@adb_decorator()
async def async_media_stop(self) -> None:
"""Send stop (back) command."""

View File

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

View File

@ -50,7 +50,7 @@
"title": "Configure Android state detection rules",
"description": "Configure detection rule for application id {rule_id}",
"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_delete": "Check to delete this rule"
}
@ -59,5 +59,49 @@
"error": {
"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 .const import DOMAIN
from .helpers import create_api
from .helpers import create_api, get_enable_ime
_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:
"""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
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(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
@ -87,3 +88,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api.disconnect()
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.components import zeroconf
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.device_registry import format_mac
from .const import DOMAIN
from .helpers import create_api
from .const import CONF_ENABLE_IME, DOMAIN
from .helpers import create_api, get_enable_ime
STEP_USER_DATA_SCHEMA = vol.Schema(
{
@ -55,7 +56,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
self.host = user_input["host"]
assert self.host
api = create_api(self.hass, self.host)
api = create_api(self.hass, self.host, enable_ime=False)
try:
self.name, self.mac = await api.async_get_name_and_mac()
assert self.mac
@ -75,7 +76,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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."""
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_start_pairing()
return await self.async_step_pair()
@ -186,3 +187,38 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={CONF_NAME: self.name},
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
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 homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
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."""
return AndroidTVRemote(
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"),
host=host,
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",
"loggers": ["androidtvremote2"],
"quality_scale": "platinum",
"requirements": ["androidtvremote2==0.0.9"],
"requirements": ["androidtvremote2==0.0.13"],
"zeroconf": ["_androidtvremote2._tcp.local."]
}

View File

@ -34,5 +34,14 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"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"
},
"mode": {
"name": "Mode"
"name": "[%key:common::config_flow::data::mode%]"
},
"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.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@ -79,16 +80,6 @@ class APCUPSdData:
return self.status[model_key]
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
def serial_no(self) -> str | None:
"""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 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)
def update(self, **kwargs: Any) -> None:
"""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.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
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.
if (serial_no := data_service.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_no)},
model=data_service.model,
manufacturer="APC",
hw_version=data_service.hw_version,
sw_version=data_service.sw_version,
)
self._attr_device_info = data_service.device_info
self.entity_description = description
self._data_service = data_service

View File

@ -21,7 +21,6 @@ from homeassistant.const import (
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, APCUPSdData
@ -496,13 +495,7 @@ class APCUPSdSensor(SensorEntity):
# Set up unique id and device info if serial number is available.
if (serial_no := data_service.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_no)},
model=data_service.model,
manufacturer="APC",
hw_version=data_service.hw_version,
sw_version=data_service.sw_version,
)
self._attr_device_info = data_service.device_info
self.entity_description = description
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."
}
}
},
"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_COMPONENTS,
URL_API_CONFIG,
URL_API_CORE_STATE,
URL_API_ERROR_LOG,
URL_API_EVENTS,
URL_API_SERVICES,
@ -55,6 +56,7 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Register the API with the HTTP interface."""
hass.http.register_view(APIStatusView)
hass.http.register_view(APICoreStateView)
hass.http.register_view(APIEventStream)
hass.http.register_view(APIConfigView)
hass.http.register_view(APIStatesView)
@ -84,6 +86,24 @@ class APIStatusView(HomeAssistantView):
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):
"""View to handle EventStream requests."""

View File

@ -88,14 +88,18 @@ class AppleTVEntity(Entity):
"""Device that sends commands to an Apple TV."""
_attr_should_poll = False
_attr_has_entity_name = True
_attr_name = None
def __init__(self, name, identifier, manager):
"""Initialize device."""
self.atv = None
self.manager = manager
self._attr_name = name
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):
"""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",
"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": {
"device_input": "Device"
"device_input": "[%key:common::config_flow::data::device%]"
}
},
"reconfigure": {

View File

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

View File

@ -1,19 +1,18 @@
"""The Aseko Pool Live integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from aioaseko import APIUnavailable, MobileAccount, Unit, Variable
from aioaseko import APIUnavailable, MobileAccount
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
_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)
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.helpers.entity_platform import AddEntitiesCallback
from . import AsekoDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
from .entity import AsekoEntity
@ -31,7 +31,7 @@ class AsekoBinarySensorDescriptionMixin:
class AsekoBinarySensorEntityDescription(
BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin
):
"""Describes a Aseko binary sensor entity."""
"""Describes an Aseko binary sensor entity."""
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.update_coordinator import CoordinatorEntity
from . import AsekoDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):

View File

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

View File

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

View File

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

View File

@ -111,7 +111,7 @@ async def websocket_run(
if start_stage == PipelineStage.STT:
# 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"]
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.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
)
from .bridge import AsusWrtBridge
from .const import (
CONF_DNSMASQ,
CONF_INTERFACE,
@ -47,7 +47,6 @@ from .const import (
PROTOCOL_SSH,
PROTOCOL_TELNET,
)
from .router import get_api, get_nvram_info
LABEL_MAC = "LABEL_MAC"
@ -143,16 +142,15 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors or {},
)
@staticmethod
async def _async_check_connection(
user_input: dict[str, Any]
self, user_input: dict[str, Any]
) -> tuple[str, str | None]:
"""Attempt to connect the AsusWrt router."""
host: str = user_input[CONF_HOST]
api = get_api(user_input)
api = AsusWrtBridge.get_bridge(self.hass, user_input)
try:
await api.connection.async_connect()
await api.async_connect()
except OSError:
_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)
return RESULT_CONN_ERROR, None
label_mac = await get_nvram_info(api, LABEL_MAC)
conf_protocol = user_input[CONF_PROTOCOL]
if conf_protocol == PROTOCOL_TELNET:
api.connection.disconnect()
unique_id = api.label_mac
await api.async_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
async def async_step_user(

View File

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

View File

@ -6,22 +6,12 @@ from datetime import datetime, timedelta
import logging
from typing import Any
from aioasuswrt.asuswrt import AsusWrt, Device as WrtDevice
from homeassistant.components.device_tracker import (
CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME,
DOMAIN as TRACKER_DOMAIN,
)
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.exceptions import ConfigEntryNotReady
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.event import async_track_time_interval
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 (
CONF_DNSMASQ,
CONF_INTERFACE,
CONF_REQUIRE_IP,
CONF_SSH_KEY,
CONF_TRACK_UNKNOWN,
DEFAULT_DNSMASQ,
DEFAULT_INTERFACE,
DEFAULT_TRACK_UNKNOWN,
DOMAIN,
PROTOCOL_TELNET,
SENSORS_BYTES,
KEY_COORDINATOR,
KEY_METHOD,
KEY_SENSORS,
SENSORS_CONNECTED_DEVICE,
SENSORS_LOAD_AVG,
SENSORS_RATES,
SENSORS_TEMPERATURES,
)
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
DEFAULT_NAME = "Asuswrt"
KEY_COORDINATOR = "coordinator"
KEY_SENSORS = "sensors"
SCAN_INTERVAL = timedelta(seconds=30)
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"
_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:
"""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."""
self._hass = hass
self._api = api
@ -90,42 +60,6 @@ class AsusWrtSensorDataHandler:
"""Return number of 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:
"""Update connected devices attribute."""
if self._connected_devices == conn_devices:
@ -134,19 +68,17 @@ class AsusWrtSensorDataHandler:
return True
async def get_coordinator(
self, sensor_type: str, should_poll: bool = True
self,
sensor_type: str,
update_method: Callable[[], Any] | None = None,
) -> DataUpdateCoordinator:
"""Get the coordinator for a specific sensor type."""
should_poll = True
if sensor_type == SENSORS_TYPE_COUNT:
should_poll = False
method = self._get_connected_devices
elif sensor_type == SENSORS_TYPE_BYTES:
method = self._get_bytes
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
elif update_method is not None:
method = update_method
else:
raise RuntimeError(f"Invalid sensor type: {sensor_type}")
@ -226,12 +158,6 @@ class AsusWrtRouter:
self.hass = hass
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._connected_devices: int = 0
self._connect_error: bool = False
@ -248,26 +174,57 @@ class AsusWrtRouter:
}
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:
"""Set up a AsusWrt router."""
self._api = get_api(dict(self._entry.data), self._options)
try:
await self._api.connection.async_connect()
except OSError as exp:
raise ConfigEntryNotReady from exp
await self._api.async_connect()
except OSError as exc:
raise ConfigEntryNotReady from exc
if not self._api.is_connected:
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
entity_reg = er.async_get(self.hass)
track_entries = er.async_entries_for_config_entry(
@ -295,6 +252,9 @@ class AsusWrtRouter:
self._devices[device_mac] = AsusWrtDevInfo(device_mac, entry.original_name)
# Migrate entities to new unique id format
self._migrate_entities_unique_id()
# Update devices
await self.update_devices()
@ -312,24 +272,24 @@ class AsusWrtRouter:
async def update_devices(self) -> None:
"""Update AsusWrt devices tracker."""
new_device = False
_LOGGER.debug("Checking devices for ASUS router %s", self._host)
_LOGGER.debug("Checking devices for ASUS router %s", self.host)
try:
api_devices = await self._api.async_get_connected_devices()
except OSError as exc:
wrt_devices = await self._api.async_get_connected_devices()
except UpdateFailed as exc:
if not self._connect_error:
self._connect_error = True
_LOGGER.error(
"Error connecting to ASUS router %s for device update: %s",
self._host,
self.host,
exc,
)
return
if self._connect_error:
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(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
)
@ -337,7 +297,6 @@ class AsusWrtRouter:
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():
dev_info = wrt_devices.pop(device_mac, None)
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.update_device_count(self._connected_devices)
sensors_types: dict[str, list[str]] = {
SENSORS_TYPE_BYTES: SENSORS_BYTES,
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(),
}
sensors_types = await self._api.async_get_available_sensors()
sensors_types[SENSORS_TYPE_COUNT] = {KEY_SENSORS: SENSORS_CONNECTED_DEVICE}
for sensor_type, sensor_names in sensors_types.items():
if not sensor_names:
for sensor_type, sensor_def in sensors_types.items():
if not (sensor_names := sensor_def.get(KEY_SENSORS)):
continue
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] = {
KEY_COORDINATOR: coordinator,
@ -392,31 +346,10 @@ class AsusWrtRouter:
if self._sensors_data_handler.update_device_count(self._connected_devices):
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:
"""Close the connection."""
if self._api is not None and self._protocol == PROTOCOL_TELNET:
self._api.connection.disconnect()
self._api = None
if self._api is not None:
await self._api.async_disconnect()
for func in self._on_close:
func()
@ -443,14 +376,17 @@ class AsusWrtRouter:
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""
return DeviceInfo(
identifiers={(DOMAIN, self.unique_id or "AsusWRT")},
name=self._host,
model=self._model,
info = DeviceInfo(
identifiers={(DOMAIN, self._entry.unique_id or "AsusWRT")},
name=self.host,
model=self._api.model or "Asus Router",
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
def signal_device_new(self) -> str:
@ -465,17 +401,12 @@ class AsusWrtRouter:
@property
def host(self) -> str:
"""Return router hostname."""
return self._host
return self._api.host
@property
def unique_id(self) -> str | None:
def unique_id(self) -> str:
"""Return router unique id."""
return self._entry.unique_id
@property
def name(self) -> str:
"""Return router name."""
return self._host if self.unique_id else DEFAULT_NAME
return self._entry.unique_id or self._entry.entry_id
@property
def devices(self) -> dict[str, AsusWrtDevInfo]:
@ -486,32 +417,3 @@ class AsusWrtRouter:
def sensors_coordinator(self) -> dict[str, Any]:
"""Return sensors coordinators."""
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,
DataUpdateCoordinator,
)
from homeassistant.util import slugify
from .const import (
DATA_ASUSWRT,
DOMAIN,
KEY_COORDINATOR,
KEY_SENSORS,
SENSORS_BYTES,
SENSORS_CONNECTED_DEVICE,
SENSORS_LOAD_AVG,
SENSORS_RATES,
SENSORS_TEMPERATURES,
)
from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter
from .router import AsusWrtRouter
@dataclass
@ -47,14 +50,14 @@ UNIT_DEVICES = "Devices"
CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
AsusWrtSensorEntityDescription(
key=SENSORS_CONNECTED_DEVICE[0],
name="Devices Connected",
translation_key="devices_connected",
icon="mdi:router-network",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UNIT_DEVICES,
),
AsusWrtSensorEntityDescription(
key=SENSORS_RATES[0],
name="Download Speed",
translation_key="download_speed",
icon="mdi:download-network",
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
@ -65,7 +68,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_RATES[1],
name="Upload Speed",
translation_key="upload_speed",
icon="mdi:upload-network",
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
@ -76,7 +79,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_BYTES[0],
name="Download",
translation_key="download",
icon="mdi:download",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
@ -87,7 +90,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_BYTES[1],
name="Upload",
translation_key="upload",
icon="mdi:upload",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
@ -98,7 +101,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_LOAD_AVG[0],
name="Load Avg (1m)",
translation_key="load_avg_1m",
icon="mdi:cpu-32-bit",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@ -107,7 +110,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_LOAD_AVG[1],
name="Load Avg (5m)",
translation_key="load_avg_5m",
icon="mdi:cpu-32-bit",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@ -116,7 +119,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_LOAD_AVG[2],
name="Load Avg (15m)",
translation_key="load_avg_15m",
icon="mdi:cpu-32-bit",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@ -125,7 +128,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_TEMPERATURES[0],
name="2.4GHz Temperature",
translation_key="24ghz_temperature",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -135,7 +138,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_TEMPERATURES[1],
name="5GHz Temperature",
translation_key="5ghz_temperature",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -145,7 +148,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
),
AsusWrtSensorEntityDescription(
key=SENSORS_TEMPERATURES[2],
name="CPU Temperature",
translation_key="cpu_temperature",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -180,6 +183,9 @@ async def async_setup_entry(
class AsusWrtSensor(CoordinatorEntity, SensorEntity):
"""Representation of a AsusWrt sensor."""
entity_description: AsusWrtSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
coordinator: DataUpdateCoordinator,
@ -188,13 +194,9 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity):
) -> None:
"""Initialize a AsusWrt sensor."""
super().__init__(coordinator)
self.entity_description: AsusWrtSensorEntityDescription = description
self.entity_description = description
self._attr_name = f"{router.name} {description.name}"
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_unique_id = slugify(f"{router.unique_id}_{description.key}")
self._attr_device_info = router.device_info
self._attr_extra_state_attributes = {"hostname": router.host}

View File

@ -36,11 +36,48 @@
"data": {
"consider_home": "Seconds to wait before considering a device away",
"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",
"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