mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 23:57:06 +00:00
Merge pull request #38785 from home-assistant/rc
This commit is contained in:
commit
8eb6f29c53
45
.coveragerc
45
.coveragerc
@ -8,6 +8,10 @@ omit =
|
|||||||
homeassistant/scripts/*.py
|
homeassistant/scripts/*.py
|
||||||
|
|
||||||
# omit pieces of code that rely on external devices being present
|
# omit pieces of code that rely on external devices being present
|
||||||
|
homeassistant/components/accuweather/__init__.py
|
||||||
|
homeassistant/components/accuweather/const.py
|
||||||
|
homeassistant/components/accuweather/sensor.py
|
||||||
|
homeassistant/components/accuweather/weather.py
|
||||||
homeassistant/components/acer_projector/switch.py
|
homeassistant/components/acer_projector/switch.py
|
||||||
homeassistant/components/actiontec/device_tracker.py
|
homeassistant/components/actiontec/device_tracker.py
|
||||||
homeassistant/components/acmeda/__init__.py
|
homeassistant/components/acmeda/__init__.py
|
||||||
@ -28,10 +32,6 @@ omit =
|
|||||||
homeassistant/components/agent_dvr/camera.py
|
homeassistant/components/agent_dvr/camera.py
|
||||||
homeassistant/components/agent_dvr/const.py
|
homeassistant/components/agent_dvr/const.py
|
||||||
homeassistant/components/agent_dvr/helpers.py
|
homeassistant/components/agent_dvr/helpers.py
|
||||||
homeassistant/components/airly/__init__.py
|
|
||||||
homeassistant/components/airly/air_quality.py
|
|
||||||
homeassistant/components/airly/sensor.py
|
|
||||||
homeassistant/components/airly/const.py
|
|
||||||
homeassistant/components/airvisual/__init__.py
|
homeassistant/components/airvisual/__init__.py
|
||||||
homeassistant/components/airvisual/air_quality.py
|
homeassistant/components/airvisual/air_quality.py
|
||||||
homeassistant/components/airvisual/sensor.py
|
homeassistant/components/airvisual/sensor.py
|
||||||
@ -69,6 +69,9 @@ omit =
|
|||||||
homeassistant/components/avion/light.py
|
homeassistant/components/avion/light.py
|
||||||
homeassistant/components/avri/const.py
|
homeassistant/components/avri/const.py
|
||||||
homeassistant/components/avri/sensor.py
|
homeassistant/components/avri/sensor.py
|
||||||
|
homeassistant/components/azure_devops/__init__.py
|
||||||
|
homeassistant/components/azure_devops/const.py
|
||||||
|
homeassistant/components/azure_devops/sensor.py
|
||||||
homeassistant/components/azure_service_bus/*
|
homeassistant/components/azure_service_bus/*
|
||||||
homeassistant/components/baidu/tts.py
|
homeassistant/components/baidu/tts.py
|
||||||
homeassistant/components/beewi_smartclim/sensor.py
|
homeassistant/components/beewi_smartclim/sensor.py
|
||||||
@ -139,6 +142,10 @@ omit =
|
|||||||
homeassistant/components/comfoconnect/*
|
homeassistant/components/comfoconnect/*
|
||||||
homeassistant/components/concord232/alarm_control_panel.py
|
homeassistant/components/concord232/alarm_control_panel.py
|
||||||
homeassistant/components/concord232/binary_sensor.py
|
homeassistant/components/concord232/binary_sensor.py
|
||||||
|
homeassistant/components/control4/__init__.py
|
||||||
|
homeassistant/components/control4/light.py
|
||||||
|
homeassistant/components/control4/const.py
|
||||||
|
homeassistant/components/control4/director_utils.py
|
||||||
homeassistant/components/coolmaster/__init__.py
|
homeassistant/components/coolmaster/__init__.py
|
||||||
homeassistant/components/coolmaster/climate.py
|
homeassistant/components/coolmaster/climate.py
|
||||||
homeassistant/components/coolmaster/const.py
|
homeassistant/components/coolmaster/const.py
|
||||||
@ -164,6 +171,8 @@ omit =
|
|||||||
homeassistant/components/devolo_home_control/binary_sensor.py
|
homeassistant/components/devolo_home_control/binary_sensor.py
|
||||||
homeassistant/components/devolo_home_control/const.py
|
homeassistant/components/devolo_home_control/const.py
|
||||||
homeassistant/components/devolo_home_control/devolo_device.py
|
homeassistant/components/devolo_home_control/devolo_device.py
|
||||||
|
homeassistant/components/devolo_home_control/devolo_multi_level_switch.py
|
||||||
|
homeassistant/components/devolo_home_control/light.py
|
||||||
homeassistant/components/devolo_home_control/sensor.py
|
homeassistant/components/devolo_home_control/sensor.py
|
||||||
homeassistant/components/devolo_home_control/subscriber.py
|
homeassistant/components/devolo_home_control/subscriber.py
|
||||||
homeassistant/components/devolo_home_control/switch.py
|
homeassistant/components/devolo_home_control/switch.py
|
||||||
@ -254,6 +263,13 @@ omit =
|
|||||||
homeassistant/components/fibaro/*
|
homeassistant/components/fibaro/*
|
||||||
homeassistant/components/filesize/sensor.py
|
homeassistant/components/filesize/sensor.py
|
||||||
homeassistant/components/fints/sensor.py
|
homeassistant/components/fints/sensor.py
|
||||||
|
homeassistant/components/firmata/__init__.py
|
||||||
|
homeassistant/components/firmata/binary_sensor.py
|
||||||
|
homeassistant/components/firmata/board.py
|
||||||
|
homeassistant/components/firmata/const.py
|
||||||
|
homeassistant/components/firmata/entity.py
|
||||||
|
homeassistant/components/firmata/pin.py
|
||||||
|
homeassistant/components/firmata/switch.py
|
||||||
homeassistant/components/fitbit/sensor.py
|
homeassistant/components/fitbit/sensor.py
|
||||||
homeassistant/components/fixer/sensor.py
|
homeassistant/components/fixer/sensor.py
|
||||||
homeassistant/components/fleetgo/device_tracker.py
|
homeassistant/components/fleetgo/device_tracker.py
|
||||||
@ -337,7 +353,8 @@ omit =
|
|||||||
homeassistant/components/hisense_aehw4a1/*
|
homeassistant/components/hisense_aehw4a1/*
|
||||||
homeassistant/components/hitron_coda/device_tracker.py
|
homeassistant/components/hitron_coda/device_tracker.py
|
||||||
homeassistant/components/hive/*
|
homeassistant/components/hive/*
|
||||||
homeassistant/components/hlk_sw16/*
|
homeassistant/components/hlk_sw16/__init__.py
|
||||||
|
homeassistant/components/hlk_sw16/switch.py
|
||||||
homeassistant/components/home_connect/*
|
homeassistant/components/home_connect/*
|
||||||
homeassistant/components/homematic/*
|
homeassistant/components/homematic/*
|
||||||
homeassistant/components/homematic/climate.py
|
homeassistant/components/homematic/climate.py
|
||||||
@ -443,8 +460,6 @@ omit =
|
|||||||
homeassistant/components/lightwave/*
|
homeassistant/components/lightwave/*
|
||||||
homeassistant/components/limitlessled/light.py
|
homeassistant/components/limitlessled/light.py
|
||||||
homeassistant/components/linksys_smart/device_tracker.py
|
homeassistant/components/linksys_smart/device_tracker.py
|
||||||
homeassistant/components/linky/__init__.py
|
|
||||||
homeassistant/components/linky/sensor.py
|
|
||||||
homeassistant/components/linode/*
|
homeassistant/components/linode/*
|
||||||
homeassistant/components/linux_battery/sensor.py
|
homeassistant/components/linux_battery/sensor.py
|
||||||
homeassistant/components/lirc/*
|
homeassistant/components/lirc/*
|
||||||
@ -537,6 +552,10 @@ omit =
|
|||||||
homeassistant/components/netatmo/camera.py
|
homeassistant/components/netatmo/camera.py
|
||||||
homeassistant/components/netatmo/climate.py
|
homeassistant/components/netatmo/climate.py
|
||||||
homeassistant/components/netatmo/const.py
|
homeassistant/components/netatmo/const.py
|
||||||
|
homeassistant/components/netatmo/data_handler.py
|
||||||
|
homeassistant/components/netatmo/helper.py
|
||||||
|
homeassistant/components/netatmo/light.py
|
||||||
|
homeassistant/components/netatmo/netatmo_entity_base.py
|
||||||
homeassistant/components/netatmo/sensor.py
|
homeassistant/components/netatmo/sensor.py
|
||||||
homeassistant/components/netatmo/webhook.py
|
homeassistant/components/netatmo/webhook.py
|
||||||
homeassistant/components/netdata/sensor.py
|
homeassistant/components/netdata/sensor.py
|
||||||
@ -605,6 +624,9 @@ omit =
|
|||||||
homeassistant/components/orvibo/switch.py
|
homeassistant/components/orvibo/switch.py
|
||||||
homeassistant/components/osramlightify/light.py
|
homeassistant/components/osramlightify/light.py
|
||||||
homeassistant/components/otp/sensor.py
|
homeassistant/components/otp/sensor.py
|
||||||
|
homeassistant/components/ovo_energy/__init__.py
|
||||||
|
homeassistant/components/ovo_energy/const.py
|
||||||
|
homeassistant/components/ovo_energy/sensor.py
|
||||||
homeassistant/components/panasonic_bluray/media_player.py
|
homeassistant/components/panasonic_bluray/media_player.py
|
||||||
homeassistant/components/panasonic_viera/media_player.py
|
homeassistant/components/panasonic_viera/media_player.py
|
||||||
homeassistant/components/pandora/media_player.py
|
homeassistant/components/pandora/media_player.py
|
||||||
@ -617,6 +639,7 @@ omit =
|
|||||||
homeassistant/components/picotts/tts.py
|
homeassistant/components/picotts/tts.py
|
||||||
homeassistant/components/piglow/light.py
|
homeassistant/components/piglow/light.py
|
||||||
homeassistant/components/pilight/*
|
homeassistant/components/pilight/*
|
||||||
|
homeassistant/components/ping/const.py
|
||||||
homeassistant/components/ping/binary_sensor.py
|
homeassistant/components/ping/binary_sensor.py
|
||||||
homeassistant/components/ping/device_tracker.py
|
homeassistant/components/ping/device_tracker.py
|
||||||
homeassistant/components/pioneer/media_player.py
|
homeassistant/components/pioneer/media_player.py
|
||||||
@ -679,7 +702,6 @@ omit =
|
|||||||
homeassistant/components/rest/binary_sensor.py
|
homeassistant/components/rest/binary_sensor.py
|
||||||
homeassistant/components/rest/notify.py
|
homeassistant/components/rest/notify.py
|
||||||
homeassistant/components/rest/switch.py
|
homeassistant/components/rest/switch.py
|
||||||
homeassistant/components/rfxtrx/*
|
|
||||||
homeassistant/components/ring/camera.py
|
homeassistant/components/ring/camera.py
|
||||||
homeassistant/components/ripple/sensor.py
|
homeassistant/components/ripple/sensor.py
|
||||||
homeassistant/components/rocketchat/notify.py
|
homeassistant/components/rocketchat/notify.py
|
||||||
@ -729,7 +751,7 @@ omit =
|
|||||||
homeassistant/components/simplisafe/lock.py
|
homeassistant/components/simplisafe/lock.py
|
||||||
homeassistant/components/simulated/sensor.py
|
homeassistant/components/simulated/sensor.py
|
||||||
homeassistant/components/sisyphus/*
|
homeassistant/components/sisyphus/*
|
||||||
homeassistant/components/sky_hub/device_tracker.py
|
homeassistant/components/sky_hub/*
|
||||||
homeassistant/components/skybeacon/sensor.py
|
homeassistant/components/skybeacon/sensor.py
|
||||||
homeassistant/components/skybell/*
|
homeassistant/components/skybell/*
|
||||||
homeassistant/components/slack/notify.py
|
homeassistant/components/slack/notify.py
|
||||||
@ -909,6 +931,7 @@ omit =
|
|||||||
homeassistant/components/vlc/media_player.py
|
homeassistant/components/vlc/media_player.py
|
||||||
homeassistant/components/vlc_telnet/media_player.py
|
homeassistant/components/vlc_telnet/media_player.py
|
||||||
homeassistant/components/volkszaehler/sensor.py
|
homeassistant/components/volkszaehler/sensor.py
|
||||||
|
homeassistant/components/volumio/__init__.py
|
||||||
homeassistant/components/volumio/media_player.py
|
homeassistant/components/volumio/media_player.py
|
||||||
homeassistant/components/volvooncall/*
|
homeassistant/components/volvooncall/*
|
||||||
homeassistant/components/w800rf32/*
|
homeassistant/components/w800rf32/*
|
||||||
@ -923,6 +946,9 @@ omit =
|
|||||||
homeassistant/components/wiffi/*
|
homeassistant/components/wiffi/*
|
||||||
homeassistant/components/wink/*
|
homeassistant/components/wink/*
|
||||||
homeassistant/components/wirelesstag/*
|
homeassistant/components/wirelesstag/*
|
||||||
|
homeassistant/components/wolflink/__init__.py
|
||||||
|
homeassistant/components/wolflink/sensor.py
|
||||||
|
homeassistant/components/wolflink/const.py
|
||||||
homeassistant/components/worldtidesinfo/sensor.py
|
homeassistant/components/worldtidesinfo/sensor.py
|
||||||
homeassistant/components/worxlandroid/sensor.py
|
homeassistant/components/worxlandroid/sensor.py
|
||||||
homeassistant/components/x10/light.py
|
homeassistant/components/x10/light.py
|
||||||
@ -955,7 +981,6 @@ omit =
|
|||||||
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
||||||
homeassistant/components/yamaha_musiccast/media_player.py
|
homeassistant/components/yamaha_musiccast/media_player.py
|
||||||
homeassistant/components/yandex_transport/*
|
homeassistant/components/yandex_transport/*
|
||||||
homeassistant/components/yeelight/*
|
|
||||||
homeassistant/components/yeelightsunflower/light.py
|
homeassistant/components/yeelightsunflower/light.py
|
||||||
homeassistant/components/yi/camera.py
|
homeassistant/components/yi/camera.py
|
||||||
homeassistant/components/zabbix/*
|
homeassistant/components/zabbix/*
|
||||||
|
28
.github/workflows/ci.yaml
vendored
28
.github/workflows/ci.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
@ -75,7 +75,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -119,7 +119,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -163,7 +163,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -229,7 +229,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -276,7 +276,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -323,7 +323,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -367,7 +367,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -414,7 +414,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -469,7 +469,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -516,7 +516,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -548,7 +548,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -737,7 +737,7 @@ jobs:
|
|||||||
-p no:sugar \
|
-p no:sugar \
|
||||||
tests
|
tests
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
uses: actions/upload-artifact@2.1.0
|
uses: actions/upload-artifact@v2.1.3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
|
||||||
path: .coverage
|
path: .coverage
|
||||||
@ -781,4 +781,4 @@ jobs:
|
|||||||
coverage report --fail-under=94
|
coverage report --fail-under=94
|
||||||
coverage xml
|
coverage xml
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v1.0.10
|
uses: codecov/codecov-action@v1.0.12
|
||||||
|
@ -12,6 +12,7 @@ addons:
|
|||||||
- libavfilter-dev
|
- libavfilter-dev
|
||||||
sources:
|
sources:
|
||||||
- sourceline: ppa:savoury1/ffmpeg4
|
- sourceline: ppa:savoury1/ffmpeg4
|
||||||
|
- sourceline: ppa:savoury1/multimedia
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- "3.7.1"
|
- "3.7.1"
|
||||||
|
18
CODEOWNERS
18
CODEOWNERS
@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza
|
|||||||
|
|
||||||
# Integrations
|
# Integrations
|
||||||
homeassistant/components/abode/* @shred86
|
homeassistant/components/abode/* @shred86
|
||||||
|
homeassistant/components/accuweather/* @bieniu
|
||||||
homeassistant/components/acmeda/* @atmurray
|
homeassistant/components/acmeda/* @atmurray
|
||||||
homeassistant/components/adguard/* @frenck
|
homeassistant/components/adguard/* @frenck
|
||||||
homeassistant/components/agent_dvr/* @ispysoftware
|
homeassistant/components/agent_dvr/* @ispysoftware
|
||||||
@ -48,6 +49,7 @@ homeassistant/components/avri/* @timvancann
|
|||||||
homeassistant/components/awair/* @ahayworth @danielsjf
|
homeassistant/components/awair/* @ahayworth @danielsjf
|
||||||
homeassistant/components/aws/* @awarecan
|
homeassistant/components/aws/* @awarecan
|
||||||
homeassistant/components/axis/* @Kane610
|
homeassistant/components/axis/* @Kane610
|
||||||
|
homeassistant/components/azure_devops/* @timmo001
|
||||||
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
||||||
homeassistant/components/azure_service_bus/* @hfurubotten
|
homeassistant/components/azure_service_bus/* @hfurubotten
|
||||||
homeassistant/components/beewi_smartclim/* @alemuro
|
homeassistant/components/beewi_smartclim/* @alemuro
|
||||||
@ -77,6 +79,7 @@ homeassistant/components/cloudflare/* @ludeeus
|
|||||||
homeassistant/components/comfoconnect/* @michaelarnauts
|
homeassistant/components/comfoconnect/* @michaelarnauts
|
||||||
homeassistant/components/config/* @home-assistant/core
|
homeassistant/components/config/* @home-assistant/core
|
||||||
homeassistant/components/configurator/* @home-assistant/core
|
homeassistant/components/configurator/* @home-assistant/core
|
||||||
|
homeassistant/components/control4/* @lawtancool
|
||||||
homeassistant/components/conversation/* @home-assistant/core
|
homeassistant/components/conversation/* @home-assistant/core
|
||||||
homeassistant/components/coolmaster/* @OnFreund
|
homeassistant/components/coolmaster/* @OnFreund
|
||||||
homeassistant/components/coronavirus/* @home_assistant/core
|
homeassistant/components/coronavirus/* @home_assistant/core
|
||||||
@ -110,7 +113,7 @@ homeassistant/components/edl21/* @mtdcr
|
|||||||
homeassistant/components/egardia/* @jeroenterheerdt
|
homeassistant/components/egardia/* @jeroenterheerdt
|
||||||
homeassistant/components/eight_sleep/* @mezz64
|
homeassistant/components/eight_sleep/* @mezz64
|
||||||
homeassistant/components/elgato/* @frenck
|
homeassistant/components/elgato/* @frenck
|
||||||
homeassistant/components/elkm1/* @bdraco
|
homeassistant/components/elkm1/* @gwww @bdraco
|
||||||
homeassistant/components/elv/* @majuss
|
homeassistant/components/elv/* @majuss
|
||||||
homeassistant/components/emby/* @mezz64
|
homeassistant/components/emby/* @mezz64
|
||||||
homeassistant/components/emoncms/* @borpin
|
homeassistant/components/emoncms/* @borpin
|
||||||
@ -128,6 +131,7 @@ homeassistant/components/ezviz/* @baqs
|
|||||||
homeassistant/components/fastdotcom/* @rohankapoorcom
|
homeassistant/components/fastdotcom/* @rohankapoorcom
|
||||||
homeassistant/components/file/* @fabaff
|
homeassistant/components/file/* @fabaff
|
||||||
homeassistant/components/filter/* @dgomes
|
homeassistant/components/filter/* @dgomes
|
||||||
|
homeassistant/components/firmata/* @DaAwesomeP
|
||||||
homeassistant/components/fixer/* @fabaff
|
homeassistant/components/fixer/* @fabaff
|
||||||
homeassistant/components/flick_electric/* @ZephireNZ
|
homeassistant/components/flick_electric/* @ZephireNZ
|
||||||
homeassistant/components/flock/* @fabaff
|
homeassistant/components/flock/* @fabaff
|
||||||
@ -168,6 +172,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl
|
|||||||
homeassistant/components/hisense_aehw4a1/* @bannhead
|
homeassistant/components/hisense_aehw4a1/* @bannhead
|
||||||
homeassistant/components/history/* @home-assistant/core
|
homeassistant/components/history/* @home-assistant/core
|
||||||
homeassistant/components/hive/* @Rendili @KJonline
|
homeassistant/components/hive/* @Rendili @KJonline
|
||||||
|
homeassistant/components/hlk_sw16/* @jameshilliard
|
||||||
homeassistant/components/home_connect/* @DavidMStraub
|
homeassistant/components/home_connect/* @DavidMStraub
|
||||||
homeassistant/components/homeassistant/* @home-assistant/core
|
homeassistant/components/homeassistant/* @home-assistant/core
|
||||||
homeassistant/components/homekit/* @bdraco
|
homeassistant/components/homekit/* @bdraco
|
||||||
@ -221,7 +226,6 @@ homeassistant/components/lametric/* @robbiet480
|
|||||||
homeassistant/components/launch_library/* @ludeeus
|
homeassistant/components/launch_library/* @ludeeus
|
||||||
homeassistant/components/lcn/* @alengwenus
|
homeassistant/components/lcn/* @alengwenus
|
||||||
homeassistant/components/life360/* @pnbruckner
|
homeassistant/components/life360/* @pnbruckner
|
||||||
homeassistant/components/linky/* @Quentame
|
|
||||||
homeassistant/components/linux_battery/* @fabaff
|
homeassistant/components/linux_battery/* @fabaff
|
||||||
homeassistant/components/local_ip/* @issacg
|
homeassistant/components/local_ip/* @issacg
|
||||||
homeassistant/components/logger/* @home-assistant/core
|
homeassistant/components/logger/* @home-assistant/core
|
||||||
@ -239,7 +243,7 @@ homeassistant/components/mediaroom/* @dgomes
|
|||||||
homeassistant/components/melcloud/* @vilppuvuorinen
|
homeassistant/components/melcloud/* @vilppuvuorinen
|
||||||
homeassistant/components/melissa/* @kennedyshead
|
homeassistant/components/melissa/* @kennedyshead
|
||||||
homeassistant/components/met/* @danielhiversen
|
homeassistant/components/met/* @danielhiversen
|
||||||
homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame
|
homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame
|
||||||
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
||||||
homeassistant/components/metoffice/* @MrHarcombe
|
homeassistant/components/metoffice/* @MrHarcombe
|
||||||
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
||||||
@ -297,6 +301,7 @@ homeassistant/components/openweathermap/* @fabaff
|
|||||||
homeassistant/components/opnsense/* @mtreinish
|
homeassistant/components/opnsense/* @mtreinish
|
||||||
homeassistant/components/orangepi_gpio/* @pascallj
|
homeassistant/components/orangepi_gpio/* @pascallj
|
||||||
homeassistant/components/oru/* @bvlaicu
|
homeassistant/components/oru/* @bvlaicu
|
||||||
|
homeassistant/components/ovo_energy/* @timmo001
|
||||||
homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare
|
homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare
|
||||||
homeassistant/components/panasonic_viera/* @joogps
|
homeassistant/components/panasonic_viera/* @joogps
|
||||||
homeassistant/components/panel_custom/* @home-assistant/frontend
|
homeassistant/components/panel_custom/* @home-assistant/frontend
|
||||||
@ -362,6 +367,7 @@ homeassistant/components/signal_messenger/* @bbernhard
|
|||||||
homeassistant/components/simplisafe/* @bachya
|
homeassistant/components/simplisafe/* @bachya
|
||||||
homeassistant/components/sinch/* @bendikrb
|
homeassistant/components/sinch/* @bendikrb
|
||||||
homeassistant/components/sisyphus/* @jkeljo
|
homeassistant/components/sisyphus/* @jkeljo
|
||||||
|
homeassistant/components/sky_hub/* @rogerselwyn
|
||||||
homeassistant/components/slide/* @ualex73
|
homeassistant/components/slide/* @ualex73
|
||||||
homeassistant/components/sma/* @kellerza
|
homeassistant/components/sma/* @kellerza
|
||||||
homeassistant/components/smappee/* @bsmappee
|
homeassistant/components/smappee/* @bsmappee
|
||||||
@ -449,6 +455,7 @@ homeassistant/components/vilfo/* @ManneW
|
|||||||
homeassistant/components/vivotek/* @HarlemSquirrel
|
homeassistant/components/vivotek/* @HarlemSquirrel
|
||||||
homeassistant/components/vizio/* @raman325
|
homeassistant/components/vizio/* @raman325
|
||||||
homeassistant/components/vlc_telnet/* @rodripf
|
homeassistant/components/vlc_telnet/* @rodripf
|
||||||
|
homeassistant/components/volumio/* @OnFreund
|
||||||
homeassistant/components/waqi/* @andrey-git
|
homeassistant/components/waqi/* @andrey-git
|
||||||
homeassistant/components/watson_tts/* @rutkai
|
homeassistant/components/watson_tts/* @rutkai
|
||||||
homeassistant/components/weather/* @fabaff
|
homeassistant/components/weather/* @fabaff
|
||||||
@ -457,16 +464,17 @@ homeassistant/components/websocket_api/* @home-assistant/core
|
|||||||
homeassistant/components/wiffi/* @mampfes
|
homeassistant/components/wiffi/* @mampfes
|
||||||
homeassistant/components/withings/* @vangorra
|
homeassistant/components/withings/* @vangorra
|
||||||
homeassistant/components/wled/* @frenck
|
homeassistant/components/wled/* @frenck
|
||||||
|
homeassistant/components/wolflink/* @adamkrol93
|
||||||
homeassistant/components/workday/* @fabaff
|
homeassistant/components/workday/* @fabaff
|
||||||
homeassistant/components/worldclock/* @fabaff
|
homeassistant/components/worldclock/* @fabaff
|
||||||
homeassistant/components/xbox_live/* @MartinHjelmare
|
homeassistant/components/xbox_live/* @MartinHjelmare
|
||||||
homeassistant/components/xfinity/* @cisasteelersfan
|
homeassistant/components/xfinity/* @cisasteelersfan
|
||||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG
|
||||||
homeassistant/components/xiaomi_tv/* @simse
|
homeassistant/components/xiaomi_tv/* @simse
|
||||||
homeassistant/components/xmpp/* @fabaff @flowolf
|
homeassistant/components/xmpp/* @fabaff @flowolf
|
||||||
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||||
homeassistant/components/yandex_transport/* @rishatik92
|
homeassistant/components/yandex_transport/* @rishatik92 @devbis
|
||||||
homeassistant/components/yeelight/* @rytilahti @zewelor
|
homeassistant/components/yeelight/* @rytilahti @zewelor
|
||||||
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
||||||
homeassistant/components/yessssms/* @flowolf
|
homeassistant/components/yessssms/* @flowolf
|
||||||
|
@ -29,12 +29,31 @@ jobs:
|
|||||||
- template: templates/azp-job-wheels.yaml@azure
|
- template: templates/azp-job-wheels.yaml@azure
|
||||||
parameters:
|
parameters:
|
||||||
builderVersion: '$(versionWheels)'
|
builderVersion: '$(versionWheels)'
|
||||||
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
|
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev'
|
||||||
builderPip: 'Cython;numpy'
|
builderPip: 'Cython;numpy'
|
||||||
skipBinary: 'aiohttp'
|
skipBinary: 'aiohttp'
|
||||||
|
wheelsRequirement: 'requirements.txt'
|
||||||
|
wheelsRequirementDiff: 'requirements_diff.txt'
|
||||||
|
wheelsConstraint: 'homeassistant/package_constraints.txt'
|
||||||
|
jobName: 'Wheels_Core'
|
||||||
|
preBuild:
|
||||||
|
- script: |
|
||||||
|
if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt
|
||||||
|
fi
|
||||||
|
displayName: 'Prepare requirements files for Home Assistant Core wheels'
|
||||||
|
- template: templates/azp-job-wheels.yaml@azure
|
||||||
|
parameters:
|
||||||
|
builderVersion: '$(versionWheels)'
|
||||||
|
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
|
||||||
|
builderPip: 'Cython;numpy;scikit-build'
|
||||||
|
skipBinary: 'aiohttp'
|
||||||
wheelsRequirement: 'requirements_wheels.txt'
|
wheelsRequirement: 'requirements_wheels.txt'
|
||||||
wheelsRequirementDiff: 'requirements_diff.txt'
|
wheelsRequirementDiff: 'requirements_diff.txt'
|
||||||
wheelsConstraint: 'homeassistant/package_constraints.txt'
|
wheelsConstraint: 'homeassistant/package_constraints.txt'
|
||||||
|
jobName: 'Wheels_Integrations'
|
||||||
preBuild:
|
preBuild:
|
||||||
- script: |
|
- script: |
|
||||||
cp requirements_all.txt requirements_wheels.txt
|
cp requirements_all.txt requirements_wheels.txt
|
||||||
|
10
build.json
10
build.json
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"image": "homeassistant/{arch}-homeassistant",
|
"image": "homeassistant/{arch}-homeassistant",
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "homeassistant/aarch64-homeassistant-base:8.0.0",
|
"aarch64": "homeassistant/aarch64-homeassistant-base:8.2.1",
|
||||||
"armhf": "homeassistant/armhf-homeassistant-base:8.0.0",
|
"armhf": "homeassistant/armhf-homeassistant-base:8.2.1",
|
||||||
"armv7": "homeassistant/armv7-homeassistant-base:8.0.0",
|
"armv7": "homeassistant/armv7-homeassistant-base:8.2.1",
|
||||||
"amd64": "homeassistant/amd64-homeassistant-base:8.0.0",
|
"amd64": "homeassistant/amd64-homeassistant-base:8.2.1",
|
||||||
"i386": "homeassistant/i386-homeassistant-base:8.0.0"
|
"i386": "homeassistant/i386-homeassistant-base:8.2.1"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"io.hass.type": "core"
|
"io.hass.type": "core"
|
||||||
|
@ -7,7 +7,7 @@ from homeassistant.util.async_ import protect_loop
|
|||||||
def enable() -> None:
|
def enable() -> None:
|
||||||
"""Enable the detection of I/O in the event loop."""
|
"""Enable the detection of I/O in the event loop."""
|
||||||
# Prevent urllib3 and requests doing I/O in event loop
|
# Prevent urllib3 and requests doing I/O in event loop
|
||||||
HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest)
|
HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest) # type: ignore
|
||||||
|
|
||||||
# Currently disabled. pytz doing I/O when getting timezone.
|
# Currently disabled. pytz doing I/O when getting timezone.
|
||||||
# Prevent files being opened inside the event loop
|
# Prevent files being opened inside the event loop
|
||||||
|
@ -6,10 +6,10 @@ import logging
|
|||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
||||||
|
|
||||||
from async_timeout import timeout
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
import yarl
|
||||||
|
|
||||||
@ -44,6 +44,11 @@ DATA_LOGGING = "logging"
|
|||||||
|
|
||||||
LOG_SLOW_STARTUP_INTERVAL = 60
|
LOG_SLOW_STARTUP_INTERVAL = 60
|
||||||
|
|
||||||
|
STAGE_1_TIMEOUT = 120
|
||||||
|
STAGE_2_TIMEOUT = 300
|
||||||
|
WRAP_UP_TIMEOUT = 300
|
||||||
|
COOLDOWN_TIME = 60
|
||||||
|
|
||||||
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
|
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
|
||||||
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
||||||
LOGGING_INTEGRATIONS = {
|
LOGGING_INTEGRATIONS = {
|
||||||
@ -136,7 +141,7 @@ async def async_setup_hass(
|
|||||||
hass.async_track_tasks()
|
hass.async_track_tasks()
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
|
||||||
with contextlib.suppress(asyncio.TimeoutError):
|
with contextlib.suppress(asyncio.TimeoutError):
|
||||||
async with timeout(10):
|
async with hass.timeout.async_timeout(10):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
safe_mode = True
|
safe_mode = True
|
||||||
@ -304,6 +309,12 @@ def async_enable_logging(
|
|||||||
"Uncaught exception", exc_info=args # type: ignore
|
"Uncaught exception", exc_info=args # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sys.version_info[:2] >= (3, 8):
|
||||||
|
threading.excepthook = lambda args: logging.getLogger(None).exception(
|
||||||
|
"Uncaught thread exception",
|
||||||
|
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
|
||||||
|
)
|
||||||
|
|
||||||
# Log errors to a file if we have write access to file or config dir
|
# Log errors to a file if we have write access to file or config dir
|
||||||
if log_file is None:
|
if log_file is None:
|
||||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||||
@ -496,24 +507,42 @@ async def _async_set_up_integrations(
|
|||||||
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
|
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
|
||||||
|
|
||||||
# Kick off loading the registries. They don't need to be awaited.
|
# Kick off loading the registries. They don't need to be awaited.
|
||||||
asyncio.gather(
|
asyncio.create_task(hass.helpers.device_registry.async_get_registry())
|
||||||
hass.helpers.device_registry.async_get_registry(),
|
asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
|
||||||
hass.helpers.entity_registry.async_get_registry(),
|
asyncio.create_task(hass.helpers.area_registry.async_get_registry())
|
||||||
hass.helpers.area_registry.async_get_registry(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start setup
|
# Start setup
|
||||||
if stage_1_domains:
|
if stage_1_domains:
|
||||||
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
|
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
|
||||||
await async_setup_multi_components(hass, stage_1_domains, config, setup_started)
|
try:
|
||||||
|
async with hass.timeout.async_timeout(
|
||||||
|
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
|
||||||
|
):
|
||||||
|
await async_setup_multi_components(
|
||||||
|
hass, stage_1_domains, config, setup_started
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning("Setup timed out for stage 1 - moving forward")
|
||||||
|
|
||||||
# Enables after dependencies
|
# Enables after dependencies
|
||||||
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
|
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
|
||||||
|
|
||||||
if stage_2_domains:
|
if stage_2_domains:
|
||||||
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
|
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
|
||||||
await async_setup_multi_components(hass, stage_2_domains, config, setup_started)
|
try:
|
||||||
|
async with hass.timeout.async_timeout(
|
||||||
|
STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME
|
||||||
|
):
|
||||||
|
await async_setup_multi_components(
|
||||||
|
hass, stage_2_domains, config, setup_started
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning("Setup timed out for stage 2 - moving forward")
|
||||||
|
|
||||||
# Wrap up startup
|
# Wrap up startup
|
||||||
_LOGGER.debug("Waiting for startup to wrap up")
|
_LOGGER.debug("Waiting for startup to wrap up")
|
||||||
|
try:
|
||||||
|
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
|
||||||
|
132
homeassistant/components/accuweather/__init__.py
Normal file
132
homeassistant/components/accuweather/__init__.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
"""The AccuWeather component."""
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||||
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
from async_timeout import timeout
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.core import Config, HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ATTR_FORECAST,
|
||||||
|
CONF_FORECAST,
|
||||||
|
COORDINATOR,
|
||||||
|
DOMAIN,
|
||||||
|
UNDO_UPDATE_LISTENER,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS = ["sensor", "weather"]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
|
||||||
|
"""Set up configured AccuWeather."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry) -> bool:
|
||||||
|
"""Set up AccuWeather as config entry."""
|
||||||
|
api_key = config_entry.data[CONF_API_KEY]
|
||||||
|
location_key = config_entry.unique_id
|
||||||
|
forecast = config_entry.options.get(CONF_FORECAST, False)
|
||||||
|
|
||||||
|
_LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast)
|
||||||
|
|
||||||
|
websession = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
coordinator = AccuWeatherDataUpdateCoordinator(
|
||||||
|
hass, websession, api_key, location_key, forecast
|
||||||
|
)
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
|
||||||
|
if not coordinator.last_update_success:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
undo_listener = config_entry.add_update_listener(update_listener)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||||
|
COORDINATOR: coordinator,
|
||||||
|
UNDO_UPDATE_LISTENER: undo_listener,
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, config_entry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||||
|
for component in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass, config_entry):
|
||||||
|
"""Update listener."""
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Class to manage fetching AccuWeather data API."""
|
||||||
|
|
||||||
|
def __init__(self, hass, session, api_key, location_key, forecast: bool):
|
||||||
|
"""Initialize."""
|
||||||
|
self.location_key = location_key
|
||||||
|
self.forecast = forecast
|
||||||
|
self.is_metric = hass.config.units.is_metric
|
||||||
|
self.accuweather = AccuWeather(api_key, session, location_key=self.location_key)
|
||||||
|
|
||||||
|
# Enabling the forecast download increases the number of requests per data
|
||||||
|
# update, we use 32 minutes for current condition only and 64 minutes for
|
||||||
|
# current condition and forecast as update interval to not exceed allowed number
|
||||||
|
# of requests. We have 50 requests allowed per day, so we use 45 and leave 5 as
|
||||||
|
# a reserve for restarting HA.
|
||||||
|
update_interval = (
|
||||||
|
timedelta(minutes=64) if self.forecast else timedelta(minutes=32)
|
||||||
|
)
|
||||||
|
_LOGGER.debug("Data will be update every %s", update_interval)
|
||||||
|
|
||||||
|
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||||
|
|
||||||
|
async def _async_update_data(self):
|
||||||
|
"""Update data via library."""
|
||||||
|
try:
|
||||||
|
async with timeout(10):
|
||||||
|
current = await self.accuweather.async_get_current_conditions()
|
||||||
|
forecast = (
|
||||||
|
await self.accuweather.async_get_forecast(metric=self.is_metric)
|
||||||
|
if self.forecast
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
except (
|
||||||
|
ApiError,
|
||||||
|
ClientConnectorError,
|
||||||
|
InvalidApiKeyError,
|
||||||
|
RequestsExceededError,
|
||||||
|
) as error:
|
||||||
|
raise UpdateFailed(error)
|
||||||
|
_LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining)
|
||||||
|
return {**current, **{ATTR_FORECAST: forecast}}
|
112
homeassistant/components/accuweather/config_flow.py
Normal file
112
homeassistant/components/accuweather/config_flow.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"""Adds config flow for AccuWeather."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||||
|
from aiohttp import ClientError
|
||||||
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
from async_timeout import timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from .const import CONF_FORECAST, DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
|
|
||||||
|
class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Config flow for AccuWeather."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
# Under the terms of use of the API, one user can use one free API key. Due to
|
||||||
|
# the small number of requests allowed, we only allow one integration instance.
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
websession = async_get_clientsession(self.hass)
|
||||||
|
try:
|
||||||
|
with timeout(10):
|
||||||
|
accuweather = AccuWeather(
|
||||||
|
user_input[CONF_API_KEY],
|
||||||
|
websession,
|
||||||
|
latitude=user_input[CONF_LATITUDE],
|
||||||
|
longitude=user_input[CONF_LONGITUDE],
|
||||||
|
)
|
||||||
|
await accuweather.async_get_location()
|
||||||
|
except (ApiError, ClientConnectorError, asyncio.TimeoutError, ClientError):
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidApiKeyError:
|
||||||
|
errors[CONF_API_KEY] = "invalid_api_key"
|
||||||
|
except RequestsExceededError:
|
||||||
|
errors[CONF_API_KEY] = "requests_exceeded"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
accuweather.location_key, raise_on_progress=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_NAME], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_LATITUDE, default=self.hass.config.latitude
|
||||||
|
): cv.latitude,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||||
|
): cv.longitude,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_NAME, default=self.hass.config.location_name
|
||||||
|
): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Options callback for AccuWeather."""
|
||||||
|
return AccuWeatherOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Config flow options for AccuWeather."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry):
|
||||||
|
"""Initialize AccuWeather options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Manage the options."""
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_FORECAST,
|
||||||
|
default=self.config_entry.options.get(CONF_FORECAST, False),
|
||||||
|
): bool
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
279
homeassistant/components/accuweather/const.py
Normal file
279
homeassistant/components/accuweather/const.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
"""Constants for AccuWeather integration."""
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
LENGTH_FEET,
|
||||||
|
LENGTH_INCHES,
|
||||||
|
LENGTH_METERS,
|
||||||
|
SPEED_KILOMETERS_PER_HOUR,
|
||||||
|
SPEED_MILES_PER_HOUR,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
TEMP_FAHRENHEIT,
|
||||||
|
TIME_HOURS,
|
||||||
|
UNIT_PERCENTAGE,
|
||||||
|
UV_INDEX,
|
||||||
|
VOLUME_CUBIC_METERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
ATTRIBUTION = "Data provided by AccuWeather"
|
||||||
|
ATTR_ICON = "icon"
|
||||||
|
ATTR_FORECAST = CONF_FORECAST = "forecast"
|
||||||
|
ATTR_LABEL = "label"
|
||||||
|
ATTR_UNIT_IMPERIAL = "Imperial"
|
||||||
|
ATTR_UNIT_METRIC = "Metric"
|
||||||
|
CONCENTRATION_PARTS_PER_CUBIC_METER = f"p/{VOLUME_CUBIC_METERS}"
|
||||||
|
COORDINATOR = "coordinator"
|
||||||
|
DOMAIN = "accuweather"
|
||||||
|
LENGTH_MILIMETERS = "mm"
|
||||||
|
MANUFACTURER = "AccuWeather, Inc."
|
||||||
|
NAME = "AccuWeather"
|
||||||
|
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||||
|
|
||||||
|
CONDITION_CLASSES = {
|
||||||
|
"clear-night": [33, 34, 37],
|
||||||
|
"cloudy": [7, 8, 38],
|
||||||
|
"exceptional": [24, 30, 31],
|
||||||
|
"fog": [11],
|
||||||
|
"hail": [25],
|
||||||
|
"lightning": [15],
|
||||||
|
"lightning-rainy": [16, 17, 41, 42],
|
||||||
|
"partlycloudy": [4, 6, 35, 36],
|
||||||
|
"pouring": [18],
|
||||||
|
"rainy": [12, 13, 14, 26, 39, 40],
|
||||||
|
"snowy": [19, 20, 21, 22, 23, 43, 44],
|
||||||
|
"snowy-rainy": [29],
|
||||||
|
"sunny": [1, 2, 3, 5],
|
||||||
|
"windy": [32],
|
||||||
|
}
|
||||||
|
|
||||||
|
FORECAST_DAYS = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
|
FORECAST_SENSOR_TYPES = {
|
||||||
|
"CloudCoverDay": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-cloudy",
|
||||||
|
ATTR_LABEL: "Cloud Cover Day",
|
||||||
|
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
|
||||||
|
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
|
||||||
|
},
|
||||||
|
"CloudCoverNight": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-cloudy",
|
||||||
|
ATTR_LABEL: "Cloud Cover Night",
|
||||||
|
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
|
||||||
|
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
|
||||||
|
},
|
||||||
|
"Grass": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:grass",
|
||||||
|
ATTR_LABEL: "Grass Pollen",
|
||||||
|
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
|
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
|
},
|
||||||
|
"HoursOfSun": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-partly-cloudy",
|
||||||
|
ATTR_LABEL: "Hours Of Sun",
|
||||||
|
ATTR_UNIT_METRIC: TIME_HOURS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TIME_HOURS,
|
||||||
|
},
|
||||||
|
"Mold": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:blur",
|
||||||
|
ATTR_LABEL: "Mold Pollen",
|
||||||
|
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
|
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
|
},
|
||||||
|
"Ozone": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:vector-triangle",
|
||||||
|
ATTR_LABEL: "Ozone",
|
||||||
|
ATTR_UNIT_METRIC: None,
|
||||||
|
ATTR_UNIT_IMPERIAL: None,
|
||||||
|
},
|
||||||
|
"Ragweed": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:sprout",
|
||||||
|
ATTR_LABEL: "Ragweed Pollen",
|
||||||
|
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
|
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
|
},
|
||||||
|
"RealFeelTemperatureMax": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "RealFeel Temperature Max",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"RealFeelTemperatureMin": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "RealFeel Temperature Min",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"RealFeelTemperatureShadeMax": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "RealFeel Temperature Shade Max",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"RealFeelTemperatureShadeMin": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "RealFeel Temperature Shade Min",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"ThunderstormProbabilityDay": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-lightning",
|
||||||
|
ATTR_LABEL: "Thunderstorm Probability Day",
|
||||||
|
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
|
||||||
|
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
|
||||||
|
},
|
||||||
|
"ThunderstormProbabilityNight": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-lightning",
|
||||||
|
ATTR_LABEL: "Thunderstorm Probability Night",
|
||||||
|
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
|
||||||
|
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
|
||||||
|
},
|
||||||
|
"Tree": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:tree-outline",
|
||||||
|
ATTR_LABEL: "Tree Pollen",
|
||||||
|
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
|
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
|
},
|
||||||
|
"UVIndex": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-sunny",
|
||||||
|
ATTR_LABEL: "UV Index",
|
||||||
|
ATTR_UNIT_METRIC: UV_INDEX,
|
||||||
|
ATTR_UNIT_IMPERIAL: UV_INDEX,
|
||||||
|
},
|
||||||
|
"WindGustDay": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-windy",
|
||||||
|
ATTR_LABEL: "Wind Gust Day",
|
||||||
|
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||||
|
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||||
|
},
|
||||||
|
"WindGustNight": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-windy",
|
||||||
|
ATTR_LABEL: "Wind Gust Night",
|
||||||
|
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||||
|
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
OPTIONAL_SENSORS = (
|
||||||
|
"ApparentTemperature",
|
||||||
|
"CloudCover",
|
||||||
|
"CloudCoverDay",
|
||||||
|
"CloudCoverNight",
|
||||||
|
"DewPoint",
|
||||||
|
"Grass",
|
||||||
|
"Mold",
|
||||||
|
"Ozone",
|
||||||
|
"Ragweed",
|
||||||
|
"RealFeelTemperatureShade",
|
||||||
|
"RealFeelTemperatureShadeMax",
|
||||||
|
"RealFeelTemperatureShadeMin",
|
||||||
|
"Tree",
|
||||||
|
"WetBulbTemperature",
|
||||||
|
"WindChillTemperature",
|
||||||
|
"WindGust",
|
||||||
|
"WindGustDay",
|
||||||
|
"WindGustNight",
|
||||||
|
)
|
||||||
|
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
"ApparentTemperature": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Apparent Temperature",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"Ceiling": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-fog",
|
||||||
|
ATTR_LABEL: "Cloud Ceiling",
|
||||||
|
ATTR_UNIT_METRIC: LENGTH_METERS,
|
||||||
|
ATTR_UNIT_IMPERIAL: LENGTH_FEET,
|
||||||
|
},
|
||||||
|
"CloudCover": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-cloudy",
|
||||||
|
ATTR_LABEL: "Cloud Cover",
|
||||||
|
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
|
||||||
|
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
|
||||||
|
},
|
||||||
|
"DewPoint": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Dew Point",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"RealFeelTemperature": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "RealFeel Temperature",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"RealFeelTemperatureShade": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "RealFeel Temperature Shade",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"Precipitation": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-rainy",
|
||||||
|
ATTR_LABEL: "Precipitation",
|
||||||
|
ATTR_UNIT_METRIC: LENGTH_MILIMETERS,
|
||||||
|
ATTR_UNIT_IMPERIAL: LENGTH_INCHES,
|
||||||
|
},
|
||||||
|
"PressureTendency": {
|
||||||
|
ATTR_DEVICE_CLASS: "accuweather__pressure_tendency",
|
||||||
|
ATTR_ICON: "mdi:gauge",
|
||||||
|
ATTR_LABEL: "Pressure Tendency",
|
||||||
|
ATTR_UNIT_METRIC: None,
|
||||||
|
ATTR_UNIT_IMPERIAL: None,
|
||||||
|
},
|
||||||
|
"UVIndex": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-sunny",
|
||||||
|
ATTR_LABEL: "UV Index",
|
||||||
|
ATTR_UNIT_METRIC: UV_INDEX,
|
||||||
|
ATTR_UNIT_IMPERIAL: UV_INDEX,
|
||||||
|
},
|
||||||
|
"WetBulbTemperature": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Wet Bulb Temperature",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"WindChillTemperature": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Wind Chill Temperature",
|
||||||
|
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||||
|
},
|
||||||
|
"WindGust": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-windy",
|
||||||
|
ATTR_LABEL: "Wind Gust",
|
||||||
|
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||||
|
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||||
|
},
|
||||||
|
}
|
8
homeassistant/components/accuweather/manifest.json
Normal file
8
homeassistant/components/accuweather/manifest.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"domain": "accuweather",
|
||||||
|
"name": "AccuWeather",
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
|
||||||
|
"requirements": ["accuweather==0.0.9"],
|
||||||
|
"codeowners": ["@bieniu"],
|
||||||
|
"config_flow": true
|
||||||
|
}
|
189
homeassistant/components/accuweather/sensor.py
Normal file
189
homeassistant/components/accuweather/sensor.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
"""Support for the AccuWeather service."""
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ATTRIBUTION,
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
CONF_NAME,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ATTR_FORECAST,
|
||||||
|
ATTR_ICON,
|
||||||
|
ATTR_LABEL,
|
||||||
|
ATTRIBUTION,
|
||||||
|
COORDINATOR,
|
||||||
|
DOMAIN,
|
||||||
|
FORECAST_DAYS,
|
||||||
|
FORECAST_SENSOR_TYPES,
|
||||||
|
MANUFACTURER,
|
||||||
|
NAME,
|
||||||
|
OPTIONAL_SENSORS,
|
||||||
|
SENSOR_TYPES,
|
||||||
|
)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Add AccuWeather entities from a config_entry."""
|
||||||
|
name = config_entry.data[CONF_NAME]
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||||
|
|
||||||
|
sensors = []
|
||||||
|
for sensor in SENSOR_TYPES:
|
||||||
|
sensors.append(AccuWeatherSensor(name, sensor, coordinator))
|
||||||
|
|
||||||
|
if coordinator.forecast:
|
||||||
|
for sensor in FORECAST_SENSOR_TYPES:
|
||||||
|
for day in FORECAST_DAYS:
|
||||||
|
# Some air quality/allergy sensors are only available for certain
|
||||||
|
# locations.
|
||||||
|
if sensor in coordinator.data[ATTR_FORECAST][0]:
|
||||||
|
sensors.append(
|
||||||
|
AccuWeatherSensor(name, sensor, coordinator, forecast_day=day)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(sensors, False)
|
||||||
|
|
||||||
|
|
||||||
|
class AccuWeatherSensor(Entity):
|
||||||
|
"""Define an AccuWeather entity."""
|
||||||
|
|
||||||
|
def __init__(self, name, kind, coordinator, forecast_day=None):
|
||||||
|
"""Initialize."""
|
||||||
|
self._name = name
|
||||||
|
self.kind = kind
|
||||||
|
self.coordinator = coordinator
|
||||||
|
self._device_class = None
|
||||||
|
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||||
|
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
|
||||||
|
self.forecast_day = forecast_day
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name."""
|
||||||
|
if self.forecast_day is not None:
|
||||||
|
return f"{self._name} {FORECAST_SENSOR_TYPES[self.kind][ATTR_LABEL]} {self.forecast_day}d"
|
||||||
|
return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique_id for this entity."""
|
||||||
|
if self.forecast_day is not None:
|
||||||
|
return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower()
|
||||||
|
return f"{self.coordinator.location_key}-{self.kind}".lower()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device info."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self.coordinator.location_key)},
|
||||||
|
"name": NAME,
|
||||||
|
"manufacturer": MANUFACTURER,
|
||||||
|
"entry_type": "service",
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Return the polling requirement of the entity."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self.coordinator.last_update_success
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state."""
|
||||||
|
if self.forecast_day is not None:
|
||||||
|
if (
|
||||||
|
FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
|
||||||
|
== DEVICE_CLASS_TEMPERATURE
|
||||||
|
):
|
||||||
|
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
|
||||||
|
self.kind
|
||||||
|
]["Value"]
|
||||||
|
if self.kind in ["WindGustDay", "WindGustNight"]:
|
||||||
|
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
|
||||||
|
self.kind
|
||||||
|
]["Speed"]["Value"]
|
||||||
|
if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
|
||||||
|
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
|
||||||
|
self.kind
|
||||||
|
]["Value"]
|
||||||
|
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][self.kind]
|
||||||
|
if self.kind == "Ceiling":
|
||||||
|
return round(self.coordinator.data[self.kind][self._unit_system]["Value"])
|
||||||
|
if self.kind == "PressureTendency":
|
||||||
|
return self.coordinator.data[self.kind]["LocalizedText"].lower()
|
||||||
|
if SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE:
|
||||||
|
return self.coordinator.data[self.kind][self._unit_system]["Value"]
|
||||||
|
if self.kind == "Precipitation":
|
||||||
|
return self.coordinator.data["PrecipitationSummary"][self.kind][
|
||||||
|
self._unit_system
|
||||||
|
]["Value"]
|
||||||
|
if self.kind == "WindGust":
|
||||||
|
return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"]
|
||||||
|
return self.coordinator.data[self.kind]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon."""
|
||||||
|
if self.forecast_day is not None:
|
||||||
|
return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON]
|
||||||
|
return SENSOR_TYPES[self.kind][ATTR_ICON]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the device_class."""
|
||||||
|
if self.forecast_day is not None:
|
||||||
|
return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
|
||||||
|
return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit the value is expressed in."""
|
||||||
|
if self.forecast_day is not None:
|
||||||
|
return FORECAST_SENSOR_TYPES[self.kind][self._unit_system]
|
||||||
|
return SENSOR_TYPES[self.kind][self._unit_system]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
if self.forecast_day is not None:
|
||||||
|
if self.kind == "WindGustDay":
|
||||||
|
self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][
|
||||||
|
self.forecast_day
|
||||||
|
][self.kind]["Direction"]["English"]
|
||||||
|
elif self.kind == "WindGustNight":
|
||||||
|
self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][
|
||||||
|
self.forecast_day
|
||||||
|
][self.kind]["Direction"]["English"]
|
||||||
|
elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
|
||||||
|
self._attrs["level"] = self.coordinator.data[ATTR_FORECAST][
|
||||||
|
self.forecast_day
|
||||||
|
][self.kind]["Category"]
|
||||||
|
return self._attrs
|
||||||
|
if self.kind == "UVIndex":
|
||||||
|
self._attrs["level"] = self.coordinator.data["UVIndexText"]
|
||||||
|
elif self.kind == "Precipitation":
|
||||||
|
self._attrs["type"] = self.coordinator.data["PrecipitationType"]
|
||||||
|
return self._attrs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self):
|
||||||
|
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||||
|
return bool(self.kind not in OPTIONAL_SENSORS)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Connect to dispatcher listening for entity data notifications."""
|
||||||
|
self.async_on_remove(
|
||||||
|
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update AccuWeather entity."""
|
||||||
|
await self.coordinator.async_request_refresh()
|
35
homeassistant/components/accuweather/strings.json
Normal file
35
homeassistant/components/accuweather/strings.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "AccuWeather",
|
||||||
|
"description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nSome sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options.",
|
||||||
|
"data": {
|
||||||
|
"name": "Name of the integration",
|
||||||
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||||
|
"requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "AccuWeather Options",
|
||||||
|
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.",
|
||||||
|
"data": {
|
||||||
|
"forecast": "Weather forecast"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
homeassistant/components/accuweather/strings.sensor.json
Normal file
9
homeassistant/components/accuweather/strings.sensor.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"accuweather__pressure_tendency": {
|
||||||
|
"steady": "Steady",
|
||||||
|
"rising": "Rising",
|
||||||
|
"falling": "Falling"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
homeassistant/components/accuweather/translations/ca.json
Normal file
35
homeassistant/components/accuweather/translations/ca.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Ha fallat la connexi\u00f3",
|
||||||
|
"invalid_api_key": "Clau API inv\u00e0lida",
|
||||||
|
"requests_exceeded": "S'ha superat el nombre m\u00e0xim de sol\u00b7licituds permeses a l'API d'AccuWeather. Has d'esperar-te o canviar la clau API."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Clau API",
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"name": "Nom de la integraci\u00f3"
|
||||||
|
},
|
||||||
|
"description": "Si necessites ajuda amb la configuraci\u00f3, consulta: https://www.home-assistant.io/integrations/accuweather/ \n\n La previsi\u00f3 meteorol\u00f2gica no est\u00e0 habilitada de manera predeterminada. Pots activar-la en les opcions de la integraci\u00f3.",
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "Previsi\u00f3 meteorol\u00f2gica"
|
||||||
|
},
|
||||||
|
"description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions es realitzaran cada 64 minuts en comptes de 32.",
|
||||||
|
"title": "Opcions d'AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
homeassistant/components/accuweather/translations/en.json
Normal file
35
homeassistant/components/accuweather/translations/en.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"invalid_api_key": "Invalid API key",
|
||||||
|
"requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API Key",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
|
"name": "Name of the integration"
|
||||||
|
},
|
||||||
|
"description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nSome sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options.",
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "Weather forecast"
|
||||||
|
},
|
||||||
|
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.",
|
||||||
|
"title": "AccuWeather Options"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
homeassistant/components/accuweather/translations/es.json
Normal file
35
homeassistant/components/accuweather/translations/es.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "No se pudo conectar",
|
||||||
|
"invalid_api_key": "Clave API no v\u00e1lida",
|
||||||
|
"requests_exceeded": "Se ha excedido el n\u00famero permitido de solicitudes a la API de Accuweather. Tienes que esperar o cambiar la Clave API."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Clave API",
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"name": "Nombre de la integraci\u00f3n"
|
||||||
|
},
|
||||||
|
"description": "Si necesitas ayuda con la configuraci\u00f3n, echa un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado por defecto. Puedes habilitarlo en las opciones de la integraci\u00f3n.",
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "Pron\u00f3stico del tiempo"
|
||||||
|
},
|
||||||
|
"description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos.",
|
||||||
|
"title": "Opciones de AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
homeassistant/components/accuweather/translations/it.json
Normal file
35
homeassistant/components/accuweather/translations/it.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Impossibile connettersi",
|
||||||
|
"invalid_api_key": "Chiave API non valida",
|
||||||
|
"requests_exceeded": "\u00c8 stato superato il numero consentito di richieste all'API di Accuweather. \u00c8 necessario attendere o modificare la chiave API."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Chiave API",
|
||||||
|
"latitude": "Latitudine",
|
||||||
|
"longitude": "Logitudine",
|
||||||
|
"name": "Nome dell'integrazione"
|
||||||
|
},
|
||||||
|
"description": "Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/accuweather/ \n\nAlcuni sensori non sono abilitati per impostazione predefinita. \u00c8 possibile abilitarli nel registro entit\u00e0 dopo la configurazione di integrazione. \nLe previsioni meteo non sono abilitate per impostazione predefinita. Puoi abilitarle nelle opzioni di integrazione.",
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "Previsioni meteo"
|
||||||
|
},
|
||||||
|
"description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 64 minuti invece che ogni 32 minuti.",
|
||||||
|
"title": "Opzioni AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
homeassistant/components/accuweather/translations/lb.json
Normal file
35
homeassistant/components/accuweather/translations/lb.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Feeler beim verbannen",
|
||||||
|
"invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel",
|
||||||
|
"requests_exceeded": "D\u00e9i zougelooss Zuel vun Ufroen un Accuweather API gouf iwwerschratt. Du muss ofwaarden oder den API Schl\u00ebssel \u00e4nneren."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API Schl\u00ebssel",
|
||||||
|
"latitude": "Breedegrad",
|
||||||
|
"longitude": "L\u00e4ngegrad",
|
||||||
|
"name": "Numm vun der Integratioun"
|
||||||
|
},
|
||||||
|
"description": "Falls du H\u00ebllef mat der Konfiguratioun brauch kuck h\u00e9i:\nhttps://www.home-assistant.io/integrations/accuweather/\n\nWieder Pr\u00e9visounen si standardm\u00e9isseg net aktiv. Du kanns d\u00e9i an den Optioune vun der Integratioun aschalten.",
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "Wieder Pr\u00e9visioun"
|
||||||
|
},
|
||||||
|
"description": "Duerch d'Limite vun der Gratis Versioun vun der AccuWeather API, wann d'Wieder Pr\u00e9visoune aktiv\u00e9iert sinn, ginn d'Aktualis\u00e9ierungen all 64 Minutten gemaach, am plaatz vun all 32 Minutten.",
|
||||||
|
"title": "AccuWeather Optiounen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
homeassistant/components/accuweather/translations/no.json
Normal file
35
homeassistant/components/accuweather/translations/no.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Tilkobling mislyktes.",
|
||||||
|
"invalid_api_key": "Ugyldig API-n\u00f8kkel",
|
||||||
|
"requests_exceeded": "Det tillatte antallet foresp\u00f8rsler til Accuweather API er overskredet. Du m\u00e5 vente eller endre API-n\u00f8kkel."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API-n\u00f8kkel",
|
||||||
|
"latitude": "Breddegrad",
|
||||||
|
"longitude": "Lengdegrad",
|
||||||
|
"name": "Navn p\u00e5 integrasjon"
|
||||||
|
},
|
||||||
|
"description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\n Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen. \n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene.",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "V\u00e6rmelding"
|
||||||
|
},
|
||||||
|
"description": "P\u00e5 grunn av begrensningene i gratisversjonen av AccuWeather API-n\u00f8kkelen, n\u00e5r du aktiverer v\u00e6rmelding, vil dataoppdateringer bli utf\u00f8rt hvert 64. minutt i stedet for hvert 32. minutt.",
|
||||||
|
"title": "AccuWeather-alternativer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
homeassistant/components/accuweather/translations/pl.json
Normal file
35
homeassistant/components/accuweather/translations/pl.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
|
||||||
|
"invalid_api_key": "Nieprawid\u0142owy klucz API.",
|
||||||
|
"requests_exceeded": "Dozwolona liczba zapyta\u0144 do interfejsu API Accuweather zosta\u0142a przekroczona. Musisz poczeka\u0107 lub zmieni\u0107 klucz API."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Klucz API",
|
||||||
|
"latitude": "Szeroko\u015b\u0107 geograficzna",
|
||||||
|
"longitude": "D\u0142ugo\u015b\u0107 geograficzna",
|
||||||
|
"name": "Nazwa integracji"
|
||||||
|
},
|
||||||
|
"description": "Je\u015bli potrzebujesz pomocy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/accuweather/ \n\nCz\u0119\u015b\u0107 sensor\u00f3w nie jest w\u0142\u0105czona domy\u015blnie. Mo\u017cesz je w\u0142\u0105czy\u0107 w rejestrze encji po konfiguracji integracji.\nPrognoza pogody nie jest domy\u015blnie w\u0142\u0105czona. Mo\u017cesz j\u0105 w\u0142\u0105czy\u0107 w opcjach integracji.",
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "Prognoza pogody"
|
||||||
|
},
|
||||||
|
"description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 64 minut zamiast co 32 minut.",
|
||||||
|
"title": "Opcje AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
homeassistant/components/accuweather/translations/pt.json
Normal file
21
homeassistant/components/accuweather/translations/pt.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "Previs\u00e3o meteorol\u00f3gica"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
homeassistant/components/accuweather/translations/ru.json
Normal file
35
homeassistant/components/accuweather/translations/ru.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
|
||||||
|
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
|
||||||
|
"requests_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a API Accuweather. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u043e\u0434\u043e\u0436\u0434\u0430\u0442\u044c \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "\u041a\u043b\u044e\u0447 API",
|
||||||
|
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
|
||||||
|
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
|
||||||
|
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
|
||||||
|
},
|
||||||
|
"description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438, \u0435\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u0430 \u043f\u043e\u043c\u043e\u0449\u044c \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439:\nhttps://www.home-assistant.io/integrations/accuweather/ \n\n\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0441\u043a\u0440\u044b\u0442\u044b \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0443\u0436\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0438 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.",
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b"
|
||||||
|
},
|
||||||
|
"description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 64 \u043c\u0438\u043d\u0443\u0442\u044b, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 32 \u043c\u0438\u043d\u0443\u0442\u044b.",
|
||||||
|
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"accuweather__pressure_tendency": {
|
||||||
|
"falling": "Klesaj\u00edc\u00ed",
|
||||||
|
"rising": "Roustouc\u00ed",
|
||||||
|
"steady": "St\u00e1l\u00fd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"accuweather__pressure_tendency": {
|
||||||
|
"falling": "Falling",
|
||||||
|
"rising": "Rising",
|
||||||
|
"steady": "Steady"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"accuweather__pressure_tendency": {
|
||||||
|
"falling": "Cayendo",
|
||||||
|
"rising": "Subiendo",
|
||||||
|
"steady": "Estable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"accuweather__pressure_tendency": {
|
||||||
|
"falling": "Diminuzione",
|
||||||
|
"rising": "Aumento",
|
||||||
|
"steady": "Stabile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"accuweather__pressure_tendency": {
|
||||||
|
"falling": "Fallende",
|
||||||
|
"rising": "Stiger",
|
||||||
|
"steady": "Jevn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"accuweather__pressure_tendency": {
|
||||||
|
"falling": "spada",
|
||||||
|
"rising": "ro\u015bnie",
|
||||||
|
"steady": "bez zmian"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"accuweather__pressure_tendency": {
|
||||||
|
"falling": "\u041f\u043e\u043d\u0438\u0436\u0430\u044e\u0449\u0435\u0435\u0441\u044f",
|
||||||
|
"rising": "\u041f\u043e\u0432\u044b\u0448\u0430\u044e\u0449\u0435\u0435\u0441\u044f",
|
||||||
|
"steady": "\u041f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0435"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/accuweather/translations/uk.json
Normal file
26
homeassistant/components/accuweather/translations/uk.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
|
||||||
|
"longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
|
||||||
|
"name": "\u041d\u0430\u0437\u0432\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457"
|
||||||
|
},
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "\u9023\u7dda\u5931\u6557",
|
||||||
|
"invalid_api_key": "API \u5bc6\u9470\u7121\u6548",
|
||||||
|
"requests_exceeded": "\u5df2\u8d85\u904e Accuweather API \u5141\u8a31\u7684\u8acb\u6c42\u6b21\u6578\u3002\u5fc5\u9808\u7b49\u5019\u6216\u8b8a\u66f4 API \u5bc6\u9470\u3002"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "API \u5bc6\u9470",
|
||||||
|
"latitude": "\u7def\u5ea6",
|
||||||
|
"longitude": "\u7d93\u5ea6",
|
||||||
|
"name": "\u6574\u5408\u540d\u7a31"
|
||||||
|
},
|
||||||
|
"description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002",
|
||||||
|
"title": "AccuWeather"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"forecast": "\u5929\u6c23\u9810\u5831"
|
||||||
|
},
|
||||||
|
"description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 64 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 32 \u5206\u9418\u3002",
|
||||||
|
"title": "AccuWeather \u9078\u9805"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
195
homeassistant/components/accuweather/weather.py
Normal file
195
homeassistant/components/accuweather/weather.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
"""Support for the AccuWeather service."""
|
||||||
|
from statistics import mean
|
||||||
|
|
||||||
|
from homeassistant.components.weather import (
|
||||||
|
ATTR_FORECAST_CONDITION,
|
||||||
|
ATTR_FORECAST_PRECIPITATION,
|
||||||
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||||
|
ATTR_FORECAST_TEMP,
|
||||||
|
ATTR_FORECAST_TEMP_LOW,
|
||||||
|
ATTR_FORECAST_TIME,
|
||||||
|
ATTR_FORECAST_WIND_BEARING,
|
||||||
|
ATTR_FORECAST_WIND_SPEED,
|
||||||
|
WeatherEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
|
from homeassistant.util.dt import utc_from_timestamp
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ATTR_FORECAST,
|
||||||
|
ATTRIBUTION,
|
||||||
|
CONDITION_CLASSES,
|
||||||
|
COORDINATOR,
|
||||||
|
DOMAIN,
|
||||||
|
MANUFACTURER,
|
||||||
|
NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Add a AccuWeather weather entity from a config_entry."""
|
||||||
|
name = config_entry.data[CONF_NAME]
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||||
|
|
||||||
|
async_add_entities([AccuWeatherEntity(name, coordinator)], False)
|
||||||
|
|
||||||
|
|
||||||
|
class AccuWeatherEntity(WeatherEntity):
|
||||||
|
"""Define an AccuWeather entity."""
|
||||||
|
|
||||||
|
def __init__(self, name, coordinator):
|
||||||
|
"""Initialize."""
|
||||||
|
self._name = name
|
||||||
|
self.coordinator = coordinator
|
||||||
|
self._attrs = {}
|
||||||
|
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attribution(self):
|
||||||
|
"""Return the attribution."""
|
||||||
|
return ATTRIBUTION
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique_id for this entity."""
|
||||||
|
return self.coordinator.location_key
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device info."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self.coordinator.location_key)},
|
||||||
|
"name": NAME,
|
||||||
|
"manufacturer": MANUFACTURER,
|
||||||
|
"entry_type": "service",
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Return the polling requirement of the entity."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self.coordinator.last_update_success
|
||||||
|
|
||||||
|
@property
|
||||||
|
def condition(self):
|
||||||
|
"""Return the current condition."""
|
||||||
|
try:
|
||||||
|
return [
|
||||||
|
k
|
||||||
|
for k, v in CONDITION_CLASSES.items()
|
||||||
|
if self.coordinator.data["WeatherIcon"] in v
|
||||||
|
][0]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature(self):
|
||||||
|
"""Return the temperature."""
|
||||||
|
return self.coordinator.data["Temperature"][self._unit_system]["Value"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_unit(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pressure(self):
|
||||||
|
"""Return the pressure."""
|
||||||
|
return self.coordinator.data["Pressure"][self._unit_system]["Value"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def humidity(self):
|
||||||
|
"""Return the humidity."""
|
||||||
|
return self.coordinator.data["RelativeHumidity"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wind_speed(self):
|
||||||
|
"""Return the wind speed."""
|
||||||
|
return self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wind_bearing(self):
|
||||||
|
"""Return the wind bearing."""
|
||||||
|
return self.coordinator.data["Wind"]["Direction"]["Degrees"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def visibility(self):
|
||||||
|
"""Return the visibility."""
|
||||||
|
return self.coordinator.data["Visibility"][self._unit_system]["Value"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ozone(self):
|
||||||
|
"""Return the ozone level."""
|
||||||
|
# We only have ozone data for certain locations and only in the forecast data.
|
||||||
|
if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get(
|
||||||
|
"Ozone"
|
||||||
|
):
|
||||||
|
return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forecast(self):
|
||||||
|
"""Return the forecast array."""
|
||||||
|
if not self.coordinator.forecast:
|
||||||
|
return None
|
||||||
|
# remap keys from library to keys understood by the weather component
|
||||||
|
forecast = [
|
||||||
|
{
|
||||||
|
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
|
||||||
|
ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"],
|
||||||
|
ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"],
|
||||||
|
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item),
|
||||||
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY: round(
|
||||||
|
mean(
|
||||||
|
[
|
||||||
|
item["PrecipitationProbabilityDay"],
|
||||||
|
item["PrecipitationProbabilityNight"],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"],
|
||||||
|
ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"],
|
||||||
|
ATTR_FORECAST_CONDITION: [
|
||||||
|
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
|
||||||
|
][0],
|
||||||
|
}
|
||||||
|
for item in self.coordinator.data[ATTR_FORECAST]
|
||||||
|
]
|
||||||
|
return forecast
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Connect to dispatcher listening for entity data notifications."""
|
||||||
|
self.async_on_remove(
|
||||||
|
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update AccuWeather entity."""
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calc_precipitation(day: dict) -> float:
|
||||||
|
"""Return sum of the precipitation."""
|
||||||
|
precip_sum = 0
|
||||||
|
precip_types = ["Rain", "Snow", "Ice"]
|
||||||
|
for precip in precip_types:
|
||||||
|
precip_sum = sum(
|
||||||
|
[
|
||||||
|
precip_sum,
|
||||||
|
day[f"{precip}Day"]["Value"],
|
||||||
|
day[f"{precip}Night"]["Value"],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return round(precip_sum, 1)
|
@ -17,8 +17,10 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "Vert",
|
"host": "Vert",
|
||||||
|
"password": "Passord",
|
||||||
"port": "",
|
"port": "",
|
||||||
"ssl": "AdGuard Hjem bruker et SSL-sertifikat",
|
"ssl": "AdGuard Hjem bruker et SSL-sertifikat",
|
||||||
|
"username": "Brukernavn",
|
||||||
"verify_ssl": "AdGuard Home bruker et riktig sertifikat"
|
"verify_ssl": "AdGuard Home bruker et riktig sertifikat"
|
||||||
},
|
},
|
||||||
"description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll."
|
"description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll."
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"username": "E-pasts"
|
"port": "Poort"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,7 +11,7 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "Vert",
|
"host": "Vert",
|
||||||
"port": "Port"
|
"port": ""
|
||||||
},
|
},
|
||||||
"title": "Konfigurere Agent DVR"
|
"title": "Konfigurere Agent DVR"
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ from .const import (
|
|||||||
ATTR_API_PM25,
|
ATTR_API_PM25,
|
||||||
ATTR_API_PM25_LIMIT,
|
ATTR_API_PM25_LIMIT,
|
||||||
ATTR_API_PM25_PERCENT,
|
ATTR_API_PM25_PERCENT,
|
||||||
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
MANUFACTURER,
|
||||||
)
|
)
|
||||||
|
|
||||||
ATTRIBUTION = "Data provided by Airly"
|
ATTRIBUTION = "Data provided by Airly"
|
||||||
@ -31,6 +33,8 @@ LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit"
|
|||||||
LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit"
|
LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit"
|
||||||
LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
|
LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up Airly air_quality entity based on a config entry."""
|
"""Set up Airly air_quality entity based on a config entry."""
|
||||||
@ -38,9 +42,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
|
|
||||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities([AirlyAirQuality(coordinator, name)], False)
|
||||||
[AirlyAirQuality(coordinator, name, config_entry.unique_id)], False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def round_state(func):
|
def round_state(func):
|
||||||
@ -58,11 +60,10 @@ def round_state(func):
|
|||||||
class AirlyAirQuality(AirQualityEntity):
|
class AirlyAirQuality(AirQualityEntity):
|
||||||
"""Define an Airly air quality."""
|
"""Define an Airly air quality."""
|
||||||
|
|
||||||
def __init__(self, coordinator, name, unique_id):
|
def __init__(self, coordinator, name):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
self._name = name
|
self._name = name
|
||||||
self._unique_id = unique_id
|
|
||||||
self._icon = "mdi:blur"
|
self._icon = "mdi:blur"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -106,7 +107,19 @@ class AirlyAirQuality(AirQualityEntity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return a unique_id for this entity."""
|
"""Return a unique_id for this entity."""
|
||||||
return self._unique_id
|
return f"{self.coordinator.latitude}-{self.coordinator.longitude}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device info."""
|
||||||
|
return {
|
||||||
|
"identifiers": {
|
||||||
|
(DOMAIN, self.coordinator.latitude, self.coordinator.longitude)
|
||||||
|
},
|
||||||
|
"name": DEFAULT_NAME,
|
||||||
|
"manufacturer": MANUFACTURER,
|
||||||
|
"entry_type": "service",
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
|
@ -15,5 +15,6 @@ ATTR_API_PRESSURE = "PRESSURE"
|
|||||||
ATTR_API_TEMPERATURE = "TEMPERATURE"
|
ATTR_API_TEMPERATURE = "TEMPERATURE"
|
||||||
DEFAULT_NAME = "Airly"
|
DEFAULT_NAME = "Airly"
|
||||||
DOMAIN = "airly"
|
DOMAIN = "airly"
|
||||||
|
MANUFACTURER = "Airly sp. z o.o."
|
||||||
MAX_REQUESTS_PER_DAY = 100
|
MAX_REQUESTS_PER_DAY = 100
|
||||||
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."
|
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."
|
||||||
|
@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airly",
|
"documentation": "https://www.home-assistant.io/integrations/airly",
|
||||||
"codeowners": ["@bieniu"],
|
"codeowners": ["@bieniu"],
|
||||||
"requirements": ["airly==0.0.2"],
|
"requirements": ["airly==0.0.2"],
|
||||||
"config_flow": true
|
"config_flow": true,
|
||||||
|
"quality_scale": "platinum"
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ from .const import (
|
|||||||
ATTR_API_PM1,
|
ATTR_API_PM1,
|
||||||
ATTR_API_PRESSURE,
|
ATTR_API_PRESSURE,
|
||||||
ATTR_API_TEMPERATURE,
|
ATTR_API_TEMPERATURE,
|
||||||
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
MANUFACTURER,
|
||||||
)
|
)
|
||||||
|
|
||||||
ATTRIBUTION = "Data provided by Airly"
|
ATTRIBUTION = "Data provided by Airly"
|
||||||
@ -27,6 +29,8 @@ ATTR_ICON = "icon"
|
|||||||
ATTR_LABEL = "label"
|
ATTR_LABEL = "label"
|
||||||
ATTR_UNIT = "unit"
|
ATTR_UNIT = "unit"
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
ATTR_API_PM1: {
|
ATTR_API_PM1: {
|
||||||
ATTR_DEVICE_CLASS: None,
|
ATTR_DEVICE_CLASS: None,
|
||||||
@ -63,8 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
|
|
||||||
sensors = []
|
sensors = []
|
||||||
for sensor in SENSOR_TYPES:
|
for sensor in SENSOR_TYPES:
|
||||||
unique_id = f"{config_entry.unique_id}-{sensor.lower()}"
|
sensors.append(AirlySensor(coordinator, name, sensor))
|
||||||
sensors.append(AirlySensor(coordinator, name, sensor, unique_id))
|
|
||||||
|
|
||||||
async_add_entities(sensors, False)
|
async_add_entities(sensors, False)
|
||||||
|
|
||||||
@ -72,11 +75,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
class AirlySensor(Entity):
|
class AirlySensor(Entity):
|
||||||
"""Define an Airly sensor."""
|
"""Define an Airly sensor."""
|
||||||
|
|
||||||
def __init__(self, coordinator, name, kind, unique_id):
|
def __init__(self, coordinator, name, kind):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
self._name = name
|
self._name = name
|
||||||
self._unique_id = unique_id
|
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self._device_class = None
|
self._device_class = None
|
||||||
self._state = None
|
self._state = None
|
||||||
@ -123,7 +125,19 @@ class AirlySensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return a unique_id for this entity."""
|
"""Return a unique_id for this entity."""
|
||||||
return self._unique_id
|
return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device info."""
|
||||||
|
return {
|
||||||
|
"identifiers": {
|
||||||
|
(DOMAIN, self.coordinator.latitude, self.coordinator.longitude)
|
||||||
|
},
|
||||||
|
"name": DEFAULT_NAME,
|
||||||
|
"manufacturer": MANUFACTURER,
|
||||||
|
"entry_type": "service",
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "Clave API de Airly",
|
"api_key": "Clave API",
|
||||||
"latitude": "Latitud",
|
"latitude": "Latitud",
|
||||||
"longitude": "Longitud",
|
"longitude": "Longitud",
|
||||||
"name": "Nombre de la integraci\u00f3n"
|
"name": "Nombre de la integraci\u00f3n"
|
||||||
|
@ -1,25 +1,39 @@
|
|||||||
{
|
{
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
"action_type": {
|
"action_type": {
|
||||||
"arm_away": "Aktivovat {entity_name} v re\u017eimu mimo domov",
|
"arm_away": "Aktivovat {entity_name} v re\u017eimu nep\u0159\u00edtomnost",
|
||||||
"arm_home": "Aktivovat {entity_name} v re\u017eimu doma",
|
"arm_home": "Aktivovat {entity_name} v re\u017eimu domov",
|
||||||
"arm_night": "Aktivovat {entity_name} v re\u017eimu noc",
|
"arm_night": "Aktivovat {entity_name} v no\u010dn\u00edm re\u017eimu",
|
||||||
"disarm": "Deaktivovat {entity_name}",
|
"disarm": "Odbezpe\u010dit {entity_name}",
|
||||||
"trigger": "Spustit {entity_name}"
|
"trigger": "Spustit {entity_name}"
|
||||||
|
},
|
||||||
|
"condition_type": {
|
||||||
|
"is_armed_away": "{entity_name} je v re\u017eimu nep\u0159\u00edtomnost",
|
||||||
|
"is_armed_home": "{entity_name} je v re\u017eimu domov",
|
||||||
|
"is_armed_night": "{entity_name} je v no\u010dn\u00edm re\u017eimu",
|
||||||
|
"is_disarmed": "{entity_name} nen\u00ed zabezpe\u010den",
|
||||||
|
"is_triggered": "{entity_name} je spu\u0161t\u011bn"
|
||||||
|
},
|
||||||
|
"trigger_type": {
|
||||||
|
"armed_away": "{entity_name} v re\u017eimu nep\u0159\u00edtomnost",
|
||||||
|
"armed_home": "{entity_name} v re\u017eimu domov",
|
||||||
|
"armed_night": "{entity_name} v no\u010dn\u00edm re\u017eimu",
|
||||||
|
"disarmed": "{entity_name} nezabezpe\u010den",
|
||||||
|
"triggered": "{entity_name} spu\u0161t\u011bn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"_": {
|
"_": {
|
||||||
"armed": "Aktivn\u00ed",
|
"armed": "Zabezpe\u010deno",
|
||||||
"armed_away": "Aktivn\u00ed re\u017eim mimo domov",
|
"armed_away": "Re\u017eim nep\u0159\u00edtomnost",
|
||||||
"armed_custom_bypass": "Aktivn\u00ed u\u017eivatelsk\u00fdm obejit\u00edm",
|
"armed_custom_bypass": "Zabezpe\u010deno u\u017eivatelsk\u00fdm obejit\u00edm",
|
||||||
"armed_home": "Aktivn\u00ed re\u017eim doma",
|
"armed_home": "Re\u017eim domov",
|
||||||
"armed_night": "Aktivn\u00ed no\u010dn\u00ed re\u017eim",
|
"armed_night": "No\u010dn\u00ed re\u017eim",
|
||||||
"arming": "Aktivov\u00e1n\u00ed",
|
"arming": "Zabezpe\u010dov\u00e1n\u00ed",
|
||||||
"disarmed": "Neaktivn\u00ed",
|
"disarmed": "Nezabezpe\u010deno",
|
||||||
"disarming": "Deaktivov\u00e1n\u00ed",
|
"disarming": "Odbezpe\u010dov\u00e1n\u00ed",
|
||||||
"pending": "Nadch\u00e1zej\u00edc\u00ed",
|
"pending": "\u010cekaj\u00edc\u00ed",
|
||||||
"triggered": "Spu\u0161t\u011bno"
|
"triggered": "Spu\u0161t\u011bn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Ovl\u00e1dac\u00ed panel alarmu"
|
"title": "Ovl\u00e1dac\u00ed panel alarmu"
|
||||||
|
@ -84,6 +84,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
|
|||||||
self._name = "Alarm Panel"
|
self._name = "Alarm Panel"
|
||||||
self._state = None
|
self._state = None
|
||||||
self._ac_power = None
|
self._ac_power = None
|
||||||
|
self._alarm_event_occurred = None
|
||||||
self._backlight_on = None
|
self._backlight_on = None
|
||||||
self._battery_low = None
|
self._battery_low = None
|
||||||
self._check_zone = None
|
self._check_zone = None
|
||||||
@ -117,6 +118,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
|
|||||||
self._state = STATE_ALARM_DISARMED
|
self._state = STATE_ALARM_DISARMED
|
||||||
|
|
||||||
self._ac_power = message.ac_power
|
self._ac_power = message.ac_power
|
||||||
|
self._alarm_event_occurred = message.alarm_event_occurred
|
||||||
self._backlight_on = message.backlight_on
|
self._backlight_on = message.backlight_on
|
||||||
self._battery_low = message.battery_low
|
self._battery_low = message.battery_low
|
||||||
self._check_zone = message.check_zone
|
self._check_zone = message.check_zone
|
||||||
@ -163,6 +165,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
|
|||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {
|
||||||
"ac_power": self._ac_power,
|
"ac_power": self._ac_power,
|
||||||
|
"alarm_event_occurred": self._alarm_event_occurred,
|
||||||
"backlight_on": self._backlight_on,
|
"backlight_on": self._backlight_on,
|
||||||
"battery_low": self._battery_low,
|
"battery_low": self._battery_low,
|
||||||
"check_zone": self._check_zone,
|
"check_zone": self._check_zone,
|
||||||
|
@ -293,7 +293,7 @@ async def async_setup_entry(hass, config_entry):
|
|||||||
Client(
|
Client(
|
||||||
config_entry.data[CONF_API_KEY],
|
config_entry.data[CONF_API_KEY],
|
||||||
config_entry.data[CONF_APP_KEY],
|
config_entry.data[CONF_APP_KEY],
|
||||||
session,
|
session=session,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
hass.loop.create_task(ambient.ws_connect())
|
hass.loop.create_task(ambient.ws_connect())
|
||||||
|
@ -43,7 +43,9 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
client = Client(user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session)
|
client = Client(
|
||||||
|
user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session=session
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
devices = await client.api.get_devices()
|
devices = await client.api.get_devices()
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"name": "Ambient Weather Station",
|
"name": "Ambient Weather Station",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ambient_station",
|
"documentation": "https://www.home-assistant.io/integrations/ambient_station",
|
||||||
"requirements": ["aioambient==1.1.1"],
|
"requirements": ["aioambient==1.2.1"],
|
||||||
"codeowners": ["@bachya"]
|
"codeowners": ["@bachya"]
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from adb_shell.auth.keygen import keygen
|
from adb_shell.auth.keygen import keygen
|
||||||
from adb_shell.auth.sign_pythonrsa import PythonRSASigner
|
|
||||||
from adb_shell.exceptions import (
|
from adb_shell.exceptions import (
|
||||||
AdbTimeoutError,
|
AdbTimeoutError,
|
||||||
InvalidChecksumError,
|
InvalidChecksumError,
|
||||||
@ -14,6 +13,7 @@ from adb_shell.exceptions import (
|
|||||||
TcpTimeoutException,
|
TcpTimeoutException,
|
||||||
)
|
)
|
||||||
from androidtv import ha_state_detection_rules_validator
|
from androidtv import ha_state_detection_rules_validator
|
||||||
|
from androidtv.adb_manager.adb_manager_sync import ADBPythonSync
|
||||||
from androidtv.constants import APPS, KEYS
|
from androidtv.constants import APPS, KEYS
|
||||||
from androidtv.exceptions import LockNotAcquiredException
|
from androidtv.exceptions import LockNotAcquiredException
|
||||||
from androidtv.setup_async import setup
|
from androidtv.setup_async import setup
|
||||||
@ -40,6 +40,7 @@ from homeassistant.const import (
|
|||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_PAUSED,
|
STATE_PAUSED,
|
||||||
@ -175,9 +176,7 @@ def setup_androidtv(hass, config):
|
|||||||
keygen(adbkey)
|
keygen(adbkey)
|
||||||
|
|
||||||
# Load the ADB key
|
# Load the ADB key
|
||||||
with open(adbkey) as priv_key:
|
signer = ADBPythonSync.load_adbkey(adbkey)
|
||||||
priv = priv_key.read()
|
|
||||||
signer = PythonRSASigner("", priv)
|
|
||||||
adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
|
adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -230,6 +229,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
)
|
)
|
||||||
raise PlatformNotReady
|
raise PlatformNotReady
|
||||||
|
|
||||||
|
async def _async_close(event):
|
||||||
|
"""Close the ADB socket connection when HA stops."""
|
||||||
|
await aftv.adb_close()
|
||||||
|
|
||||||
|
# Close the ADB connection when HA stops
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close)
|
||||||
|
|
||||||
device_args = [
|
device_args = [
|
||||||
aftv,
|
aftv,
|
||||||
config[CONF_NAME],
|
config[CONF_NAME],
|
||||||
|
@ -11,6 +11,10 @@
|
|||||||
"description": "Vil du legge Arcam FMJ p\u00e5 ` {host} ` til Home Assistant? "
|
"description": "Vil du legge Arcam FMJ p\u00e5 ` {host} ` til Home Assistant? "
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Vert",
|
||||||
|
"port": ""
|
||||||
|
},
|
||||||
"description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten."
|
"description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"email": "E-post (valgfritt)",
|
"email": "E-post (valgfritt)",
|
||||||
"host": "Vert",
|
"host": "Vert",
|
||||||
"port": "Port "
|
"port": ""
|
||||||
},
|
},
|
||||||
"title": "Koble til enheten"
|
"title": "Koble til enheten"
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,13 @@ from homeassistant.const import (
|
|||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, CoreState, HomeAssistant, callback
|
from homeassistant.core import (
|
||||||
|
Context,
|
||||||
|
CoreState,
|
||||||
|
HomeAssistant,
|
||||||
|
callback,
|
||||||
|
split_entity_id,
|
||||||
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import condition, extract_domain_configs
|
from homeassistant.helpers import condition, extract_domain_configs
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -61,6 +67,7 @@ CONF_TRIGGER = "trigger"
|
|||||||
CONF_CONDITION_TYPE = "condition_type"
|
CONF_CONDITION_TYPE = "condition_type"
|
||||||
CONF_INITIAL_STATE = "initial_state"
|
CONF_INITIAL_STATE = "initial_state"
|
||||||
CONF_SKIP_CONDITION = "skip_condition"
|
CONF_SKIP_CONDITION = "skip_condition"
|
||||||
|
CONF_STOP_ACTIONS = "stop_actions"
|
||||||
|
|
||||||
CONDITION_USE_TRIGGER_VALUES = "use_trigger_values"
|
CONDITION_USE_TRIGGER_VALUES = "use_trigger_values"
|
||||||
CONDITION_TYPE_AND = "and"
|
CONDITION_TYPE_AND = "and"
|
||||||
@ -69,6 +76,7 @@ CONDITION_TYPE_OR = "or"
|
|||||||
|
|
||||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||||
DEFAULT_INITIAL_STATE = True
|
DEFAULT_INITIAL_STATE = True
|
||||||
|
DEFAULT_STOP_ACTIONS = True
|
||||||
|
|
||||||
EVENT_AUTOMATION_RELOADED = "automation_reloaded"
|
EVENT_AUTOMATION_RELOADED = "automation_reloaded"
|
||||||
EVENT_AUTOMATION_TRIGGERED = "automation_triggered"
|
EVENT_AUTOMATION_TRIGGERED = "automation_triggered"
|
||||||
@ -219,7 +227,11 @@ async def async_setup(hass, config):
|
|||||||
)
|
)
|
||||||
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
|
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
|
||||||
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
|
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
|
||||||
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
|
component.async_register_entity_service(
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{vol.Optional(CONF_STOP_ACTIONS, default=DEFAULT_STOP_ACTIONS): cv.boolean},
|
||||||
|
"async_turn_off",
|
||||||
|
)
|
||||||
|
|
||||||
async def reload_service_handler(service_call):
|
async def reload_service_handler(service_call):
|
||||||
"""Remove all automations and load new ones from config."""
|
"""Remove all automations and load new ones from config."""
|
||||||
@ -255,11 +267,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
self._async_detach_triggers = None
|
self._async_detach_triggers = None
|
||||||
self._cond_func = cond_func
|
self._cond_func = cond_func
|
||||||
self.action_script = action_script
|
self.action_script = action_script
|
||||||
|
self.action_script.change_listener = self.async_write_ha_state
|
||||||
self._last_triggered = None
|
self._last_triggered = None
|
||||||
self._initial_state = initial_state
|
self._initial_state = initial_state
|
||||||
self._is_enabled = False
|
self._is_enabled = False
|
||||||
self._referenced_entities: Optional[Set[str]] = None
|
self._referenced_entities: Optional[Set[str]] = None
|
||||||
self._referenced_devices: Optional[Set[str]] = None
|
self._referenced_devices: Optional[Set[str]] = None
|
||||||
|
self._logger = _LOGGER
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -282,11 +296,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
attrs = {
|
attrs = {
|
||||||
ATTR_LAST_TRIGGERED: self._last_triggered,
|
ATTR_LAST_TRIGGERED: self._last_triggered,
|
||||||
ATTR_MODE: self.action_script.script_mode,
|
ATTR_MODE: self.action_script.script_mode,
|
||||||
|
ATTR_CUR: self.action_script.runs,
|
||||||
}
|
}
|
||||||
if self.action_script.supports_max:
|
if self.action_script.supports_max:
|
||||||
attrs[ATTR_MAX] = self.action_script.max_runs
|
attrs[ATTR_MAX] = self.action_script.max_runs
|
||||||
if self.is_on:
|
|
||||||
attrs[ATTR_CUR] = self.action_script.runs
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -337,13 +350,18 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
"""Startup with initial state or previous state."""
|
"""Startup with initial state or previous state."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
self._logger = logging.getLogger(
|
||||||
|
f"{__name__}.{split_entity_id(self.entity_id)[1]}"
|
||||||
|
)
|
||||||
|
self.action_script.update_logger(self._logger)
|
||||||
|
|
||||||
state = await self.async_get_last_state()
|
state = await self.async_get_last_state()
|
||||||
if state:
|
if state:
|
||||||
enable_automation = state.state == STATE_ON
|
enable_automation = state.state == STATE_ON
|
||||||
last_triggered = state.attributes.get("last_triggered")
|
last_triggered = state.attributes.get("last_triggered")
|
||||||
if last_triggered is not None:
|
if last_triggered is not None:
|
||||||
self._last_triggered = parse_datetime(last_triggered)
|
self._last_triggered = parse_datetime(last_triggered)
|
||||||
_LOGGER.debug(
|
self._logger.debug(
|
||||||
"Loaded automation %s with state %s from state "
|
"Loaded automation %s with state %s from state "
|
||||||
" storage last state %s",
|
" storage last state %s",
|
||||||
self.entity_id,
|
self.entity_id,
|
||||||
@ -352,7 +370,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
enable_automation = DEFAULT_INITIAL_STATE
|
enable_automation = DEFAULT_INITIAL_STATE
|
||||||
_LOGGER.debug(
|
self._logger.debug(
|
||||||
"Automation %s not in state storage, state %s from default is used",
|
"Automation %s not in state storage, state %s from default is used",
|
||||||
self.entity_id,
|
self.entity_id,
|
||||||
enable_automation,
|
enable_automation,
|
||||||
@ -360,7 +378,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
|
|
||||||
if self._initial_state is not None:
|
if self._initial_state is not None:
|
||||||
enable_automation = self._initial_state
|
enable_automation = self._initial_state
|
||||||
_LOGGER.debug(
|
self._logger.debug(
|
||||||
"Automation %s initial state %s overridden from "
|
"Automation %s initial state %s overridden from "
|
||||||
"config initial_state",
|
"config initial_state",
|
||||||
self.entity_id,
|
self.entity_id,
|
||||||
@ -376,6 +394,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
|
if CONF_STOP_ACTIONS in kwargs:
|
||||||
|
await self.async_disable(kwargs[CONF_STOP_ACTIONS])
|
||||||
|
else:
|
||||||
await self.async_disable()
|
await self.async_disable()
|
||||||
|
|
||||||
async def async_trigger(self, variables, skip_condition=False, context=None):
|
async def async_trigger(self, variables, skip_condition=False, context=None):
|
||||||
@ -403,12 +424,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
context=trigger_context,
|
context=trigger_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.info("Executing %s", self._name)
|
self._logger.info("Executing %s", self._name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.action_script.async_run(variables, trigger_context)
|
await self.action_script.async_run(variables, trigger_context)
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("While executing automation %s", self.entity_id)
|
self._logger.exception("While executing automation %s", self.entity_id)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self):
|
async def async_will_remove_from_hass(self):
|
||||||
"""Remove listeners when removing automation from Home Assistant."""
|
"""Remove listeners when removing automation from Home Assistant."""
|
||||||
@ -444,9 +465,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_disable(self):
|
async def async_disable(self, stop_actions=DEFAULT_STOP_ACTIONS):
|
||||||
"""Disable the automation entity."""
|
"""Disable the automation entity."""
|
||||||
if not self._is_enabled:
|
if not self._is_enabled and not self.action_script.runs:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._is_enabled = False
|
self._is_enabled = False
|
||||||
@ -455,6 +476,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
self._async_detach_triggers()
|
self._async_detach_triggers()
|
||||||
self._async_detach_triggers = None
|
self._async_detach_triggers = None
|
||||||
|
|
||||||
|
if stop_actions:
|
||||||
await self.action_script.async_stop()
|
await self.action_script.async_stop()
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@ -478,13 +500,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
results = await asyncio.gather(*triggers)
|
results = await asyncio.gather(*triggers)
|
||||||
|
|
||||||
if None in results:
|
if None in results:
|
||||||
_LOGGER.error("Error setting up trigger %s", self._name)
|
self._logger.error("Error setting up trigger %s", self._name)
|
||||||
|
|
||||||
removes = [remove for remove in results if remove is not None]
|
removes = [remove for remove in results if remove is not None]
|
||||||
if not removes:
|
if not removes:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_LOGGER.info("Initialized trigger %s", self._name)
|
self._logger.info("Initialized trigger %s", self._name)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def remove_triggers():
|
def remove_triggers():
|
||||||
|
@ -12,6 +12,9 @@ turn_off:
|
|||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the automation to turn off.
|
description: Name of the automation to turn off.
|
||||||
example: "automation.notify_home"
|
example: "automation.notify_home"
|
||||||
|
stop_actions:
|
||||||
|
description: Stop currently running actions (defaults to true).
|
||||||
|
example: false
|
||||||
|
|
||||||
toggle:
|
toggle:
|
||||||
description: Toggle an automation.
|
description: Toggle an automation.
|
||||||
@ -27,7 +30,7 @@ trigger:
|
|||||||
description: Name of the automation to trigger.
|
description: Name of the automation to trigger.
|
||||||
example: "automation.notify_home"
|
example: "automation.notify_home"
|
||||||
skip_condition:
|
skip_condition:
|
||||||
description: Whether or not the condition will be skipped (defaults to True).
|
description: Whether or not the condition will be skipped (defaults to true).
|
||||||
example: true
|
example: true
|
||||||
|
|
||||||
reload:
|
reload:
|
||||||
|
@ -73,16 +73,13 @@ async def async_attach_trigger(
|
|||||||
|
|
||||||
from_s = event.data.get("old_state")
|
from_s = event.data.get("old_state")
|
||||||
to_s = event.data.get("new_state")
|
to_s = event.data.get("new_state")
|
||||||
|
old_state = getattr(from_s, "state", None)
|
||||||
|
new_state = getattr(to_s, "state", None)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(from_s is not None and not match_from_state(from_s.state))
|
not match_from_state(old_state)
|
||||||
or (to_s is not None and not match_to_state(to_s.state))
|
or not match_to_state(new_state)
|
||||||
or (
|
or (not match_all and old_state == new_state)
|
||||||
not match_all
|
|
||||||
and from_s is not None
|
|
||||||
and to_s is not None
|
|
||||||
and from_s.state == to_s.state
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -104,15 +101,6 @@ async def async_attach_trigger(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ignore changes to state attributes if from/to is in use
|
|
||||||
if (
|
|
||||||
not match_all
|
|
||||||
and from_s is not None
|
|
||||||
and to_s is not None
|
|
||||||
and from_s.state == to_s.state
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not time_delta:
|
if not time_delta:
|
||||||
call_action()
|
call_action()
|
||||||
return
|
return
|
||||||
|
@ -13,20 +13,37 @@ from homeassistant.helpers.event import async_track_time_change
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema(
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
{vol.Required(CONF_PLATFORM): "time", vol.Required(CONF_AT): cv.time}
|
{
|
||||||
|
vol.Required(CONF_PLATFORM): "time",
|
||||||
|
vol.Required(CONF_AT): vol.All(cv.ensure_list, [cv.time]),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_attach_trigger(hass, config, action, automation_info):
|
async def async_attach_trigger(hass, config, action, automation_info):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
at_time = config.get(CONF_AT)
|
at_times = config[CONF_AT]
|
||||||
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def time_automation_listener(now):
|
def time_automation_listener(now):
|
||||||
"""Listen for time changes and calls action."""
|
"""Listen for time changes and calls action."""
|
||||||
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
|
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
|
||||||
|
|
||||||
return async_track_time_change(
|
removes = [
|
||||||
hass, time_automation_listener, hour=hours, minute=minutes, second=seconds
|
async_track_time_change(
|
||||||
|
hass,
|
||||||
|
time_automation_listener,
|
||||||
|
hour=at_time.hour,
|
||||||
|
minute=at_time.minute,
|
||||||
|
second=at_time.second,
|
||||||
)
|
)
|
||||||
|
for at_time in at_times
|
||||||
|
]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def remove_track_time_changes():
|
||||||
|
"""Remove tracked time changes."""
|
||||||
|
for remove in removes:
|
||||||
|
remove()
|
||||||
|
|
||||||
|
return remove_track_time_changes
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Kontoen er allerede konfigurert",
|
||||||
|
"no_devices": "Ingen enheter funnet p\u00e5 nettverket",
|
||||||
|
"reauth_successful": "Tilgangstoken oppdatert"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"auth": "Ugyldig tilgangstoken",
|
||||||
"unknown": "Ukjent Awair API-feil."
|
"unknown": "Ukjent Awair API-feil."
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"reauth": {
|
"reauth": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"access_token": "Tilgangstoken",
|
||||||
"email": "Epost"
|
"email": "Epost"
|
||||||
},
|
},
|
||||||
"description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt."
|
"description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt."
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"access_token": "Tilgangstoken",
|
||||||
"email": "Epost "
|
"email": "Epost "
|
||||||
},
|
},
|
||||||
"description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login"
|
"description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login"
|
||||||
|
26
homeassistant/components/awair/translations/pl.json
Normal file
26
homeassistant/components/awair/translations/pl.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Konto jest ju\u017c skonfigurowane.",
|
||||||
|
"no_devices": "Nie znaleziono urz\u0105dze\u0144 w sieci.",
|
||||||
|
"reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowano."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"auth": "Token dost\u0119pu"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Token dost\u0119pu",
|
||||||
|
"email": "Adres e-mail"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Token dost\u0119pu",
|
||||||
|
"email": "Adres e-mail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "Vert",
|
"host": "Vert",
|
||||||
"password": "Passord",
|
"password": "Passord",
|
||||||
"port": "Port",
|
"port": "",
|
||||||
"username": "Brukernavn"
|
"username": "Brukernavn"
|
||||||
},
|
},
|
||||||
"title": "Sett opp Axis enhet"
|
"title": "Sett opp Axis enhet"
|
||||||
|
121
homeassistant/components/azure_devops/__init__.py
Normal file
121
homeassistant/components/azure_devops/__init__.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"""Support for Azure DevOps."""
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from aioazuredevops.client import DevOpsClient
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from homeassistant.components.azure_devops.const import (
|
||||||
|
CONF_ORG,
|
||||||
|
CONF_PAT,
|
||||||
|
CONF_PROJECT,
|
||||||
|
DATA_AZURE_DEVOPS_CLIENT,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
|
"""Set up the Azure DevOps components."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Azure DevOps from a config entry."""
|
||||||
|
client = DevOpsClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if entry.data[CONF_PAT] is not None:
|
||||||
|
await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG])
|
||||||
|
if not client.authorized:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Could not authorize with Azure DevOps. You may need to update your token"
|
||||||
|
)
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=entry.data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT])
|
||||||
|
except aiohttp.ClientError as exception:
|
||||||
|
_LOGGER.warning(exception)
|
||||||
|
raise ConfigEntryNotReady from exception
|
||||||
|
|
||||||
|
instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"
|
||||||
|
hass.data.setdefault(instance_key, {})[DATA_AZURE_DEVOPS_CLIENT] = client
|
||||||
|
|
||||||
|
# Setup components
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, "sensor")
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool:
|
||||||
|
"""Unload Azure DevOps config entry."""
|
||||||
|
del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"]
|
||||||
|
|
||||||
|
return await hass.config_entries.async_forward_entry_unload(entry, "sensor")
|
||||||
|
|
||||||
|
|
||||||
|
class AzureDevOpsEntity(Entity):
|
||||||
|
"""Defines a base Azure DevOps entity."""
|
||||||
|
|
||||||
|
def __init__(self, organization: str, project: str, name: str, icon: str) -> None:
|
||||||
|
"""Initialize the Azure DevOps entity."""
|
||||||
|
self._name = name
|
||||||
|
self._icon = icon
|
||||||
|
self._available = True
|
||||||
|
self.organization = organization
|
||||||
|
self.project = project
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the entity."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return the mdi icon of the entity."""
|
||||||
|
return self._icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self._available
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Update Azure DevOps entity."""
|
||||||
|
if await self._azure_devops_update():
|
||||||
|
self._available = True
|
||||||
|
else:
|
||||||
|
if self._available:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"An error occurred while updating Azure DevOps sensor.",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
self._available = False
|
||||||
|
|
||||||
|
async def _azure_devops_update(self) -> None:
|
||||||
|
"""Update Azure DevOps entity."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class AzureDevOpsDeviceEntity(AzureDevOpsEntity):
|
||||||
|
"""Defines a Azure DevOps device entity."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return device information about this Azure DevOps instance."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self.organization, self.project,)},
|
||||||
|
"manufacturer": self.organization,
|
||||||
|
"name": self.project,
|
||||||
|
}
|
134
homeassistant/components/azure_devops/config_flow.py
Normal file
134
homeassistant/components/azure_devops/config_flow.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
"""Config flow to configure the Azure DevOps integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioazuredevops.client import DevOpsClient
|
||||||
|
import aiohttp
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.azure_devops.const import ( # pylint:disable=unused-import
|
||||||
|
CONF_ORG,
|
||||||
|
CONF_PAT,
|
||||||
|
CONF_PROJECT,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigFlow
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a Azure DevOps config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize config flow."""
|
||||||
|
self._organization = None
|
||||||
|
self._project = None
|
||||||
|
self._pat = None
|
||||||
|
|
||||||
|
async def _show_setup_form(self, errors=None):
|
||||||
|
"""Show the setup form to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_ORG, default=self._organization): str,
|
||||||
|
vol.Required(CONF_PROJECT, default=self._project): str,
|
||||||
|
vol.Optional(CONF_PAT): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _show_reauth_form(self, errors=None):
|
||||||
|
"""Show the reauth form to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth",
|
||||||
|
description_placeholders={
|
||||||
|
"project_url": f"{self._organization}/{self._project}"
|
||||||
|
},
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_PAT): str}),
|
||||||
|
errors=errors or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _check_setup(self):
|
||||||
|
"""Check the setup of the flow."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
client = DevOpsClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._pat is not None:
|
||||||
|
await client.authorize(self._pat, self._organization)
|
||||||
|
if not client.authorized:
|
||||||
|
errors["base"] = "authorization_error"
|
||||||
|
return errors
|
||||||
|
project_info = await client.get_project(self._organization, self._project)
|
||||||
|
if project_info is None:
|
||||||
|
errors["base"] = "project_error"
|
||||||
|
return errors
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
errors["base"] = "connection_error"
|
||||||
|
return errors
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initiated by the user."""
|
||||||
|
if user_input is None:
|
||||||
|
return await self._show_setup_form(user_input)
|
||||||
|
|
||||||
|
self._organization = user_input[CONF_ORG]
|
||||||
|
self._project = user_input[CONF_PROJECT]
|
||||||
|
self._pat = user_input.get(CONF_PAT)
|
||||||
|
|
||||||
|
await self.async_set_unique_id(f"{self._organization}_{self._project}")
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
errors = await self._check_setup()
|
||||||
|
if errors is not None:
|
||||||
|
return await self._show_setup_form(errors)
|
||||||
|
return self._async_create_entry()
|
||||||
|
|
||||||
|
async def async_step_reauth(self, user_input):
|
||||||
|
"""Handle configuration by re-auth."""
|
||||||
|
if user_input.get(CONF_ORG) and user_input.get(CONF_PROJECT):
|
||||||
|
self._organization = user_input[CONF_ORG]
|
||||||
|
self._project = user_input[CONF_PROJECT]
|
||||||
|
self._pat = user_input[CONF_PAT]
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
self.context["title_placeholders"] = {
|
||||||
|
"project_url": f"{self._organization}/{self._project}",
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.async_set_unique_id(f"{self._organization}_{self._project}")
|
||||||
|
|
||||||
|
errors = await self._check_setup()
|
||||||
|
if errors is not None:
|
||||||
|
return await self._show_reauth_form(errors)
|
||||||
|
|
||||||
|
for entry in self._async_current_entries():
|
||||||
|
if entry.unique_id == self.unique_id:
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
entry,
|
||||||
|
data={
|
||||||
|
CONF_ORG: self._organization,
|
||||||
|
CONF_PROJECT: self._project,
|
||||||
|
CONF_PAT: self._pat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self.async_abort(reason="reauth_successful")
|
||||||
|
|
||||||
|
def _async_create_entry(self):
|
||||||
|
"""Handle create entry."""
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"{self._organization}/{self._project}",
|
||||||
|
data={
|
||||||
|
CONF_ORG: self._organization,
|
||||||
|
CONF_PROJECT: self._project,
|
||||||
|
CONF_PAT: self._pat,
|
||||||
|
},
|
||||||
|
)
|
11
homeassistant/components/azure_devops/const.py
Normal file
11
homeassistant/components/azure_devops/const.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"""Constants for the Azure DevOps integration."""
|
||||||
|
DOMAIN = "azure_devops"
|
||||||
|
|
||||||
|
DATA_AZURE_DEVOPS_CLIENT = "azure_devops_client"
|
||||||
|
DATA_ORG = "organization"
|
||||||
|
DATA_PROJECT = "project"
|
||||||
|
DATA_PAT = "personal_access_token"
|
||||||
|
|
||||||
|
CONF_ORG = "organization"
|
||||||
|
CONF_PROJECT = "project"
|
||||||
|
CONF_PAT = "personal_access_token"
|
8
homeassistant/components/azure_devops/manifest.json
Normal file
8
homeassistant/components/azure_devops/manifest.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"domain": "azure_devops",
|
||||||
|
"name": "Azure DevOps",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
||||||
|
"requirements": ["aioazuredevops==1.3.5"],
|
||||||
|
"codeowners": ["@timmo001"]
|
||||||
|
}
|
148
homeassistant/components/azure_devops/sensor.py
Normal file
148
homeassistant/components/azure_devops/sensor.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"""Support for Azure DevOps sensors."""
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from aioazuredevops.builds import DevOpsBuild
|
||||||
|
from aioazuredevops.client import DevOpsClient
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from homeassistant.components.azure_devops import AzureDevOpsDeviceEntity
|
||||||
|
from homeassistant.components.azure_devops.const import (
|
||||||
|
CONF_ORG,
|
||||||
|
CONF_PROJECT,
|
||||||
|
DATA_AZURE_DEVOPS_CLIENT,
|
||||||
|
DATA_ORG,
|
||||||
|
DATA_PROJECT,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=300)
|
||||||
|
PARALLEL_UPDATES = 4
|
||||||
|
|
||||||
|
BUILDS_QUERY = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||||
|
) -> None:
|
||||||
|
"""Set up Azure DevOps sensor based on a config entry."""
|
||||||
|
instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"
|
||||||
|
client = hass.data[instance_key][DATA_AZURE_DEVOPS_CLIENT]
|
||||||
|
organization = entry.data[DATA_ORG]
|
||||||
|
project = entry.data[DATA_PROJECT]
|
||||||
|
sensors = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
builds: List[DevOpsBuild] = await client.get_builds(
|
||||||
|
organization, project, BUILDS_QUERY
|
||||||
|
)
|
||||||
|
except aiohttp.ClientError as exception:
|
||||||
|
_LOGGER.warning(exception)
|
||||||
|
raise PlatformNotReady from exception
|
||||||
|
|
||||||
|
for build in builds:
|
||||||
|
sensors.append(
|
||||||
|
AzureDevOpsLatestBuildSensor(client, organization, project, build)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(sensors, True)
|
||||||
|
|
||||||
|
|
||||||
|
class AzureDevOpsSensor(AzureDevOpsDeviceEntity):
|
||||||
|
"""Defines a Azure DevOps sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: DevOpsClient,
|
||||||
|
organization: str,
|
||||||
|
project: str,
|
||||||
|
key: str,
|
||||||
|
name: str,
|
||||||
|
icon: str,
|
||||||
|
measurement: str = "",
|
||||||
|
unit_of_measurement: str = "",
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Azure DevOps sensor."""
|
||||||
|
self._state = None
|
||||||
|
self._attributes = None
|
||||||
|
self._available = False
|
||||||
|
self._unit_of_measurement = unit_of_measurement
|
||||||
|
self.measurement = measurement
|
||||||
|
self.client = client
|
||||||
|
self.organization = organization
|
||||||
|
self.project = project
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
super().__init__(organization, project, name, icon)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return the unique ID for this sensor."""
|
||||||
|
return "_".join([self.organization, self.key])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self) -> object:
|
||||||
|
"""Return the attributes of the sensor."""
|
||||||
|
return self._attributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self) -> str:
|
||||||
|
"""Return the unit this state is expressed in."""
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
|
||||||
|
class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor):
|
||||||
|
"""Defines a Azure DevOps card count sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, client: DevOpsClient, organization: str, project: str, build: DevOpsBuild
|
||||||
|
):
|
||||||
|
"""Initialize Azure DevOps sensor."""
|
||||||
|
self.build: DevOpsBuild = build
|
||||||
|
super().__init__(
|
||||||
|
client,
|
||||||
|
organization,
|
||||||
|
project,
|
||||||
|
f"{build.project.id}_{build.definition.id}_latest_build",
|
||||||
|
f"{build.project.name} {build.definition.name} Latest Build",
|
||||||
|
"mdi:pipe",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _azure_devops_update(self) -> bool:
|
||||||
|
"""Update Azure DevOps entity."""
|
||||||
|
try:
|
||||||
|
build: DevOpsBuild = await self.client.get_build(
|
||||||
|
self.organization, self.project, self.build.id
|
||||||
|
)
|
||||||
|
except aiohttp.ClientError as exception:
|
||||||
|
_LOGGER.warning(exception)
|
||||||
|
self._available = False
|
||||||
|
return False
|
||||||
|
self._state = build.build_number
|
||||||
|
self._attributes = {
|
||||||
|
"definition_id": build.definition.id,
|
||||||
|
"definition_name": build.definition.name,
|
||||||
|
"id": build.id,
|
||||||
|
"reason": build.reason,
|
||||||
|
"result": build.result,
|
||||||
|
"source_branch": build.source_branch,
|
||||||
|
"source_version": build.source_version,
|
||||||
|
"status": build.status,
|
||||||
|
"url": build.links.web,
|
||||||
|
"queue_time": build.queue_time,
|
||||||
|
"start_time": build.start_time,
|
||||||
|
"finish_time": build.finish_time,
|
||||||
|
}
|
||||||
|
self._available = True
|
||||||
|
return True
|
33
homeassistant/components/azure_devops/strings.json
Normal file
33
homeassistant/components/azure_devops/strings.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "Authorization error. Check you have access to the project and have the correct credentials.",
|
||||||
|
"connection_error": "Could not connect to Azure DevOps.",
|
||||||
|
"project_error": "Could not get project info."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "Organization",
|
||||||
|
"project": "Project",
|
||||||
|
"personal_access_token": "Personal Access Token (PAT)"
|
||||||
|
},
|
||||||
|
"description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.",
|
||||||
|
"title": "Add Azure DevOps Project"
|
||||||
|
},
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "Personal Access Token (PAT)"
|
||||||
|
},
|
||||||
|
"description": "Authentication failed for {project_url}. Please enter your current credentials.",
|
||||||
|
"title": "Reauthentication"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
33
homeassistant/components/azure_devops/translations/ca.json
Normal file
33
homeassistant/components/azure_devops/translations/ca.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "El compte ja ha estat configurat",
|
||||||
|
"reauth_successful": "Token d'acc\u00e9s actualitzat correctament"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "Error d'autoritzaci\u00f3. Comprova que tens acc\u00e9s al projecte i tens les credencials correctes.",
|
||||||
|
"connection_error": "No s'ha pogut connectar a Azure DevOps.",
|
||||||
|
"project_error": "No s'ha pogut obtenir la informaci\u00f3 del projecte."
|
||||||
|
},
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "Token d'Acc\u00e9s Personal (PAT)"
|
||||||
|
},
|
||||||
|
"description": "L'autenticaci\u00f3 de {project_url} ha fallat. Si us plau, introdueix les teves credencials actuals.",
|
||||||
|
"title": "Reautenticaci\u00f3"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "Organitzaci\u00f3",
|
||||||
|
"personal_access_token": "Token d'Acc\u00e9s Personal (PAT)",
|
||||||
|
"project": "Projecte"
|
||||||
|
},
|
||||||
|
"description": "Configura una inst\u00e0ncia d'Azure DevOps per accedir al teu projecte. El token d'acc\u00e9s personal nom\u00e9s \u00e9s necessari per a projectes privats.",
|
||||||
|
"title": "Afegeix un projecte Azure DevOps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
33
homeassistant/components/azure_devops/translations/en.json
Normal file
33
homeassistant/components/azure_devops/translations/en.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Account is already configured",
|
||||||
|
"reauth_successful": "Access Token updated successfully"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "Authorization error. Check you have access to the project and have the correct credentials.",
|
||||||
|
"connection_error": "Could not connect to Azure DevOps.",
|
||||||
|
"project_error": "Could not get project info."
|
||||||
|
},
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "Personal Access Token (PAT)"
|
||||||
|
},
|
||||||
|
"description": "Authentication failed for {project_url}. Please enter your current credentials.",
|
||||||
|
"title": "Reauthentication"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "Organization",
|
||||||
|
"personal_access_token": "Personal Access Token (PAT)",
|
||||||
|
"project": "Project"
|
||||||
|
},
|
||||||
|
"description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.",
|
||||||
|
"title": "Add Azure DevOps Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
33
homeassistant/components/azure_devops/translations/es.json
Normal file
33
homeassistant/components/azure_devops/translations/es.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "La cuenta ya ha sido configurada",
|
||||||
|
"reauth_successful": "Token de acceso actualizado correctamente "
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "Error de autorizaci\u00f3n. Comprueba que tienes acceso al proyecto y las credenciales son correctas.",
|
||||||
|
"connection_error": "No se pudo conectar con Azure DevOps",
|
||||||
|
"project_error": "No se pudo obtener informaci\u00f3n del proyecto."
|
||||||
|
},
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "Token Personal de Acceso (PAT)"
|
||||||
|
},
|
||||||
|
"description": "Error de autenticaci\u00f3n para {project_url}. Por favor, introduce tus credenciales actuales.",
|
||||||
|
"title": "Reautenticaci\u00f3n"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "Organizaci\u00f3n",
|
||||||
|
"personal_access_token": "Token Personal de Acceso (PAT)",
|
||||||
|
"project": "Proyecto"
|
||||||
|
},
|
||||||
|
"description": "Configura una instancia de Azure DevOps para acceder a tu proyecto. Un Token Personal de Acceso s\u00f3lo es necesario para un proyecto privado.",
|
||||||
|
"title": "A\u00f1adir Proyecto Azure DevOps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
33
homeassistant/components/azure_devops/translations/it.json
Normal file
33
homeassistant/components/azure_devops/translations/it.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
|
||||||
|
"reauth_successful": "Token di accesso aggiornato correttamente"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "Errore di autorizzazione. Verificare di avere accesso al progetto e disporre delle credenziali corrette.",
|
||||||
|
"connection_error": "Impossibile connettersi ad Azure DevOps.",
|
||||||
|
"project_error": "Non \u00e8 stato possibile ottenere informazioni sul progetto."
|
||||||
|
},
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "Token di Accesso Personale (PAT)"
|
||||||
|
},
|
||||||
|
"description": "Autenticazione non riuscita per {project_url}. Si prega di inserire le proprie credenziali attuali.",
|
||||||
|
"title": "Riautenticazione"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "Organizzazione",
|
||||||
|
"personal_access_token": "Token di Accesso Personale (PAT)",
|
||||||
|
"project": "Progetto"
|
||||||
|
},
|
||||||
|
"description": "Configurare un'istanza di DevOps di Azure per accedere al progetto. Un Token di Accesso Personale (PAT) \u00e8 richiesto solo per un progetto privato.",
|
||||||
|
"title": "Aggiungere un progetto Azure DevOps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
33
homeassistant/components/azure_devops/translations/lb.json
Normal file
33
homeassistant/components/azure_devops/translations/lb.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Kont ass scho konfigur\u00e9iert",
|
||||||
|
"reauth_successful": "Acc\u00e8s Jeton erfollegr\u00e4ich aktualis\u00e9iert"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "Feeler bei der Authorisatioun. Iwwerpr\u00e9if ob d\u00e4in Kont den acc\u00e8s zum Projet souw\u00e9i d\u00e9i richteg Umeldungsinformatioune huet",
|
||||||
|
"connection_error": "Konnt sech net mat Azure DevOps verbannen",
|
||||||
|
"project_error": "Konnt keng Projet Informatiounen ausliesen."
|
||||||
|
},
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "Pers\u00e9inlechen Acc\u00e8s Jeton (PAT)"
|
||||||
|
},
|
||||||
|
"description": "Feeler bei der Authentifikatioun fir {project_url}. G\u00ebff deng aktuell Umeldungsinformatiounen an.",
|
||||||
|
"title": "Reauthentifikatioun"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "Organisatioun",
|
||||||
|
"personal_access_token": "Pers\u00e9inlechen Acc\u00e8s Jeton (PAT)",
|
||||||
|
"project": "Projet"
|
||||||
|
},
|
||||||
|
"description": "Riicht eng Azure DevOps Instanz an fir d\u00e4in Projet z'acc\u00e9d\u00e9ieren. E Pers\u00e9inlechen Acc\u00e8s Jetons ass n\u00ebmme fir ee Private Projet n\u00e9ideg.",
|
||||||
|
"title": "Azure DevOps Project dob\u00e4isetzen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
33
homeassistant/components/azure_devops/translations/no.json
Normal file
33
homeassistant/components/azure_devops/translations/no.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Kontoen er allerede konfigurert",
|
||||||
|
"reauth_successful": "Tilgangstoken oppdatert"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "Autoriseringsfeil. Sjekk at du har tilgang til prosjektet og har riktig legitimasjon.",
|
||||||
|
"connection_error": "Kunne ikke koble til Azure DevOps.",
|
||||||
|
"project_error": "Kunne ikke f\u00e5 prosjektinformasjon."
|
||||||
|
},
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "Token for personlig tilgang (PAT)"
|
||||||
|
},
|
||||||
|
"description": "Autentiseringen mislyktes for {project_url} . Vennligst skriv inn gjeldende legitimasjon.",
|
||||||
|
"title": "reautentisering"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "Organisasjon",
|
||||||
|
"personal_access_token": "Token for personlig tilgang (PAT)",
|
||||||
|
"project": "Prosjekt"
|
||||||
|
},
|
||||||
|
"description": "Sett opp en Azure DevOps-forekomst for \u00e5 f\u00e5 tilgang til prosjektet ditt. En personlig tilgangstoken er bare n\u00f8dvendig for et privat prosjekt.",
|
||||||
|
"title": "Legg til Azure DevOps Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
33
homeassistant/components/azure_devops/translations/ru.json
Normal file
33
homeassistant/components/azure_devops/translations/ru.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.",
|
||||||
|
"reauth_successful": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443 \u0412\u0430\u0441 \u0435\u0441\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043f\u0440\u043e\u0435\u043a\u0442\u0443, \u0430 \u0442\u0430\u043a \u0436\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
|
||||||
|
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Azure DevOps.",
|
||||||
|
"project_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0435."
|
||||||
|
},
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (PAT)"
|
||||||
|
},
|
||||||
|
"description": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 {project_url}. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
|
||||||
|
"title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "\u041e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f",
|
||||||
|
"personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (PAT)",
|
||||||
|
"project": "\u041f\u0440\u043e\u0435\u043a\u0442"
|
||||||
|
},
|
||||||
|
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0430\u0441\u0442\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432.",
|
||||||
|
"title": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
19
homeassistant/components/azure_devops/translations/uk.json
Normal file
19
homeassistant/components/azure_devops/translations/uk.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "Azure DevOps: {project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "\u041e\u0440\u0433\u0430\u043d\u0456\u0437\u0430\u0446\u0456\u044f",
|
||||||
|
"personal_access_token": "\u0422\u043e\u043a\u0435\u043d \u043e\u0441\u043e\u0431\u0438\u0441\u0442\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)",
|
||||||
|
"project": "\u041f\u0440\u043e\u0454\u043a\u0442"
|
||||||
|
},
|
||||||
|
"title": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
|
||||||
|
"reauth_successful": "\u5b58\u53d6\u5bc6\u9470\u5df2\u6210\u529f\u66f4\u65b0"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"authorization_error": "\u8a8d\u8b49\u932f\u8aa4\u3002\u8acb\u78ba\u8a8d\u64c1\u6709\u5c08\u6848\u5b58\u53d6\u6b0a\u8207\u6b63\u78ba\u7684\u8b49\u66f8\u3002",
|
||||||
|
"connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 Azure DevOps\u3002",
|
||||||
|
"project_error": "\u7121\u6cd5\u53d6\u5f97\u5c08\u6848\u8cc7\u8a0a\u3002"
|
||||||
|
},
|
||||||
|
"flow_title": "Azure DevOps\uff1a{project_url}",
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"personal_access_token": "\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff08PAT\uff09"
|
||||||
|
},
|
||||||
|
"description": "{project_url}\u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u8b49\u66f8\u3002",
|
||||||
|
"title": "\u91cd\u65b0\u8a8d\u8b49"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"organization": "\u7d44\u7e54",
|
||||||
|
"personal_access_token": "\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff08PAT\uff09",
|
||||||
|
"project": "\u5c08\u6848"
|
||||||
|
},
|
||||||
|
"description": "\u8a2d\u5b9a Azure DevOps \u4ee5\u5b58\u53d6\u5c08\u6848\u3002\u79c1\u4eba\u5c08\u6848\u5247\u9700\u8981\u8f38\u5165\u300c\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff09\u3002",
|
||||||
|
"title": "\u65b0\u589e Azure DevOps \u5c08\u6848"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Azure DevOps"
|
||||||
|
}
|
@ -131,7 +131,7 @@
|
|||||||
"on": "H\u00famedo"
|
"on": "H\u00famedo"
|
||||||
},
|
},
|
||||||
"motion": {
|
"motion": {
|
||||||
"off": "Sin movimiento",
|
"off": "No detectado",
|
||||||
"on": "Detectado"
|
"on": "Detectado"
|
||||||
},
|
},
|
||||||
"occupancy": {
|
"occupancy": {
|
||||||
|
12
homeassistant/components/blebox/translations/nl.json
Normal file
12
homeassistant/components/blebox/translations/nl.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "IP-adres",
|
||||||
|
"port": "Poort"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "IP adresse",
|
"host": "IP adresse",
|
||||||
"port": "Port"
|
"port": ""
|
||||||
},
|
},
|
||||||
"description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.",
|
"description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.",
|
||||||
"title": "Konfigurere BleBox-enheten"
|
"title": "Konfigurere BleBox-enheten"
|
||||||
|
@ -1,32 +1,25 @@
|
|||||||
"""Support for Blink Home Camera System."""
|
"""Support for Blink Home Camera System."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from copy import deepcopy
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from blinkpy.auth import Auth
|
||||||
from blinkpy.blinkpy import Blink
|
from blinkpy.blinkpy import Blink
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
from homeassistant.components import persistent_notification
|
||||||
from homeassistant.const import (
|
from homeassistant.components.blink.const import (
|
||||||
CONF_FILENAME,
|
|
||||||
CONF_NAME,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_PIN,
|
|
||||||
CONF_SCAN_INTERVAL,
|
|
||||||
CONF_USERNAME,
|
|
||||||
)
|
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
DEFAULT_OFFSET,
|
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DEVICE_ID,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SERVICE_REFRESH,
|
SERVICE_REFRESH,
|
||||||
SERVICE_SAVE_VIDEO,
|
SERVICE_SAVE_VIDEO,
|
||||||
SERVICE_SEND_PIN,
|
SERVICE_SEND_PIN,
|
||||||
)
|
)
|
||||||
|
from homeassistant.const import CONF_FILENAME, CONF_NAME, CONF_PIN, CONF_SCAN_INTERVAL
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -35,58 +28,50 @@ SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string})
|
SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
DOMAIN: vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
|
||||||
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def _blink_startup_wrapper(hass, entry):
|
||||||
def _blink_startup_wrapper(entry):
|
|
||||||
"""Startup wrapper for blink."""
|
"""Startup wrapper for blink."""
|
||||||
blink = Blink(
|
blink = Blink()
|
||||||
username=entry.data[CONF_USERNAME],
|
auth_data = deepcopy(dict(entry.data))
|
||||||
password=entry.data[CONF_PASSWORD],
|
blink.auth = Auth(auth_data, no_prompt=True)
|
||||||
motion_interval=DEFAULT_OFFSET,
|
|
||||||
legacy_subdomain=False,
|
|
||||||
no_prompt=True,
|
|
||||||
device_id=DEVICE_ID,
|
|
||||||
)
|
|
||||||
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||||
|
|
||||||
try:
|
if blink.start():
|
||||||
blink.login_response = entry.data["login_response"]
|
|
||||||
blink.setup_params(entry.data["login_response"])
|
|
||||||
except KeyError:
|
|
||||||
blink.get_auth_token()
|
|
||||||
|
|
||||||
blink.setup_params(entry.data["login_response"])
|
|
||||||
blink.setup_post_verify()
|
blink.setup_post_verify()
|
||||||
|
elif blink.auth.check_key_required():
|
||||||
|
_LOGGER.debug("Attempting a reauth flow")
|
||||||
|
_reauth_flow_wrapper(hass, auth_data)
|
||||||
|
|
||||||
return blink
|
return blink
|
||||||
|
|
||||||
|
|
||||||
|
def _reauth_flow_wrapper(hass, data):
|
||||||
|
"""Reauth flow wrapper."""
|
||||||
|
hass.add_job(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
persistent_notification.async_create(
|
||||||
|
hass,
|
||||||
|
"Blink configuration migrated to a new version. Please go to the integrations page to re-configure (such as sending a new 2FA key).",
|
||||||
|
"Blink Migration",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up a config entry."""
|
"""Set up a Blink component."""
|
||||||
hass.data[DOMAIN] = {}
|
hass.data[DOMAIN] = {}
|
||||||
if DOMAIN not in config:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
conf = config.get(DOMAIN, {})
|
|
||||||
|
|
||||||
if not hass.config_entries.async_entries(DOMAIN):
|
|
||||||
hass.async_create_task(
|
|
||||||
hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass, entry):
|
||||||
|
"""Handle migration of a previous version config entry."""
|
||||||
|
data = {**entry.data}
|
||||||
|
if entry.version == 1:
|
||||||
|
data.pop("login_response", None)
|
||||||
|
await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data)
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -95,12 +80,11 @@ async def async_setup_entry(hass, entry):
|
|||||||
_async_import_options_from_data_if_missing(hass, entry)
|
_async_import_options_from_data_if_missing(hass, entry)
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
|
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
|
||||||
_blink_startup_wrapper, entry
|
_blink_startup_wrapper, hass, entry
|
||||||
)
|
)
|
||||||
|
|
||||||
if not hass.data[DOMAIN][entry.entry_id].available:
|
if not hass.data[DOMAIN][entry.entry_id].available:
|
||||||
_LOGGER.error("Blink unavailable for setup")
|
raise ConfigEntryNotReady
|
||||||
return False
|
|
||||||
|
|
||||||
for component in PLATFORMS:
|
for component in PLATFORMS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
@ -118,7 +102,7 @@ async def async_setup_entry(hass, entry):
|
|||||||
def send_pin(call):
|
def send_pin(call):
|
||||||
"""Call blink to send new pin."""
|
"""Call blink to send new pin."""
|
||||||
pin = call.data[CONF_PIN]
|
pin = call.data[CONF_PIN]
|
||||||
hass.data[DOMAIN][entry.entry_id].login_handler.send_auth_key(
|
hass.data[DOMAIN][entry.entry_id].auth.send_auth_key(
|
||||||
hass.data[DOMAIN][entry.entry_id], pin,
|
hass.data[DOMAIN][entry.entry_id], pin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
"""Config flow to configure Blink."""
|
"""Config flow to configure Blink."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from blinkpy.blinkpy import Blink
|
from blinkpy.auth import Auth, LoginError, TokenRefreshFailed
|
||||||
|
from blinkpy.blinkpy import Blink, BlinkSetupError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, core, exceptions
|
from homeassistant import config_entries, core, exceptions
|
||||||
|
from homeassistant.components.blink.const import (
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
|
DEVICE_ID,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
@ -13,36 +19,36 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
|
||||||
from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(hass: core.HomeAssistant, blink):
|
def validate_input(hass: core.HomeAssistant, auth):
|
||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
response = await hass.async_add_executor_job(blink.get_auth_token)
|
try:
|
||||||
if not response:
|
auth.startup()
|
||||||
|
except (LoginError, TokenRefreshFailed):
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
if blink.key_required:
|
if auth.check_key_required():
|
||||||
raise Require2FA
|
raise Require2FA
|
||||||
|
|
||||||
return blink.login_response
|
|
||||||
|
def _send_blink_2fa_pin(auth, pin):
|
||||||
|
"""Send 2FA pin to blink servers."""
|
||||||
|
blink = Blink()
|
||||||
|
blink.auth = auth
|
||||||
|
blink.setup_urls()
|
||||||
|
return auth.send_auth_key(blink, pin)
|
||||||
|
|
||||||
|
|
||||||
class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a Blink config flow."""
|
"""Handle a Blink config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 2
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize the blink flow."""
|
"""Initialize the blink flow."""
|
||||||
self.blink = None
|
self.auth = None
|
||||||
self.data = {
|
|
||||||
CONF_USERNAME: "",
|
|
||||||
CONF_PASSWORD: "",
|
|
||||||
"login_response": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
@ -53,28 +59,19 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Handle a flow initiated by the user."""
|
"""Handle a flow initiated by the user."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
data = {CONF_USERNAME: "", CONF_PASSWORD: "", "device_id": DEVICE_ID}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.data[CONF_USERNAME] = user_input["username"]
|
data[CONF_USERNAME] = user_input["username"]
|
||||||
self.data[CONF_PASSWORD] = user_input["password"]
|
data[CONF_PASSWORD] = user_input["password"]
|
||||||
|
|
||||||
await self.async_set_unique_id(self.data[CONF_USERNAME])
|
self.auth = Auth(data, no_prompt=True)
|
||||||
|
await self.async_set_unique_id(data[CONF_USERNAME])
|
||||||
if CONF_SCAN_INTERVAL in user_input:
|
|
||||||
self.data[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL]
|
|
||||||
|
|
||||||
self.blink = Blink(
|
|
||||||
username=self.data[CONF_USERNAME],
|
|
||||||
password=self.data[CONF_PASSWORD],
|
|
||||||
motion_interval=DEFAULT_OFFSET,
|
|
||||||
legacy_subdomain=False,
|
|
||||||
no_prompt=True,
|
|
||||||
device_id=DEVICE_ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await validate_input(self.hass, self.blink)
|
await self.hass.async_add_executor_job(
|
||||||
self.data["login_response"] = response
|
validate_input, self.hass, self.auth
|
||||||
return self.async_create_entry(title=DOMAIN, data=self.data,)
|
)
|
||||||
|
return self._async_finish_flow()
|
||||||
except Require2FA:
|
except Require2FA:
|
||||||
return await self.async_step_2fa()
|
return await self.async_step_2fa()
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
@ -94,23 +91,40 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def async_step_2fa(self, user_input=None):
|
async def async_step_2fa(self, user_input=None):
|
||||||
"""Handle 2FA step."""
|
"""Handle 2FA step."""
|
||||||
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
pin = user_input.get(CONF_PIN)
|
pin = user_input.get(CONF_PIN)
|
||||||
if await self.hass.async_add_executor_job(
|
try:
|
||||||
self.blink.login_handler.send_auth_key, self.blink, pin
|
valid_token = await self.hass.async_add_executor_job(
|
||||||
):
|
_send_blink_2fa_pin, self.auth, pin
|
||||||
return await self.async_step_user(user_input=self.data)
|
)
|
||||||
|
except BlinkSetupError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
else:
|
||||||
|
if valid_token:
|
||||||
|
return self._async_finish_flow()
|
||||||
|
errors["base"] = "invalid_access_token"
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="2fa",
|
step_id="2fa",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{vol.Optional("pin"): vol.All(str, vol.Length(min=1))}
|
{vol.Optional("pin"): vol.All(str, vol.Length(min=1))}
|
||||||
),
|
),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(self, import_data):
|
async def async_step_reauth(self, entry_data):
|
||||||
"""Import blink config from configuration.yaml."""
|
"""Perform reauth upon migration of old entries."""
|
||||||
return await self.async_step_user(import_data)
|
return await self.async_step_user(entry_data)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_finish_flow(self):
|
||||||
|
"""Finish with setup."""
|
||||||
|
return self.async_create_entry(title=DOMAIN, data=self.auth.login_attributes)
|
||||||
|
|
||||||
|
|
||||||
class BlinkOptionsFlowHandler(config_entries.OptionsFlow):
|
class BlinkOptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
DOMAIN = "blink"
|
DOMAIN = "blink"
|
||||||
DEVICE_ID = "Home Assistant"
|
DEVICE_ID = "Home Assistant"
|
||||||
|
|
||||||
|
CONF_MIGRATE = "migrate"
|
||||||
CONF_CAMERA = "camera"
|
CONF_CAMERA = "camera"
|
||||||
CONF_ALARM_CONTROL_PANEL = "alarm_control_panel"
|
CONF_ALARM_CONTROL_PANEL = "alarm_control_panel"
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "blink",
|
"domain": "blink",
|
||||||
"name": "Blink",
|
"name": "Blink",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||||
"requirements": ["blinkpy==0.15.1"],
|
"requirements": ["blinkpy==0.16.3"],
|
||||||
"codeowners": ["@fronzbot"],
|
"codeowners": ["@fronzbot"],
|
||||||
"config_flow": true
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,13 @@
|
|||||||
"2fa": {
|
"2fa": {
|
||||||
"title": "Two-factor authentication",
|
"title": "Two-factor authentication",
|
||||||
"data": { "2fa": "Two-factor code" },
|
"data": { "2fa": "Two-factor code" },
|
||||||
"description": "Enter the pin sent to your email. If the email does not contain a pin, leave blank"
|
"description": "Enter the pin sent to your email"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
"""The Bond integration."""
|
"""The Bond integration."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||||
|
import logging
|
||||||
|
|
||||||
from bond import Bond
|
from aiohttp import ClientError, ClientTimeout
|
||||||
|
from bond_api import Bond
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .utils import BondHub
|
from .utils import BondHub
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
PLATFORMS = ["cover", "fan", "light", "switch"]
|
PLATFORMS = ["cover", "fan", "light", "switch"]
|
||||||
|
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: dict):
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
@ -25,11 +32,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
host = entry.data[CONF_HOST]
|
host = entry.data[CONF_HOST]
|
||||||
token = entry.data[CONF_ACCESS_TOKEN]
|
token = entry.data[CONF_ACCESS_TOKEN]
|
||||||
|
|
||||||
bond = Bond(bondIp=host, bondToken=token)
|
bond = Bond(host=host, token=token, timeout=ClientTimeout(total=_API_TIMEOUT))
|
||||||
hub = BondHub(bond)
|
hub = BondHub(bond)
|
||||||
await hass.async_add_executor_job(hub.setup)
|
try:
|
||||||
|
await hub.setup()
|
||||||
|
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||||
|
raise ConfigEntryNotReady from error
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = hub
|
hass.data[DOMAIN][entry.entry_id] = hub
|
||||||
|
|
||||||
|
if not entry.unique_id:
|
||||||
|
hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id)
|
||||||
|
|
||||||
device_registry = await dr.async_get_registry(hass)
|
device_registry = await dr.async_get_registry(hass)
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=entry.entry_id,
|
config_entry_id=entry.entry_id,
|
||||||
|
@ -1,42 +1,45 @@
|
|||||||
"""Config flow for Bond integration."""
|
"""Config flow for Bond integration."""
|
||||||
from json import JSONDecodeError
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from bond import Bond
|
from aiohttp import ClientConnectionError, ClientResponseError
|
||||||
from requests.exceptions import ConnectionError as RequestConnectionError
|
from bond_api import Bond
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, core, exceptions
|
from homeassistant import config_entries, exceptions
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
|
||||||
|
|
||||||
|
from .const import CONF_BOND_ID
|
||||||
from .const import DOMAIN # pylint:disable=unused-import
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema(
|
DATA_SCHEMA_USER = vol.Schema(
|
||||||
{vol.Required(CONF_HOST): str, vol.Required(CONF_ACCESS_TOKEN): str}
|
{vol.Required(CONF_HOST): str, vol.Required(CONF_ACCESS_TOKEN): str}
|
||||||
)
|
)
|
||||||
|
DATA_SCHEMA_DISCOVERY = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(hass: core.HomeAssistant, data):
|
async def _validate_input(data: Dict[str, Any]) -> str:
|
||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
def authenticate(bond_hub: Bond) -> bool:
|
|
||||||
try:
|
try:
|
||||||
bond_hub.getDeviceIds()
|
|
||||||
return True
|
|
||||||
except RequestConnectionError:
|
|
||||||
raise CannotConnect
|
|
||||||
except JSONDecodeError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN])
|
bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN])
|
||||||
|
version = await bond.version()
|
||||||
|
# call to non-version API is needed to validate authentication
|
||||||
|
await bond.devices()
|
||||||
|
except ClientConnectionError:
|
||||||
|
raise InputValidationError("cannot_connect")
|
||||||
|
except ClientResponseError as error:
|
||||||
|
if error.status == 401:
|
||||||
|
raise InputValidationError("invalid_auth")
|
||||||
|
raise InputValidationError("unknown")
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
raise InputValidationError("unknown")
|
||||||
|
|
||||||
if not await hass.async_add_executor_job(authenticate, bond):
|
# Return unique ID from the hub to be stored in the config entry.
|
||||||
raise InvalidAuth
|
return version["bondid"]
|
||||||
|
|
||||||
# Return info that you want to store in the config entry.
|
|
||||||
return {"title": data[CONF_HOST]}
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
@ -45,30 +48,73 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
VERSION = 1
|
VERSION = 1
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
_discovered: dict = None
|
||||||
"""Handle the initial step."""
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: Optional[Dict[str, Any]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Handle a flow initialized by zeroconf discovery."""
|
||||||
|
name: str = discovery_info[CONF_NAME]
|
||||||
|
host: str = discovery_info[CONF_HOST]
|
||||||
|
bond_id = name.partition(".")[0]
|
||||||
|
await self.async_set_unique_id(bond_id)
|
||||||
|
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||||
|
|
||||||
|
self._discovered = {
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_BOND_ID: bond_id,
|
||||||
|
}
|
||||||
|
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||||
|
self.context.update({"title_placeholders": self._discovered})
|
||||||
|
|
||||||
|
return await self.async_step_confirm()
|
||||||
|
|
||||||
|
async def async_step_confirm(
|
||||||
|
self, user_input: Dict[str, Any] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Handle confirmation flow for discovered bond hub."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
data = user_input.copy()
|
||||||
|
data[CONF_HOST] = self._discovered[CONF_HOST]
|
||||||
|
try:
|
||||||
|
return await self._try_create_entry(data)
|
||||||
|
except InputValidationError as error:
|
||||||
|
errors["base"] = error.base
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="confirm",
|
||||||
|
data_schema=DATA_SCHEMA_DISCOVERY,
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders=self._discovered,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: Dict[str, Any] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
info = await validate_input(self.hass, user_input)
|
return await self._try_create_entry(user_input)
|
||||||
except CannotConnect:
|
except InputValidationError as error:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = error.base
|
||||||
except InvalidAuth:
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Unexpected exception")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
else:
|
|
||||||
return self.async_create_entry(title=info["title"], data=user_input)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _try_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
class CannotConnect(exceptions.HomeAssistantError):
|
bond_id = await _validate_input(data)
|
||||||
"""Error to indicate we cannot connect."""
|
await self.async_set_unique_id(bond_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(title=bond_id, data=data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuth(exceptions.HomeAssistantError):
|
class InputValidationError(exceptions.HomeAssistantError):
|
||||||
"""Error to indicate there is invalid auth."""
|
"""Error to indicate we cannot proceed due to invalid input."""
|
||||||
|
|
||||||
|
def __init__(self, base: str):
|
||||||
|
"""Initialize with error base."""
|
||||||
|
super().__init__()
|
||||||
|
self.base = base
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
"""Constants for the Bond integration."""
|
"""Constants for the Bond integration."""
|
||||||
|
|
||||||
DOMAIN = "bond"
|
DOMAIN = "bond"
|
||||||
|
|
||||||
|
CONF_BOND_ID: str = "bond_id"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Support for Bond covers."""
|
"""Support for Bond covers."""
|
||||||
from typing import Any, Callable, List, Optional
|
from typing import Any, Callable, List, Optional
|
||||||
|
|
||||||
from bond import DeviceTypes
|
from bond_api import Action, DeviceType
|
||||||
|
|
||||||
from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity
|
from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -21,12 +21,10 @@ async def async_setup_entry(
|
|||||||
"""Set up Bond cover devices."""
|
"""Set up Bond cover devices."""
|
||||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
devices = await hass.async_add_executor_job(hub.get_bond_devices)
|
|
||||||
|
|
||||||
covers = [
|
covers = [
|
||||||
BondCover(hub, device)
|
BondCover(hub, device)
|
||||||
for device in devices
|
for device in hub.devices
|
||||||
if device.type == DeviceTypes.MOTORIZED_SHADES
|
if device.type == DeviceType.MOTORIZED_SHADES
|
||||||
]
|
]
|
||||||
|
|
||||||
async_add_entities(covers, True)
|
async_add_entities(covers, True)
|
||||||
@ -41,30 +39,28 @@ class BondCover(BondEntity, CoverEntity):
|
|||||||
|
|
||||||
self._closed: Optional[bool] = None
|
self._closed: Optional[bool] = None
|
||||||
|
|
||||||
|
def _apply_state(self, state: dict):
|
||||||
|
cover_open = state.get("open")
|
||||||
|
self._closed = True if cover_open == 0 else False if cover_open == 1 else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self) -> Optional[str]:
|
def device_class(self) -> Optional[str]:
|
||||||
"""Get device class."""
|
"""Get device class."""
|
||||||
return DEVICE_CLASS_SHADE
|
return DEVICE_CLASS_SHADE
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Fetch assumed state of the cover from the hub using API."""
|
|
||||||
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
|
|
||||||
cover_open = state.get("open")
|
|
||||||
self._closed = True if cover_open == 0 else False if cover_open == 1 else None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
"""Return if the cover is closed or not."""
|
"""Return if the cover is closed or not."""
|
||||||
return self._closed
|
return self._closed
|
||||||
|
|
||||||
def open_cover(self, **kwargs: Any) -> None:
|
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||||
"""Open the cover."""
|
"""Open the cover."""
|
||||||
self._hub.bond.open(self._device.device_id)
|
await self._hub.bond.action(self._device.device_id, Action.open())
|
||||||
|
|
||||||
def close_cover(self, **kwargs: Any) -> None:
|
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||||
"""Close cover."""
|
"""Close cover."""
|
||||||
self._hub.bond.close(self._device.device_id)
|
await self._hub.bond.action(self._device.device_id, Action.close())
|
||||||
|
|
||||||
def stop_cover(self, **kwargs):
|
async def async_stop_cover(self, **kwargs):
|
||||||
"""Hold cover."""
|
"""Hold cover."""
|
||||||
self._hub.bond.hold(self._device.device_id)
|
await self._hub.bond.action(self._device.device_id, Action.hold())
|
||||||
|
@ -1,24 +1,35 @@
|
|||||||
"""An abstract class common to all Bond entities."""
|
"""An abstract class common to all Bond entities."""
|
||||||
|
from abc import abstractmethod
|
||||||
|
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||||
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from aiohttp import ClientError
|
||||||
|
|
||||||
from homeassistant.const import ATTR_NAME
|
from homeassistant.const import ATTR_NAME
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .utils import BondDevice, BondHub
|
from .utils import BondDevice, BondHub
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
class BondEntity:
|
|
||||||
|
class BondEntity(Entity):
|
||||||
"""Generic Bond entity encapsulating common features of any Bond controlled device."""
|
"""Generic Bond entity encapsulating common features of any Bond controlled device."""
|
||||||
|
|
||||||
def __init__(self, hub: BondHub, device: BondDevice):
|
def __init__(self, hub: BondHub, device: BondDevice):
|
||||||
"""Initialize entity with API and device info."""
|
"""Initialize entity with API and device info."""
|
||||||
self._hub = hub
|
self._hub = hub
|
||||||
self._device = device
|
self._device = device
|
||||||
|
self._available = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> Optional[str]:
|
def unique_id(self) -> Optional[str]:
|
||||||
"""Get unique ID for the entity."""
|
"""Get unique ID for the entity."""
|
||||||
return self._device.device_id
|
hub_id = self._hub.bond_id
|
||||||
|
device_id = self._device.device_id
|
||||||
|
return f"{hub_id}_{device_id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> Optional[str]:
|
def name(self) -> Optional[str]:
|
||||||
@ -37,4 +48,30 @@ class BondEntity:
|
|||||||
@property
|
@property
|
||||||
def assumed_state(self) -> bool:
|
def assumed_state(self) -> bool:
|
||||||
"""Let HA know this entity relies on an assumed state tracked by Bond."""
|
"""Let HA know this entity relies on an assumed state tracked by Bond."""
|
||||||
return True
|
return self._hub.is_bridge and not self._device.trust_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Report availability of this entity based on last API call results."""
|
||||||
|
return self._available
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Fetch assumed state of the cover from the hub using API."""
|
||||||
|
try:
|
||||||
|
state: dict = await self._hub.bond.device_state(self._device.device_id)
|
||||||
|
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||||
|
if self._available:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Entity %s has become unavailable", self.entity_id, exc_info=error
|
||||||
|
)
|
||||||
|
self._available = False
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("Device state for %s is:\n%s", self.entity_id, state)
|
||||||
|
if not self._available:
|
||||||
|
_LOGGER.info("Entity %s has come back", self.entity_id)
|
||||||
|
self._available = True
|
||||||
|
self._apply_state(state)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _apply_state(self, state: dict):
|
||||||
|
raise NotImplementedError
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import math
|
import math
|
||||||
from typing import Any, Callable, List, Optional
|
from typing import Any, Callable, List, Optional
|
||||||
|
|
||||||
from bond import DeviceTypes, Directions
|
from bond_api import Action, DeviceType, Direction
|
||||||
|
|
||||||
from homeassistant.components.fan import (
|
from homeassistant.components.fan import (
|
||||||
DIRECTION_FORWARD,
|
DIRECTION_FORWARD,
|
||||||
@ -32,12 +32,8 @@ async def async_setup_entry(
|
|||||||
"""Set up Bond fan devices."""
|
"""Set up Bond fan devices."""
|
||||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
devices = await hass.async_add_executor_job(hub.get_bond_devices)
|
|
||||||
|
|
||||||
fans = [
|
fans = [
|
||||||
BondFan(hub, device)
|
BondFan(hub, device) for device in hub.devices if DeviceType.is_fan(device.type)
|
||||||
for device in devices
|
|
||||||
if device.type == DeviceTypes.CEILING_FAN
|
|
||||||
]
|
]
|
||||||
|
|
||||||
async_add_entities(fans, True)
|
async_add_entities(fans, True)
|
||||||
@ -54,6 +50,11 @@ class BondFan(BondEntity, FanEntity):
|
|||||||
self._speed: Optional[int] = None
|
self._speed: Optional[int] = None
|
||||||
self._direction: Optional[int] = None
|
self._direction: Optional[int] = None
|
||||||
|
|
||||||
|
def _apply_state(self, state: dict):
|
||||||
|
self._power = state.get("power")
|
||||||
|
self._speed = state.get("speed")
|
||||||
|
self._direction = state.get("direction")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
@ -74,7 +75,7 @@ class BondFan(BondEntity, FanEntity):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# map 1..max_speed Bond speed to 1..3 HA speed
|
# map 1..max_speed Bond speed to 1..3 HA speed
|
||||||
max_speed = self._device.props.get("max_speed", 3)
|
max_speed = max(self._device.props.get("max_speed", 3), self._speed)
|
||||||
ha_speed = math.ceil(self._speed * (len(self.speed_list) - 1) / max_speed)
|
ha_speed = math.ceil(self._speed * (len(self.speed_list) - 1) / max_speed)
|
||||||
return self.speed_list[ha_speed]
|
return self.speed_list[ha_speed]
|
||||||
|
|
||||||
@ -87,21 +88,14 @@ class BondFan(BondEntity, FanEntity):
|
|||||||
def current_direction(self) -> Optional[str]:
|
def current_direction(self) -> Optional[str]:
|
||||||
"""Return fan rotation direction."""
|
"""Return fan rotation direction."""
|
||||||
direction = None
|
direction = None
|
||||||
if self._direction == Directions.FORWARD:
|
if self._direction == Direction.FORWARD:
|
||||||
direction = DIRECTION_FORWARD
|
direction = DIRECTION_FORWARD
|
||||||
elif self._direction == Directions.REVERSE:
|
elif self._direction == Direction.REVERSE:
|
||||||
direction = DIRECTION_REVERSE
|
direction = DIRECTION_REVERSE
|
||||||
|
|
||||||
return direction
|
return direction
|
||||||
|
|
||||||
def update(self):
|
async def async_set_speed(self, speed: str) -> None:
|
||||||
"""Fetch assumed state of the fan from the hub using API."""
|
|
||||||
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
|
|
||||||
self._power = state.get("power")
|
|
||||||
self._speed = state.get("speed")
|
|
||||||
self._direction = state.get("direction")
|
|
||||||
|
|
||||||
def set_speed(self, speed: str) -> None:
|
|
||||||
"""Set the desired speed for the fan."""
|
"""Set the desired speed for the fan."""
|
||||||
max_speed = self._device.props.get("max_speed", 3)
|
max_speed = self._device.props.get("max_speed", 3)
|
||||||
if speed == SPEED_LOW:
|
if speed == SPEED_LOW:
|
||||||
@ -110,21 +104,27 @@ class BondFan(BondEntity, FanEntity):
|
|||||||
bond_speed = max_speed
|
bond_speed = max_speed
|
||||||
else:
|
else:
|
||||||
bond_speed = math.ceil(max_speed / 2)
|
bond_speed = math.ceil(max_speed / 2)
|
||||||
self._hub.bond.setSpeed(self._device.device_id, speed=bond_speed)
|
|
||||||
|
|
||||||
def turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
|
await self._hub.bond.action(
|
||||||
|
self._device.device_id, Action.set_speed(bond_speed)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
|
||||||
"""Turn on the fan."""
|
"""Turn on the fan."""
|
||||||
if speed is not None:
|
if speed is not None:
|
||||||
self.set_speed(speed)
|
await self.async_set_speed(speed)
|
||||||
self._hub.bond.turnOn(self._device.device_id)
|
else:
|
||||||
|
await self._hub.bond.action(self._device.device_id, Action.turn_on())
|
||||||
|
|
||||||
def turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the fan off."""
|
"""Turn the fan off."""
|
||||||
self._hub.bond.turnOff(self._device.device_id)
|
await self._hub.bond.action(self._device.device_id, Action.turn_off())
|
||||||
|
|
||||||
def set_direction(self, direction: str) -> None:
|
async def async_set_direction(self, direction: str):
|
||||||
"""Set fan rotation direction."""
|
"""Set fan rotation direction."""
|
||||||
bond_direction = (
|
bond_direction = (
|
||||||
Directions.REVERSE if direction == DIRECTION_REVERSE else Directions.FORWARD
|
Direction.REVERSE if direction == DIRECTION_REVERSE else Direction.FORWARD
|
||||||
|
)
|
||||||
|
await self._hub.bond.action(
|
||||||
|
self._device.device_id, Action.set_direction(bond_direction)
|
||||||
)
|
)
|
||||||
self._hub.bond.setDirection(self._device.device_id, bond_direction)
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Support for Bond lights."""
|
"""Support for Bond lights."""
|
||||||
from typing import Any, Callable, List, Optional
|
from typing import Any, Callable, List, Optional
|
||||||
|
|
||||||
from bond import DeviceTypes
|
from bond_api import Action, DeviceType
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
@ -26,21 +26,19 @@ async def async_setup_entry(
|
|||||||
"""Set up Bond light devices."""
|
"""Set up Bond light devices."""
|
||||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
devices = await hass.async_add_executor_job(hub.get_bond_devices)
|
lights: List[Entity] = [
|
||||||
|
|
||||||
lights = [
|
|
||||||
BondLight(hub, device)
|
BondLight(hub, device)
|
||||||
for device in devices
|
for device in hub.devices
|
||||||
if device.type == DeviceTypes.CEILING_FAN and device.supports_light()
|
if DeviceType.is_fan(device.type) and device.supports_light()
|
||||||
]
|
]
|
||||||
async_add_entities(lights, True)
|
|
||||||
|
|
||||||
fireplaces = [
|
fireplaces: List[Entity] = [
|
||||||
BondFireplace(hub, device)
|
BondFireplace(hub, device)
|
||||||
for device in devices
|
for device in hub.devices
|
||||||
if device.type == DeviceTypes.FIREPLACE
|
if DeviceType.is_fireplace(device.type)
|
||||||
]
|
]
|
||||||
async_add_entities(fireplaces, True)
|
|
||||||
|
async_add_entities(lights + fireplaces, True)
|
||||||
|
|
||||||
|
|
||||||
class BondLight(BondEntity, LightEntity):
|
class BondLight(BondEntity, LightEntity):
|
||||||
@ -49,26 +47,49 @@ class BondLight(BondEntity, LightEntity):
|
|||||||
def __init__(self, hub: BondHub, device: BondDevice):
|
def __init__(self, hub: BondHub, device: BondDevice):
|
||||||
"""Create HA entity representing Bond fan."""
|
"""Create HA entity representing Bond fan."""
|
||||||
super().__init__(hub, device)
|
super().__init__(hub, device)
|
||||||
|
self._brightness: Optional[int] = None
|
||||||
self._light: Optional[int] = None
|
self._light: Optional[int] = None
|
||||||
|
|
||||||
|
def _apply_state(self, state: dict):
|
||||||
|
self._light = state.get("light")
|
||||||
|
self._brightness = state.get("brightness")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> Optional[int]:
|
||||||
|
"""Flag supported features."""
|
||||||
|
features = 0
|
||||||
|
if self._device.supports_set_brightness():
|
||||||
|
features |= SUPPORT_BRIGHTNESS
|
||||||
|
|
||||||
|
return features
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return if light is currently on."""
|
"""Return if light is currently on."""
|
||||||
return self._light == 1
|
return self._light == 1
|
||||||
|
|
||||||
def update(self):
|
@property
|
||||||
"""Fetch assumed state of the light from the hub using API."""
|
def brightness(self) -> int:
|
||||||
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
|
"""Return the brightness of this light between 1..255."""
|
||||||
self._light = state.get("light")
|
brightness_value = (
|
||||||
|
round(self._brightness * 255 / 100) if self._brightness else None
|
||||||
|
)
|
||||||
|
return brightness_value
|
||||||
|
|
||||||
def turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn on the light."""
|
"""Turn on the light."""
|
||||||
self._hub.bond.turnLightOn(self._device.device_id)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
|
if brightness:
|
||||||
|
await self._hub.bond.action(
|
||||||
|
self._device.device_id,
|
||||||
|
Action.set_brightness(round((brightness * 100) / 255)),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self._hub.bond.action(self._device.device_id, Action.turn_light_on())
|
||||||
|
|
||||||
def turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn off the light."""
|
"""Turn off the light."""
|
||||||
self._hub.bond.turnLightOff(self._device.device_id)
|
await self._hub.bond.action(self._device.device_id, Action.turn_light_off())
|
||||||
|
|
||||||
|
|
||||||
class BondFireplace(BondEntity, LightEntity):
|
class BondFireplace(BondEntity, LightEntity):
|
||||||
@ -82,6 +103,10 @@ class BondFireplace(BondEntity, LightEntity):
|
|||||||
# Bond flame level, 0-100
|
# Bond flame level, 0-100
|
||||||
self._flame: Optional[int] = None
|
self._flame: Optional[int] = None
|
||||||
|
|
||||||
|
def _apply_state(self, state: dict):
|
||||||
|
self._power = state.get("power")
|
||||||
|
self._flame = state.get("flame")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> Optional[int]:
|
def supported_features(self) -> Optional[int]:
|
||||||
"""Flag brightness as supported feature to represent flame level."""
|
"""Flag brightness as supported feature to represent flame level."""
|
||||||
@ -92,18 +117,18 @@ class BondFireplace(BondEntity, LightEntity):
|
|||||||
"""Return True if power is on."""
|
"""Return True if power is on."""
|
||||||
return self._power == 1
|
return self._power == 1
|
||||||
|
|
||||||
def turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the fireplace on."""
|
"""Turn the fireplace on."""
|
||||||
self._hub.bond.turnOn(self._device.device_id)
|
|
||||||
|
|
||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
if brightness:
|
if brightness:
|
||||||
flame = round((brightness * 100) / 255)
|
flame = round((brightness * 100) / 255)
|
||||||
self._hub.bond.setFlame(self._device.device_id, flame)
|
await self._hub.bond.action(self._device.device_id, Action.set_flame(flame))
|
||||||
|
else:
|
||||||
|
await self._hub.bond.action(self._device.device_id, Action.turn_on())
|
||||||
|
|
||||||
def turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the fireplace off."""
|
"""Turn the fireplace off."""
|
||||||
self._hub.bond.turnOff(self._device.device_id)
|
await self._hub.bond.action(self._device.device_id, Action.turn_off())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
@ -114,9 +139,3 @@ class BondFireplace(BondEntity, LightEntity):
|
|||||||
def icon(self) -> Optional[str]:
|
def icon(self) -> Optional[str]:
|
||||||
"""Show fireplace icon for the entity."""
|
"""Show fireplace icon for the entity."""
|
||||||
return "mdi:fireplace" if self._power == 1 else "mdi:fireplace-off"
|
return "mdi:fireplace" if self._power == 1 else "mdi:fireplace-off"
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Fetch assumed state of the device from the hub using API."""
|
|
||||||
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
|
|
||||||
self._power = state.get("power")
|
|
||||||
self._flame = state.get("flame")
|
|
||||||
|
@ -3,10 +3,8 @@
|
|||||||
"name": "Bond",
|
"name": "Bond",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/bond",
|
"documentation": "https://www.home-assistant.io/integrations/bond",
|
||||||
"requirements": [
|
"requirements": ["bond-api==0.1.8"],
|
||||||
"bond-home==0.0.9"
|
"zeroconf": ["_bond._tcp.local."],
|
||||||
],
|
"codeowners": ["@prystupa"],
|
||||||
"codeowners": [
|
"quality_scale": "platinum"
|
||||||
"@prystupa"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"flow_title": "Bond: {bond_id} ({host})",
|
||||||
"step": {
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "Do you want to set up {bond_id}?",
|
||||||
|
"data": {
|
||||||
|
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
@ -12,6 +19,9 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"""Support for Bond generic devices."""
|
"""Support for Bond generic devices."""
|
||||||
from typing import Any, Callable, List, Optional
|
from typing import Any, Callable, List, Optional
|
||||||
|
|
||||||
from bond import DeviceTypes
|
from bond_api import Action, DeviceType
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from ..switch import SwitchEntity
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import BondEntity
|
from .entity import BondEntity
|
||||||
from .utils import BondDevice, BondHub
|
from .utils import BondDevice, BondHub
|
||||||
@ -21,12 +21,10 @@ async def async_setup_entry(
|
|||||||
"""Set up Bond generic devices."""
|
"""Set up Bond generic devices."""
|
||||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
devices = await hass.async_add_executor_job(hub.get_bond_devices)
|
|
||||||
|
|
||||||
switches = [
|
switches = [
|
||||||
BondSwitch(hub, device)
|
BondSwitch(hub, device)
|
||||||
for device in devices
|
for device in hub.devices
|
||||||
if device.type == DeviceTypes.GENERIC_DEVICE
|
if DeviceType.is_generic(device.type)
|
||||||
]
|
]
|
||||||
|
|
||||||
async_add_entities(switches, True)
|
async_add_entities(switches, True)
|
||||||
@ -41,20 +39,18 @@ class BondSwitch(BondEntity, SwitchEntity):
|
|||||||
|
|
||||||
self._power: Optional[bool] = None
|
self._power: Optional[bool] = None
|
||||||
|
|
||||||
|
def _apply_state(self, state: dict):
|
||||||
|
self._power = state.get("power")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if power is on."""
|
"""Return True if power is on."""
|
||||||
return self._power == 1
|
return self._power == 1
|
||||||
|
|
||||||
def turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
self._hub.bond.turnOn(self._device.device_id)
|
await self._hub.bond.action(self._device.device_id, Action.turn_on())
|
||||||
|
|
||||||
def turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
self._hub.bond.turnOff(self._device.device_id)
|
await self._hub.bond.action(self._device.device_id, Action.turn_off())
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Fetch assumed state of the device from the hub using API."""
|
|
||||||
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
|
|
||||||
self._power = state.get("power")
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "El dispositiu ja est\u00e0 configurat"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Ha fallat la connexi\u00f3",
|
"cannot_connect": "Ha fallat la connexi\u00f3",
|
||||||
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
|
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
|
||||||
|
16
homeassistant/components/bond/translations/cs.json
Normal file
16
homeassistant/components/bond/translations/cs.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakofigurovan\u00e9"
|
||||||
|
},
|
||||||
|
"flow_title": "Bond: {bond_id} ({host})",
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "P\u0159\u00edstupov\u00fd token"
|
||||||
|
},
|
||||||
|
"description": "Chcete nastavit {bond_id} ?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,21 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Failed to connect",
|
"cannot_connect": "Failed to connect",
|
||||||
"invalid_auth": "Invalid authentication",
|
"invalid_auth": "Invalid authentication",
|
||||||
"unknown": "Unexpected error"
|
"unknown": "Unexpected error"
|
||||||
},
|
},
|
||||||
|
"flow_title": "Bond: {bond_id} ({host})",
|
||||||
"step": {
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Access Token"
|
||||||
|
},
|
||||||
|
"description": "Do you want to set up {bond_id}?"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"access_token": "Access Token",
|
"access_token": "Access Token",
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "El dispositivo ya est\u00e1 configurado"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "No se pudo conectar",
|
"cannot_connect": "No se pudo conectar",
|
||||||
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
|
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
|
||||||
"unknown": "Error inesperado"
|
"unknown": "Error inesperado"
|
||||||
},
|
},
|
||||||
|
"flow_title": "Bond: {bond_id} ({host})",
|
||||||
"step": {
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Token de acceso"
|
||||||
|
},
|
||||||
|
"description": "\u00bfQuieres configurar {bond_id}?"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"access_token": "Token de acceso",
|
"access_token": "Token de acceso",
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Impossibile connettersi",
|
"cannot_connect": "Impossibile connettersi",
|
||||||
"invalid_auth": "Autenticazione non valida",
|
"invalid_auth": "Autenticazione non valida",
|
||||||
"unknown": "Errore imprevisto"
|
"unknown": "Errore imprevisto"
|
||||||
},
|
},
|
||||||
|
"flow_title": "Bond: {bond_id} ({host})",
|
||||||
"step": {
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Token di accesso"
|
||||||
|
},
|
||||||
|
"description": "Vuoi configurare {bond_id}?"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"access_token": "Token di accesso",
|
"access_token": "Token di accesso",
|
||||||
|
17
homeassistant/components/bond/translations/lb.json
Normal file
17
homeassistant/components/bond/translations/lb.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Feeler beim verbannen",
|
||||||
|
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
|
||||||
|
"unknown": "Onerwaarte Feeler"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Acc\u00e8s jeton",
|
||||||
|
"host": "Host"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
homeassistant/components/bond/translations/no.json
Normal file
27
homeassistant/components/bond/translations/no.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Enheten er allerede konfigurert"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Tilkobling mislyktes.",
|
||||||
|
"invalid_auth": "Ugyldig godkjenning",
|
||||||
|
"unknown": "Uventet feil"
|
||||||
|
},
|
||||||
|
"flow_title": "Obligasjon: {bond_id} ( {host} )",
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Tilgangstoken"
|
||||||
|
},
|
||||||
|
"description": "Vil du konfigurere {bond_id}?"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Tilgangstoken",
|
||||||
|
"host": "Vert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
homeassistant/components/bond/translations/pl.json
Normal file
17
homeassistant/components/bond/translations/pl.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
|
||||||
|
"invalid_auth": "Niepoprawne uwierzytelnienie.",
|
||||||
|
"unknown": "Nieoczekiwany b\u0142\u0105d."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Token dost\u0119pu",
|
||||||
|
"host": "Nazwa hosta lub adres IP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,21 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
|
"cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
|
||||||
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
|
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
|
||||||
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
|
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
|
||||||
},
|
},
|
||||||
|
"flow_title": "Bond {bond_id} ({host})",
|
||||||
"step": {
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430"
|
||||||
|
},
|
||||||
|
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {bond_id}?"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430",
|
"access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430",
|
||||||
|
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