mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
2023.8.0 (#97609)
This commit is contained in:
commit
111510b11a
@ -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/**
|
||||
|
32
.coveragerc
32
.coveragerc
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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
|
||||
|
||||
|
6
.github/workflows/builder.yml
vendored
6
.github/workflows/builder.yml
vendored
@ -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 }}
|
||||
|
||||
|
38
.github/workflows/ci.yaml
vendored
38
.github/workflows/ci.yaml
vendored
@ -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
|
||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -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 }}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.*
|
||||
|
24
CODEOWNERS
24
CODEOWNERS
@ -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
|
||||
|
10
build.yaml
10
build.yaml
@ -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 |
@ -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",
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "u_tec",
|
||||
"name": "U-tec",
|
||||
"integrations": ["ultraloq"]
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"],
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
22
homeassistant/components/ads/strings.json
Normal file
22
homeassistant/components/ads/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
36
homeassistant/components/aftership/strings.json
Normal file
36
homeassistant/components/aftership/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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"],
|
||||
|
@ -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."""
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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"],
|
||||
)
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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]:
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]:
|
||||
|
@ -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:
|
||||
|
130
homeassistant/components/amcrest/strings.json
Normal file
130
homeassistant/components/amcrest/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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."]
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
"name": "State"
|
||||
},
|
||||
"mode": {
|
||||
"name": "Mode"
|
||||
"name": "[%key:common::config_flow::data::mode%]"
|
||||
},
|
||||
"target_temperature": {
|
||||
"name": "Target temperature"
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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": {
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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, ...] = (
|
||||
|
37
homeassistant/components/aseko_pool_live/coordinator.py
Normal file
37
homeassistant/components/aseko_pool_live/coordinator.py
Normal 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}
|
@ -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]):
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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]:
|
||||
|
273
homeassistant/components/asuswrt/bridge.py
Normal file
273
homeassistant/components/asuswrt/bridge.py
Normal 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
|
@ -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(
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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),
|
||||
)
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user