mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 16:57:19 +00:00
Merge pull request #47319 from home-assistant/rc
This commit is contained in:
commit
e9785fcd3d
71
.coveragerc
71
.coveragerc
@ -23,6 +23,8 @@ omit =
|
||||
homeassistant/components/adguard/sensor.py
|
||||
homeassistant/components/adguard/switch.py
|
||||
homeassistant/components/ads/*
|
||||
homeassistant/components/aemet/abstract_aemet_sensor.py
|
||||
homeassistant/components/aemet/weather_update_coordinator.py
|
||||
homeassistant/components/aftership/sensor.py
|
||||
homeassistant/components/agent_dvr/__init__.py
|
||||
homeassistant/components/agent_dvr/alarm_control_panel.py
|
||||
@ -67,11 +69,14 @@ omit =
|
||||
homeassistant/components/arwn/sensor.py
|
||||
homeassistant/components/asterisk_cdr/mailbox.py
|
||||
homeassistant/components/asterisk_mbox/*
|
||||
homeassistant/components/asuswrt/__init__.py
|
||||
homeassistant/components/asuswrt/router.py
|
||||
homeassistant/components/aten_pe/*
|
||||
homeassistant/components/atome/*
|
||||
homeassistant/components/aurora/__init__.py
|
||||
homeassistant/components/aurora/binary_sensor.py
|
||||
homeassistant/components/aurora/const.py
|
||||
homeassistant/components/aurora/sensor.py
|
||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||
homeassistant/components/avea/light.py
|
||||
homeassistant/components/avion/light.py
|
||||
@ -140,6 +145,7 @@ omit =
|
||||
homeassistant/components/clickatell/notify.py
|
||||
homeassistant/components/clicksend/notify.py
|
||||
homeassistant/components/clicksend_tts/notify.py
|
||||
homeassistant/components/climacell/weather.py
|
||||
homeassistant/components/cmus/media_player.py
|
||||
homeassistant/components/co2signal/*
|
||||
homeassistant/components/coinbase/*
|
||||
@ -156,7 +162,6 @@ omit =
|
||||
homeassistant/components/coolmaster/const.py
|
||||
homeassistant/components/cppm_tracker/device_tracker.py
|
||||
homeassistant/components/cpuspeed/sensor.py
|
||||
homeassistant/components/crimereports/sensor.py
|
||||
homeassistant/components/cups/sensor.py
|
||||
homeassistant/components/currencylayer/sensor.py
|
||||
homeassistant/components/daikin/*
|
||||
@ -172,7 +177,6 @@ omit =
|
||||
homeassistant/components/denonavr/media_player.py
|
||||
homeassistant/components/denonavr/receiver.py
|
||||
homeassistant/components/deutsche_bahn/sensor.py
|
||||
homeassistant/components/devolo_home_control/__init__.py
|
||||
homeassistant/components/devolo_home_control/binary_sensor.py
|
||||
homeassistant/components/devolo_home_control/climate.py
|
||||
homeassistant/components/devolo_home_control/const.py
|
||||
@ -268,6 +272,8 @@ omit =
|
||||
homeassistant/components/evohome/*
|
||||
homeassistant/components/ezviz/*
|
||||
homeassistant/components/familyhub/camera.py
|
||||
homeassistant/components/faa_delays/__init__.py
|
||||
homeassistant/components/faa_delays/binary_sensor.py
|
||||
homeassistant/components/fastdotcom/*
|
||||
homeassistant/components/ffmpeg/camera.py
|
||||
homeassistant/components/fibaro/*
|
||||
@ -358,7 +364,9 @@ omit =
|
||||
homeassistant/components/guardian/sensor.py
|
||||
homeassistant/components/guardian/switch.py
|
||||
homeassistant/components/guardian/util.py
|
||||
homeassistant/components/habitica/*
|
||||
homeassistant/components/habitica/__init__.py
|
||||
homeassistant/components/habitica/const.py
|
||||
homeassistant/components/habitica/sensor.py
|
||||
homeassistant/components/hangouts/*
|
||||
homeassistant/components/hangouts/__init__.py
|
||||
homeassistant/components/hangouts/const.py
|
||||
@ -463,7 +471,11 @@ omit =
|
||||
homeassistant/components/kaiterra/*
|
||||
homeassistant/components/kankun/switch.py
|
||||
homeassistant/components/keba/*
|
||||
homeassistant/components/keenetic_ndms2/__init__.py
|
||||
homeassistant/components/keenetic_ndms2/binary_sensor.py
|
||||
homeassistant/components/keenetic_ndms2/const.py
|
||||
homeassistant/components/keenetic_ndms2/device_tracker.py
|
||||
homeassistant/components/keenetic_ndms2/router.py
|
||||
homeassistant/components/kef/*
|
||||
homeassistant/components/keyboard/*
|
||||
homeassistant/components/keyboard_remote/*
|
||||
@ -519,6 +531,10 @@ omit =
|
||||
homeassistant/components/lutron_caseta/switch.py
|
||||
homeassistant/components/lw12wifi/light.py
|
||||
homeassistant/components/lyft/sensor.py
|
||||
homeassistant/components/lyric/__init__.py
|
||||
homeassistant/components/lyric/api.py
|
||||
homeassistant/components/lyric/climate.py
|
||||
homeassistant/components/lyric/sensor.py
|
||||
homeassistant/components/magicseaweed/sensor.py
|
||||
homeassistant/components/mailgun/notify.py
|
||||
homeassistant/components/map/*
|
||||
@ -560,6 +576,7 @@ omit =
|
||||
homeassistant/components/mochad/*
|
||||
homeassistant/components/modbus/climate.py
|
||||
homeassistant/components/modbus/cover.py
|
||||
homeassistant/components/modbus/modbus.py
|
||||
homeassistant/components/modbus/switch.py
|
||||
homeassistant/components/modbus/sensor.py
|
||||
homeassistant/components/modem_callerid/sensor.py
|
||||
@ -571,11 +588,27 @@ omit =
|
||||
homeassistant/components/mpd/media_player.py
|
||||
homeassistant/components/mqtt_room/sensor.py
|
||||
homeassistant/components/msteams/notify.py
|
||||
homeassistant/components/mullvad/__init__.py
|
||||
homeassistant/components/mullvad/binary_sensor.py
|
||||
homeassistant/components/nest/const.py
|
||||
homeassistant/components/mvglive/sensor.py
|
||||
homeassistant/components/mychevy/*
|
||||
homeassistant/components/mycroft/*
|
||||
homeassistant/components/mycroft/notify.py
|
||||
homeassistant/components/mysensors/*
|
||||
homeassistant/components/mysensors/__init__.py
|
||||
homeassistant/components/mysensors/binary_sensor.py
|
||||
homeassistant/components/mysensors/climate.py
|
||||
homeassistant/components/mysensors/const.py
|
||||
homeassistant/components/mysensors/cover.py
|
||||
homeassistant/components/mysensors/device.py
|
||||
homeassistant/components/mysensors/device_tracker.py
|
||||
homeassistant/components/mysensors/gateway.py
|
||||
homeassistant/components/mysensors/handler.py
|
||||
homeassistant/components/mysensors/helpers.py
|
||||
homeassistant/components/mysensors/light.py
|
||||
homeassistant/components/mysensors/notify.py
|
||||
homeassistant/components/mysensors/sensor.py
|
||||
homeassistant/components/mysensors/switch.py
|
||||
homeassistant/components/mystrom/binary_sensor.py
|
||||
homeassistant/components/mystrom/light.py
|
||||
homeassistant/components/mystrom/switch.py
|
||||
@ -621,7 +654,8 @@ omit =
|
||||
homeassistant/components/norway_air/air_quality.py
|
||||
homeassistant/components/notify_events/notify.py
|
||||
homeassistant/components/nsw_fuel_station/sensor.py
|
||||
homeassistant/components/nuimo_controller/*
|
||||
homeassistant/components/nuki/__init__.py
|
||||
homeassistant/components/nuki/const.py
|
||||
homeassistant/components/nuki/lock.py
|
||||
homeassistant/components/nut/sensor.py
|
||||
homeassistant/components/nx584/alarm_control_panel.py
|
||||
@ -687,6 +721,7 @@ omit =
|
||||
homeassistant/components/pandora/media_player.py
|
||||
homeassistant/components/pcal9535a/*
|
||||
homeassistant/components/pencom/switch.py
|
||||
homeassistant/components/philips_js/__init__.py
|
||||
homeassistant/components/philips_js/media_player.py
|
||||
homeassistant/components/pi_hole/sensor.py
|
||||
homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py
|
||||
@ -699,7 +734,11 @@ omit =
|
||||
homeassistant/components/ping/device_tracker.py
|
||||
homeassistant/components/pioneer/media_player.py
|
||||
homeassistant/components/pjlink/media_player.py
|
||||
homeassistant/components/plaato/*
|
||||
homeassistant/components/plaato/__init__.py
|
||||
homeassistant/components/plaato/binary_sensor.py
|
||||
homeassistant/components/plaato/const.py
|
||||
homeassistant/components/plaato/entity.py
|
||||
homeassistant/components/plaato/sensor.py
|
||||
homeassistant/components/plex/media_player.py
|
||||
homeassistant/components/plum_lightpad/light.py
|
||||
homeassistant/components/pocketcasts/sensor.py
|
||||
@ -754,6 +793,8 @@ omit =
|
||||
homeassistant/components/rest/switch.py
|
||||
homeassistant/components/ring/camera.py
|
||||
homeassistant/components/ripple/sensor.py
|
||||
homeassistant/components/rituals_perfume_genie/switch.py
|
||||
homeassistant/components/rituals_perfume_genie/__init__.py
|
||||
homeassistant/components/rocketchat/notify.py
|
||||
homeassistant/components/roomba/binary_sensor.py
|
||||
homeassistant/components/roomba/braava.py
|
||||
@ -876,7 +917,6 @@ omit =
|
||||
homeassistant/components/switcher_kis/switch.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthru/sensor.py
|
||||
homeassistant/components/synology/camera.py
|
||||
homeassistant/components/synology_chat/notify.py
|
||||
homeassistant/components/synology_dsm/__init__.py
|
||||
homeassistant/components/synology_dsm/binary_sensor.py
|
||||
@ -944,7 +984,10 @@ omit =
|
||||
homeassistant/components/toon/sensor.py
|
||||
homeassistant/components/toon/switch.py
|
||||
homeassistant/components/torque/sensor.py
|
||||
homeassistant/components/totalconnect/*
|
||||
homeassistant/components/totalconnect/__init__.py
|
||||
homeassistant/components/totalconnect/alarm_control_panel.py
|
||||
homeassistant/components/totalconnect/binary_sensor.py
|
||||
homeassistant/components/totalconnect/const.py
|
||||
homeassistant/components/touchline/climate.py
|
||||
homeassistant/components/tplink/common.py
|
||||
homeassistant/components/tplink/switch.py
|
||||
@ -963,7 +1006,14 @@ omit =
|
||||
homeassistant/components/transmission/const.py
|
||||
homeassistant/components/transmission/errors.py
|
||||
homeassistant/components/travisci/sensor.py
|
||||
homeassistant/components/tuya/*
|
||||
homeassistant/components/tuya/__init__.py
|
||||
homeassistant/components/tuya/climate.py
|
||||
homeassistant/components/tuya/const.py
|
||||
homeassistant/components/tuya/cover.py
|
||||
homeassistant/components/tuya/fan.py
|
||||
homeassistant/components/tuya/light.py
|
||||
homeassistant/components/tuya/scene.py
|
||||
homeassistant/components/tuya/switch.py
|
||||
homeassistant/components/twentemilieu/const.py
|
||||
homeassistant/components/twentemilieu/sensor.py
|
||||
homeassistant/components/twilio_call/notify.py
|
||||
@ -1001,6 +1051,7 @@ omit =
|
||||
homeassistant/components/vesync/common.py
|
||||
homeassistant/components/vesync/const.py
|
||||
homeassistant/components/vesync/fan.py
|
||||
homeassistant/components/vesync/light.py
|
||||
homeassistant/components/vesync/switch.py
|
||||
homeassistant/components/viaggiatreno/sensor.py
|
||||
homeassistant/components/vicare/*
|
||||
@ -1043,7 +1094,6 @@ omit =
|
||||
homeassistant/components/xbox/sensor.py
|
||||
homeassistant/components/xbox_live/sensor.py
|
||||
homeassistant/components/xeoma/camera.py
|
||||
homeassistant/components/xfinity/device_tracker.py
|
||||
homeassistant/components/xiaomi/camera.py
|
||||
homeassistant/components/xiaomi_aqara/__init__.py
|
||||
homeassistant/components/xiaomi_aqara/binary_sensor.py
|
||||
@ -1056,6 +1106,7 @@ omit =
|
||||
homeassistant/components/xiaomi_miio/__init__.py
|
||||
homeassistant/components/xiaomi_miio/air_quality.py
|
||||
homeassistant/components/xiaomi_miio/alarm_control_panel.py
|
||||
homeassistant/components/xiaomi_miio/device.py
|
||||
homeassistant/components/xiaomi_miio/device_tracker.py
|
||||
homeassistant/components/xiaomi_miio/fan.py
|
||||
homeassistant/components/xiaomi_miio/gateway.py
|
||||
|
@ -2,13 +2,14 @@
|
||||
"name": "Home Assistant Dev",
|
||||
"context": "..",
|
||||
"dockerFile": "../Dockerfile.dev",
|
||||
"postCreateCommand": "mkdir -p config && pip3 install -e .",
|
||||
"postCreateCommand": "script/setup",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": { "DEVCONTAINER": "1" },
|
||||
"appPort": 8123,
|
||||
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
|
||||
"extensions": [
|
||||
"ms-python.vscode-pylance",
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"ms-azure-devops.azure-pipelines",
|
||||
"redhat.vscode-yaml",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
@ -19,12 +20,11 @@
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"terminal.integrated.shell.linux": "/usr/bin/zsh",
|
||||
"yaml.customTags": [
|
||||
"!input scalar",
|
||||
"!secret scalar",
|
||||
|
53
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
53
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@ -1,53 +0,0 @@
|
||||
---
|
||||
name: Report a bug with Home Assistant Core
|
||||
about: Report an issue with Home Assistant Core
|
||||
---
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template, please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/core/releases
|
||||
- Do not report issues for integrations if you are using custom components or integrations.
|
||||
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||
-->
|
||||
## The problem
|
||||
<!--
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us what you were trying to do and what happened.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Provide details about the versions you are using, which helps us to reproduce
|
||||
and find the issue quicker. Version information is found in the
|
||||
Home Assistant frontend: Configuration -> Info.
|
||||
-->
|
||||
|
||||
- Home Assistant Core release with the issue:
|
||||
- Last working Home Assistant Core release (if known):
|
||||
- Operating environment (OS/Container/Supervised/Core):
|
||||
- Integration causing this issue:
|
||||
- Link to integration documentation on our website:
|
||||
|
||||
## Problem-relevant `configuration.yaml`
|
||||
<!--
|
||||
An example configuration that caused the problem for you. Fill this out even
|
||||
if it seems unimportant to you. Please be sure to remove personal information
|
||||
like passwords, private URLs and other credentials.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Traceback/Error logs
|
||||
<!--
|
||||
If you come across any trace or error logs, please provide them.
|
||||
-->
|
||||
|
||||
```txt
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
102
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
102
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
name: Report an issue with Home Assistant Core
|
||||
about: Report an issue with Home Assistant Core.
|
||||
title: ""
|
||||
issue_body: true
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This issue form is for reporting bugs only!
|
||||
|
||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
||||
|
||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: The problem
|
||||
description: >-
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us what you were trying to do and what happened.
|
||||
|
||||
Provide a clear and concise description of what the problem is.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Environment
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What is version of Home Assistant Core has the issue?
|
||||
placeholder: core-
|
||||
description: >
|
||||
Can be found in the Configuration panel -> Info.
|
||||
- type: input
|
||||
attributes:
|
||||
label: What was the last working version of Home Assistant Core?
|
||||
placeholder: core-
|
||||
description: >
|
||||
If known, otherwise leave blank.
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What type of installation are you running?
|
||||
description: >
|
||||
If you don't know, you can find it in: Configuration panel -> Info.
|
||||
options:
|
||||
- Home Assistant OS
|
||||
- Home Assistant Container
|
||||
- Home Assistant Supervised
|
||||
- Home Assistant Core
|
||||
- type: input
|
||||
attributes:
|
||||
label: Integration causing the issue
|
||||
description: >
|
||||
The name of the integration, for example, Automation or Philips Hue.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Link to integration documentation on our website
|
||||
placeholder: "https://www.home-assistant.io/integrations/..."
|
||||
description: |
|
||||
Providing a link [to the documentation][docs] help us categorizing the
|
||||
issue, while providing a useful reference at the same time.
|
||||
|
||||
[docs]: https://www.home-assistant.io/integrations
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Details
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Example YAML snippet
|
||||
description: |
|
||||
If this issue has an example piece of YAML that can help reproducing this problem, please provide.
|
||||
This can be an piece of YAML from, e.g., an automation, script, scene or configuration.
|
||||
value: |
|
||||
```yaml
|
||||
# Put your YAML below this line
|
||||
|
||||
```
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything in the logs that might be useful for us?
|
||||
description: For example, error message, or stack traces.
|
||||
value: |
|
||||
```txt
|
||||
# Put your logs below this line
|
||||
|
||||
```
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Additional information
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
If you have any additional information for us, use the field below.
|
||||
Please note, you can attach screenshots or screen recordings here, by
|
||||
dragging and dropping files in the field below.
|
54
.github/workflows/ci.yaml
vendored
54
.github/workflows/ci.yaml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -52,7 +52,7 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -79,7 +79,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -95,7 +95,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -124,7 +124,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -140,7 +140,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -169,7 +169,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -185,7 +185,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -236,7 +236,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -252,7 +252,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -284,7 +284,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -300,7 +300,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -332,7 +332,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -348,7 +348,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -377,7 +377,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -393,7 +393,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -425,7 +425,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -441,7 +441,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -481,7 +481,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -497,7 +497,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@ -528,7 +528,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -560,7 +560,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -591,7 +591,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -630,7 +630,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -664,7 +664,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -700,7 +700,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -760,7 +760,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
|
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
# - No PRs marked as no-stale
|
||||
# - No issues marked as no-stale or help-wanted
|
||||
- name: 90 days stale issues & PRs policy
|
||||
uses: actions/stale@v3.0.15
|
||||
uses: actions/stale@v3.0.17
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
@ -53,7 +53,7 @@ jobs:
|
||||
# - No PRs marked as no-stale or new-integrations
|
||||
# - No issues (-1)
|
||||
- name: 30 days stale PRs policy
|
||||
uses: actions/stale@v3.0.15
|
||||
uses: actions/stale@v3.0.17
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
# - No Issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: Needs more information stale issues policy
|
||||
uses: actions/stale@v3.0.15
|
||||
uses: actions/stale@v3.0.17
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "needs-more-information"
|
||||
|
@ -59,8 +59,8 @@ repos:
|
||||
rev: v1.24.2
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/prettier/prettier
|
||||
rev: 2.0.4
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.2.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
stages: [manual]
|
||||
@ -90,4 +90,4 @@ repos:
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc)$
|
||||
files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|homeassistant/.+/services\.yaml)$
|
||||
|
31
CODEOWNERS
31
CODEOWNERS
@ -24,6 +24,7 @@ homeassistant/components/accuweather/* @bieniu
|
||||
homeassistant/components/acmeda/* @atmurray
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/advantage_air/* @Bre77
|
||||
homeassistant/components/aemet/* @noltari
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
homeassistant/components/airly/* @bieniu
|
||||
homeassistant/components/airnow/* @asymworks
|
||||
@ -82,6 +83,7 @@ homeassistant/components/circuit/* @braam
|
||||
homeassistant/components/cisco_ios/* @fbradyirl
|
||||
homeassistant/components/cisco_mobility_express/* @fbradyirl
|
||||
homeassistant/components/cisco_webex_teams/* @fbradyirl
|
||||
homeassistant/components/climacell/* @raman325
|
||||
homeassistant/components/cloud/* @home-assistant/cloud
|
||||
homeassistant/components/cloudflare/* @ludeeus @ctalkington
|
||||
homeassistant/components/color_extractor/* @GenericStudent
|
||||
@ -144,6 +146,7 @@ homeassistant/components/esphome/* @OttoWinter
|
||||
homeassistant/components/essent/* @TheLastProject
|
||||
homeassistant/components/evohome/* @zxdavb
|
||||
homeassistant/components/ezviz/* @baqs
|
||||
homeassistant/components/faa_delays/* @ntilley905
|
||||
homeassistant/components/fastdotcom/* @rohankapoorcom
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
@ -158,7 +161,7 @@ homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/forked_daapd/* @uvjustin
|
||||
homeassistant/components/fortios/* @kimfrellsen
|
||||
homeassistant/components/foscam/* @skgsergio
|
||||
homeassistant/components/freebox/* @snoof85 @Quentame
|
||||
homeassistant/components/freebox/* @hacf-fr @Quentame
|
||||
homeassistant/components/fronius/* @nielstron
|
||||
homeassistant/components/frontend/* @home-assistant/frontend
|
||||
homeassistant/components/garmin_connect/* @cyberjunky
|
||||
@ -181,6 +184,7 @@ homeassistant/components/griddy/* @bdraco
|
||||
homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/growatt_server/* @indykoning
|
||||
homeassistant/components/guardian/* @bachya
|
||||
homeassistant/components/habitica/* @ASMfreaK @leikoilja
|
||||
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey
|
||||
homeassistant/components/hassio/* @home-assistant/supervisor
|
||||
homeassistant/components/hdmi_cec/* @newAM
|
||||
@ -241,6 +245,7 @@ homeassistant/components/keba/* @dannerph
|
||||
homeassistant/components/keenetic_ndms2/* @foxel
|
||||
homeassistant/components/kef/* @basnijholt
|
||||
homeassistant/components/keyboard_remote/* @bendavid
|
||||
homeassistant/components/kmtronic/* @dgomes
|
||||
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
|
||||
homeassistant/components/kodi/* @OnFreund @cgtobi
|
||||
homeassistant/components/konnected/* @heythisisnate @kit-klein
|
||||
@ -250,6 +255,8 @@ homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
homeassistant/components/life360/* @pnbruckner
|
||||
homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/litejet/* @joncar
|
||||
homeassistant/components/litterrobot/* @natekspencer
|
||||
homeassistant/components/local_ip/* @issacg
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
homeassistant/components/logi_circle/* @evanjd
|
||||
@ -260,8 +267,10 @@ homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/lupusec/* @majuss
|
||||
homeassistant/components/lutron/* @JonGilmore
|
||||
homeassistant/components/lutron_caseta/* @swails @bdraco
|
||||
homeassistant/components/lyric/* @timmo001
|
||||
homeassistant/components/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mazda/* @bdr99
|
||||
homeassistant/components/mcp23017/* @jardiamj
|
||||
homeassistant/components/media_source/* @hunterjm
|
||||
homeassistant/components/mediaroom/* @dgomes
|
||||
@ -283,10 +292,12 @@ homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||
homeassistant/components/moon/* @fabaff
|
||||
homeassistant/components/motion_blinds/* @starkillerOG
|
||||
homeassistant/components/mpd/* @fabaff
|
||||
homeassistant/components/mqtt/* @home-assistant/core @emontnemery
|
||||
homeassistant/components/mqtt/* @emontnemery
|
||||
homeassistant/components/msteams/* @peroyvind
|
||||
homeassistant/components/mullvad/* @meichthys
|
||||
homeassistant/components/my/* @home-assistant/core
|
||||
homeassistant/components/myq/* @bdraco
|
||||
homeassistant/components/mysensors/* @MartinHjelmare
|
||||
homeassistant/components/mysensors/* @MartinHjelmare @functionpointer
|
||||
homeassistant/components/mystrom/* @fabaff
|
||||
homeassistant/components/neato/* @dshokouhi @Santobert
|
||||
homeassistant/components/nederlandse_spoorwegen/* @YarmoM
|
||||
@ -310,7 +321,7 @@ homeassistant/components/notion/* @bachya
|
||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||
homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
|
||||
homeassistant/components/nuheat/* @bdraco
|
||||
homeassistant/components/nuki/* @pschmitt @pvizeli
|
||||
homeassistant/components/nuki/* @pschmitt @pvizeli @pree
|
||||
homeassistant/components/numato/* @clssn
|
||||
homeassistant/components/number/* @home-assistant/core @Shulyaka
|
||||
homeassistant/components/nut/* @bdraco
|
||||
@ -374,9 +385,11 @@ homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/recollect_waste/* @bachya
|
||||
homeassistant/components/rejseplanen/* @DarkFox
|
||||
homeassistant/components/repetier/* @MTrab
|
||||
homeassistant/components/rflink/* @javicalle
|
||||
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
|
||||
homeassistant/components/ring/* @balloob
|
||||
homeassistant/components/risco/* @OnFreund
|
||||
homeassistant/components/rituals_perfume_genie/* @milanmeu
|
||||
homeassistant/components/rmvtransport/* @cgtobi
|
||||
homeassistant/components/roku/* @ctalkington
|
||||
homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn
|
||||
@ -416,6 +429,7 @@ homeassistant/components/smappee/* @bsmappee
|
||||
homeassistant/components/smart_meter_texas/* @grahamwetzler
|
||||
homeassistant/components/smarthab/* @outadoc
|
||||
homeassistant/components/smartthings/* @andrewsayre
|
||||
homeassistant/components/smarttub/* @mdz
|
||||
homeassistant/components/smarty/* @z0mbieprocess
|
||||
homeassistant/components/sms/* @ocalvo
|
||||
homeassistant/components/smtp/* @fabaff
|
||||
@ -442,6 +456,7 @@ homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/stookalert/* @fwestenberg
|
||||
homeassistant/components/stream/* @hunterjm @uvjustin
|
||||
homeassistant/components/stt/* @pvizeli
|
||||
homeassistant/components/subaru/* @G-Two
|
||||
homeassistant/components/suez_water/* @ooii
|
||||
homeassistant/components/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
@ -455,7 +470,7 @@ homeassistant/components/syncthru/* @nielstron
|
||||
homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/tado/* @michaelarnauts @bdraco
|
||||
homeassistant/components/tado/* @michaelarnauts @bdraco @noltari
|
||||
homeassistant/components/tag/* @balloob @dmulcahey
|
||||
homeassistant/components/tahoma/* @philklei
|
||||
homeassistant/components/tankerkoenig/* @guillempages
|
||||
@ -477,7 +492,6 @@ homeassistant/components/toon/* @frenck
|
||||
homeassistant/components/totalconnect/* @austinmroczek
|
||||
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
||||
homeassistant/components/traccar/* @ludeeus
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/trafikverket_weatherstation/* @endor-force
|
||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
@ -485,6 +499,7 @@ homeassistant/components/tts/* @pvizeli
|
||||
homeassistant/components/tuya/* @ollo69
|
||||
homeassistant/components/twentemilieu/* @frenck
|
||||
homeassistant/components/twinkly/* @dr1rrb
|
||||
homeassistant/components/ubus/* @noltari
|
||||
homeassistant/components/unifi/* @Kane610
|
||||
homeassistant/components/unifiled/* @florisvdk
|
||||
homeassistant/components/upb/* @gwww
|
||||
@ -506,7 +521,7 @@ homeassistant/components/vicare/* @oischinger
|
||||
homeassistant/components/vilfo/* @ManneW
|
||||
homeassistant/components/vivotek/* @HarlemSquirrel
|
||||
homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/vlc_telnet/* @rodripf
|
||||
homeassistant/components/vlc_telnet/* @rodripf @dmcc
|
||||
homeassistant/components/volkszaehler/* @fabaff
|
||||
homeassistant/components/volumio/* @OnFreund
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
@ -514,6 +529,7 @@ homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/webostv/* @bendavid
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @esev
|
||||
homeassistant/components/wiffi/* @mampfes
|
||||
homeassistant/components/wilight/* @leofig-rj
|
||||
homeassistant/components/withings/* @vangorra
|
||||
@ -523,7 +539,6 @@ homeassistant/components/workday/* @fabaff
|
||||
homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/xbox/* @hunterjm
|
||||
homeassistant/components/xbox_live/* @MartinHjelmare
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG
|
||||
homeassistant/components/xiaomi_tv/* @simse
|
||||
|
@ -1,7 +1,11 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
libudev-dev \
|
||||
libavformat-dev \
|
||||
libavcodec-dev \
|
||||
@ -10,6 +14,7 @@ RUN \
|
||||
libswscale-dev \
|
||||
libswresample-dev \
|
||||
libavfilter-dev \
|
||||
libpcap-dev \
|
||||
git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
@ -14,7 +14,7 @@ schedules:
|
||||
always: true
|
||||
variables:
|
||||
- name: versionBuilder
|
||||
value: '2020.11.0'
|
||||
value: '2021.02.0'
|
||||
- group: docker
|
||||
- group: github
|
||||
- group: twine
|
||||
@ -114,10 +114,12 @@ stages:
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 15
|
||||
maxParallel: 17
|
||||
matrix:
|
||||
qemux86-64:
|
||||
buildMachine: 'qemux86-64'
|
||||
generic-x86-64:
|
||||
buildMachine: 'generic-x86-64'
|
||||
intel-nuc:
|
||||
buildMachine: 'intel-nuc'
|
||||
qemux86:
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
@ -24,11 +26,19 @@ _ProviderKey = Tuple[str, Optional[str]]
|
||||
_ProviderDict = Dict[_ProviderKey, AuthProvider]
|
||||
|
||||
|
||||
class InvalidAuthError(Exception):
|
||||
"""Raised when a authentication error occurs."""
|
||||
|
||||
|
||||
class InvalidProvider(Exception):
|
||||
"""Authentication provider not found."""
|
||||
|
||||
|
||||
async def auth_manager_from_config(
|
||||
hass: HomeAssistant,
|
||||
provider_configs: List[Dict[str, Any]],
|
||||
module_configs: List[Dict[str, Any]],
|
||||
) -> "AuthManager":
|
||||
) -> AuthManager:
|
||||
"""Initialize an auth manager from config.
|
||||
|
||||
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
||||
@ -68,7 +78,7 @@ async def auth_manager_from_config(
|
||||
class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||
"""Manage authentication flows."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, auth_manager: "AuthManager"):
|
||||
def __init__(self, hass: HomeAssistant, auth_manager: AuthManager):
|
||||
"""Init auth manager flows."""
|
||||
super().__init__(hass)
|
||||
self.auth_manager = auth_manager
|
||||
@ -96,7 +106,7 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||
return result
|
||||
|
||||
# we got final result
|
||||
if isinstance(result["data"], models.User):
|
||||
if isinstance(result["data"], models.Credentials):
|
||||
result["result"] = result["data"]
|
||||
return result
|
||||
|
||||
@ -120,11 +130,12 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||
modules = await self.auth_manager.async_get_enabled_mfa(user)
|
||||
|
||||
if modules:
|
||||
flow.credential = credentials
|
||||
flow.user = user
|
||||
flow.available_mfa_modules = modules
|
||||
return await flow.async_step_select_mfa_module()
|
||||
|
||||
result["result"] = await self.auth_manager.async_get_or_create_user(credentials)
|
||||
result["result"] = credentials
|
||||
return result
|
||||
|
||||
|
||||
@ -156,7 +167,7 @@ class AuthManager:
|
||||
return list(self._mfa_modules.values())
|
||||
|
||||
def get_auth_provider(
|
||||
self, provider_type: str, provider_id: str
|
||||
self, provider_type: str, provider_id: Optional[str]
|
||||
) -> Optional[AuthProvider]:
|
||||
"""Return an auth provider, None if not found."""
|
||||
return self._providers.get((provider_type, provider_id))
|
||||
@ -367,6 +378,7 @@ class AuthManager:
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: Optional[str] = None,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||
credential: Optional[models.Credentials] = None,
|
||||
) -> models.RefreshToken:
|
||||
"""Create a new refresh token for a user."""
|
||||
if not user.is_active:
|
||||
@ -415,6 +427,7 @@ class AuthManager:
|
||||
client_icon,
|
||||
token_type,
|
||||
access_token_expiration,
|
||||
credential,
|
||||
)
|
||||
|
||||
async def async_get_refresh_token(
|
||||
@ -440,6 +453,8 @@ class AuthManager:
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> str:
|
||||
"""Create a new access token."""
|
||||
self.async_validate_refresh_token(refresh_token, remote_ip)
|
||||
|
||||
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
||||
|
||||
now = dt_util.utcnow()
|
||||
@ -453,6 +468,40 @@ class AuthManager:
|
||||
algorithm="HS256",
|
||||
).decode()
|
||||
|
||||
@callback
|
||||
def _async_resolve_provider(
|
||||
self, refresh_token: models.RefreshToken
|
||||
) -> Optional[AuthProvider]:
|
||||
"""Get the auth provider for the given refresh token.
|
||||
|
||||
Raises an exception if the expected provider is no longer available or return
|
||||
None if no provider was expected for this refresh token.
|
||||
"""
|
||||
if refresh_token.credential is None:
|
||||
return None
|
||||
|
||||
provider = self.get_auth_provider(
|
||||
refresh_token.credential.auth_provider_type,
|
||||
refresh_token.credential.auth_provider_id,
|
||||
)
|
||||
if provider is None:
|
||||
raise InvalidProvider(
|
||||
f"Auth provider {refresh_token.credential.auth_provider_type}, {refresh_token.credential.auth_provider_id} not available"
|
||||
)
|
||||
return provider
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> None:
|
||||
"""Validate that a refresh token is usable.
|
||||
|
||||
Will raise InvalidAuthError on errors.
|
||||
"""
|
||||
provider = self._async_resolve_provider(refresh_token)
|
||||
if provider:
|
||||
provider.async_validate_refresh_token(refresh_token, remote_ip)
|
||||
|
||||
async def async_validate_access_token(
|
||||
self, token: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
|
@ -208,6 +208,7 @@ class AuthStore:
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: str = models.TOKEN_TYPE_NORMAL,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||
credential: Optional[models.Credentials] = None,
|
||||
) -> models.RefreshToken:
|
||||
"""Create a new token for a user."""
|
||||
kwargs: Dict[str, Any] = {
|
||||
@ -215,6 +216,7 @@ class AuthStore:
|
||||
"client_id": client_id,
|
||||
"token_type": token_type,
|
||||
"access_token_expiration": access_token_expiration,
|
||||
"credential": credential,
|
||||
}
|
||||
if client_name:
|
||||
kwargs["client_name"] = client_name
|
||||
@ -309,6 +311,7 @@ class AuthStore:
|
||||
|
||||
users: Dict[str, models.User] = OrderedDict()
|
||||
groups: Dict[str, models.Group] = OrderedDict()
|
||||
credentials: Dict[str, models.Credentials] = OrderedDict()
|
||||
|
||||
# Soft-migrating data as we load. We are going to make sure we have a
|
||||
# read only group and an admin group. There are two states that we can
|
||||
@ -415,15 +418,15 @@ class AuthStore:
|
||||
)
|
||||
|
||||
for cred_dict in data["credentials"]:
|
||||
users[cred_dict["user_id"]].credentials.append(
|
||||
models.Credentials(
|
||||
credential = models.Credentials(
|
||||
id=cred_dict["id"],
|
||||
is_new=False,
|
||||
auth_provider_type=cred_dict["auth_provider_type"],
|
||||
auth_provider_id=cred_dict["auth_provider_id"],
|
||||
data=cred_dict["data"],
|
||||
)
|
||||
)
|
||||
credentials[cred_dict["id"]] = credential
|
||||
users[cred_dict["user_id"]].credentials.append(credential)
|
||||
|
||||
for rt_dict in data["refresh_tokens"]:
|
||||
# Filter out the old keys that don't have jwt_key (pre-0.76)
|
||||
@ -469,6 +472,8 @@ class AuthStore:
|
||||
jwt_key=rt_dict["jwt_key"],
|
||||
last_used_at=last_used_at,
|
||||
last_used_ip=rt_dict.get("last_used_ip"),
|
||||
credential=credentials.get(rt_dict.get("credential_id")),
|
||||
version=rt_dict.get("version"),
|
||||
)
|
||||
users[rt_dict["user_id"]].refresh_tokens[token.id] = token
|
||||
|
||||
@ -542,6 +547,10 @@ class AuthStore:
|
||||
if refresh_token.last_used_at
|
||||
else None,
|
||||
"last_used_ip": refresh_token.last_used_ip,
|
||||
"credential_id": refresh_token.credential.id
|
||||
if refresh_token.credential
|
||||
else None,
|
||||
"version": refresh_token.version,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Pluggable auth modules for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
@ -66,7 +68,7 @@ class MultiFactorAuthModule:
|
||||
"""Return a voluptuous schema to define mfa auth module's input."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> "SetupFlow":
|
||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
Mfa module should extend SetupFlow
|
||||
|
@ -198,7 +198,7 @@ class TotpSetupFlow(SetupFlow):
|
||||
errors: Dict[str, str] = {}
|
||||
|
||||
if user_input:
|
||||
verified = await self.hass.async_add_executor_job( # type: ignore
|
||||
verified = await self.hass.async_add_executor_job(
|
||||
pyotp.TOTP(self._ota_secret).verify, user_input["code"]
|
||||
)
|
||||
if verified:
|
||||
|
@ -6,6 +6,7 @@ import uuid
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.const import __version__
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import permissions as perm_mdl
|
||||
@ -106,6 +107,10 @@ class RefreshToken:
|
||||
last_used_at: Optional[datetime] = attr.ib(default=None)
|
||||
last_used_ip: Optional[str] = attr.ib(default=None)
|
||||
|
||||
credential: Optional["Credentials"] = attr.ib(default=None)
|
||||
|
||||
version: Optional[str] = attr.ib(default=__version__)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Credentials:
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
@ -16,7 +18,7 @@ from homeassistant.util.decorator import Registry
|
||||
|
||||
from ..auth_store import AuthStore
|
||||
from ..const import MFA_SESSION_EXPIRATION
|
||||
from ..models import Credentials, User, UserMeta
|
||||
from ..models import Credentials, RefreshToken, User, UserMeta
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_REQS = "auth_prov_reqs_processed"
|
||||
@ -92,7 +94,7 @@ class AuthProvider:
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow":
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return the data flow for logging in with auth provider.
|
||||
|
||||
Auth provider should extend LoginFlow and return an instance.
|
||||
@ -117,6 +119,16 @@ class AuthProvider:
|
||||
async def async_initialize(self) -> None:
|
||||
"""Initialize the auth provider."""
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> None:
|
||||
"""Verify a refresh token is still valid.
|
||||
|
||||
Optional hook for an auth provider to verify validity of a refresh token.
|
||||
Should raise InvalidAuthError on errors.
|
||||
"""
|
||||
|
||||
|
||||
async def auth_provider_from_config(
|
||||
hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
|
||||
@ -182,6 +194,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
self.created_at = dt_util.utcnow()
|
||||
self.invalid_mfa_times = 0
|
||||
self.user: Optional[User] = None
|
||||
self.credential: Optional[Credentials] = None
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
@ -222,6 +235,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of mfa validation."""
|
||||
assert self.credential
|
||||
assert self.user
|
||||
|
||||
errors = {}
|
||||
@ -257,7 +271,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
return self.async_abort(reason="too_many_retry")
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(self.user)
|
||||
return await self.async_finish(self.credential)
|
||||
|
||||
description_placeholders: Dict[str, Optional[str]] = {
|
||||
"mfa_module_name": auth_module.name,
|
||||
|
@ -8,12 +8,12 @@ from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_COMMAND
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
CONF_COMMAND = "command"
|
||||
CONF_ARGS = "args"
|
||||
CONF_META = "meta"
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Home Assistant auth provider."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
@ -31,7 +33,7 @@ CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_provider(hass: HomeAssistant) -> "HassAuthProvider":
|
||||
def async_get_provider(hass: HomeAssistant) -> HassAuthProvider:
|
||||
"""Get the provider."""
|
||||
for prv in hass.auth.auth_providers:
|
||||
if prv.type == "homeassistant":
|
||||
|
@ -8,13 +8,12 @@ from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from .. import AuthManager
|
||||
from ..models import Credentials, User, UserMeta
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
AUTH_PROVIDER_TYPE = "legacy_api_password"
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
@ -30,23 +29,6 @@ class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
async def async_validate_password(hass: HomeAssistant, password: str) -> Optional[User]:
|
||||
"""Return a user if password is valid. None if not."""
|
||||
auth = cast(AuthManager, hass.auth) # type: ignore
|
||||
providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE)
|
||||
if not providers:
|
||||
raise ValueError("Legacy API password provider not found")
|
||||
|
||||
try:
|
||||
provider = cast(LegacyApiPasswordAuthProvider, providers[0])
|
||||
provider.async_validate_login(password)
|
||||
return await auth.async_get_or_create_user(
|
||||
await provider.async_get_or_create_credentials({})
|
||||
)
|
||||
except InvalidAuthError:
|
||||
return None
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE)
|
||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
"""An auth provider support legacy api_password."""
|
||||
|
@ -3,7 +3,14 @@
|
||||
It shows list of users if access from trusted network.
|
||||
Abort login flow if not access from trusted network.
|
||||
"""
|
||||
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
IPv4Network,
|
||||
IPv6Address,
|
||||
IPv6Network,
|
||||
ip_address,
|
||||
ip_network,
|
||||
)
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
import voluptuous as vol
|
||||
@ -13,7 +20,8 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
from .. import InvalidAuthError
|
||||
from ..models import Credentials, RefreshToken, UserMeta
|
||||
|
||||
IPAddress = Union[IPv4Address, IPv6Address]
|
||||
IPNetwork = Union[IPv4Network, IPv6Network]
|
||||
@ -46,10 +54,6 @@ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when try to access from untrusted networks."""
|
||||
|
||||
|
||||
class InvalidUserError(HomeAssistantError):
|
||||
"""Raised when try to login as invalid user."""
|
||||
|
||||
@ -163,6 +167,17 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
):
|
||||
raise InvalidAuthError("Not in trusted_networks")
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> None:
|
||||
"""Verify a refresh token is still valid."""
|
||||
if remote_ip is None:
|
||||
raise InvalidAuthError(
|
||||
"Unknown remote ip can't be used for trusted network provider."
|
||||
)
|
||||
self.async_validate_access(ip_address(remote_ip))
|
||||
|
||||
|
||||
class TrustedNetworksLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
@ -17,6 +17,7 @@ from homeassistant import config as conf_util, config_entries, core, loader
|
||||
from homeassistant.components import http
|
||||
from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import area_registry, device_registry, entity_registry
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import (
|
||||
DATA_SETUP,
|
||||
@ -510,10 +511,12 @@ async def _async_set_up_integrations(
|
||||
|
||||
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
|
||||
|
||||
# Kick off loading the registries. They don't need to be awaited.
|
||||
asyncio.create_task(hass.helpers.device_registry.async_get_registry())
|
||||
asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
|
||||
asyncio.create_task(hass.helpers.area_registry.async_get_registry())
|
||||
# Load the registries
|
||||
await asyncio.gather(
|
||||
device_registry.async_load(hass),
|
||||
entity_registry.async_load(hass),
|
||||
area_registry.async_load(hass),
|
||||
)
|
||||
|
||||
# Start setup
|
||||
if stage_1_domains:
|
||||
|
@ -13,6 +13,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DATE,
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TIME,
|
||||
CONF_PASSWORD,
|
||||
@ -32,7 +33,6 @@ SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
ATTR_DEVICE_TYPE = "device_type"
|
||||
ATTR_EVENT_CODE = "event_code"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "La riautenticazione ha avuto successo",
|
||||
"reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
|
||||
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
|
||||
},
|
||||
"error": {
|
||||
|
@ -1,9 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "\ud558\ub098\uc758 Abode \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
"reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
"username": "\uc774\uba54\uc77c"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "Herauthenticatie was succesvol",
|
||||
"single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan."
|
||||
},
|
||||
"error": {
|
||||
@ -12,9 +13,14 @@
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "MFA-code (6-cijfers)"
|
||||
}
|
||||
},
|
||||
"title": "Voer uw MFA-code voor Abode in"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Wachtwoord",
|
||||
"username": "E-mail"
|
||||
},
|
||||
"title": "Vul uw Abode-inloggegevens in"
|
||||
},
|
||||
"user": {
|
||||
|
@ -6,7 +6,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
|
||||
"invalid_mfa_code": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 MFA."
|
||||
},
|
||||
"step": {
|
||||
|
@ -17,6 +17,7 @@ from homeassistant.components.weather import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
LENGTH_FEET,
|
||||
@ -33,7 +34,6 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by AccuWeather"
|
||||
ATTR_ICON = "icon"
|
||||
ATTR_FORECAST = CONF_FORECAST = "forecast"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT_IMPERIAL = "Imperial"
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "accuweather",
|
||||
"name": "AccuWeather",
|
||||
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
|
||||
"requirements": ["accuweather==0.0.11"],
|
||||
"requirements": ["accuweather==0.1.0"],
|
||||
"codeowners": ["@bieniu"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum"
|
||||
|
@ -1,9 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4",
|
||||
"name": "\uc774\ub984"
|
||||
},
|
||||
"description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694:\nhttps://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken",
|
||||
"invalid_api_key": "API-sleutel",
|
||||
"requests_exceeded": "Het toegestane aantal verzoeken aan de Accuweather API is overschreden. U moet wachten of de API-sleutel wijzigen."
|
||||
},
|
||||
|
@ -9,6 +9,7 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
||||
from homeassistant.const import (
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
CONF_TIMEOUT,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
@ -17,7 +18,6 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TIMEOUT = "timeout"
|
||||
CONF_WRITE_TIMEOUT = "write_timeout"
|
||||
|
||||
DEFAULT_NAME = "Acer Projector"
|
||||
@ -74,7 +74,6 @@ class AcerSwitch(SwitchEntity):
|
||||
|
||||
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
|
||||
"""Init of the Acer projector."""
|
||||
|
||||
self.ser = serial.Serial(
|
||||
port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs
|
||||
)
|
||||
@ -90,7 +89,6 @@ class AcerSwitch(SwitchEntity):
|
||||
|
||||
def _write_read(self, msg):
|
||||
"""Write to the projector and read the return."""
|
||||
|
||||
ret = ""
|
||||
# Sometimes the projector won't answer for no reason or the projector
|
||||
# was disconnected during runtime.
|
||||
|
@ -32,7 +32,7 @@ class AcmedaBase(entity.Entity):
|
||||
device.id, remove_config_entry_id=self.registry_entry.config_entry_id
|
||||
)
|
||||
|
||||
await self.async_remove()
|
||||
await self.async_remove(force_remove=True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Entity has been added to hass."""
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Geen apparaten gevonden op het netwerk"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -2,7 +2,10 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
@ -14,9 +17,9 @@
|
||||
"host": "\ud638\uc2a4\ud2b8",
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
"port": "\ud3ec\ud2b8",
|
||||
"ssl": "AdGuard Home \uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4",
|
||||
"ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
|
||||
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
|
||||
"verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4"
|
||||
"verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
|
||||
},
|
||||
"description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694."
|
||||
}
|
||||
|
18
homeassistant/components/advantage_air/translations/ko.json
Normal file
18
homeassistant/components/advantage_air/translations/ko.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP \uc8fc\uc18c",
|
||||
"port": "\ud3ec\ud2b8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,13 @@
|
||||
"abort": {
|
||||
"already_configured": "Apparaat is al geconfigureerd"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP-adres",
|
||||
"port": "Poort"
|
||||
},
|
||||
"description": "Maak verbinding met de API van uw Advantage Air-tablet voor wandmontage.",
|
||||
|
61
homeassistant/components/aemet/__init__.py
Normal file
61
homeassistant/components/aemet/__init__.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""The AEMET OpenData component."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aemet_opendata.interface import AEMET
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import COMPONENTS, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the AEMET OpenData component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Set up AEMET OpenData as config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
|
||||
aemet = AEMET(api_key)
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude)
|
||||
|
||||
await weather_coordinator.async_refresh()
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||
ENTRY_NAME: name,
|
||||
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
|
||||
}
|
||||
|
||||
for component in COMPONENTS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in COMPONENTS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
57
homeassistant/components/aemet/abstract_aemet_sensor.py
Normal file
57
homeassistant/components/aemet/abstract_aemet_sensor.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Abstraction form AEMET OpenData sensors."""
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
class AbstractAemetSensor(CoordinatorEntity):
|
||||
"""Abstract class for an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._sensor_type = sensor_type
|
||||
self._sensor_name = sensor_configuration[SENSOR_NAME]
|
||||
self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
|
||||
self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._name} {self._sensor_name}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device_class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
58
homeassistant/components/aemet/config_flow.py
Normal file
58
homeassistant/components/aemet/config_flow.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""Config flow for AEMET OpenData."""
|
||||
from aemet_opendata import AEMET
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DEFAULT_NAME
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
|
||||
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for AEMET OpenData."""
|
||||
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
latitude = user_input[CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LONGITUDE]
|
||||
|
||||
await self.async_set_unique_id(f"{latitude}-{longitude}")
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY])
|
||||
if not api_online:
|
||||
errors["base"] = "invalid_api_key"
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Optional(
|
||||
CONF_LATITUDE, default=self.hass.config.latitude
|
||||
): cv.latitude,
|
||||
vol.Optional(
|
||||
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||
): cv.longitude,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
|
||||
async def _is_aemet_api_online(hass, api_key):
|
||||
aemet = AEMET(api_key)
|
||||
return await hass.async_add_executor_job(
|
||||
aemet.get_conventional_observation_stations, False
|
||||
)
|
326
homeassistant/components/aemet/const.py
Normal file
326
homeassistant/components/aemet/const.py
Normal file
@ -0,0 +1,326 @@
|
||||
"""Constant values for the AEMET OpenData component."""
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
ATTR_CONDITION_FOG,
|
||||
ATTR_CONDITION_LIGHTNING,
|
||||
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_POURING,
|
||||
ATTR_CONDITION_RAINY,
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
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,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
PERCENTAGE,
|
||||
PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
PRESSURE_HPA,
|
||||
SPEED_KILOMETERS_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||
COMPONENTS = ["sensor", "weather"]
|
||||
DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
ENTRY_NAME = "name"
|
||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
||||
UPDATE_LISTENER = "update_listener"
|
||||
SENSOR_NAME = "sensor_name"
|
||||
SENSOR_UNIT = "sensor_unit"
|
||||
SENSOR_DEVICE_CLASS = "sensor_device_class"
|
||||
|
||||
ATTR_API_CONDITION = "condition"
|
||||
ATTR_API_FORECAST_DAILY = "forecast-daily"
|
||||
ATTR_API_FORECAST_HOURLY = "forecast-hourly"
|
||||
ATTR_API_HUMIDITY = "humidity"
|
||||
ATTR_API_PRESSURE = "pressure"
|
||||
ATTR_API_RAIN = "rain"
|
||||
ATTR_API_RAIN_PROB = "rain-probability"
|
||||
ATTR_API_SNOW = "snow"
|
||||
ATTR_API_SNOW_PROB = "snow-probability"
|
||||
ATTR_API_STATION_ID = "station-id"
|
||||
ATTR_API_STATION_NAME = "station-name"
|
||||
ATTR_API_STATION_TIMESTAMP = "station-timestamp"
|
||||
ATTR_API_STORM_PROB = "storm-probability"
|
||||
ATTR_API_TEMPERATURE = "temperature"
|
||||
ATTR_API_TEMPERATURE_FEELING = "temperature-feeling"
|
||||
ATTR_API_TOWN_ID = "town-id"
|
||||
ATTR_API_TOWN_NAME = "town-name"
|
||||
ATTR_API_TOWN_TIMESTAMP = "town-timestamp"
|
||||
ATTR_API_WIND_BEARING = "wind-bearing"
|
||||
ATTR_API_WIND_MAX_SPEED = "wind-max-speed"
|
||||
ATTR_API_WIND_SPEED = "wind-speed"
|
||||
|
||||
CONDITIONS_MAP = {
|
||||
ATTR_CONDITION_CLEAR_NIGHT: {
|
||||
"11n", # Despejado (de noche)
|
||||
},
|
||||
ATTR_CONDITION_CLOUDY: {
|
||||
"14", # Nuboso
|
||||
"14n", # Nuboso (de noche)
|
||||
"15", # Muy nuboso
|
||||
"15n", # Muy nuboso (de noche)
|
||||
"16", # Cubierto
|
||||
"16n", # Cubierto (de noche)
|
||||
"17", # Nubes altas
|
||||
"17n", # Nubes altas (de noche)
|
||||
},
|
||||
ATTR_CONDITION_FOG: {
|
||||
"81", # Niebla
|
||||
"81n", # Niebla (de noche)
|
||||
"82", # Bruma - Neblina
|
||||
"82n", # Bruma - Neblina (de noche)
|
||||
},
|
||||
ATTR_CONDITION_LIGHTNING: {
|
||||
"51", # Intervalos nubosos con tormenta
|
||||
"51n", # Intervalos nubosos con tormenta (de noche)
|
||||
"52", # Nuboso con tormenta
|
||||
"52n", # Nuboso con tormenta (de noche)
|
||||
"53", # Muy nuboso con tormenta
|
||||
"53n", # Muy nuboso con tormenta (de noche)
|
||||
"54", # Cubierto con tormenta
|
||||
"54n", # Cubierto con tormenta (de noche)
|
||||
},
|
||||
ATTR_CONDITION_LIGHTNING_RAINY: {
|
||||
"61", # Intervalos nubosos con tormenta y lluvia escasa
|
||||
"61n", # Intervalos nubosos con tormenta y lluvia escasa (de noche)
|
||||
"62", # Nuboso con tormenta y lluvia escasa
|
||||
"62n", # Nuboso con tormenta y lluvia escasa (de noche)
|
||||
"63", # Muy nuboso con tormenta y lluvia escasa
|
||||
"63n", # Muy nuboso con tormenta y lluvia escasa (de noche)
|
||||
"64", # Cubierto con tormenta y lluvia escasa
|
||||
"64n", # Cubierto con tormenta y lluvia escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_PARTLYCLOUDY: {
|
||||
"12", # Poco nuboso
|
||||
"12n", # Poco nuboso (de noche)
|
||||
"13", # Intervalos nubosos
|
||||
"13n", # Intervalos nubosos (de noche)
|
||||
},
|
||||
ATTR_CONDITION_POURING: {
|
||||
"27", # Chubascos
|
||||
"27n", # Chubascos (de noche)
|
||||
},
|
||||
ATTR_CONDITION_RAINY: {
|
||||
"23", # Intervalos nubosos con lluvia
|
||||
"23n", # Intervalos nubosos con lluvia (de noche)
|
||||
"24", # Nuboso con lluvia
|
||||
"24n", # Nuboso con lluvia (de noche)
|
||||
"25", # Muy nuboso con lluvia
|
||||
"25n", # Muy nuboso con lluvia (de noche)
|
||||
"26", # Cubierto con lluvia
|
||||
"26n", # Cubierto con lluvia (de noche)
|
||||
"43", # Intervalos nubosos con lluvia escasa
|
||||
"43n", # Intervalos nubosos con lluvia escasa (de noche)
|
||||
"44", # Nuboso con lluvia escasa
|
||||
"44n", # Nuboso con lluvia escasa (de noche)
|
||||
"45", # Muy nuboso con lluvia escasa
|
||||
"45n", # Muy nuboso con lluvia escasa (de noche)
|
||||
"46", # Cubierto con lluvia escasa
|
||||
"46n", # Cubierto con lluvia escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_SNOWY: {
|
||||
"33", # Intervalos nubosos con nieve
|
||||
"33n", # Intervalos nubosos con nieve (de noche)
|
||||
"34", # Nuboso con nieve
|
||||
"34n", # Nuboso con nieve (de noche)
|
||||
"35", # Muy nuboso con nieve
|
||||
"35n", # Muy nuboso con nieve (de noche)
|
||||
"36", # Cubierto con nieve
|
||||
"36n", # Cubierto con nieve (de noche)
|
||||
"71", # Intervalos nubosos con nieve escasa
|
||||
"71n", # Intervalos nubosos con nieve escasa (de noche)
|
||||
"72", # Nuboso con nieve escasa
|
||||
"72n", # Nuboso con nieve escasa (de noche)
|
||||
"73", # Muy nuboso con nieve escasa
|
||||
"73n", # Muy nuboso con nieve escasa (de noche)
|
||||
"74", # Cubierto con nieve escasa
|
||||
"74n", # Cubierto con nieve escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_SUNNY: {
|
||||
"11", # Despejado
|
||||
},
|
||||
}
|
||||
|
||||
FORECAST_MONITORED_CONDITIONS = [
|
||||
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,
|
||||
]
|
||||
MONITORED_CONDITIONS = [
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_RAIN,
|
||||
ATTR_API_RAIN_PROB,
|
||||
ATTR_API_SNOW,
|
||||
ATTR_API_SNOW_PROB,
|
||||
ATTR_API_STATION_ID,
|
||||
ATTR_API_STATION_NAME,
|
||||
ATTR_API_STATION_TIMESTAMP,
|
||||
ATTR_API_STORM_PROB,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_TEMPERATURE_FEELING,
|
||||
ATTR_API_TOWN_ID,
|
||||
ATTR_API_TOWN_NAME,
|
||||
ATTR_API_TOWN_TIMESTAMP,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_MAX_SPEED,
|
||||
ATTR_API_WIND_SPEED,
|
||||
]
|
||||
|
||||
FORECAST_MODE_DAILY = "daily"
|
||||
FORECAST_MODE_HOURLY = "hourly"
|
||||
FORECAST_MODES = [
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODE_HOURLY,
|
||||
]
|
||||
FORECAST_MODE_ATTR_API = {
|
||||
FORECAST_MODE_DAILY: ATTR_API_FORECAST_DAILY,
|
||||
FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
|
||||
}
|
||||
|
||||
FORECAST_SENSOR_TYPES = {
|
||||
ATTR_FORECAST_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION: {
|
||||
SENSOR_NAME: "Precipitation",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: {
|
||||
SENSOR_NAME: "Precipitation probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP_LOW: {
|
||||
SENSOR_NAME: "Temperature Low",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TIME: {
|
||||
SENSOR_NAME: "Time",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_FORECAST_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_FORECAST_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
WEATHER_SENSOR_TYPES = {
|
||||
ATTR_API_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_API_HUMIDITY: {
|
||||
SENSOR_NAME: "Humidity",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
},
|
||||
ATTR_API_PRESSURE: {
|
||||
SENSOR_NAME: "Pressure",
|
||||
SENSOR_UNIT: PRESSURE_HPA,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||
},
|
||||
ATTR_API_RAIN: {
|
||||
SENSOR_NAME: "Rain",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_RAIN_PROB: {
|
||||
SENSOR_NAME: "Rain probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_SNOW: {
|
||||
SENSOR_NAME: "Snow",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_SNOW_PROB: {
|
||||
SENSOR_NAME: "Snow probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_STATION_ID: {
|
||||
SENSOR_NAME: "Station ID",
|
||||
},
|
||||
ATTR_API_STATION_NAME: {
|
||||
SENSOR_NAME: "Station name",
|
||||
},
|
||||
ATTR_API_STATION_TIMESTAMP: {
|
||||
SENSOR_NAME: "Station timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_STORM_PROB: {
|
||||
SENSOR_NAME: "Storm probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE_FEELING: {
|
||||
SENSOR_NAME: "Temperature feeling",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TOWN_ID: {
|
||||
SENSOR_NAME: "Town ID",
|
||||
},
|
||||
ATTR_API_TOWN_NAME: {
|
||||
SENSOR_NAME: "Town name",
|
||||
},
|
||||
ATTR_API_TOWN_TIMESTAMP: {
|
||||
SENSOR_NAME: "Town timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_API_WIND_MAX_SPEED: {
|
||||
SENSOR_NAME: "Wind max speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
|
||||
WIND_BEARING_MAP = {
|
||||
"C": None,
|
||||
"N": 0.0,
|
||||
"NE": 45.0,
|
||||
"E": 90.0,
|
||||
"SE": 135.0,
|
||||
"S": 180.0,
|
||||
"SO": 225.0,
|
||||
"O": 270.0,
|
||||
"NO": 315.0,
|
||||
}
|
8
homeassistant/components/aemet/manifest.json
Normal file
8
homeassistant/components/aemet/manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "aemet",
|
||||
"name": "AEMET OpenData",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"requirements": ["AEMET-OpenData==0.1.8"],
|
||||
"codeowners": ["@noltari"]
|
||||
}
|
114
homeassistant/components/aemet/sensor.py
Normal file
114
homeassistant/components/aemet/sensor.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from .abstract_aemet_sensor import AbstractAemetSensor
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
FORECAST_MODE_ATTR_API,
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODES,
|
||||
FORECAST_MONITORED_CONDITIONS,
|
||||
FORECAST_SENSOR_TYPES,
|
||||
MONITORED_CONDITIONS,
|
||||
WEATHER_SENSOR_TYPES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AEMET OpenData sensor entities based on a config entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
name = domain_data[ENTRY_NAME]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
weather_sensor_types = WEATHER_SENSOR_TYPES
|
||||
forecast_sensor_types = FORECAST_SENSOR_TYPES
|
||||
|
||||
entities = []
|
||||
for sensor_type in MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetSensor(
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
weather_sensor_types[sensor_type],
|
||||
weather_coordinator,
|
||||
)
|
||||
)
|
||||
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
|
||||
for sensor_type in FORECAST_MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetForecastSensor(
|
||||
f"{name} Forecast",
|
||||
unique_id,
|
||||
sensor_type,
|
||||
forecast_sensor_types[sensor_type],
|
||||
weather_coordinator,
|
||||
mode,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AemetSensor(AbstractAemetSensor):
|
||||
"""Implementation of an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||
)
|
||||
self._weather_coordinator = weather_coordinator
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._weather_coordinator.data.get(self._sensor_type)
|
||||
|
||||
|
||||
class AemetForecastSensor(AbstractAemetSensor):
|
||||
"""Implementation of an AEMET OpenData forecast sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||
)
|
||||
self._weather_coordinator = weather_coordinator
|
||||
self._forecast_mode = forecast_mode
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._forecast_mode == FORECAST_MODE_DAILY
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
forecasts = self._weather_coordinator.data.get(
|
||||
FORECAST_MODE_ATTR_API[self._forecast_mode]
|
||||
)
|
||||
if forecasts:
|
||||
return forecasts[0].get(self._sensor_type)
|
||||
return None
|
22
homeassistant/components/aemet/strings.json
Normal file
22
homeassistant/components/aemet/strings.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"name": "Name of the integration"
|
||||
},
|
||||
"description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/ca.json
Normal file
22
homeassistant/components/aemet/translations/ca.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Clau API inv\u00e0lida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Clau API",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud",
|
||||
"name": "Nom de la integraci\u00f3"
|
||||
},
|
||||
"description": "Configura la integraci\u00f3 d'AEMET OpenData. Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/aemet/translations/cs.json
Normal file
20
homeassistant/components/aemet/translations/cs.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Kl\u00ed\u010d API",
|
||||
"latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
|
||||
"longitude": "Zem\u011bpisn\u00e1 d\u00e9lka",
|
||||
"name": "N\u00e1zev integrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
homeassistant/components/aemet/translations/de.json
Normal file
19
homeassistant/components/aemet/translations/de.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Standort ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API-Schl\u00fcssel",
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "L\u00e4ngengrad"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/en.json
Normal file
22
homeassistant/components/aemet/translations/en.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Location is already configured"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Invalid API key"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Name of the integration"
|
||||
},
|
||||
"description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/es.json
Normal file
22
homeassistant/components/aemet/translations/es.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Clave API no v\u00e1lida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud",
|
||||
"name": "Nombre de la integraci\u00f3n"
|
||||
},
|
||||
"description": "Configurar la integraci\u00f3n de AEMET OpenData. Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/et.json
Normal file
22
homeassistant/components/aemet/translations/et.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Asukoht on juba m\u00e4\u00e4ratud"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Vale API v\u00f5ti"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API v\u00f5ti",
|
||||
"latitude": "Laiuskraad",
|
||||
"longitude": "Pikkuskraad",
|
||||
"name": "Sidumise nimi"
|
||||
},
|
||||
"description": "Seadista AEMET OpenData sidumine. API v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/fr.json
Normal file
22
homeassistant/components/aemet/translations/fr.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Cl\u00e9 API invalide"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Nom de l'int\u00e9gration"
|
||||
},
|
||||
"description": "Configurez l'int\u00e9gration AEMET OpenData. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/it.json
Normal file
22
homeassistant/components/aemet/translations/it.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "La posizione \u00e8 gi\u00e0 configurata"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Chiave API non valida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Chiave API",
|
||||
"latitude": "Latitudine",
|
||||
"longitude": "Logitudine",
|
||||
"name": "Nome dell'integrazione"
|
||||
},
|
||||
"description": "Imposta l'integrazione di AEMET OpenData. Per generare la chiave API, vai su https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/aemet/translations/ko.json
Normal file
21
homeassistant/components/aemet/translations/ko.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4",
|
||||
"name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984"
|
||||
},
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/aemet/translations/nl.json
Normal file
21
homeassistant/components/aemet/translations/nl.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Locatie is al geconfigureerd."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Ongeldige API-sleutel"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel",
|
||||
"latitude": "Breedtegraad",
|
||||
"longitude": "Lengtegraad",
|
||||
"name": "Naam van de integratie"
|
||||
},
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/no.json
Normal file
22
homeassistant/components/aemet/translations/no.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Plasseringen er allerede konfigurert"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Ugyldig API-n\u00f8kkel"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API-n\u00f8kkel",
|
||||
"latitude": "Breddegrad",
|
||||
"longitude": "Lengdegrad",
|
||||
"name": "Navnet p\u00e5 integrasjonen"
|
||||
},
|
||||
"description": "Sett opp AEMET OpenData-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/pl.json
Normal file
22
homeassistant/components/aemet/translations/pl.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Lokalizacja jest ju\u017c skonfigurowana"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Nieprawid\u0142owy klucz API"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Klucz API",
|
||||
"latitude": "Szeroko\u015b\u0107 geograficzna",
|
||||
"longitude": "D\u0142ugo\u015b\u0107 geograficzna",
|
||||
"name": "Nazwa integracji"
|
||||
},
|
||||
"description": "Skonfiguruj integracj\u0119 AEMET OpenData. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/ru.json
Normal file
22
homeassistant/components/aemet/translations/ru.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \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": "\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 AEMET OpenData. \u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://opendata.aemet.es/centrodedescargas/altaUsuario.",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/zh-Hant.json
Normal file
22
homeassistant/components/aemet/translations/zh-Hant.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "API \u5bc6\u9470\u7121\u6548"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \u5bc6\u9470",
|
||||
"latitude": "\u7def\u5ea6",
|
||||
"longitude": "\u7d93\u5ea6",
|
||||
"name": "\u6574\u5408\u540d\u7a31"
|
||||
},
|
||||
"description": "\u6b32\u8a2d\u5b9a AEMET OpenData \u6574\u5408\u3002\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u7522\u751f API \u5bc6\u9470",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
homeassistant/components/aemet/weather.py
Normal file
113
homeassistant/components/aemet/weather.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from homeassistant.components.weather import WeatherEntity
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_SPEED,
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
FORECAST_MODE_ATTR_API,
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AEMET OpenData weather entity based on a config entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
entities = []
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
unique_id = f"{config_entry.unique_id} {mode}"
|
||||
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, False)
|
||||
|
||||
|
||||
class AemetWeather(CoordinatorEntity, WeatherEntity):
|
||||
"""Implementation of an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._forecast_mode = forecast_mode
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
return self.coordinator.data[ATTR_API_CONDITION]
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._forecast_mode == FORECAST_MODE_DAILY
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
"""Return the forecast array."""
|
||||
return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]]
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
"""Return the humidity."""
|
||||
return self.coordinator.data[ATTR_API_HUMIDITY]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the pressure."""
|
||||
return self.coordinator.data[ATTR_API_PRESSURE]
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_TEMPERATURE]
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_WIND_BEARING]
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_WIND_SPEED]
|
637
homeassistant/components/aemet/weather_update_coordinator.py
Normal file
637
homeassistant/components/aemet/weather_update_coordinator.py
Normal file
@ -0,0 +1,637 @@
|
||||
"""Weather data coordinator for the AEMET OpenData service."""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aemet_opendata.const import (
|
||||
AEMET_ATTR_DATE,
|
||||
AEMET_ATTR_DAY,
|
||||
AEMET_ATTR_DIRECTION,
|
||||
AEMET_ATTR_ELABORATED,
|
||||
AEMET_ATTR_FORECAST,
|
||||
AEMET_ATTR_HUMIDITY,
|
||||
AEMET_ATTR_ID,
|
||||
AEMET_ATTR_IDEMA,
|
||||
AEMET_ATTR_MAX,
|
||||
AEMET_ATTR_MIN,
|
||||
AEMET_ATTR_NAME,
|
||||
AEMET_ATTR_PRECIPITATION,
|
||||
AEMET_ATTR_PRECIPITATION_PROBABILITY,
|
||||
AEMET_ATTR_SKY_STATE,
|
||||
AEMET_ATTR_SNOW,
|
||||
AEMET_ATTR_SNOW_PROBABILITY,
|
||||
AEMET_ATTR_SPEED,
|
||||
AEMET_ATTR_STATION_DATE,
|
||||
AEMET_ATTR_STATION_HUMIDITY,
|
||||
AEMET_ATTR_STATION_LOCATION,
|
||||
AEMET_ATTR_STATION_PRESSURE_SEA,
|
||||
AEMET_ATTR_STATION_TEMPERATURE,
|
||||
AEMET_ATTR_STORM_PROBABILITY,
|
||||
AEMET_ATTR_TEMPERATURE,
|
||||
AEMET_ATTR_TEMPERATURE_FEELING,
|
||||
AEMET_ATTR_WIND,
|
||||
AEMET_ATTR_WIND_GUST,
|
||||
ATTR_DATA,
|
||||
)
|
||||
from aemet_opendata.helpers import (
|
||||
get_forecast_day_value,
|
||||
get_forecast_hour_value,
|
||||
get_forecast_interval_value,
|
||||
)
|
||||
import async_timeout
|
||||
|
||||
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,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_FORECAST_DAILY,
|
||||
ATTR_API_FORECAST_HOURLY,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_RAIN,
|
||||
ATTR_API_RAIN_PROB,
|
||||
ATTR_API_SNOW,
|
||||
ATTR_API_SNOW_PROB,
|
||||
ATTR_API_STATION_ID,
|
||||
ATTR_API_STATION_NAME,
|
||||
ATTR_API_STATION_TIMESTAMP,
|
||||
ATTR_API_STORM_PROB,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_TEMPERATURE_FEELING,
|
||||
ATTR_API_TOWN_ID,
|
||||
ATTR_API_TOWN_NAME,
|
||||
ATTR_API_TOWN_TIMESTAMP,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_MAX_SPEED,
|
||||
ATTR_API_WIND_SPEED,
|
||||
CONDITIONS_MAP,
|
||||
DOMAIN,
|
||||
WIND_BEARING_MAP,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
|
||||
def format_condition(condition: str) -> str:
|
||||
"""Return condition from dict CONDITIONS_MAP."""
|
||||
for key, value in CONDITIONS_MAP.items():
|
||||
if condition in value:
|
||||
return key
|
||||
_LOGGER.error('condition "%s" not found in CONDITIONS_MAP', condition)
|
||||
return condition
|
||||
|
||||
|
||||
def format_float(value) -> float:
|
||||
"""Try converting string to float."""
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def format_int(value) -> int:
|
||||
"""Try converting string to int."""
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class TownNotFound(UpdateFailed):
|
||||
"""Raised when town is not found."""
|
||||
|
||||
|
||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Weather data update coordinator."""
|
||||
|
||||
def __init__(self, hass, aemet, latitude, longitude):
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
||||
)
|
||||
|
||||
self._aemet = aemet
|
||||
self._station = None
|
||||
self._town = None
|
||||
self._latitude = latitude
|
||||
self._longitude = longitude
|
||||
self._data = {
|
||||
"daily": None,
|
||||
"hourly": None,
|
||||
"station": None,
|
||||
}
|
||||
|
||||
async def _async_update_data(self):
|
||||
data = {}
|
||||
with async_timeout.timeout(120):
|
||||
weather_response = await self._get_aemet_weather()
|
||||
data = self._convert_weather_response(weather_response)
|
||||
return data
|
||||
|
||||
async def _get_aemet_weather(self):
|
||||
"""Poll weather data from AEMET OpenData."""
|
||||
weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast)
|
||||
return weather
|
||||
|
||||
def _get_weather_station(self):
|
||||
if not self._station:
|
||||
self._station = (
|
||||
self._aemet.get_conventional_observation_station_by_coordinates(
|
||||
self._latitude, self._longitude
|
||||
)
|
||||
)
|
||||
if self._station:
|
||||
_LOGGER.debug(
|
||||
"station found for coordinates [%s, %s]: %s",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
self._station,
|
||||
)
|
||||
if not self._station:
|
||||
_LOGGER.debug(
|
||||
"station not found for coordinates [%s, %s]",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
)
|
||||
return self._station
|
||||
|
||||
def _get_weather_town(self):
|
||||
if not self._town:
|
||||
self._town = self._aemet.get_town_by_coordinates(
|
||||
self._latitude, self._longitude
|
||||
)
|
||||
if self._town:
|
||||
_LOGGER.debug(
|
||||
"town found for coordinates [%s, %s]: %s",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
self._town,
|
||||
)
|
||||
if not self._town:
|
||||
_LOGGER.error(
|
||||
"town not found for coordinates [%s, %s]",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
)
|
||||
raise TownNotFound
|
||||
return self._town
|
||||
|
||||
def _get_weather_and_forecast(self):
|
||||
"""Get weather and forecast data from AEMET OpenData."""
|
||||
|
||||
self._get_weather_town()
|
||||
|
||||
daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID])
|
||||
if not daily:
|
||||
_LOGGER.error(
|
||||
'error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
|
||||
hourly = self._aemet.get_specific_forecast_town_hourly(
|
||||
self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
if not hourly:
|
||||
_LOGGER.error(
|
||||
'error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
|
||||
station = None
|
||||
if self._get_weather_station():
|
||||
station = self._aemet.get_conventional_observation_station_data(
|
||||
self._station[AEMET_ATTR_IDEMA]
|
||||
)
|
||||
if not station:
|
||||
_LOGGER.error(
|
||||
'error fetching data for station "%s"',
|
||||
self._station[AEMET_ATTR_IDEMA],
|
||||
)
|
||||
|
||||
if daily:
|
||||
self._data["daily"] = daily
|
||||
if hourly:
|
||||
self._data["hourly"] = hourly
|
||||
if station:
|
||||
self._data["station"] = station
|
||||
|
||||
return AemetWeather(
|
||||
self._data["daily"],
|
||||
self._data["hourly"],
|
||||
self._data["station"],
|
||||
)
|
||||
|
||||
def _convert_weather_response(self, weather_response):
|
||||
"""Format the weather response correctly."""
|
||||
if not weather_response or not weather_response.hourly:
|
||||
return None
|
||||
|
||||
elaborated = dt_util.parse_datetime(
|
||||
weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED]
|
||||
)
|
||||
now = dt_util.now()
|
||||
hour = now.hour
|
||||
|
||||
# Get current day
|
||||
day = None
|
||||
for cur_day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
cur_day_date = dt_util.parse_datetime(cur_day[AEMET_ATTR_DATE])
|
||||
if now.date() == cur_day_date.date():
|
||||
day = cur_day
|
||||
break
|
||||
|
||||
# Get station data
|
||||
station_data = None
|
||||
if weather_response.station:
|
||||
station_data = weather_response.station[ATTR_DATA][-1]
|
||||
|
||||
condition = None
|
||||
humidity = None
|
||||
pressure = None
|
||||
rain = None
|
||||
rain_prob = None
|
||||
snow = None
|
||||
snow_prob = None
|
||||
station_id = None
|
||||
station_name = None
|
||||
station_timestamp = None
|
||||
storm_prob = None
|
||||
temperature = None
|
||||
temperature_feeling = None
|
||||
town_id = None
|
||||
town_name = None
|
||||
town_timestamp = dt_util.as_utc(elaborated)
|
||||
wind_bearing = None
|
||||
wind_max_speed = None
|
||||
wind_speed = None
|
||||
|
||||
# Get weather values
|
||||
if day:
|
||||
condition = self._get_condition(day, hour)
|
||||
humidity = self._get_humidity(day, hour)
|
||||
rain = self._get_rain(day, hour)
|
||||
rain_prob = self._get_rain_prob(day, hour)
|
||||
snow = self._get_snow(day, hour)
|
||||
snow_prob = self._get_snow_prob(day, hour)
|
||||
station_id = self._get_station_id()
|
||||
station_name = self._get_station_name()
|
||||
storm_prob = self._get_storm_prob(day, hour)
|
||||
temperature = self._get_temperature(day, hour)
|
||||
temperature_feeling = self._get_temperature_feeling(day, hour)
|
||||
town_id = self._get_town_id()
|
||||
town_name = self._get_town_name()
|
||||
wind_bearing = self._get_wind_bearing(day, hour)
|
||||
wind_max_speed = self._get_wind_max_speed(day, hour)
|
||||
wind_speed = self._get_wind_speed(day, hour)
|
||||
|
||||
# Overwrite weather values with closest station data (if present)
|
||||
if station_data:
|
||||
if AEMET_ATTR_STATION_DATE in station_data:
|
||||
station_dt = dt_util.parse_datetime(
|
||||
station_data[AEMET_ATTR_STATION_DATE] + "Z"
|
||||
)
|
||||
station_timestamp = dt_util.as_utc(station_dt).isoformat()
|
||||
if AEMET_ATTR_STATION_HUMIDITY in station_data:
|
||||
humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY])
|
||||
if AEMET_ATTR_STATION_PRESSURE_SEA in station_data:
|
||||
pressure = format_float(station_data[AEMET_ATTR_STATION_PRESSURE_SEA])
|
||||
if AEMET_ATTR_STATION_TEMPERATURE in station_data:
|
||||
temperature = format_float(station_data[AEMET_ATTR_STATION_TEMPERATURE])
|
||||
|
||||
# Get forecast from weather data
|
||||
forecast_daily = self._get_daily_forecast_from_weather_response(
|
||||
weather_response, now
|
||||
)
|
||||
forecast_hourly = self._get_hourly_forecast_from_weather_response(
|
||||
weather_response, now
|
||||
)
|
||||
|
||||
return {
|
||||
ATTR_API_CONDITION: condition,
|
||||
ATTR_API_FORECAST_DAILY: forecast_daily,
|
||||
ATTR_API_FORECAST_HOURLY: forecast_hourly,
|
||||
ATTR_API_HUMIDITY: humidity,
|
||||
ATTR_API_TEMPERATURE: temperature,
|
||||
ATTR_API_TEMPERATURE_FEELING: temperature_feeling,
|
||||
ATTR_API_PRESSURE: pressure,
|
||||
ATTR_API_RAIN: rain,
|
||||
ATTR_API_RAIN_PROB: rain_prob,
|
||||
ATTR_API_SNOW: snow,
|
||||
ATTR_API_SNOW_PROB: snow_prob,
|
||||
ATTR_API_STATION_ID: station_id,
|
||||
ATTR_API_STATION_NAME: station_name,
|
||||
ATTR_API_STATION_TIMESTAMP: station_timestamp,
|
||||
ATTR_API_STORM_PROB: storm_prob,
|
||||
ATTR_API_TOWN_ID: town_id,
|
||||
ATTR_API_TOWN_NAME: town_name,
|
||||
ATTR_API_TOWN_TIMESTAMP: town_timestamp,
|
||||
ATTR_API_WIND_BEARING: wind_bearing,
|
||||
ATTR_API_WIND_MAX_SPEED: wind_max_speed,
|
||||
ATTR_API_WIND_SPEED: wind_speed,
|
||||
}
|
||||
|
||||
def _get_daily_forecast_from_weather_response(self, weather_response, now):
|
||||
if weather_response.daily:
|
||||
parse = False
|
||||
forecast = []
|
||||
for day in weather_response.daily[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
|
||||
if now.date() == day_date.date():
|
||||
parse = True
|
||||
if parse:
|
||||
cur_forecast = self._convert_forecast_day(day_date, day)
|
||||
if cur_forecast:
|
||||
forecast.append(cur_forecast)
|
||||
return forecast
|
||||
return None
|
||||
|
||||
def _get_hourly_forecast_from_weather_response(self, weather_response, now):
|
||||
if weather_response.hourly:
|
||||
parse = False
|
||||
hour = now.hour
|
||||
forecast = []
|
||||
for day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
|
||||
hour_start = 0
|
||||
if now.date() == day_date.date():
|
||||
parse = True
|
||||
hour_start = now.hour
|
||||
if parse:
|
||||
for hour in range(hour_start, 24):
|
||||
cur_forecast = self._convert_forecast_hour(day_date, day, hour)
|
||||
if cur_forecast:
|
||||
forecast.append(cur_forecast)
|
||||
return forecast
|
||||
return None
|
||||
|
||||
def _convert_forecast_day(self, date, day):
|
||||
condition = self._get_condition_day(day)
|
||||
if not condition:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_FORECAST_CONDITION: condition,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day(
|
||||
day
|
||||
),
|
||||
ATTR_FORECAST_TEMP: self._get_temperature_day(day),
|
||||
ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day),
|
||||
ATTR_FORECAST_TIME: dt_util.as_utc(date),
|
||||
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day),
|
||||
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day),
|
||||
}
|
||||
|
||||
def _convert_forecast_hour(self, date, day, hour):
|
||||
condition = self._get_condition(day, hour)
|
||||
if not condition:
|
||||
return None
|
||||
|
||||
forecast_dt = date.replace(hour=hour, minute=0, second=0)
|
||||
|
||||
return {
|
||||
ATTR_FORECAST_CONDITION: condition,
|
||||
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour),
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob(
|
||||
day, hour
|
||||
),
|
||||
ATTR_FORECAST_TEMP: self._get_temperature(day, hour),
|
||||
ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt),
|
||||
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
|
||||
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
|
||||
}
|
||||
|
||||
def _calc_precipitation(self, day, hour):
|
||||
"""Calculate the precipitation."""
|
||||
rain_value = self._get_rain(day, hour)
|
||||
if not rain_value:
|
||||
rain_value = 0
|
||||
|
||||
snow_value = self._get_snow(day, hour)
|
||||
if not snow_value:
|
||||
snow_value = 0
|
||||
|
||||
if round(rain_value + snow_value, 1) == 0:
|
||||
return None
|
||||
return round(rain_value + snow_value, 1)
|
||||
|
||||
def _calc_precipitation_prob(self, day, hour):
|
||||
"""Calculate the precipitation probability (hour)."""
|
||||
rain_value = self._get_rain_prob(day, hour)
|
||||
if not rain_value:
|
||||
rain_value = 0
|
||||
|
||||
snow_value = self._get_snow_prob(day, hour)
|
||||
if not snow_value:
|
||||
snow_value = 0
|
||||
|
||||
if rain_value == 0 and snow_value == 0:
|
||||
return None
|
||||
return max(rain_value, snow_value)
|
||||
|
||||
@staticmethod
|
||||
def _get_condition(day_data, hour):
|
||||
"""Get weather condition (hour) from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_SKY_STATE], hour)
|
||||
if val:
|
||||
return format_condition(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_condition_day(day_data):
|
||||
"""Get weather condition (day) from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_SKY_STATE])
|
||||
if val:
|
||||
return format_condition(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_humidity(day_data, hour):
|
||||
"""Get humidity from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_HUMIDITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_precipitation_prob_day(day_data):
|
||||
"""Get humidity from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY])
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_rain(day_data, hour):
|
||||
"""Get rain from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_PRECIPITATION], hour)
|
||||
if val:
|
||||
return format_float(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_rain_prob(day_data, hour):
|
||||
"""Get rain probability from weather data."""
|
||||
val = get_forecast_interval_value(
|
||||
day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY], hour
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_snow(day_data, hour):
|
||||
"""Get snow from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_SNOW], hour)
|
||||
if val:
|
||||
return format_float(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_snow_prob(day_data, hour):
|
||||
"""Get snow probability from weather data."""
|
||||
val = get_forecast_interval_value(day_data[AEMET_ATTR_SNOW_PROBABILITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
def _get_station_id(self):
|
||||
"""Get station ID from weather data."""
|
||||
if self._station:
|
||||
return self._station[AEMET_ATTR_IDEMA]
|
||||
return None
|
||||
|
||||
def _get_station_name(self):
|
||||
"""Get station name from weather data."""
|
||||
if self._station:
|
||||
return self._station[AEMET_ATTR_STATION_LOCATION]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_storm_prob(day_data, hour):
|
||||
"""Get storm probability from weather data."""
|
||||
val = get_forecast_interval_value(day_data[AEMET_ATTR_STORM_PROBABILITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature(day_data, hour):
|
||||
"""Get temperature (hour) from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_day(day_data):
|
||||
"""Get temperature (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MAX
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_low_day(day_data):
|
||||
"""Get temperature (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MIN
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_feeling(day_data, hour):
|
||||
"""Get temperature from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
def _get_town_id(self):
|
||||
"""Get town ID from weather data."""
|
||||
if self._town:
|
||||
return self._town[AEMET_ATTR_ID]
|
||||
return None
|
||||
|
||||
def _get_town_name(self):
|
||||
"""Get town name from weather data."""
|
||||
if self._town:
|
||||
return self._town[AEMET_ATTR_NAME]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_bearing(day_data, hour):
|
||||
"""Get wind bearing (hour) from weather data."""
|
||||
val = get_forecast_hour_value(
|
||||
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_DIRECTION
|
||||
)[0]
|
||||
if val in WIND_BEARING_MAP:
|
||||
return WIND_BEARING_MAP[val]
|
||||
_LOGGER.error("%s not found in Wind Bearing map", val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_bearing_day(day_data):
|
||||
"""Get wind bearing (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_DIRECTION
|
||||
)
|
||||
if val in WIND_BEARING_MAP:
|
||||
return WIND_BEARING_MAP[val]
|
||||
_LOGGER.error("%s not found in Wind Bearing map", val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_max_speed(day_data, hour):
|
||||
"""Get wind max speed from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_WIND_GUST], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_speed(day_data, hour):
|
||||
"""Get wind speed (hour) from weather data."""
|
||||
val = get_forecast_hour_value(
|
||||
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_SPEED
|
||||
)[0]
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_speed_day(day_data):
|
||||
"""Get wind speed (day) from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_SPEED)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AemetWeather:
|
||||
"""Class to harmonize weather data model."""
|
||||
|
||||
daily: dict = field(default_factory=dict)
|
||||
hourly: dict = field(default_factory=dict)
|
||||
station: dict = field(default_factory=dict)
|
@ -1,6 +1,4 @@
|
||||
"""Config flow to configure Agent devices."""
|
||||
import logging
|
||||
|
||||
from agent import AgentConnectionError, AgentError
|
||||
from agent.a import Agent
|
||||
import voluptuous as vol
|
||||
@ -13,7 +11,6 @@ from .const import DOMAIN, SERVER_URL # pylint:disable=unused-import
|
||||
from .helpers import generate_url
|
||||
|
||||
DEFAULT_PORT = 8090
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
@ -4,7 +4,8 @@
|
||||
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
|
||||
"already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
@ -4,6 +4,7 @@
|
||||
"already_configured": "Apparaat is al geconfigureerd"
|
||||
},
|
||||
"error": {
|
||||
"already_in_progress": "De configuratiestroom is al aan de gang",
|
||||
"cannot_connect": "Kan geen verbinding maken"
|
||||
},
|
||||
"step": {
|
||||
|
@ -2,7 +2,10 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
@ -13,7 +16,6 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AQI = "air_quality_index"
|
||||
ATTR_ATTRIBUTION = "attribution"
|
||||
ATTR_CO2 = "carbon_dioxide"
|
||||
ATTR_CO = "carbon_monoxide"
|
||||
ATTR_N2O = "nitrogen_oxide"
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""The Airly component."""
|
||||
"""The Airly integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
@ -19,14 +19,13 @@ from .const import (
|
||||
ATTR_API_PM25,
|
||||
ATTR_API_PM25_LIMIT,
|
||||
ATTR_API_PM25_PERCENT,
|
||||
ATTRIBUTION,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
LABEL_ADVICE,
|
||||
MANUFACTURER,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by Airly"
|
||||
|
||||
LABEL_ADVICE = "advice"
|
||||
LABEL_AQI_DESCRIPTION = f"{ATTR_AQI}_description"
|
||||
LABEL_AQI_LEVEL = f"{ATTR_AQI}_level"
|
||||
LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit"
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Constants for Airly integration."""
|
||||
|
||||
ATTR_API_ADVICE = "ADVICE"
|
||||
ATTR_API_CAQI = "CAQI"
|
||||
ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION"
|
||||
@ -13,9 +14,15 @@ ATTR_API_PM25_LIMIT = "PM25_LIMIT"
|
||||
ATTR_API_PM25_PERCENT = "PM25_PERCENT"
|
||||
ATTR_API_PRESSURE = "PRESSURE"
|
||||
ATTR_API_TEMPERATURE = "TEMPERATURE"
|
||||
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
ATTRIBUTION = "Data provided by Airly"
|
||||
CONF_USE_NEAREST = "use_nearest"
|
||||
DEFAULT_NAME = "Airly"
|
||||
DOMAIN = "airly"
|
||||
LABEL_ADVICE = "advice"
|
||||
MANUFACTURER = "Airly sp. z o.o."
|
||||
MAX_REQUESTS_PER_DAY = 100
|
||||
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."
|
||||
|
@ -2,6 +2,7 @@
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONF_NAME,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
@ -18,17 +19,14 @@ from .const import (
|
||||
ATTR_API_PM1,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_LABEL,
|
||||
ATTR_UNIT,
|
||||
ATTRIBUTION,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by Airly"
|
||||
|
||||
ATTR_ICON = "icon"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SENSOR_TYPES = {
|
||||
|
@ -22,7 +22,9 @@
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "Servidor d'Airly accessible"
|
||||
"can_reach_server": "Servidor d'Airly accessible",
|
||||
"requests_per_day": "Sol\u00b7licituds per dia permeses",
|
||||
"requests_remaining": "Sol\u00b7licituds permeses restants"
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,9 @@
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "Reach Airly server"
|
||||
"can_reach_server": "Reach Airly server",
|
||||
"requests_per_day": "Allowed requests per day",
|
||||
"requests_remaining": "Remaining allowed requests"
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,9 @@
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "\u00dchendus Airly serveriga"
|
||||
"can_reach_server": "\u00dchendus Airly serveriga",
|
||||
"requests_per_day": "Lubatud taotlusi p\u00e4evas",
|
||||
"requests_remaining": "J\u00e4\u00e4nud lubatud taotlusi"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc774 \uc88c\ud45c\uc5d0 \ub300\ud55c Airly \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
"already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||
"wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"step": {
|
||||
@ -12,7 +13,7 @@
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4",
|
||||
"name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984"
|
||||
"name": "\uc774\ub984"
|
||||
},
|
||||
"description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694",
|
||||
"title": "Airly"
|
||||
|
@ -22,7 +22,9 @@
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly"
|
||||
"can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly",
|
||||
"requests_per_day": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0434\u0435\u043d\u044c",
|
||||
"requests_remaining": "\u0421\u0447\u0451\u0442\u0447\u0438\u043a \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432"
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,9 @@
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668"
|
||||
"can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668",
|
||||
"requests_per_day": "\u6bcf\u65e5\u5141\u8a31\u7684\u8acb\u6c42",
|
||||
"requests_remaining": "\u5176\u9918\u5141\u8a31\u7684\u8acb\u6c42"
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
)
|
||||
@ -20,7 +21,6 @@ from .const import (
|
||||
|
||||
ATTRIBUTION = "Data provided by AirNow"
|
||||
|
||||
ATTR_ICON = "icon"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
|
21
homeassistant/components/airnow/translations/ko.json
Normal file
21
homeassistant/components/airnow/translations/ko.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
homeassistant/components/airnow/translations/nl.json
Normal file
24
homeassistant/components/airnow/translations/nl.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Apparaat is al geconfigureerd"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken",
|
||||
"invalid_auth": "Ongeldige authenticatie",
|
||||
"invalid_location": "Geen resultaten gevonden voor die locatie",
|
||||
"unknown": "Onverwachte fout"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel",
|
||||
"latitude": "Breedtegraad",
|
||||
"longitude": "Lengtegraad"
|
||||
},
|
||||
"title": "AirNow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "AirNow"
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
|
||||
"invalid_location": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.",
|
||||
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
|
||||
},
|
||||
|
@ -23,6 +23,7 @@ from homeassistant.const import (
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
@ -37,7 +38,7 @@ from .const import (
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
LOGGER,
|
||||
)
|
||||
@ -145,7 +146,7 @@ def _standardize_geography_config_entry(hass, config_entry):
|
||||
# If the config entry data doesn't contain the integration type, add it:
|
||||
entry_updates["data"] = {
|
||||
**config_entry.data,
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
}
|
||||
|
||||
if not entry_updates:
|
||||
@ -232,7 +233,6 @@ async def async_setup_entry(hass, config_entry):
|
||||
update_method=async_update_data,
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||
async_sync_geo_coordinator_update_intervals(
|
||||
hass, config_entry.data[CONF_API_KEY]
|
||||
)
|
||||
@ -262,9 +262,11 @@ async def async_setup_entry(hass, config_entry):
|
||||
update_method=async_update_data,
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||
|
||||
await coordinator.async_refresh()
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
@ -299,10 +301,14 @@ async def async_migrate_entry(hass, config_entry):
|
||||
|
||||
# For any geographies that remain, create a new config entry for each one:
|
||||
for geography in geographies:
|
||||
if CONF_LATITUDE in geography:
|
||||
source = "geography_by_coords"
|
||||
else:
|
||||
source = "geography_by_name"
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": "geography"},
|
||||
context={"source": source},
|
||||
data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **geography},
|
||||
)
|
||||
)
|
||||
@ -327,7 +333,10 @@ async def async_unload_entry(hass, config_entry):
|
||||
remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id)
|
||||
remove_listener()
|
||||
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||
if (
|
||||
config_entry.data[CONF_INTEGRATION_TYPE]
|
||||
== INTEGRATION_TYPE_GEOGRAPHY_COORDS
|
||||
):
|
||||
# Re-calculate the update interval period for any remaining consumers of
|
||||
# this API key:
|
||||
async_sync_geo_coordinator_update_intervals(
|
||||
|
@ -2,7 +2,12 @@
|
||||
import asyncio
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import InvalidKeyError, NodeProError
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
InvalidKeyError,
|
||||
NodeProError,
|
||||
NotFoundError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@ -13,20 +18,46 @@ from homeassistant.const import (
|
||||
CONF_LONGITUDE,
|
||||
CONF_PASSWORD,
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
from . import async_get_geography_id
|
||||
from .const import ( # pylint: disable=unused-import
|
||||
CONF_GEOGRAPHIES,
|
||||
CONF_CITY,
|
||||
CONF_COUNTRY,
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
API_KEY_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string})
|
||||
GEOGRAPHY_NAME_SCHEMA = API_KEY_DATA_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_CITY): cv.string,
|
||||
vol.Required(CONF_STATE): cv.string,
|
||||
vol.Required(CONF_COUNTRY): cv.string,
|
||||
}
|
||||
)
|
||||
NODE_PRO_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): cv.string}
|
||||
)
|
||||
PICK_INTEGRATION_TYPE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("type"): vol.In(
|
||||
[
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle an AirVisual config flow."""
|
||||
@ -36,16 +67,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the config flow."""
|
||||
self._entry_data_for_reauth = None
|
||||
self._geo_id = None
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
|
||||
self.api_key_data_schema = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
|
||||
@property
|
||||
def geography_schema(self):
|
||||
def geography_coords_schema(self):
|
||||
"""Return the data schema for the cloud API."""
|
||||
return self.api_key_data_schema.extend(
|
||||
return API_KEY_DATA_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_LATITUDE, default=self.hass.config.latitude
|
||||
@ -56,23 +84,71 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def pick_integration_type_schema(self):
|
||||
"""Return the data schema for picking the integration type."""
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required("type"): vol.In(
|
||||
[INTEGRATION_TYPE_GEOGRAPHY, INTEGRATION_TYPE_NODE_PRO]
|
||||
)
|
||||
}
|
||||
async def _async_finish_geography(self, user_input, integration_type):
|
||||
"""Validate a Cloud API key."""
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
|
||||
|
||||
# If this is the first (and only the first) time we've seen this API key, check
|
||||
# that it's valid:
|
||||
valid_keys = self.hass.data.setdefault("airvisual_checked_api_keys", set())
|
||||
valid_keys_lock = self.hass.data.setdefault(
|
||||
"airvisual_checked_api_keys_lock", asyncio.Lock()
|
||||
)
|
||||
|
||||
@property
|
||||
def node_pro_schema(self):
|
||||
"""Return the data schema for a Node/Pro."""
|
||||
return vol.Schema(
|
||||
{vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): str}
|
||||
if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
|
||||
coro = cloud_api.air_quality.nearest_city()
|
||||
error_schema = self.geography_coords_schema
|
||||
error_step = "geography_by_coords"
|
||||
else:
|
||||
coro = cloud_api.air_quality.city(
|
||||
user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY]
|
||||
)
|
||||
error_schema = GEOGRAPHY_NAME_SCHEMA
|
||||
error_step = "geography_by_name"
|
||||
|
||||
async with valid_keys_lock:
|
||||
if user_input[CONF_API_KEY] not in valid_keys:
|
||||
try:
|
||||
await coro
|
||||
except InvalidKeyError:
|
||||
return self.async_show_form(
|
||||
step_id=error_step,
|
||||
data_schema=error_schema,
|
||||
errors={CONF_API_KEY: "invalid_api_key"},
|
||||
)
|
||||
except NotFoundError:
|
||||
return self.async_show_form(
|
||||
step_id=error_step,
|
||||
data_schema=error_schema,
|
||||
errors={CONF_CITY: "location_not_found"},
|
||||
)
|
||||
except AirVisualError as err:
|
||||
LOGGER.error(err)
|
||||
return self.async_show_form(
|
||||
step_id=error_step,
|
||||
data_schema=error_schema,
|
||||
errors={"base": "unknown"},
|
||||
)
|
||||
|
||||
valid_keys.add(user_input[CONF_API_KEY])
|
||||
|
||||
existing_entry = await self.async_set_unique_id(self._geo_id)
|
||||
if existing_entry:
|
||||
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Cloud API ({self._geo_id})",
|
||||
data={**user_input, CONF_INTEGRATION_TYPE: integration_type},
|
||||
)
|
||||
|
||||
async def _async_init_geography(self, user_input, integration_type):
|
||||
"""Handle the initialization of the integration via the cloud API."""
|
||||
self._geo_id = async_get_geography_id(user_input)
|
||||
await self._async_set_unique_id(self._geo_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self._async_finish_geography(user_input, integration_type)
|
||||
|
||||
async def _async_set_unique_id(self, unique_id):
|
||||
"""Set the unique ID of the config flow and abort if it already exists."""
|
||||
@ -85,73 +161,32 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Define the config flow to handle options."""
|
||||
return AirVisualOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_geography(self, user_input=None):
|
||||
"""Handle the initialization of the integration via the cloud API."""
|
||||
async def async_step_geography_by_coords(self, user_input=None):
|
||||
"""Handle the initialization of the cloud API based on latitude/longitude."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="geography", data_schema=self.geography_schema
|
||||
step_id="geography_by_coords", data_schema=self.geography_coords_schema
|
||||
)
|
||||
|
||||
self._geo_id = async_get_geography_id(user_input)
|
||||
await self._async_set_unique_id(self._geo_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Find older config entries without unique ID:
|
||||
for entry in self._async_current_entries():
|
||||
if entry.version != 1:
|
||||
continue
|
||||
|
||||
if any(
|
||||
self._geo_id == async_get_geography_id(geography)
|
||||
for geography in entry.data[CONF_GEOGRAPHIES]
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
return await self.async_step_geography_finish(
|
||||
user_input, "geography", self.geography_schema
|
||||
return await self._async_init_geography(
|
||||
user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS
|
||||
)
|
||||
|
||||
async def async_step_geography_finish(self, user_input, error_step, error_schema):
|
||||
"""Validate a Cloud API key."""
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
|
||||
|
||||
# If this is the first (and only the first) time we've seen this API key, check
|
||||
# that it's valid:
|
||||
valid_keys = self.hass.data.setdefault("airvisual_checked_api_keys", set())
|
||||
valid_keys_lock = self.hass.data.setdefault(
|
||||
"airvisual_checked_api_keys_lock", asyncio.Lock()
|
||||
)
|
||||
|
||||
async with valid_keys_lock:
|
||||
if user_input[CONF_API_KEY] not in valid_keys:
|
||||
try:
|
||||
await cloud_api.air_quality.nearest_city()
|
||||
except InvalidKeyError:
|
||||
async def async_step_geography_by_name(self, user_input=None):
|
||||
"""Handle the initialization of the cloud API based on city/state/country."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id=error_step,
|
||||
data_schema=error_schema,
|
||||
errors={CONF_API_KEY: "invalid_api_key"},
|
||||
step_id="geography_by_name", data_schema=GEOGRAPHY_NAME_SCHEMA
|
||||
)
|
||||
|
||||
valid_keys.add(user_input[CONF_API_KEY])
|
||||
|
||||
existing_entry = await self.async_set_unique_id(self._geo_id)
|
||||
if existing_entry:
|
||||
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Cloud API ({self._geo_id})",
|
||||
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
|
||||
return await self._async_init_geography(
|
||||
user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME
|
||||
)
|
||||
|
||||
async def async_step_node_pro(self, user_input=None):
|
||||
"""Handle the initialization of the integration with a Node/Pro."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="node_pro", data_schema=self.node_pro_schema
|
||||
)
|
||||
return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA)
|
||||
|
||||
await self._async_set_unique_id(user_input[CONF_IP_ADDRESS])
|
||||
|
||||
@ -163,7 +198,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.error("Error connecting to Node/Pro unit: %s", err)
|
||||
return self.async_show_form(
|
||||
step_id="node_pro",
|
||||
data_schema=self.node_pro_schema,
|
||||
data_schema=NODE_PRO_SCHEMA,
|
||||
errors={CONF_IP_ADDRESS: "cannot_connect"},
|
||||
)
|
||||
|
||||
@ -176,39 +211,34 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_reauth(self, data):
|
||||
"""Handle configuration by re-auth."""
|
||||
self._entry_data_for_reauth = data
|
||||
self._geo_id = async_get_geography_id(data)
|
||||
self._latitude = data[CONF_LATITUDE]
|
||||
self._longitude = data[CONF_LONGITUDE]
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input=None):
|
||||
"""Handle re-auth completion."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", data_schema=self.api_key_data_schema
|
||||
step_id="reauth_confirm", data_schema=API_KEY_DATA_SCHEMA
|
||||
)
|
||||
|
||||
conf = {
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
CONF_LATITUDE: self._latitude,
|
||||
CONF_LONGITUDE: self._longitude,
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
|
||||
}
|
||||
conf = {CONF_API_KEY: user_input[CONF_API_KEY], **self._entry_data_for_reauth}
|
||||
|
||||
return await self.async_step_geography_finish(
|
||||
conf, "reauth_confirm", self.api_key_data_schema
|
||||
return await self._async_finish_geography(
|
||||
conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE]
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the start of the config flow."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=self.pick_integration_type_schema
|
||||
step_id="user", data_schema=PICK_INTEGRATION_TYPE_SCHEMA
|
||||
)
|
||||
|
||||
if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||
return await self.async_step_geography()
|
||||
if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
|
||||
return await self.async_step_geography_by_coords()
|
||||
if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_NAME:
|
||||
return await self.async_step_geography_by_name()
|
||||
return await self.async_step_node_pro()
|
||||
|
||||
|
||||
|
@ -4,7 +4,8 @@ import logging
|
||||
DOMAIN = "airvisual"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
INTEGRATION_TYPE_GEOGRAPHY = "Geographical Location"
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS = "Geographical Location by Latitude/Longitude"
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME = "Geographical Location by Name"
|
||||
INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro"
|
||||
|
||||
CONF_CITY = "city"
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""Support for AirVisual air quality sensors."""
|
||||
from logging import getLogger
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
@ -27,11 +25,10 @@ from .const import (
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
)
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
ATTR_CITY = "city"
|
||||
ATTR_COUNTRY = "country"
|
||||
ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
|
||||
@ -58,14 +55,23 @@ NODE_PRO_SENSORS = [
|
||||
(SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS),
|
||||
]
|
||||
|
||||
POLLUTANT_MAPPING = {
|
||||
"co": {"label": "Carbon Monoxide", "unit": CONCENTRATION_PARTS_PER_MILLION},
|
||||
"n2": {"label": "Nitrogen Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||
"o3": {"label": "Ozone", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||
"p1": {"label": "PM10", "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
"p2": {"label": "PM2.5", "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
"s2": {"label": "Sulfur Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION},
|
||||
}
|
||||
|
||||
@callback
|
||||
def async_get_pollutant_label(symbol):
|
||||
"""Get a pollutant's label based on its symbol."""
|
||||
if symbol == "co":
|
||||
return "Carbon Monoxide"
|
||||
if symbol == "n2":
|
||||
return "Nitrogen Dioxide"
|
||||
if symbol == "o3":
|
||||
return "Ozone"
|
||||
if symbol == "p1":
|
||||
return "PM10"
|
||||
if symbol == "p2":
|
||||
return "PM2.5"
|
||||
if symbol == "s2":
|
||||
return "Sulfur Dioxide"
|
||||
return symbol
|
||||
|
||||
|
||||
@callback
|
||||
@ -84,11 +90,32 @@ def async_get_pollutant_level_info(value):
|
||||
return ("Hazardous", "mdi:biohazard")
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pollutant_unit(symbol):
|
||||
"""Get a pollutant's unit based on its symbol."""
|
||||
if symbol == "co":
|
||||
return CONCENTRATION_PARTS_PER_MILLION
|
||||
if symbol == "n2":
|
||||
return CONCENTRATION_PARTS_PER_BILLION
|
||||
if symbol == "o3":
|
||||
return CONCENTRATION_PARTS_PER_BILLION
|
||||
if symbol == "p1":
|
||||
return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
if symbol == "p2":
|
||||
return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
if symbol == "s2":
|
||||
return CONCENTRATION_PARTS_PER_BILLION
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AirVisual sensors based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
|
||||
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] in [
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
]:
|
||||
sensors = [
|
||||
AirVisualGeographySensor(
|
||||
coordinator,
|
||||
@ -173,23 +200,38 @@ class AirVisualGeographySensor(AirVisualEntity):
|
||||
self._state = data[f"aqi{self._locale}"]
|
||||
elif self._kind == SENSOR_KIND_POLLUTANT:
|
||||
symbol = data[f"main{self._locale}"]
|
||||
self._state = POLLUTANT_MAPPING[symbol]["label"]
|
||||
self._state = async_get_pollutant_label(symbol)
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_POLLUTANT_SYMBOL: symbol,
|
||||
ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]["unit"],
|
||||
ATTR_POLLUTANT_UNIT: async_get_pollutant_unit(symbol),
|
||||
}
|
||||
)
|
||||
|
||||
if CONF_LATITUDE in self._config_entry.data:
|
||||
# Displaying the geography on the map relies upon putting the latitude/longitude
|
||||
# in the entity attributes with "latitude" and "longitude" as the keys.
|
||||
# Conversely, we can hide the location on the map by using other keys, like
|
||||
# "lati" and "long".
|
||||
#
|
||||
# We use any coordinates in the config entry and, in the case of a geography by
|
||||
# name, we fall back to the latitude longitude provided in the coordinator data:
|
||||
latitude = self._config_entry.data.get(
|
||||
CONF_LATITUDE,
|
||||
self.coordinator.data["location"]["coordinates"][1],
|
||||
)
|
||||
longitude = self._config_entry.data.get(
|
||||
CONF_LONGITUDE,
|
||||
self.coordinator.data["location"]["coordinates"][0],
|
||||
)
|
||||
|
||||
if self._config_entry.options[CONF_SHOW_ON_MAP]:
|
||||
self._attrs[ATTR_LATITUDE] = self._config_entry.data[CONF_LATITUDE]
|
||||
self._attrs[ATTR_LONGITUDE] = self._config_entry.data[CONF_LONGITUDE]
|
||||
self._attrs[ATTR_LATITUDE] = latitude
|
||||
self._attrs[ATTR_LONGITUDE] = longitude
|
||||
self._attrs.pop("lati", None)
|
||||
self._attrs.pop("long", None)
|
||||
else:
|
||||
self._attrs["lati"] = self._config_entry.data[CONF_LATITUDE]
|
||||
self._attrs["long"] = self._config_entry.data[CONF_LONGITUDE]
|
||||
self._attrs["lati"] = latitude
|
||||
self._attrs["long"] = longitude
|
||||
self._attrs.pop(ATTR_LATITUDE, None)
|
||||
self._attrs.pop(ATTR_LONGITUDE, None)
|
||||
|
||||
|
@ -1,15 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"geography": {
|
||||
"geography_by_coords": {
|
||||
"title": "Configure a Geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a geographical location.",
|
||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]"
|
||||
}
|
||||
},
|
||||
"geography_by_name": {
|
||||
"title": "Configure a Geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a city/state/country.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"state": "state"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"title": "Configure an AirVisual Node/Pro",
|
||||
"description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.",
|
||||
@ -26,17 +36,13 @@
|
||||
},
|
||||
"user": {
|
||||
"title": "Configure AirVisual",
|
||||
"description": "Pick what type of AirVisual data you want to monitor.",
|
||||
"data": {
|
||||
"cloud_api": "Geographical Location",
|
||||
"node_pro": "AirVisual Node Pro",
|
||||
"type": "Integration Type"
|
||||
}
|
||||
"description": "Pick what type of AirVisual data you want to monitor."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"general_error": "[%key:common::config_flow::error::unknown%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"location_not_found": "Location not found",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
|
@ -17,6 +17,20 @@
|
||||
"longitude": "Zem\u011bpisn\u00e1 d\u00e9lka"
|
||||
}
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Kl\u00ed\u010d API",
|
||||
"latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
|
||||
"longitude": "Zem\u011bpisn\u00e1 d\u00e9lka"
|
||||
}
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Kl\u00ed\u010d API",
|
||||
"city": "M\u011bsto",
|
||||
"country": "Zem\u011b"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Hostitel",
|
||||
|
@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"general_error": "Unerwarteter Fehler",
|
||||
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
|
||||
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel",
|
||||
"location_not_found": "Standort nicht gefunden"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -16,8 +17,28 @@
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "L\u00e4ngengrad"
|
||||
},
|
||||
"description": "Verwende die AirVisual Cloud API, um einen geografischen Standort zu \u00fcberwachen.",
|
||||
"title": "Konfigurieren Sie eine Geografie"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "API-Schl\u00fcssel",
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "L\u00e4ngengrad"
|
||||
},
|
||||
"description": "Verwende die AirVisual Cloud API, um einen L\u00e4ngengrad/Breitengrad zu \u00fcberwachen.",
|
||||
"title": "Konfiguriere einen Standort"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "API-Schl\u00fcssel",
|
||||
"city": "Stadt",
|
||||
"country": "Land",
|
||||
"state": "Bundesland"
|
||||
},
|
||||
"description": "Verwende die AirVisual Cloud API, um ein(e) Stadt/Bundesland/Land zu \u00fcberwachen.",
|
||||
"title": "Konfiguriere einen Standort"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
@ -29,7 +50,8 @@
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API-Key"
|
||||
}
|
||||
},
|
||||
"title": "AirVisual erneut authentifizieren"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"general_error": "Unexpected error",
|
||||
"invalid_api_key": "Invalid API key"
|
||||
"invalid_api_key": "Invalid API key",
|
||||
"location_not_found": "Location not found"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -25,7 +26,17 @@
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
},
|
||||
"description": "Use the AirVisual cloud API to monitor a geographical location.",
|
||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||
"title": "Configure a Geography"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"state": "state"
|
||||
},
|
||||
"description": "Use the AirVisual cloud API to monitor a city/state/country.",
|
||||
"title": "Configure a Geography"
|
||||
},
|
||||
"node_pro": {
|
||||
|
@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar",
|
||||
"general_error": "Se ha producido un error desconocido.",
|
||||
"invalid_api_key": "Se proporciona una clave API no v\u00e1lida."
|
||||
"invalid_api_key": "Se proporciona una clave API no v\u00e1lida.",
|
||||
"location_not_found": "Ubicaci\u00f3n no encontrada"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -19,6 +20,25 @@
|
||||
"description": "Utilizar la API en la nube de AirVisual para monitorizar una ubicaci\u00f3n geogr\u00e1fica.",
|
||||
"title": "Configurar una Geograf\u00eda"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud"
|
||||
},
|
||||
"description": "Utilice la API de la nube de AirVisual para supervisar una latitud/longitud.",
|
||||
"title": "Configurar una geograf\u00eda"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"city": "Ciudad",
|
||||
"country": "Pa\u00eds",
|
||||
"state": "estado"
|
||||
},
|
||||
"description": "Utilice la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.",
|
||||
"title": "Configurar una geograf\u00eda"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Direcci\u00f3n IP/Nombre de host de la Unidad",
|
||||
|
@ -17,7 +17,7 @@
|
||||
"latitude": "Laiuskraad",
|
||||
"longitude": "Pikkuskraad"
|
||||
},
|
||||
"description": "Kasutage AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.",
|
||||
"description": "Kasuta AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.",
|
||||
"title": "Seadista Geography"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
|
@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"general_error": "Erreur inattendue",
|
||||
"invalid_api_key": "Cl\u00e9 API invalide"
|
||||
"invalid_api_key": "Cl\u00e9 API invalide",
|
||||
"location_not_found": "Emplacement introuvable"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -19,6 +20,25 @@
|
||||
"description": "Utilisez l'API cloud AirVisual pour surveiller une position g\u00e9ographique.",
|
||||
"title": "Configurer une g\u00e9ographie"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Clef d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
},
|
||||
"description": "Utilisez l'API cloud AirVisual pour surveiller une latitude / longitude.",
|
||||
"title": "Configurer un lieu g\u00e9ographique"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Clef d'API",
|
||||
"city": "Ville",
|
||||
"country": "Pays",
|
||||
"state": "Etat"
|
||||
},
|
||||
"description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.",
|
||||
"title": "Configurer un lieu g\u00e9ographique"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "H\u00f4te",
|
||||
|
@ -2,12 +2,13 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "La posizione \u00e8 gi\u00e0 configurata o Node/Pro ID sono gi\u00e0 registrati.",
|
||||
"reauth_successful": "La riautenticazione ha avuto successo"
|
||||
"reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi",
|
||||
"general_error": "Errore imprevisto",
|
||||
"invalid_api_key": "Chiave API non valida"
|
||||
"invalid_api_key": "Chiave API non valida",
|
||||
"location_not_found": "Posizione non trovata"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -19,6 +20,25 @@
|
||||
"description": "Utilizzare l'API di AirVisual cloud per monitorare una posizione geografica.",
|
||||
"title": "Configurare una Geografia"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Chiave API",
|
||||
"latitude": "Latitudine",
|
||||
"longitude": "Logitudine"
|
||||
},
|
||||
"description": "Usa l'API cloud di AirVisual per monitorare una latitudine/longitudine.",
|
||||
"title": "Configurare un'area geografica"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Chiave API",
|
||||
"city": "Citt\u00e0",
|
||||
"country": "Nazione",
|
||||
"state": "Stato"
|
||||
},
|
||||
"description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.",
|
||||
"title": "Configurare un'area geografica"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
|
@ -1,11 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc88c\ud45c\uac12 \ub610\ub294 Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
"already_configured": "Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uac70\ub098 \uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
|
||||
"reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"general_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"general_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -17,14 +19,31 @@
|
||||
"description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc9c0\ub9ac\uc801 \uc704\uce58\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.",
|
||||
"title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4"
|
||||
}
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\uae30\uae30 IP \uc8fc\uc18c/\ud638\uc2a4\ud2b8 \uc774\ub984",
|
||||
"ip_address": "\ud638\uc2a4\ud2b8",
|
||||
"password": "\ube44\ubc00\ubc88\ud638"
|
||||
},
|
||||
"description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
|
||||
"title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"cloud_api": "\uc9c0\ub9ac\uc801 \uc704\uce58",
|
||||
|
@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "Feeler beim verbannen",
|
||||
"general_error": "Onerwaarte Feeler",
|
||||
"invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel"
|
||||
"invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel",
|
||||
"location_not_found": "Standuert net fonnt."
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -19,6 +20,12 @@
|
||||
"description": "Benotz Airvisual cloud API fir eng geografescher Lag z'iwwerwaachen.",
|
||||
"title": "Geografie ariichten"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"city": "Stad",
|
||||
"country": "Land"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
|
@ -1,12 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd."
|
||||
"already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd.",
|
||||
"reauth_successful": "Herauthenticatie was succesvol"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken",
|
||||
"general_error": "Er is een onbekende fout opgetreden.",
|
||||
"invalid_api_key": "Ongeldige API-sleutel"
|
||||
"invalid_api_key": "Ongeldige API-sleutel",
|
||||
"location_not_found": "Locatie niet gevonden"
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -18,6 +20,21 @@
|
||||
"description": "Gebruik de AirVisual cloud API om een geografische locatie te bewaken.",
|
||||
"title": "Configureer een geografie"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel",
|
||||
"latitude": "Breedtegraad",
|
||||
"longitude": "Lengtegraad"
|
||||
}
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel",
|
||||
"city": "Stad",
|
||||
"country": "Land"
|
||||
},
|
||||
"description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken."
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "IP adres/hostname van component",
|
||||
|
@ -7,7 +7,8 @@
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
|
||||
"general_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
|
||||
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
|
||||
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
|
||||
"location_not_found": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e."
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
@ -19,6 +20,25 @@
|
||||
"description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e API AirVisual.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "\u041a\u043b\u044e\u0447 API",
|
||||
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
|
||||
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430"
|
||||
},
|
||||
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "\u041a\u043b\u044e\u0447 API",
|
||||
"city": "\u0413\u043e\u0440\u043e\u0434",
|
||||
"country": "\u0421\u0442\u0440\u0430\u043d\u0430",
|
||||
"state": "\u0448\u0442\u0430\u0442"
|
||||
},
|
||||
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u0425\u043e\u0441\u0442",
|
||||
|
@ -21,7 +21,8 @@
|
||||
"data": {
|
||||
"cloud_api": "Geografisk Plats",
|
||||
"type": "Integrationstyp"
|
||||
}
|
||||
},
|
||||
"title": "Konfigurera AirVisual"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,14 +132,12 @@ async def async_attach_trigger(
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
from_state = None
|
||||
|
||||
if config[CONF_TYPE] == "triggered":
|
||||
to_state = STATE_ALARM_TRIGGERED
|
||||
elif config[CONF_TYPE] == "disarmed":
|
||||
to_state = STATE_ALARM_DISARMED
|
||||
elif config[CONF_TYPE] == "arming":
|
||||
from_state = STATE_ALARM_DISARMED
|
||||
to_state = STATE_ALARM_ARMING
|
||||
elif config[CONF_TYPE] == "armed_home":
|
||||
to_state = STATE_ALARM_ARMED_HOME
|
||||
@ -153,8 +151,6 @@ async def async_attach_trigger(
|
||||
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
|
||||
state_trigger.CONF_TO: to_state,
|
||||
}
|
||||
if from_state:
|
||||
state_config[state_trigger.CONF_FROM] = from_state
|
||||
state_config = state_trigger.TRIGGER_SCHEMA(state_config)
|
||||
return await state_trigger.async_attach_trigger(
|
||||
hass, state_config, action, automation_info, platform_type="device"
|
||||
|
@ -1,61 +1,74 @@
|
||||
# Describes the format for available alarm control panel services
|
||||
|
||||
alarm_disarm:
|
||||
name: Disarm
|
||||
description: Send the alarm the command for disarm.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to disarm.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to disarm the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_arm_custom_bypass:
|
||||
name: Arm with custom bypass
|
||||
description: Send arm custom bypass command.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm custom bypass.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
description: An optional code to arm custom bypass the alarm control panel with.
|
||||
name: Code
|
||||
description:
|
||||
An optional code to arm custom bypass the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_arm_home:
|
||||
name: Arm home
|
||||
description: Send the alarm the command for arm home.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm home.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to arm home the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_arm_away:
|
||||
name: Arm away
|
||||
description: Send the alarm the command for arm away.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm away.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to arm away the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_arm_night:
|
||||
name: Arm night
|
||||
description: Send the alarm the command for arm night.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm night.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to arm night the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
||||
alarm_trigger:
|
||||
name: Trigger
|
||||
description: Send the alarm the command for trigger.
|
||||
target:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to trigger.
|
||||
example: "alarm_control_panel.downstairs"
|
||||
code:
|
||||
name: Code
|
||||
description: An optional code to trigger the alarm control panel with.
|
||||
example: "1234"
|
||||
selector:
|
||||
text:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "alarmdecoder",
|
||||
"name": "AlarmDecoder",
|
||||
"documentation": "https://www.home-assistant.io/integrations/alarmdecoder",
|
||||
"requirements": ["adext==0.3"],
|
||||
"requirements": ["adext==0.4.1"],
|
||||
"codeowners": ["@ajschmidt8"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "AlarmDecoder\uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"protocol": {
|
||||
"data": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "AlarmDecoder-apparaat is al geconfigureerd."
|
||||
"already_configured": "Apparaat is al geconfigureerd"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Succesvol verbonden met AlarmDecoder."
|
||||
|
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