mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 08:47:09 +00:00
Merge pull request #51370 from home-assistant/rc
This commit is contained in:
commit
51704d151d
59
.coveragerc
59
.coveragerc
@ -8,8 +8,10 @@ omit =
|
||||
homeassistant/scripts/*.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/acer_projector/switch.py
|
||||
homeassistant/components/acer_projector/*
|
||||
homeassistant/components/actiontec/const.py
|
||||
homeassistant/components/actiontec/device_tracker.py
|
||||
homeassistant/components/actiontec/model.py
|
||||
homeassistant/components/acmeda/__init__.py
|
||||
homeassistant/components/acmeda/base.py
|
||||
homeassistant/components/acmeda/const.py
|
||||
@ -23,9 +25,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/aftership/*
|
||||
homeassistant/components/agent_dvr/__init__.py
|
||||
homeassistant/components/agent_dvr/alarm_control_panel.py
|
||||
homeassistant/components/agent_dvr/camera.py
|
||||
@ -36,14 +37,14 @@ omit =
|
||||
homeassistant/components/airvisual/__init__.py
|
||||
homeassistant/components/airvisual/air_quality.py
|
||||
homeassistant/components/airvisual/sensor.py
|
||||
homeassistant/components/aladdin_connect/cover.py
|
||||
homeassistant/components/aladdin_connect/*
|
||||
homeassistant/components/alarmdecoder/__init__.py
|
||||
homeassistant/components/alarmdecoder/alarm_control_panel.py
|
||||
homeassistant/components/alarmdecoder/binary_sensor.py
|
||||
homeassistant/components/alarmdecoder/const.py
|
||||
homeassistant/components/alarmdecoder/sensor.py
|
||||
homeassistant/components/alpha_vantage/sensor.py
|
||||
homeassistant/components/amazon_polly/tts.py
|
||||
homeassistant/components/amazon_polly/*
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/*
|
||||
homeassistant/components/amcrest/*
|
||||
@ -113,6 +114,10 @@ omit =
|
||||
homeassistant/components/bmw_connected_drive/lock.py
|
||||
homeassistant/components/bmw_connected_drive/notify.py
|
||||
homeassistant/components/bmw_connected_drive/sensor.py
|
||||
homeassistant/components/bosch_shc/__init__.py
|
||||
homeassistant/components/bosch_shc/const.py
|
||||
homeassistant/components/bosch_shc/binary_sensor.py
|
||||
homeassistant/components/bosch_shc/entity.py
|
||||
homeassistant/components/braviatv/__init__.py
|
||||
homeassistant/components/braviatv/const.py
|
||||
homeassistant/components/braviatv/media_player.py
|
||||
@ -304,7 +309,7 @@ omit =
|
||||
homeassistant/components/firmata/pin.py
|
||||
homeassistant/components/firmata/sensor.py
|
||||
homeassistant/components/firmata/switch.py
|
||||
homeassistant/components/fitbit/sensor.py
|
||||
homeassistant/components/fitbit/*
|
||||
homeassistant/components/fixer/sensor.py
|
||||
homeassistant/components/fleetgo/device_tracker.py
|
||||
homeassistant/components/flexit/climate.py
|
||||
@ -330,9 +335,12 @@ omit =
|
||||
homeassistant/components/freebox/sensor.py
|
||||
homeassistant/components/freebox/switch.py
|
||||
homeassistant/components/fritz/__init__.py
|
||||
homeassistant/components/fritz/binary_sensor.py
|
||||
homeassistant/components/fritz/common.py
|
||||
homeassistant/components/fritz/const.py
|
||||
homeassistant/components/fritz/device_tracker.py
|
||||
homeassistant/components/fritz/sensor.py
|
||||
homeassistant/components/fritz/services.py
|
||||
homeassistant/components/fritzbox_callmonitor/__init__.py
|
||||
homeassistant/components/fritzbox_callmonitor/const.py
|
||||
homeassistant/components/fritzbox_callmonitor/base.py
|
||||
@ -342,6 +350,9 @@ omit =
|
||||
homeassistant/components/frontier_silicon/media_player.py
|
||||
homeassistant/components/futurenow/light.py
|
||||
homeassistant/components/garadget/cover.py
|
||||
homeassistant/components/garages_amsterdam/__init__.py
|
||||
homeassistant/components/garages_amsterdam/binary_sensor.py
|
||||
homeassistant/components/garages_amsterdam/sensor.py
|
||||
homeassistant/components/garmin_connect/__init__.py
|
||||
homeassistant/components/garmin_connect/const.py
|
||||
homeassistant/components/garmin_connect/sensor.py
|
||||
@ -358,6 +369,7 @@ omit =
|
||||
homeassistant/components/goalfeed/*
|
||||
homeassistant/components/goalzero/__init__.py
|
||||
homeassistant/components/goalzero/binary_sensor.py
|
||||
homeassistant/components/goalzero/switch.py
|
||||
homeassistant/components/google/*
|
||||
homeassistant/components/google_cloud/tts.py
|
||||
homeassistant/components/google_maps/device_tracker.py
|
||||
@ -371,6 +383,7 @@ omit =
|
||||
homeassistant/components/greenwave/light.py
|
||||
homeassistant/components/group/notify.py
|
||||
homeassistant/components/growatt_server/sensor.py
|
||||
homeassistant/components/growatt_server/__init__.py
|
||||
homeassistant/components/gstreamer/media_player.py
|
||||
homeassistant/components/gtfs/sensor.py
|
||||
homeassistant/components/guardian/__init__.py
|
||||
@ -545,7 +558,6 @@ omit =
|
||||
homeassistant/components/life360/*
|
||||
homeassistant/components/lifx/*
|
||||
homeassistant/components/lifx_cloud/scene.py
|
||||
homeassistant/components/lifx_legacy/light.py
|
||||
homeassistant/components/lightwave/*
|
||||
homeassistant/components/limitlessled/light.py
|
||||
homeassistant/components/linksys_smart/device_tracker.py
|
||||
@ -599,6 +611,9 @@ omit =
|
||||
homeassistant/components/meteo_france/sensor.py
|
||||
homeassistant/components/meteo_france/weather.py
|
||||
homeassistant/components/meteoalarm/*
|
||||
homeassistant/components/meteoclimatic/__init__.py
|
||||
homeassistant/components/meteoclimatic/const.py
|
||||
homeassistant/components/meteoclimatic/weather.py
|
||||
homeassistant/components/metoffice/sensor.py
|
||||
homeassistant/components/metoffice/weather.py
|
||||
homeassistant/components/microsoft/tts.py
|
||||
@ -618,9 +633,6 @@ omit =
|
||||
homeassistant/components/mjpeg/camera.py
|
||||
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/modem_callerid/sensor.py
|
||||
homeassistant/components/motion_blinds/__init__.py
|
||||
homeassistant/components/motion_blinds/const.py
|
||||
@ -656,7 +668,7 @@ omit =
|
||||
homeassistant/components/mystrom/binary_sensor.py
|
||||
homeassistant/components/mystrom/light.py
|
||||
homeassistant/components/mystrom/switch.py
|
||||
homeassistant/components/n26/*
|
||||
homeassistant/components/myq/__init__.py
|
||||
homeassistant/components/nad/media_player.py
|
||||
homeassistant/components/nanoleaf/light.py
|
||||
homeassistant/components/neato/__init__.py
|
||||
@ -703,6 +715,7 @@ omit =
|
||||
homeassistant/components/omnilogic/__init__.py
|
||||
homeassistant/components/omnilogic/common.py
|
||||
homeassistant/components/omnilogic/sensor.py
|
||||
homeassistant/components/omnilogic/switch.py
|
||||
homeassistant/components/ondilo_ico/__init__.py
|
||||
homeassistant/components/ondilo_ico/api.py
|
||||
homeassistant/components/ondilo_ico/const.py
|
||||
@ -862,6 +875,7 @@ omit =
|
||||
homeassistant/components/russound_rnet/media_player.py
|
||||
homeassistant/components/sabnzbd/*
|
||||
homeassistant/components/saj/sensor.py
|
||||
homeassistant/components/samsungtv/bridge.py
|
||||
homeassistant/components/satel_integra/*
|
||||
homeassistant/components/schluter/*
|
||||
homeassistant/components/scrape/sensor.py
|
||||
@ -905,6 +919,11 @@ omit =
|
||||
homeassistant/components/skybeacon/sensor.py
|
||||
homeassistant/components/skybell/*
|
||||
homeassistant/components/slack/notify.py
|
||||
homeassistant/components/sia/__init__.py
|
||||
homeassistant/components/sia/alarm_control_panel.py
|
||||
homeassistant/components/sia/const.py
|
||||
homeassistant/components/sia/hub.py
|
||||
homeassistant/components/sia/utils.py
|
||||
homeassistant/components/sinch/*
|
||||
homeassistant/components/slide/*
|
||||
homeassistant/components/sma/__init__.py
|
||||
@ -931,7 +950,12 @@ omit =
|
||||
homeassistant/components/soma/__init__.py
|
||||
homeassistant/components/soma/cover.py
|
||||
homeassistant/components/soma/sensor.py
|
||||
homeassistant/components/somfy/*
|
||||
homeassistant/components/somfy/__init__.py
|
||||
homeassistant/components/somfy/api.py
|
||||
homeassistant/components/somfy/climate.py
|
||||
homeassistant/components/somfy/cover.py
|
||||
homeassistant/components/somfy/sensor.py
|
||||
homeassistant/components/somfy/switch.py
|
||||
homeassistant/components/somfy_mylink/__init__.py
|
||||
homeassistant/components/somfy_mylink/cover.py
|
||||
homeassistant/components/sonos/*
|
||||
@ -940,7 +964,6 @@ omit =
|
||||
homeassistant/components/speedtestdotnet/*
|
||||
homeassistant/components/spider/*
|
||||
homeassistant/components/splunk/*
|
||||
homeassistant/components/spotcrime/sensor.py
|
||||
homeassistant/components/spotify/__init__.py
|
||||
homeassistant/components/spotify/media_player.py
|
||||
homeassistant/components/spotify/system_health.py
|
||||
@ -964,6 +987,10 @@ omit =
|
||||
homeassistant/components/switchbot/switch.py
|
||||
homeassistant/components/switcher_kis/switch.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthing/__init__.py
|
||||
homeassistant/components/syncthing/sensor.py
|
||||
homeassistant/components/syncthru/__init__.py
|
||||
homeassistant/components/syncthru/binary_sensor.py
|
||||
homeassistant/components/syncthru/sensor.py
|
||||
homeassistant/components/synology_chat/notify.py
|
||||
homeassistant/components/synology_dsm/__init__.py
|
||||
@ -973,6 +1000,10 @@ omit =
|
||||
homeassistant/components/synology_dsm/switch.py
|
||||
homeassistant/components/synology_srm/device_tracker.py
|
||||
homeassistant/components/syslog/notify.py
|
||||
homeassistant/components/system_bridge/__init__.py
|
||||
homeassistant/components/system_bridge/const.py
|
||||
homeassistant/components/system_bridge/binary_sensor.py
|
||||
homeassistant/components/system_bridge/sensor.py
|
||||
homeassistant/components/systemmonitor/sensor.py
|
||||
homeassistant/components/tado/*
|
||||
homeassistant/components/tado/device_tracker.py
|
||||
@ -1033,7 +1064,6 @@ omit =
|
||||
homeassistant/components/toon/switch.py
|
||||
homeassistant/components/torque/sensor.py
|
||||
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
|
||||
@ -1206,7 +1236,6 @@ omit =
|
||||
homeassistant/components/supla/*
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/zwave_js/discovery.py
|
||||
homeassistant/components/zwave_js/light.py
|
||||
homeassistant/components/zwave_js/sensor.py
|
||||
|
||||
[report]
|
||||
|
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -36,19 +36,6 @@
|
||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
|
||||
## Example entry for `configuration.yaml`:
|
||||
<!--
|
||||
Supplying a configuration snippet, makes it easier for a maintainer to test
|
||||
your PR. Furthermore, for new integrations, it gives an impression of how
|
||||
the configuration would look like.
|
||||
Note: Remove this section if this PR does not have an example entry.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
# Example configuration.yaml
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
<!--
|
||||
Details are important, and help maintainers processing your PR.
|
||||
|
28
.github/workflows/builder.yml
vendored
28
.github/workflows/builder.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -54,7 +54,7 @@ jobs:
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
@ -84,7 +84,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
@ -102,20 +102,20 @@ jobs:
|
||||
version="$(python setup.py -V)"
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2021.04.2
|
||||
uses: home-assistant/builder@2021.05.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@ -151,23 +151,23 @@ jobs:
|
||||
- tinker
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2021.04.2
|
||||
uses: home-assistant/builder@2021.05.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@ -182,7 +182,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
@ -214,16 +214,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
40
.github/workflows/ci.yaml
vendored
40
.github/workflows/ci.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v2.2.2
|
||||
@ -84,7 +84,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -124,7 +124,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -164,7 +164,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -207,7 +207,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
@ -226,7 +226,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -269,7 +269,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -312,7 +312,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -352,7 +352,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -395,7 +395,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -436,7 +436,7 @@ jobs:
|
||||
# needs: prepare-base
|
||||
# steps:
|
||||
# - name: Check out code from GitHub
|
||||
# uses: actions/checkout@v2
|
||||
# uses: actions/checkout@v2.3.4
|
||||
# - name: Run ShellCheck
|
||||
# uses: ludeeus/action-shellcheck@0.3.0
|
||||
|
||||
@ -446,7 +446,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -493,7 +493,7 @@ jobs:
|
||||
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.5
|
||||
@ -517,7 +517,7 @@ jobs:
|
||||
needs: prepare-base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
@ -551,7 +551,7 @@ jobs:
|
||||
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Generate partial Python venv restore key
|
||||
id: generate-python-key
|
||||
run: >-
|
||||
@ -595,7 +595,7 @@ jobs:
|
||||
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.5
|
||||
@ -626,7 +626,7 @@ jobs:
|
||||
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.5
|
||||
@ -660,7 +660,7 @@ jobs:
|
||||
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.5
|
||||
@ -718,7 +718,7 @@ jobs:
|
||||
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.5
|
||||
@ -740,4 +740,4 @@ jobs:
|
||||
coverage report --fail-under=94
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1.4.1
|
||||
uses: codecov/codecov-action@v1.5.0
|
||||
|
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.18
|
||||
uses: actions/stale@v3.0.19
|
||||
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.18
|
||||
uses: actions/stale@v3.0.19
|
||||
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.18
|
||||
uses: actions/stale@v3.0.19
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "needs-more-information"
|
||||
|
64
.github/workflows/translations.yaml
vendored
Normal file
64
.github/workflows/translations.yaml
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
name: Translations
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: 3.8
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
||||
python3 -m script.translations upload
|
||||
|
||||
download:
|
||||
name: Download
|
||||
needs: upload
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download Translations
|
||||
run: |
|
||||
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
||||
python3 -m script.translations download
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
with:
|
||||
name: GitHub Action
|
||||
email: github-action@users.noreply.github.com
|
||||
|
||||
- name: Update translation
|
||||
run: |
|
||||
git add homeassistant
|
||||
git commit -am "[ci skip] Translation update"
|
||||
git push
|
168
.github/workflows/wheels.yml
vendored
Normal file
168
.github/workflows/wheels.yml
vendored
Normal file
@ -0,0 +1,168 @@
|
||||
name: Build wheels
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 4 * * *"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
paths:
|
||||
- "requirements.txt"
|
||||
- "requirements_all.txt"
|
||||
|
||||
jobs:
|
||||
init:
|
||||
name: Initialize wheels builder
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
architectures: ${{ steps.info.outputs.architectures }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
uses: home-assistant/actions/helpers/info@master
|
||||
|
||||
- name: Create requirements_diff file
|
||||
run: |
|
||||
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
|
||||
touch requirements_diff.txt
|
||||
else
|
||||
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt
|
||||
fi
|
||||
|
||||
- name: Write env-file
|
||||
run: |
|
||||
(
|
||||
echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false"
|
||||
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
|
||||
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
|
||||
echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true"
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
|
||||
core:
|
||||
name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for core
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
tag:
|
||||
- "3.8-alpine3.12"
|
||||
- "3.9-alpine3.13"
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2021.05.4
|
||||
with:
|
||||
tag: ${{ matrix.tag }}
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-host: wheels.hass.io
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
env-file: true
|
||||
apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev"
|
||||
pip: "Cython;numpy"
|
||||
skip-binary: aiohttp
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: 'requirements_diff.txt'
|
||||
requirements: "requirements.txt"
|
||||
|
||||
integrations:
|
||||
name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for integrations
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
tag:
|
||||
- "3.8-alpine3.12"
|
||||
- "3.9-alpine3.13"
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Uncomment packages
|
||||
run: |
|
||||
requirement_files="requirements_all.txt requirements_diff.txt"
|
||||
for requirement_file in ${requirement_files}; do
|
||||
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
|
||||
sed -i "s|# bluepy|bluepy|g" ${requirement_file}
|
||||
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||
sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file}
|
||||
sed -i "s|# raspihats|raspihats|g" ${requirement_file}
|
||||
sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file}
|
||||
sed -i "s|# blinkt|blinkt|g" ${requirement_file}
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
|
||||
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
|
||||
sed -i "s|# evdev|evdev|g" ${requirement_file}
|
||||
sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file}
|
||||
sed -i "s|# i2csense|i2csense|g" ${requirement_file}
|
||||
sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file}
|
||||
sed -i "s|# pycups|pycups|g" ${requirement_file}
|
||||
sed -i "s|# homekit|homekit|g" ${requirement_file}
|
||||
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
|
||||
sed -i "s|# decora|decora|g" ${requirement_file}
|
||||
sed -i "s|# avion|avion|g" ${requirement_file}
|
||||
sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file}
|
||||
sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file}
|
||||
sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
|
||||
sed -i "s|# bme680|bme680|g" ${requirement_file}
|
||||
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
|
||||
done
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2021.05.4
|
||||
with:
|
||||
tag: ${{ matrix.tag }}
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-host: wheels.hass.io
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
env-file: true
|
||||
apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev"
|
||||
pip: "Cython;numpy;scikit-build"
|
||||
skip-binary: aiohttp
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: 'requirements_diff.txt'
|
||||
requirements: "requirements_all.txt"
|
@ -1,11 +1,11 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.12.0
|
||||
rev: v2.16.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.4b0
|
||||
rev: 21.5b1
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
@ -17,13 +17,13 @@ repos:
|
||||
hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort
|
||||
- --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba
|
||||
- --skip="./.*,*.csv,*.json"
|
||||
- --quiet-level=2
|
||||
exclude_types: [csv, json]
|
||||
exclude: ^tests/fixtures/
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.9.1
|
||||
rev: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
@ -31,7 +31,7 @@ repos:
|
||||
- pyflakes==2.3.1
|
||||
- flake8-docstrings==1.6.0
|
||||
- pydocstyle==6.0.0
|
||||
- flake8-comprehensions==3.4.0
|
||||
- flake8-comprehensions==3.5.0
|
||||
- flake8-noqa==1.1.0
|
||||
- mccabe==0.6.1
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
@ -61,7 +61,7 @@ repos:
|
||||
- --branch=master
|
||||
- --branch=rc
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.24.2
|
||||
rev: v1.26.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
|
@ -3,14 +3,34 @@
|
||||
# to enable strict mypy checks.
|
||||
|
||||
homeassistant.components
|
||||
homeassistant.components.acer_projector.*
|
||||
homeassistant.components.accuweather.*
|
||||
homeassistant.components.actiontec.*
|
||||
homeassistant.components.aftership.*
|
||||
homeassistant.components.air_quality.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.ampio.*
|
||||
homeassistant.components.automation.*
|
||||
homeassistant.components.binary_sensor.*
|
||||
homeassistant.components.bluetooth_tracker.*
|
||||
homeassistant.components.bond.*
|
||||
homeassistant.components.brother.*
|
||||
homeassistant.components.calendar.*
|
||||
homeassistant.components.camera.*
|
||||
homeassistant.components.canary.*
|
||||
homeassistant.components.cover.*
|
||||
homeassistant.components.device_automation.*
|
||||
homeassistant.components.device_tracker.*
|
||||
homeassistant.components.dunehd.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.fitbit.*
|
||||
homeassistant.components.fritzbox.*
|
||||
homeassistant.components.frontend.*
|
||||
homeassistant.components.geo_location.*
|
||||
homeassistant.components.gios.*
|
||||
homeassistant.components.group.*
|
||||
homeassistant.components.history.*
|
||||
homeassistant.components.http.*
|
||||
@ -19,16 +39,21 @@ homeassistant.components.hyperion.*
|
||||
homeassistant.components.image_processing.*
|
||||
homeassistant.components.integration.*
|
||||
homeassistant.components.knx.*
|
||||
homeassistant.components.kraken.*
|
||||
homeassistant.components.light.*
|
||||
homeassistant.components.lock.*
|
||||
homeassistant.components.mailbox.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.nam.*
|
||||
homeassistant.components.network.*
|
||||
homeassistant.components.notify.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.onewire.*
|
||||
homeassistant.components.persistent_notification.*
|
||||
homeassistant.components.proximity.*
|
||||
homeassistant.components.recorder.purge
|
||||
homeassistant.components.recorder.repack
|
||||
homeassistant.components.recorder.statistics
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.sensor.*
|
||||
@ -36,8 +61,11 @@ homeassistant.components.slack.*
|
||||
homeassistant.components.sonos.media_player
|
||||
homeassistant.components.sun.*
|
||||
homeassistant.components.switch.*
|
||||
homeassistant.components.synology_dsm.*
|
||||
homeassistant.components.systemmonitor.*
|
||||
homeassistant.components.tcp.*
|
||||
homeassistant.components.tts.*
|
||||
homeassistant.components.upcloud.*
|
||||
homeassistant.components.vacuum.*
|
||||
homeassistant.components.water_heater.*
|
||||
homeassistant.components.weather.*
|
||||
|
26
CODEOWNERS
26
CODEOWNERS
@ -64,19 +64,20 @@ homeassistant/components/azure_service_bus/* @hfurubotten
|
||||
homeassistant/components/beewi_smartclim/* @alemuro
|
||||
homeassistant/components/bitcoin/* @fabaff
|
||||
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||
homeassistant/components/blebox/* @gadgetmobile
|
||||
homeassistant/components/blebox/* @bbx-a @bbx-jp
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/blueprint/* @home-assistant/core
|
||||
homeassistant/components/bmp280/* @belidzs
|
||||
homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe
|
||||
homeassistant/components/bond/* @prystupa
|
||||
homeassistant/components/bosch_shc/* @tschamm
|
||||
homeassistant/components/braviatv/* @bieniu
|
||||
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
||||
homeassistant/components/brother/* @bieniu
|
||||
homeassistant/components/brunt/* @eavanvalkenburg
|
||||
homeassistant/components/bsblan/* @liudger
|
||||
homeassistant/components/bt_smarthub/* @jxwolstenholme
|
||||
homeassistant/components/buienradar/* @mjj4791 @ties
|
||||
homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221
|
||||
homeassistant/components/cast/* @emontnemery
|
||||
homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren
|
||||
homeassistant/components/circuit/* @braam
|
||||
@ -111,6 +112,7 @@ homeassistant/components/device_automation/* @home-assistant/core
|
||||
homeassistant/components/devolo_home_control/* @2Fake @Shutgun
|
||||
homeassistant/components/dexcom/* @gagebenne
|
||||
homeassistant/components/dhcp/* @bdraco
|
||||
homeassistant/components/dht/* @thegardenmonkey
|
||||
homeassistant/components/digital_ocean/* @fabaff
|
||||
homeassistant/components/directv/* @ctalkington
|
||||
homeassistant/components/discogs/* @thibmaek
|
||||
@ -168,9 +170,11 @@ homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74
|
||||
homeassistant/components/fritzbox/* @mib1185
|
||||
homeassistant/components/fronius/* @nielstron
|
||||
homeassistant/components/frontend/* @home-assistant/frontend
|
||||
homeassistant/components/garages_amsterdam/* @klaasnicolaas
|
||||
homeassistant/components/garmin_connect/* @cyberjunky
|
||||
homeassistant/components/gdacs/* @exxamalte
|
||||
homeassistant/components/geniushub/* @zxdavb
|
||||
homeassistant/components/geo_json_events/* @exxamalte
|
||||
homeassistant/components/geo_rss_events/* @exxamalte
|
||||
homeassistant/components/geonetnz_quakes/* @exxamalte
|
||||
homeassistant/components/geonetnz_volcano/* @exxamalte
|
||||
@ -178,7 +182,7 @@ homeassistant/components/gios/* @bieniu
|
||||
homeassistant/components/gitter/* @fabaff
|
||||
homeassistant/components/glances/* @fabaff @engrbm87
|
||||
homeassistant/components/goalzero/* @tkdrob
|
||||
homeassistant/components/gogogate2/* @vangorra
|
||||
homeassistant/components/gogogate2/* @vangorra @bdraco
|
||||
homeassistant/components/google_assistant/* @home-assistant/cloud
|
||||
homeassistant/components/google_cloud/* @lufton
|
||||
homeassistant/components/gpsd/* @fabaff
|
||||
@ -203,7 +207,7 @@ homeassistant/components/home_connect/* @DavidMStraub
|
||||
homeassistant/components/home_plus_control/* @chemaaa
|
||||
homeassistant/components/homeassistant/* @home-assistant/core
|
||||
homeassistant/components/homekit/* @bdraco
|
||||
homeassistant/components/homekit_controller/* @Jc2k
|
||||
homeassistant/components/homekit_controller/* @Jc2k @bdraco
|
||||
homeassistant/components/homematic/* @pvizeli @danielperna84
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/huawei_lte/* @scop @fphammerle
|
||||
@ -253,10 +257,12 @@ homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
|
||||
homeassistant/components/kodi/* @OnFreund @cgtobi
|
||||
homeassistant/components/konnected/* @heythisisnate @kit-klein
|
||||
homeassistant/components/kostal_plenticore/* @stegm
|
||||
homeassistant/components/kraken/* @eifinger
|
||||
homeassistant/components/kulersky/* @emlove
|
||||
homeassistant/components/lametric/* @robbiet480
|
||||
homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
homeassistant/components/lg_netcast/* @Drafteed
|
||||
homeassistant/components/life360/* @pnbruckner
|
||||
homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/litejet/* @joncar
|
||||
@ -284,6 +290,7 @@ homeassistant/components/met/* @danielhiversen @thimic
|
||||
homeassistant/components/met_eireann/* @DylanGore
|
||||
homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame
|
||||
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
||||
homeassistant/components/meteoclimatic/* @adrianmo
|
||||
homeassistant/components/metoffice/* @MrHarcombe
|
||||
homeassistant/components/miflora/* @danielhiversen @basnijholt
|
||||
homeassistant/components/mikrotik/* @engrbm87
|
||||
@ -306,6 +313,7 @@ homeassistant/components/my/* @home-assistant/core
|
||||
homeassistant/components/myq/* @bdraco
|
||||
homeassistant/components/mysensors/* @MartinHjelmare @functionpointer
|
||||
homeassistant/components/mystrom/* @fabaff
|
||||
homeassistant/components/nam/* @bieniu
|
||||
homeassistant/components/neato/* @dshokouhi @Santobert
|
||||
homeassistant/components/nederlandse_spoorwegen/* @YarmoM
|
||||
homeassistant/components/nello/* @pschmitt
|
||||
@ -327,7 +335,6 @@ homeassistant/components/notify_events/* @matrozov @papajojo
|
||||
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 @pree
|
||||
homeassistant/components/numato/* @clssn
|
||||
homeassistant/components/number/* @home-assistant/core @Shulyaka
|
||||
@ -407,7 +414,7 @@ homeassistant/components/rpi_power/* @shenxn @swetoast
|
||||
homeassistant/components/ruckus_unleashed/* @gabe565
|
||||
homeassistant/components/safe_mode/* @home-assistant/core
|
||||
homeassistant/components/saj/* @fredericvl
|
||||
homeassistant/components/samsungtv/* @escoand
|
||||
homeassistant/components/samsungtv/* @escoand @chemelli74
|
||||
homeassistant/components/scene/* @home-assistant/core
|
||||
homeassistant/components/schluter/* @prairieapps
|
||||
homeassistant/components/scrape/* @fabaff
|
||||
@ -425,6 +432,7 @@ homeassistant/components/shell_command/* @home-assistant/core
|
||||
homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74
|
||||
homeassistant/components/shiftr/* @fabaff
|
||||
homeassistant/components/shodan/* @fabaff
|
||||
homeassistant/components/sia/* @eavanvalkenburg
|
||||
homeassistant/components/sighthound/* @robmarkcole
|
||||
homeassistant/components/signal_messenger/* @bbernhard
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
@ -450,7 +458,7 @@ homeassistant/components/soma/* @ratsept
|
||||
homeassistant/components/somfy/* @tetienne
|
||||
homeassistant/components/sonarr/* @ctalkington
|
||||
homeassistant/components/songpal/* @rytilahti @shenxn
|
||||
homeassistant/components/sonos/* @cgtobi
|
||||
homeassistant/components/sonos/* @cgtobi @jjlawren
|
||||
homeassistant/components/spaceapi/* @fabaff
|
||||
homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87
|
||||
homeassistant/components/spider/* @peternijssen
|
||||
@ -475,10 +483,12 @@ homeassistant/components/swiss_public_transport/* @fabaff
|
||||
homeassistant/components/switchbot/* @danielhiversen
|
||||
homeassistant/components/switcher_kis/* @tomerfi
|
||||
homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/syncthing/* @zhulik
|
||||
homeassistant/components/syncthru/* @nielstron
|
||||
homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/system_bridge/* @timmo001
|
||||
homeassistant/components/tado/* @michaelarnauts @bdraco @noltari
|
||||
homeassistant/components/tag/* @balloob @dmulcahey
|
||||
homeassistant/components/tahoma/* @philklei
|
||||
@ -535,6 +545,7 @@ homeassistant/components/vlc_telnet/* @rodripf @dmcc
|
||||
homeassistant/components/volkszaehler/* @fabaff
|
||||
homeassistant/components/volumio/* @OnFreund
|
||||
homeassistant/components/wake_on_lan/* @ntilley905
|
||||
homeassistant/components/wallbox/* @hesselonline
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
@ -554,6 +565,7 @@ homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG
|
||||
homeassistant/components/xiaomi_tv/* @simse
|
||||
homeassistant/components/xmpp/* @fabaff @flowolf
|
||||
homeassistant/components/yale_smart_alarm/* @gjohansson-ST
|
||||
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||
homeassistant/components/yandex_transport/* @rishatik92 @devbis
|
||||
homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
|
@ -1,232 +0,0 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- rc
|
||||
- dev
|
||||
- master
|
||||
pr:
|
||||
- rc
|
||||
- dev
|
||||
- master
|
||||
|
||||
resources:
|
||||
containers:
|
||||
- container: 38
|
||||
image: homeassistant/ci-azure:3.8
|
||||
repositories:
|
||||
- repository: azure
|
||||
type: github
|
||||
name: "home-assistant/ci-azure"
|
||||
endpoint: "home-assistant"
|
||||
variables:
|
||||
- name: PythonMain
|
||||
value: "38"
|
||||
- name: versionHadolint
|
||||
value: "v1.17.6"
|
||||
|
||||
stages:
|
||||
- stage: "Overview"
|
||||
jobs:
|
||||
- job: "Lint"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "requirements_test.txt | homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -r requirements_test.txt
|
||||
pre-commit install-hooks
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files
|
||||
displayName: "Run executables check"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run codespell --all-files
|
||||
displayName: "Run codespell"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run flake8 --all-files
|
||||
displayName: "Run flake8"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run bandit --all-files
|
||||
displayName: "Run bandit"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run isort --all-files --show-diff-on-failure
|
||||
displayName: "Run isort"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run check-json --all-files
|
||||
displayName: "Run check-json"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run yamllint --all-files
|
||||
displayName: "Run yamllint"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run pyupgrade --all-files --show-diff-on-failure
|
||||
displayName: "Run pyupgrade"
|
||||
# Prettier seems to hang on Azure, unknown why yet.
|
||||
# Temporarily disable the check to no block PRs
|
||||
# - script: |
|
||||
# . venv/bin/activate
|
||||
# pre-commit run prettier --all-files --show-diff-on-failure
|
||||
# displayName: 'Run prettier'
|
||||
- job: "Validate"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
python -m script.hassfest --action validate
|
||||
displayName: "Validate manifests"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
./script/gen_requirements_all.py validate
|
||||
displayName: "requirements_all validate"
|
||||
- job: "CheckFormat"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "requirements_test.txt | homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -r requirements_test.txt
|
||||
pre-commit install-hooks
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run black --all-files --show-diff-on-failure
|
||||
displayName: "Check Black formatting"
|
||||
- job: "Docker"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- script: sudo docker pull hadolint/hadolint:$(versionHadolint)
|
||||
displayName: "Install Hadolint"
|
||||
- script: |
|
||||
set -e
|
||||
for dockerfile in Dockerfile Dockerfile.dev
|
||||
do
|
||||
echo "Linting: $dockerfile"
|
||||
docker run --rm -i \
|
||||
-v "$(pwd)/.hadolint.yaml:/.hadolint.yaml:ro" \
|
||||
hadolint/hadolint:$(versionHadolint) < "$dockerfile"
|
||||
done
|
||||
displayName: "Run Hadolint"
|
||||
|
||||
- stage: "Tests"
|
||||
dependsOn:
|
||||
- "Overview"
|
||||
jobs:
|
||||
- job: "PyTest"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
matrix:
|
||||
Python38:
|
||||
python.container: "38"
|
||||
container: $[ variables['python.container'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "requirements_test_all.txt | requirements_test.txt | homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test_all.txt
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: "Install Home Assistant"
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar tests
|
||||
script/check_dirty
|
||||
displayName: "Run pytest for python $(python.container)"
|
||||
condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain']))
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
|
||||
codecov --token $(codecovToken)
|
||||
script/check_dirty
|
||||
displayName: "Run pytest for python $(python.container) / coverage"
|
||||
condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain']))
|
||||
|
||||
- stage: "FullCheck"
|
||||
dependsOn:
|
||||
- "Overview"
|
||||
jobs:
|
||||
- job: "Pylint"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools wheel
|
||||
pip install -r requirements_all.txt
|
||||
pip install -r requirements_test.txt
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: "Install Home Assistant"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pylint homeassistant
|
||||
displayName: "Run pylint"
|
||||
- job: "Mypy"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "requirements_test.txt | setup.py | homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -e . -r requirements_test.txt
|
||||
pre-commit install-hooks
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run mypy --all-files
|
||||
displayName: "Run mypy"
|
@ -1,65 +0,0 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
pr: none
|
||||
schedules:
|
||||
- cron: "0 0 * * *"
|
||||
displayName: "translation update"
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
always: true
|
||||
variables:
|
||||
- group: translation
|
||||
resources:
|
||||
repositories:
|
||||
- repository: azure
|
||||
type: github
|
||||
name: 'home-assistant/ci-azure'
|
||||
endpoint: 'home-assistant'
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Upload'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.8'
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- script: |
|
||||
export LOKALISE_TOKEN="$(lokaliseToken)"
|
||||
export AZURE_BRANCH="$(Build.SourceBranchName)"
|
||||
|
||||
python3 -m script.translations upload
|
||||
displayName: 'Upload Translation'
|
||||
|
||||
- job: 'Download'
|
||||
dependsOn:
|
||||
- 'Upload'
|
||||
condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual'))
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.7'
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
- template: templates/azp-step-git-init.yaml@azure
|
||||
- script: |
|
||||
export LOKALISE_TOKEN="$(lokaliseToken)"
|
||||
|
||||
python3 -m script.translations download
|
||||
displayName: 'Download Translation'
|
||||
- script: |
|
||||
git checkout dev
|
||||
git add homeassistant
|
||||
git commit -am "[ci skip] Translation update"
|
||||
git push
|
||||
displayName: 'Update translation'
|
@ -1,100 +0,0 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
- rc
|
||||
paths:
|
||||
include:
|
||||
- requirements_all.txt
|
||||
pr: none
|
||||
schedules:
|
||||
- cron: '0 */4 * * *'
|
||||
displayName: 'daily builds'
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: '1.13.0-3.8-alpine3.12'
|
||||
resources:
|
||||
repositories:
|
||||
- repository: azure
|
||||
type: github
|
||||
name: 'home-assistant/ci-azure'
|
||||
endpoint: 'home-assistant'
|
||||
|
||||
jobs:
|
||||
- template: templates/azp-job-wheels.yaml@azure
|
||||
parameters:
|
||||
builderVersion: '$(versionWheels)'
|
||||
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev'
|
||||
builderPip: 'Cython;numpy'
|
||||
skipBinary: 'aiohttp'
|
||||
wheelsRequirement: 'requirements.txt'
|
||||
wheelsRequirementDiff: 'requirements_diff.txt'
|
||||
wheelsConstraint: 'homeassistant/package_constraints.txt'
|
||||
jobName: 'Wheels_Core'
|
||||
preBuild:
|
||||
- script: |
|
||||
if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then
|
||||
exit 0
|
||||
else
|
||||
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt
|
||||
fi
|
||||
displayName: 'Prepare requirements files for Home Assistant Core wheels'
|
||||
- template: templates/azp-job-wheels.yaml@azure
|
||||
parameters:
|
||||
builderVersion: '$(versionWheels)'
|
||||
builderApk: 'build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
|
||||
builderPip: 'Cython;numpy;scikit-build'
|
||||
builderEnvFile: true
|
||||
skipBinary: 'aiohttp'
|
||||
wheelsRequirement: 'requirements_wheels.txt'
|
||||
wheelsRequirementDiff: 'requirements_diff.txt'
|
||||
wheelsConstraint: 'homeassistant/package_constraints.txt'
|
||||
jobName: 'Wheels_Integrations'
|
||||
preBuild:
|
||||
- script: |
|
||||
cp requirements_all.txt requirements_wheels.txt
|
||||
if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then
|
||||
touch requirements_diff.txt
|
||||
else
|
||||
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements_all.txt
|
||||
fi
|
||||
|
||||
requirement_files="requirements_wheels.txt requirements_diff.txt"
|
||||
for requirement_file in ${requirement_files}; do
|
||||
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
|
||||
sed -i "s|# bluepy|bluepy|g" ${requirement_file}
|
||||
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||
sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file}
|
||||
sed -i "s|# raspihats|raspihats|g" ${requirement_file}
|
||||
sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file}
|
||||
sed -i "s|# blinkt|blinkt|g" ${requirement_file}
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
|
||||
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
|
||||
sed -i "s|# evdev|evdev|g" ${requirement_file}
|
||||
sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file}
|
||||
sed -i "s|# i2csense|i2csense|g" ${requirement_file}
|
||||
sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file}
|
||||
sed -i "s|# pycups|pycups|g" ${requirement_file}
|
||||
sed -i "s|# homekit|homekit|g" ${requirement_file}
|
||||
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
|
||||
sed -i "s|# decora|decora|g" ${requirement_file}
|
||||
sed -i "s|# avion|avion|g" ${requirement_file}
|
||||
sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file}
|
||||
sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file}
|
||||
sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
|
||||
sed -i "s|# py_noaa|py_noaa|g" ${requirement_file}
|
||||
sed -i "s|# bme680|bme680|g" ${requirement_file}
|
||||
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
|
||||
done
|
||||
|
||||
# Write env for build settings
|
||||
(
|
||||
echo "GRPC_BUILD_WITH_BORING_SSL_ASM="
|
||||
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1"
|
||||
) > .env_file
|
||||
displayName: 'Prepare requirements files for Home Assistant wheels'
|
10
build.json
10
build.json
@ -2,11 +2,11 @@
|
||||
"image": "homeassistant/{arch}-homeassistant",
|
||||
"shadow_repository": "ghcr.io/home-assistant",
|
||||
"build_from": {
|
||||
"aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.04.3",
|
||||
"armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.04.3",
|
||||
"armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.04.3",
|
||||
"amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.04.3",
|
||||
"i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.04.3"
|
||||
"aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.05.0",
|
||||
"armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.05.0",
|
||||
"armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.05.0",
|
||||
"amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.05.0",
|
||||
"i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.05.0"
|
||||
},
|
||||
"labels": {
|
||||
"io.hass.type": "core",
|
||||
|
@ -9,13 +9,12 @@ from typing import Any, Dict, Mapping, Optional, Tuple, cast
|
||||
import jwt
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import auth_store, models
|
||||
from .const import GROUP_ID_ADMIN
|
||||
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN
|
||||
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
||||
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
||||
|
||||
@ -79,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) -> None:
|
||||
"""Init auth manager flows."""
|
||||
super().__init__(hass)
|
||||
self.auth_manager = auth_manager
|
||||
|
@ -95,31 +95,29 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
user for user in users if not user.system_generated and user.is_active
|
||||
]
|
||||
for ip_net, user_or_group_list in self.trusted_users.items():
|
||||
if ip_addr in ip_net:
|
||||
user_list = [
|
||||
user_id
|
||||
for user_id in user_or_group_list
|
||||
if isinstance(user_id, str)
|
||||
]
|
||||
group_list = [
|
||||
group[CONF_GROUP]
|
||||
for group in user_or_group_list
|
||||
if isinstance(group, dict)
|
||||
]
|
||||
flattened_group_list = [
|
||||
group for sublist in group_list for group in sublist
|
||||
]
|
||||
available_users = [
|
||||
user
|
||||
for user in available_users
|
||||
if (
|
||||
user.id in user_list
|
||||
or any(
|
||||
group.id in flattened_group_list for group in user.groups
|
||||
)
|
||||
)
|
||||
]
|
||||
break
|
||||
if ip_addr not in ip_net:
|
||||
continue
|
||||
|
||||
user_list = [
|
||||
user_id for user_id in user_or_group_list if isinstance(user_id, str)
|
||||
]
|
||||
group_list = [
|
||||
group[CONF_GROUP]
|
||||
for group in user_or_group_list
|
||||
if isinstance(group, dict)
|
||||
]
|
||||
flattened_group_list = [
|
||||
group for sublist in group_list for group in sublist
|
||||
]
|
||||
available_users = [
|
||||
user
|
||||
for user in available_users
|
||||
if (
|
||||
user.id in user_list
|
||||
or any(group.id in flattened_group_list for group in user.groups)
|
||||
)
|
||||
]
|
||||
break
|
||||
|
||||
return TrustedNetworksLoginFlow(
|
||||
self,
|
||||
@ -136,13 +134,22 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
|
||||
users = await self.store.async_get_users()
|
||||
for user in users:
|
||||
if not user.system_generated and user.is_active and user.id == user_id:
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data["user_id"] == user_id:
|
||||
return credential
|
||||
cred = self.async_create_credentials({"user_id": user_id})
|
||||
await self.store.async_link_user(user, cred)
|
||||
return cred
|
||||
if user.id != user_id:
|
||||
continue
|
||||
|
||||
if user.system_generated:
|
||||
continue
|
||||
|
||||
if not user.is_active:
|
||||
continue
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data["user_id"] == user_id:
|
||||
return credential
|
||||
|
||||
cred = self.async_create_credentials({"user_id": user_id})
|
||||
await self.store.async_link_user(user, cred)
|
||||
return cred
|
||||
|
||||
# We only allow login as exist user
|
||||
raise InvalidUserError
|
||||
|
@ -45,15 +45,19 @@ ATTR_EVENT_BY = "event_by"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_POLLING, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
},
|
||||
vol.All(
|
||||
# Deprecated in Home Assistant 2021.6
|
||||
cv.deprecated(DOMAIN),
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_POLLING, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
@ -18,7 +18,6 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Abode."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
|
@ -1,21 +1,44 @@
|
||||
capture_image:
|
||||
name: Capture image
|
||||
description: Request a new image capture from a camera device.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity
|
||||
description: Entity id of the camera to request an image.
|
||||
example: camera.downstairs_motion_camera
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: abode
|
||||
domain: camera
|
||||
|
||||
change_setting:
|
||||
name: Change setting
|
||||
description: Change an Abode system setting.
|
||||
fields:
|
||||
setting:
|
||||
name: Setting
|
||||
description: Setting to change.
|
||||
required: true
|
||||
example: beeper_mute
|
||||
selector:
|
||||
text:
|
||||
value:
|
||||
name: Value
|
||||
description: Value of the setting.
|
||||
required: true
|
||||
example: "1"
|
||||
selector:
|
||||
text:
|
||||
|
||||
trigger_automation:
|
||||
name: Trigger automation
|
||||
description: Trigger an Abode automation.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity
|
||||
description: Entity id of the automation to trigger.
|
||||
example: switch.my_automation
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: abode
|
||||
domain: switch
|
||||
|
@ -1,12 +1,18 @@
|
||||
"""The AccuWeather component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@ -23,11 +29,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = ["sensor", "weather"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up AccuWeather as config entry."""
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
location_key = config_entry.unique_id
|
||||
forecast = config_entry.options.get(CONF_FORECAST, False)
|
||||
api_key: str = entry.data[CONF_API_KEY]
|
||||
assert entry.unique_id is not None
|
||||
location_key = entry.unique_id
|
||||
forecast: bool = entry.options.get(CONF_FORECAST, False)
|
||||
|
||||
_LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast)
|
||||
|
||||
@ -38,41 +45,46 @@ async def async_setup_entry(hass, config_entry) -> bool:
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
undo_listener = config_entry.add_update_listener(update_listener)
|
||||
undo_listener = entry.add_update_listener(update_listener)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = {
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
COORDINATOR: coordinator,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
|
||||
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_listener(hass, config_entry):
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[Dict[str, Any]]):
|
||||
"""Class to manage fetching AccuWeather data API."""
|
||||
|
||||
def __init__(self, hass, session, api_key, location_key, forecast: bool):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
session: ClientSession,
|
||||
api_key: str,
|
||||
location_key: str,
|
||||
forecast: bool,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.location_key = location_key
|
||||
self.forecast = forecast
|
||||
@ -91,7 +103,7 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||
|
||||
async def _async_update_data(self):
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
try:
|
||||
async with timeout(10):
|
||||
@ -108,5 +120,5 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
RequestsExceededError,
|
||||
) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
_LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining)
|
||||
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
|
||||
return {**current, **{ATTR_FORECAST: forecast}}
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Adds config flow for AccuWeather."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp import ClientError
|
||||
@ -8,8 +11,10 @@ from async_timeout import timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@ -20,9 +25,10 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for AccuWeather."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
# Under the terms of use of the API, one user can use one free API key. Due to
|
||||
# the small number of requests allowed, we only allow one integration instance.
|
||||
@ -78,7 +84,9 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> AccuWeatherOptionsFlowHandler:
|
||||
"""Options callback for AccuWeather."""
|
||||
return AccuWeatherOptionsFlowHandler(config_entry)
|
||||
|
||||
@ -86,15 +94,19 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Config flow options for AccuWeather."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
def __init__(self, entry: ConfigEntry) -> None:
|
||||
"""Initialize AccuWeather options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.config_entry = entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
@ -1,4 +1,8 @@
|
||||
"""Constants for AccuWeather integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
@ -33,18 +37,25 @@ from homeassistant.const import (
|
||||
UV_INDEX,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by AccuWeather"
|
||||
ATTR_FORECAST = CONF_FORECAST = "forecast"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT_IMPERIAL = "Imperial"
|
||||
ATTR_UNIT_METRIC = "Metric"
|
||||
COORDINATOR = "coordinator"
|
||||
DOMAIN = "accuweather"
|
||||
MANUFACTURER = "AccuWeather, Inc."
|
||||
NAME = "AccuWeather"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
from .model import SensorDescription
|
||||
|
||||
CONDITION_CLASSES = {
|
||||
API_IMPERIAL: Final = "Imperial"
|
||||
API_METRIC: Final = "Metric"
|
||||
ATTRIBUTION: Final = "Data provided by AccuWeather"
|
||||
ATTR_ENABLED: Final = "enabled"
|
||||
ATTR_FORECAST: Final = "forecast"
|
||||
ATTR_LABEL: Final = "label"
|
||||
ATTR_UNIT_IMPERIAL: Final = "unit_imperial"
|
||||
ATTR_UNIT_METRIC: Final = "unit_metric"
|
||||
CONF_FORECAST: Final = "forecast"
|
||||
COORDINATOR: Final = "coordinator"
|
||||
DOMAIN: Final = "accuweather"
|
||||
MANUFACTURER: Final = "AccuWeather, Inc."
|
||||
MAX_FORECAST_DAYS: Final = 4
|
||||
NAME: Final = "AccuWeather"
|
||||
UNDO_UPDATE_LISTENER: Final = "undo_update_listener"
|
||||
|
||||
CONDITION_CLASSES: Final[dict[str, list[int]]] = {
|
||||
ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37],
|
||||
ATTR_CONDITION_CLOUDY: [7, 8, 38],
|
||||
ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31],
|
||||
@ -61,15 +72,14 @@ CONDITION_CLASSES = {
|
||||
ATTR_CONDITION_WINDY: [32],
|
||||
}
|
||||
|
||||
FORECAST_DAYS = [0, 1, 2, 3, 4]
|
||||
|
||||
FORECAST_SENSOR_TYPES = {
|
||||
FORECAST_SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
|
||||
"CloudCoverDay": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
ATTR_ICON: "mdi:weather-cloudy",
|
||||
ATTR_LABEL: "Cloud Cover Day",
|
||||
ATTR_UNIT_METRIC: PERCENTAGE,
|
||||
ATTR_UNIT_IMPERIAL: PERCENTAGE,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"CloudCoverNight": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -77,6 +87,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Cloud Cover Night",
|
||||
ATTR_UNIT_METRIC: PERCENTAGE,
|
||||
ATTR_UNIT_IMPERIAL: PERCENTAGE,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"Grass": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -84,6 +95,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Grass Pollen",
|
||||
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"HoursOfSun": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -91,6 +103,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Hours Of Sun",
|
||||
ATTR_UNIT_METRIC: TIME_HOURS,
|
||||
ATTR_UNIT_IMPERIAL: TIME_HOURS,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"Mold": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -98,6 +111,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Mold Pollen",
|
||||
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"Ozone": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -105,6 +119,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Ozone",
|
||||
ATTR_UNIT_METRIC: None,
|
||||
ATTR_UNIT_IMPERIAL: None,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"Ragweed": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -112,6 +127,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Ragweed Pollen",
|
||||
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"RealFeelTemperatureMax": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -119,6 +135,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "RealFeel Temperature Max",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"RealFeelTemperatureMin": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -126,6 +143,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "RealFeel Temperature Min",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"RealFeelTemperatureShadeMax": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -133,6 +151,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "RealFeel Temperature Shade Max",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"RealFeelTemperatureShadeMin": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -140,6 +159,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "RealFeel Temperature Shade Min",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"ThunderstormProbabilityDay": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -147,6 +167,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Thunderstorm Probability Day",
|
||||
ATTR_UNIT_METRIC: PERCENTAGE,
|
||||
ATTR_UNIT_IMPERIAL: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"ThunderstormProbabilityNight": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -154,6 +175,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Thunderstorm Probability Night",
|
||||
ATTR_UNIT_METRIC: PERCENTAGE,
|
||||
ATTR_UNIT_IMPERIAL: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"Tree": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -161,6 +183,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Tree Pollen",
|
||||
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"UVIndex": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -168,6 +191,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "UV Index",
|
||||
ATTR_UNIT_METRIC: UV_INDEX,
|
||||
ATTR_UNIT_IMPERIAL: UV_INDEX,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"WindGustDay": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -175,6 +199,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Wind Gust Day",
|
||||
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"WindGustNight": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -182,6 +207,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Wind Gust Night",
|
||||
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"WindDay": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -189,6 +215,7 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Wind Day",
|
||||
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"WindNight": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -196,37 +223,18 @@ FORECAST_SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Wind Night",
|
||||
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
}
|
||||
|
||||
OPTIONAL_SENSORS = (
|
||||
"ApparentTemperature",
|
||||
"CloudCover",
|
||||
"CloudCoverDay",
|
||||
"CloudCoverNight",
|
||||
"DewPoint",
|
||||
"Grass",
|
||||
"Mold",
|
||||
"Ozone",
|
||||
"Ragweed",
|
||||
"RealFeelTemperatureShade",
|
||||
"RealFeelTemperatureShadeMax",
|
||||
"RealFeelTemperatureShadeMin",
|
||||
"Tree",
|
||||
"WetBulbTemperature",
|
||||
"WindChillTemperature",
|
||||
"WindGust",
|
||||
"WindGustDay",
|
||||
"WindGustNight",
|
||||
)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
|
||||
"ApparentTemperature": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_ICON: None,
|
||||
ATTR_LABEL: "Apparent Temperature",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"Ceiling": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -234,6 +242,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Cloud Ceiling",
|
||||
ATTR_UNIT_METRIC: LENGTH_METERS,
|
||||
ATTR_UNIT_IMPERIAL: LENGTH_FEET,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"CloudCover": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -241,6 +250,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Cloud Cover",
|
||||
ATTR_UNIT_METRIC: PERCENTAGE,
|
||||
ATTR_UNIT_IMPERIAL: PERCENTAGE,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"DewPoint": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -248,6 +258,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Dew Point",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"RealFeelTemperature": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -255,6 +266,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "RealFeel Temperature",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"RealFeelTemperatureShade": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -262,6 +274,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "RealFeel Temperature Shade",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"Precipitation": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -269,6 +282,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Precipitation",
|
||||
ATTR_UNIT_METRIC: LENGTH_MILLIMETERS,
|
||||
ATTR_UNIT_IMPERIAL: LENGTH_INCHES,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"PressureTendency": {
|
||||
ATTR_DEVICE_CLASS: "accuweather__pressure_tendency",
|
||||
@ -276,6 +290,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Pressure Tendency",
|
||||
ATTR_UNIT_METRIC: None,
|
||||
ATTR_UNIT_IMPERIAL: None,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"UVIndex": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -283,6 +298,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "UV Index",
|
||||
ATTR_UNIT_METRIC: UV_INDEX,
|
||||
ATTR_UNIT_IMPERIAL: UV_INDEX,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"WetBulbTemperature": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -290,6 +306,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Wet Bulb Temperature",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"WindChillTemperature": {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
@ -297,6 +314,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Wind Chill Temperature",
|
||||
ATTR_UNIT_METRIC: TEMP_CELSIUS,
|
||||
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
"Wind": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -304,6 +322,7 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Wind",
|
||||
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||
ATTR_ENABLED: True,
|
||||
},
|
||||
"WindGust": {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -311,5 +330,6 @@ SENSOR_TYPES = {
|
||||
ATTR_LABEL: "Wind Gust",
|
||||
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
|
||||
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
|
||||
ATTR_ENABLED: False,
|
||||
},
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "accuweather",
|
||||
"name": "AccuWeather",
|
||||
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
|
||||
"requirements": ["accuweather==0.1.1"],
|
||||
"requirements": ["accuweather==0.2.0"],
|
||||
"codeowners": ["@bieniu"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum",
|
||||
|
15
homeassistant/components/accuweather/model.py
Normal file
15
homeassistant/components/accuweather/model.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Type definitions for AccuWeather integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class SensorDescription(TypedDict):
|
||||
"""Sensor description class."""
|
||||
|
||||
device_class: str | None
|
||||
icon: str | None
|
||||
label: str
|
||||
unit_metric: str | None
|
||||
unit_imperial: str | None
|
||||
enabled: bool
|
@ -1,44 +1,62 @@
|
||||
"""Support for the AccuWeather service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONF_NAME,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AccuWeatherDataUpdateCoordinator
|
||||
from .const import (
|
||||
API_IMPERIAL,
|
||||
API_METRIC,
|
||||
ATTR_ENABLED,
|
||||
ATTR_FORECAST,
|
||||
ATTR_ICON,
|
||||
ATTR_LABEL,
|
||||
ATTR_UNIT_IMPERIAL,
|
||||
ATTR_UNIT_METRIC,
|
||||
ATTRIBUTION,
|
||||
COORDINATOR,
|
||||
DOMAIN,
|
||||
FORECAST_DAYS,
|
||||
FORECAST_SENSOR_TYPES,
|
||||
MANUFACTURER,
|
||||
MAX_FORECAST_DAYS,
|
||||
NAME,
|
||||
OPTIONAL_SENSORS,
|
||||
SENSOR_TYPES,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Add AccuWeather entities from a config_entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
name: str = entry.data[CONF_NAME]
|
||||
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
|
||||
COORDINATOR
|
||||
]
|
||||
|
||||
sensors = []
|
||||
sensors: list[AccuWeatherSensor] = []
|
||||
for sensor in SENSOR_TYPES:
|
||||
sensors.append(AccuWeatherSensor(name, sensor, coordinator))
|
||||
|
||||
if coordinator.forecast:
|
||||
for sensor in FORECAST_SENSOR_TYPES:
|
||||
for day in FORECAST_DAYS:
|
||||
for day in range(MAX_FORECAST_DAYS + 1):
|
||||
# Some air quality/allergy sensors are only available for certain
|
||||
# locations.
|
||||
if sensor in coordinator.data[ATTR_FORECAST][0]:
|
||||
@ -46,38 +64,56 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
AccuWeatherSensor(name, sensor, coordinator, forecast_day=day)
|
||||
)
|
||||
|
||||
async_add_entities(sensors, False)
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Define an AccuWeather entity."""
|
||||
|
||||
def __init__(self, name, kind, coordinator, forecast_day=None):
|
||||
coordinator: AccuWeatherDataUpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
kind: str,
|
||||
coordinator: AccuWeatherDataUpdateCoordinator,
|
||||
forecast_day: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
if forecast_day is None:
|
||||
self._description = SENSOR_TYPES[kind]
|
||||
self._sensor_data: dict[str, Any]
|
||||
if kind == "Precipitation":
|
||||
self._sensor_data = coordinator.data["PrecipitationSummary"][kind]
|
||||
else:
|
||||
self._sensor_data = coordinator.data[kind]
|
||||
else:
|
||||
self._description = FORECAST_SENSOR_TYPES[kind]
|
||||
self._sensor_data = coordinator.data[ATTR_FORECAST][forecast_day][kind]
|
||||
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
|
||||
self._name = name
|
||||
self.kind = kind
|
||||
self._device_class = None
|
||||
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
|
||||
self.forecast_day = forecast_day
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name."""
|
||||
if self.forecast_day is not None:
|
||||
return f"{self._name} {FORECAST_SENSOR_TYPES[self.kind][ATTR_LABEL]} {self.forecast_day}d"
|
||||
return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
|
||||
return f"{self._name} {self._description[ATTR_LABEL]} {self.forecast_day}d"
|
||||
return f"{self._name} {self._description[ATTR_LABEL]}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique_id for this entity."""
|
||||
if self.forecast_day is not None:
|
||||
return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower()
|
||||
return f"{self.coordinator.location_key}-{self.kind}".lower()
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.coordinator.location_key)},
|
||||
@ -87,72 +123,54 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> StateType:
|
||||
"""Return the state."""
|
||||
if self.forecast_day is not None:
|
||||
if (
|
||||
FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
|
||||
== DEVICE_CLASS_TEMPERATURE
|
||||
):
|
||||
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
|
||||
self.kind
|
||||
]["Value"]
|
||||
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
|
||||
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
|
||||
self.kind
|
||||
]["Speed"]["Value"]
|
||||
if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
|
||||
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
|
||||
self.kind
|
||||
]["Value"]
|
||||
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][self.kind]
|
||||
if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE:
|
||||
return cast(float, self._sensor_data["Value"])
|
||||
if self.kind == "UVIndex":
|
||||
return cast(int, self._sensor_data["Value"])
|
||||
if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "Ozone"]:
|
||||
return cast(int, self._sensor_data["Value"])
|
||||
if self.kind == "Ceiling":
|
||||
return round(self.coordinator.data[self.kind][self._unit_system]["Value"])
|
||||
return round(self._sensor_data[self._unit_system]["Value"])
|
||||
if self.kind == "PressureTendency":
|
||||
return self.coordinator.data[self.kind]["LocalizedText"].lower()
|
||||
if SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE:
|
||||
return self.coordinator.data[self.kind][self._unit_system]["Value"]
|
||||
return cast(str, self._sensor_data["LocalizedText"].lower())
|
||||
if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE:
|
||||
return cast(float, self._sensor_data[self._unit_system]["Value"])
|
||||
if self.kind == "Precipitation":
|
||||
return self.coordinator.data["PrecipitationSummary"][self.kind][
|
||||
self._unit_system
|
||||
]["Value"]
|
||||
return cast(float, self._sensor_data[self._unit_system]["Value"])
|
||||
if self.kind in ["Wind", "WindGust"]:
|
||||
return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"]
|
||||
return self.coordinator.data[self.kind]
|
||||
return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"])
|
||||
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
|
||||
return cast(StateType, self._sensor_data["Speed"]["Value"])
|
||||
return cast(StateType, self._sensor_data)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon."""
|
||||
if self.forecast_day is not None:
|
||||
return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON]
|
||||
return SENSOR_TYPES[self.kind][ATTR_ICON]
|
||||
return self._description[ATTR_ICON]
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
def device_class(self) -> str | None:
|
||||
"""Return the device_class."""
|
||||
if self.forecast_day is not None:
|
||||
return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
|
||||
return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
|
||||
return self._description[ATTR_DEVICE_CLASS]
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit the value is expressed in."""
|
||||
if self.forecast_day is not None:
|
||||
return FORECAST_SENSOR_TYPES[self.kind][self._unit_system]
|
||||
return SENSOR_TYPES[self.kind][self._unit_system]
|
||||
if self.coordinator.is_metric:
|
||||
return self._description[ATTR_UNIT_METRIC]
|
||||
return self._description[ATTR_UNIT_IMPERIAL]
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
if self.forecast_day is not None:
|
||||
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
|
||||
self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][
|
||||
self.forecast_day
|
||||
][self.kind]["Direction"]["English"]
|
||||
self._attrs["direction"] = self._sensor_data["Direction"]["English"]
|
||||
elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
|
||||
self._attrs["level"] = self.coordinator.data[ATTR_FORECAST][
|
||||
self.forecast_day
|
||||
][self.kind]["Category"]
|
||||
self._attrs["level"] = self._sensor_data["Category"]
|
||||
return self._attrs
|
||||
if self.kind == "UVIndex":
|
||||
self._attrs["level"] = self.coordinator.data["UVIndexText"]
|
||||
@ -161,6 +179,6 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self):
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return bool(self.kind not in OPTIONAL_SENSORS)
|
||||
return self._description[ATTR_ENABLED]
|
||||
|
@ -1,4 +1,8 @@
|
||||
"""Provide info to system health."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from accuweather.const import ENDPOINT
|
||||
|
||||
from homeassistant.components import system_health
|
||||
@ -15,7 +19,7 @@ def async_register(
|
||||
register.async_register_info(system_health_info)
|
||||
|
||||
|
||||
async def system_health_info(hass):
|
||||
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Get info for the info page."""
|
||||
remaining_requests = list(hass.data[DOMAIN].values())[0][
|
||||
COORDINATOR
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Support for the AccuWeather service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from statistics import mean
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
@ -12,11 +15,18 @@ from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
WeatherEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from . import AccuWeatherDataUpdateCoordinator
|
||||
from .const import (
|
||||
API_IMPERIAL,
|
||||
API_METRIC,
|
||||
ATTR_FORECAST,
|
||||
ATTRIBUTION,
|
||||
CONDITION_CLASSES,
|
||||
@ -29,42 +39,49 @@ from .const import (
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Add a AccuWeather weather entity from a config_entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
name: str = entry.data[CONF_NAME]
|
||||
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
|
||||
COORDINATOR
|
||||
]
|
||||
|
||||
async_add_entities([AccuWeatherEntity(name, coordinator)], False)
|
||||
async_add_entities([AccuWeatherEntity(name, coordinator)])
|
||||
|
||||
|
||||
class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
|
||||
"""Define an AccuWeather entity."""
|
||||
|
||||
def __init__(self, name, coordinator):
|
||||
coordinator: AccuWeatherDataUpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self, name: str, coordinator: AccuWeatherDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._attrs = {}
|
||||
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
|
||||
self._unit_system = API_METRIC if self.coordinator.is_metric else API_IMPERIAL
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
def attribution(self) -> str:
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique_id for this entity."""
|
||||
return self.coordinator.location_key
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.coordinator.location_key)},
|
||||
@ -74,7 +91,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
|
||||
}
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
try:
|
||||
return [
|
||||
@ -86,52 +103,60 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
def temperature(self) -> float:
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data["Temperature"][self._unit_system]["Value"]
|
||||
return cast(
|
||||
float, self.coordinator.data["Temperature"][self._unit_system]["Value"]
|
||||
)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
def pressure(self) -> float:
|
||||
"""Return the pressure."""
|
||||
return self.coordinator.data["Pressure"][self._unit_system]["Value"]
|
||||
return cast(
|
||||
float, self.coordinator.data["Pressure"][self._unit_system]["Value"]
|
||||
)
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
def humidity(self) -> int:
|
||||
"""Return the humidity."""
|
||||
return self.coordinator.data["RelativeHumidity"]
|
||||
return cast(int, self.coordinator.data["RelativeHumidity"])
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
def wind_speed(self) -> float:
|
||||
"""Return the wind speed."""
|
||||
return self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"]
|
||||
return cast(
|
||||
float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"]
|
||||
)
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
def wind_bearing(self) -> int:
|
||||
"""Return the wind bearing."""
|
||||
return self.coordinator.data["Wind"]["Direction"]["Degrees"]
|
||||
return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"])
|
||||
|
||||
@property
|
||||
def visibility(self):
|
||||
def visibility(self) -> float:
|
||||
"""Return the visibility."""
|
||||
return self.coordinator.data["Visibility"][self._unit_system]["Value"]
|
||||
return cast(
|
||||
float, self.coordinator.data["Visibility"][self._unit_system]["Value"]
|
||||
)
|
||||
|
||||
@property
|
||||
def ozone(self):
|
||||
def ozone(self) -> int | None:
|
||||
"""Return the ozone level."""
|
||||
# We only have ozone data for certain locations and only in the forecast data.
|
||||
if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get(
|
||||
"Ozone"
|
||||
):
|
||||
return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"]
|
||||
return cast(int, self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"])
|
||||
return None
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
def forecast(self) -> list[dict[str, Any]] | None:
|
||||
"""Return the forecast array."""
|
||||
if not self.coordinator.forecast:
|
||||
return None
|
||||
@ -161,7 +186,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
|
||||
return forecast
|
||||
|
||||
@staticmethod
|
||||
def _calc_precipitation(day: dict) -> float:
|
||||
def _calc_precipitation(day: dict[str, Any]) -> float:
|
||||
"""Return sum of the precipitation."""
|
||||
precip_sum = 0
|
||||
precip_types = ["Rain", "Snow", "Ice"]
|
||||
|
34
homeassistant/components/acer_projector/const.py
Normal file
34
homeassistant/components/acer_projector/const.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""Use serial protocol of Acer projector to obtain state of the projector."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
|
||||
CONF_WRITE_TIMEOUT: Final = "write_timeout"
|
||||
|
||||
DEFAULT_NAME: Final = "Acer Projector"
|
||||
DEFAULT_TIMEOUT: Final = 1
|
||||
DEFAULT_WRITE_TIMEOUT: Final = 1
|
||||
|
||||
ECO_MODE: Final = "ECO Mode"
|
||||
|
||||
ICON: Final = "mdi:projector"
|
||||
|
||||
INPUT_SOURCE: Final = "Input Source"
|
||||
|
||||
LAMP: Final = "Lamp"
|
||||
LAMP_HOURS: Final = "Lamp Hours"
|
||||
|
||||
MODEL: Final = "Model"
|
||||
|
||||
# Commands known to the projector
|
||||
CMD_DICT: Final[dict[str, str]] = {
|
||||
LAMP: "* 0 Lamp ?\r",
|
||||
LAMP_HOURS: "* 0 Lamp\r",
|
||||
INPUT_SOURCE: "* 0 Src ?\r",
|
||||
ECO_MODE: "* 0 IR 052\r",
|
||||
MODEL: "* 0 IR 035\r",
|
||||
STATE_ON: "* 0 IR 001\r",
|
||||
STATE_OFF: "* 0 IR 002\r",
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
"""Use serial protocol of Acer projector to obtain state of the projector."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import serial
|
||||
import voluptuous as vol
|
||||
@ -14,39 +17,26 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CMD_DICT,
|
||||
CONF_WRITE_TIMEOUT,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_TIMEOUT,
|
||||
DEFAULT_WRITE_TIMEOUT,
|
||||
ECO_MODE,
|
||||
ICON,
|
||||
INPUT_SOURCE,
|
||||
LAMP,
|
||||
LAMP_HOURS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_WRITE_TIMEOUT = "write_timeout"
|
||||
|
||||
DEFAULT_NAME = "Acer Projector"
|
||||
DEFAULT_TIMEOUT = 1
|
||||
DEFAULT_WRITE_TIMEOUT = 1
|
||||
|
||||
ECO_MODE = "ECO Mode"
|
||||
|
||||
ICON = "mdi:projector"
|
||||
|
||||
INPUT_SOURCE = "Input Source"
|
||||
|
||||
LAMP = "Lamp"
|
||||
LAMP_HOURS = "Lamp Hours"
|
||||
|
||||
MODEL = "Model"
|
||||
|
||||
# Commands known to the projector
|
||||
CMD_DICT = {
|
||||
LAMP: "* 0 Lamp ?\r",
|
||||
LAMP_HOURS: "* 0 Lamp\r",
|
||||
INPUT_SOURCE: "* 0 Src ?\r",
|
||||
ECO_MODE: "* 0 IR 052\r",
|
||||
MODEL: "* 0 IR 035\r",
|
||||
STATE_ON: "* 0 IR 001\r",
|
||||
STATE_OFF: "* 0 IR 002\r",
|
||||
}
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_FILENAME): cv.isdevice,
|
||||
@ -59,7 +49,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType,
|
||||
) -> None:
|
||||
"""Connect with serial port and return Acer Projector."""
|
||||
serial_port = config[CONF_FILENAME]
|
||||
name = config[CONF_NAME]
|
||||
@ -72,10 +67,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
class AcerSwitch(SwitchEntity):
|
||||
"""Represents an Acer Projector as a switch."""
|
||||
|
||||
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
serial_port: str,
|
||||
name: str,
|
||||
timeout: int,
|
||||
write_timeout: int,
|
||||
) -> None:
|
||||
"""Init of the Acer projector."""
|
||||
self.ser = serial.Serial(
|
||||
port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs
|
||||
port=serial_port, timeout=timeout, write_timeout=write_timeout
|
||||
)
|
||||
self._serial_port = serial_port
|
||||
self._name = name
|
||||
@ -87,7 +88,7 @@ class AcerSwitch(SwitchEntity):
|
||||
ECO_MODE: STATE_UNKNOWN,
|
||||
}
|
||||
|
||||
def _write_read(self, msg):
|
||||
def _write_read(self, msg: str) -> str:
|
||||
"""Write to the projector and read the return."""
|
||||
ret = ""
|
||||
# Sometimes the projector won't answer for no reason or the projector
|
||||
@ -96,8 +97,7 @@ class AcerSwitch(SwitchEntity):
|
||||
try:
|
||||
if not self.ser.is_open:
|
||||
self.ser.open()
|
||||
msg = msg.encode("utf-8")
|
||||
self.ser.write(msg)
|
||||
self.ser.write(msg.encode("utf-8"))
|
||||
# Size is an experience value there is no real limit.
|
||||
# AFAIK there is no limit and no end character so we will usually
|
||||
# need to wait for timeout
|
||||
@ -107,7 +107,7 @@ class AcerSwitch(SwitchEntity):
|
||||
self.ser.close()
|
||||
return ret
|
||||
|
||||
def _write_read_format(self, msg):
|
||||
def _write_read_format(self, msg: str) -> str:
|
||||
"""Write msg, obtain answer and format output."""
|
||||
# answers are formatted as ***\answer\r***
|
||||
awns = self._write_read(msg)
|
||||
@ -117,29 +117,33 @@ class AcerSwitch(SwitchEntity):
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
def available(self) -> bool:
|
||||
"""Return if projector is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return name of the projector."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if the projector is turned on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return state attributes."""
|
||||
return self._attributes
|
||||
|
||||
def update(self):
|
||||
def update(self) -> None:
|
||||
"""Get the latest state from the projector."""
|
||||
msg = CMD_DICT[LAMP]
|
||||
awns = self._write_read_format(msg)
|
||||
awns = self._write_read_format(CMD_DICT[LAMP])
|
||||
if awns == "Lamp 1":
|
||||
self._state = True
|
||||
self._available = True
|
||||
@ -155,14 +159,14 @@ class AcerSwitch(SwitchEntity):
|
||||
awns = self._write_read_format(msg)
|
||||
self._attributes[key] = awns
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the projector on."""
|
||||
msg = CMD_DICT[STATE_ON]
|
||||
self._write_read(msg)
|
||||
self._state = STATE_ON
|
||||
self._state = True
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the projector off."""
|
||||
msg = CMD_DICT[STATE_OFF]
|
||||
self._write_read(msg)
|
||||
self._state = STATE_OFF
|
||||
self._state = False
|
||||
|
@ -13,7 +13,7 @@ from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER
|
||||
class AcmedaBase(entity.Entity):
|
||||
"""Base representation of an Acmeda roller."""
|
||||
|
||||
def __init__(self, roller: aiopulse.Roller):
|
||||
def __init__(self, roller: aiopulse.Roller) -> None:
|
||||
"""Initialize the roller."""
|
||||
self.roller = roller
|
||||
|
||||
|
@ -17,7 +17,6 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Acmeda config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the config flow."""
|
||||
|
12
homeassistant/components/actiontec/const.py
Normal file
12
homeassistant/components/actiontec/const.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""Support for Actiontec MI424WR (Verizon FIOS) routers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Final
|
||||
|
||||
LEASES_REGEX: Final[re.Pattern] = re.compile(
|
||||
r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})"
|
||||
+ r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))"
|
||||
+ r"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
|
||||
+ r"\ssec"
|
||||
)
|
@ -1,30 +1,28 @@
|
||||
"""Support for Actiontec MI424WR (Verizon FIOS) routers."""
|
||||
from collections import namedtuple
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import telnetlib
|
||||
from typing import Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN,
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
|
||||
DeviceScanner,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import LEASES_REGEX
|
||||
from .model import Device
|
||||
|
||||
_LEASES_REGEX = re.compile(
|
||||
r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})"
|
||||
+ r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))"
|
||||
+ r"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
|
||||
+ r"\ssec"
|
||||
)
|
||||
_LOGGER: Final = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
@ -33,43 +31,38 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None:
|
||||
"""Validate the configuration and return an Actiontec scanner."""
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
|
||||
|
||||
class ActiontecDeviceScanner(DeviceScanner):
|
||||
"""This class queries an actiontec router for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, config: ConfigType) -> None:
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.last_results = []
|
||||
self.host: str = config[CONF_HOST]
|
||||
self.username: str = config[CONF_USERNAME]
|
||||
self.password: str = config[CONF_PASSWORD]
|
||||
self.last_results: list[Device] = []
|
||||
data = self.get_actiontec_data()
|
||||
self.success_init = data is not None
|
||||
_LOGGER.info("Scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
def scan_devices(self) -> list[str]:
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client.mac for client in self.last_results]
|
||||
return [client.mac_address for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
def get_device_name(self, device: str) -> str | None:
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
if client.mac == device:
|
||||
return client.ip
|
||||
if client.mac_address == device:
|
||||
return client.ip_address
|
||||
return None
|
||||
|
||||
def _update_info(self):
|
||||
def _update_info(self) -> bool:
|
||||
"""Ensure the information from the router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
@ -78,19 +71,16 @@ class ActiontecDeviceScanner(DeviceScanner):
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
now = dt_util.now()
|
||||
actiontec_data = self.get_actiontec_data()
|
||||
if not actiontec_data:
|
||||
if actiontec_data is None:
|
||||
return False
|
||||
self.last_results = [
|
||||
Device(data["mac"], name, now)
|
||||
for name, data in actiontec_data.items()
|
||||
if data["timevalid"] > -60
|
||||
device for device in actiontec_data if device.timevalid > -60
|
||||
]
|
||||
_LOGGER.info("Scan successful")
|
||||
return True
|
||||
|
||||
def get_actiontec_data(self):
|
||||
def get_actiontec_data(self) -> list[Device] | None:
|
||||
"""Retrieve data from Actiontec MI424WR and return parsed result."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
@ -106,18 +96,20 @@ class ActiontecDeviceScanner(DeviceScanner):
|
||||
telnet.write(b"exit\n")
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
return
|
||||
return None
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router. Telnet enabled?")
|
||||
return None
|
||||
|
||||
devices = {}
|
||||
devices: list[Device] = []
|
||||
for lease in leases_result:
|
||||
match = _LEASES_REGEX.search(lease.decode("utf-8"))
|
||||
match = LEASES_REGEX.search(lease.decode("utf-8"))
|
||||
if match is not None:
|
||||
devices[match.group("ip")] = {
|
||||
"ip": match.group("ip"),
|
||||
"mac": match.group("mac").upper(),
|
||||
"timevalid": int(match.group("timevalid")),
|
||||
}
|
||||
devices.append(
|
||||
Device(
|
||||
match.group("ip"),
|
||||
match.group("mac").upper(),
|
||||
int(match.group("timevalid")),
|
||||
)
|
||||
)
|
||||
return devices
|
||||
|
11
homeassistant/components/actiontec/model.py
Normal file
11
homeassistant/components/actiontec/model.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Model definitions for Actiontec MI424WR (Verizon FIOS) routers."""
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
"""Actiontec device class."""
|
||||
|
||||
ip_address: str
|
||||
mac_address: str
|
||||
timevalid: int
|
@ -2,22 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.adguard.const import (
|
||||
CONF_FORCE,
|
||||
DATA_ADGUARD_CLIENT,
|
||||
DATA_ADGUARD_VERSION,
|
||||
DOMAIN,
|
||||
SERVICE_ADD_URL,
|
||||
SERVICE_DISABLE_URL,
|
||||
SERVICE_ENABLE_URL,
|
||||
SERVICE_REFRESH,
|
||||
SERVICE_REMOVE_URL,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@ -33,7 +21,19 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
||||
from .const import (
|
||||
CONF_FORCE,
|
||||
DATA_ADGUARD_CLIENT,
|
||||
DATA_ADGUARD_VERSION,
|
||||
DOMAIN,
|
||||
SERVICE_ADD_URL,
|
||||
SERVICE_DISABLE_URL,
|
||||
SERVICE_ENABLE_URL,
|
||||
SERVICE_REFRESH,
|
||||
SERVICE_REMOVE_URL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -194,7 +194,7 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
|
||||
"""Defines a AdGuard Home device entity."""
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict[str, Any]:
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this AdGuard Home instance."""
|
||||
return {
|
||||
"identifiers": {
|
||||
|
@ -6,7 +6,6 @@ from typing import Any
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@ -26,7 +25,6 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a AdGuard Home config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
_hassio_discovery = None
|
||||
|
||||
@ -67,13 +65,9 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return await self._show_setup_form(user_input)
|
||||
|
||||
entries = self._async_current_entries()
|
||||
for entry in entries:
|
||||
if (
|
||||
entry.data[CONF_HOST] == user_input[CONF_HOST]
|
||||
and entry.data[CONF_PORT] == user_input[CONF_PORT]
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
self._async_abort_entries_match(
|
||||
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Callable
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
|
||||
@ -11,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdGuardHomeDeviceEntity
|
||||
from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN
|
||||
@ -23,7 +22,7 @@ PARALLEL_UPDATES = 4
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: Callable[[list[Entity], bool], None],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdGuard Home sensor based on a config entry."""
|
||||
adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT]
|
||||
|
@ -1,37 +1,65 @@
|
||||
add_url:
|
||||
name: Add url
|
||||
description: Add a new filter subscription to AdGuard Home.
|
||||
fields:
|
||||
name:
|
||||
name: Name
|
||||
description: The name of the filter subscription.
|
||||
required: true
|
||||
example: Example
|
||||
selector:
|
||||
text:
|
||||
url:
|
||||
name: Url
|
||||
description: The filter URL to subscribe to, containing the filter rules.
|
||||
required: true
|
||||
example: https://www.example.com/filter/1.txt
|
||||
selector:
|
||||
text:
|
||||
|
||||
remove_url:
|
||||
name: Remove url
|
||||
description: Removes a filter subscription from AdGuard Home.
|
||||
fields:
|
||||
url:
|
||||
name: Url
|
||||
description: The filter subscription URL to remove.
|
||||
required: true
|
||||
example: https://www.example.com/filter/1.txt
|
||||
selector:
|
||||
text:
|
||||
|
||||
enable_url:
|
||||
name: Enable url
|
||||
description: Enables a filter subscription in AdGuard Home.
|
||||
fields:
|
||||
url:
|
||||
name: Url
|
||||
description: The filter subscription URL to enable.
|
||||
required: true
|
||||
example: https://www.example.com/filter/1.txt
|
||||
selector:
|
||||
text:
|
||||
|
||||
disable_url:
|
||||
name: Disable url
|
||||
description: Disables a filter subscription in AdGuard Home.
|
||||
fields:
|
||||
url:
|
||||
name: Url
|
||||
description: The filter subscription URL to disable.
|
||||
required: true
|
||||
example: https://www.example.com/filter/1.txt
|
||||
selector:
|
||||
text:
|
||||
|
||||
refresh:
|
||||
name: Refresh
|
||||
description: Refresh all filter subscriptions in AdGuard Home.
|
||||
fields:
|
||||
force:
|
||||
description: Force update (by passes AdGuard Home throttling).
|
||||
example: '"true" to force, "false" or omit for a regular refresh.'
|
||||
name: Force
|
||||
description: Force update (bypasses AdGuard Home throttling). "true" to force, or "false" to omit for a regular refresh.
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
|
||||
|
||||
@ -11,7 +10,7 @@ from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdGuardHomeDeviceEntity
|
||||
from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN
|
||||
@ -25,7 +24,7 @@ PARALLEL_UPDATES = 1
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: Callable[[list[Entity], bool], None],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdGuard Home switch based on a config entry."""
|
||||
adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT]
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.",
|
||||
"single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home."
|
||||
"existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "El servei ja est\u00e0 configurat",
|
||||
"existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.",
|
||||
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
|
||||
"existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Ha fallat la connexi\u00f3"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Slu\u017eba je ji\u017e nastavena",
|
||||
"existing_instance_updated": "St\u00e1vaj\u00edc\u00ed nastaven\u00ed aktualizov\u00e1no.",
|
||||
"single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
|
||||
"existing_instance_updated": "St\u00e1vaj\u00edc\u00ed nastaven\u00ed aktualizov\u00e1no."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Opdaterede eksisterende konfiguration.",
|
||||
"single_instance_allowed": "Kun en enkelt konfiguration af AdGuard Home er tilladt."
|
||||
"existing_instance_updated": "Opdaterede eksisterende konfiguration."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.",
|
||||
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
|
||||
"already_configured": "Der Dienst ist bereits konfiguriert",
|
||||
"existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Service is already configured",
|
||||
"existing_instance_updated": "Updated existing configuration.",
|
||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||
"existing_instance_updated": "Updated existing configuration."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.",
|
||||
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
|
||||
"existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "El servicio ya est\u00e1 configurado",
|
||||
"existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.",
|
||||
"single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
|
||||
"existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Teenus on juba seadistatud",
|
||||
"existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud.",
|
||||
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
|
||||
"existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00dchendamine nurjus"
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.",
|
||||
"single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e."
|
||||
"already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9",
|
||||
"existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec de connexion"
|
||||
|
@ -1,8 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
},
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Memperbarui konfigurasi yang ada.",
|
||||
"single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
|
||||
"existing_instance_updated": "Memperbarui konfigurasi yang ada."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Gagal terhubung"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Il servizio \u00e8 gi\u00e0 configurato",
|
||||
"existing_instance_updated": "Configurazione esistente aggiornata.",
|
||||
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
|
||||
"existing_instance_updated": "Configurazione esistente aggiornata."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.",
|
||||
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
|
||||
"existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Feeler beim verbannen"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Service is al geconfigureerd",
|
||||
"existing_instance_updated": "Bestaande configuratie bijgewerkt.",
|
||||
"single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan."
|
||||
"existing_instance_updated": "Bestaande configuratie bijgewerkt."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Tjenesten er allerede konfigurert",
|
||||
"existing_instance_updated": "Oppdatert eksisterende konfigurasjon.",
|
||||
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
|
||||
"existing_instance_updated": "Oppdatert eksisterende konfigurasjon."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Tilkobling mislyktes"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana",
|
||||
"existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119",
|
||||
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
|
||||
"existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada.",
|
||||
"single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do AdGuard Home \u00e9 permitida."
|
||||
"existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
|
@ -1,8 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Falha na liga\u00e7\u00e3o"
|
||||
},
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
|
||||
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.",
|
||||
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
|
||||
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.",
|
||||
"single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home."
|
||||
"existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Uppdaterade existerande konfiguration.",
|
||||
"single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten."
|
||||
"existing_instance_updated": "Uppdaterade existerande konfiguration."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
|
@ -1,8 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Ba\u011flanma hatas\u0131"
|
||||
},
|
||||
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430.",
|
||||
"single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
|
||||
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
|
||||
|
@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
|
||||
"existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002",
|
||||
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
|
||||
"existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u9023\u7dda\u5931\u6557"
|
||||
|
@ -1,15 +1,34 @@
|
||||
# Describes the format for available ADS services
|
||||
|
||||
write_data_by_name:
|
||||
name: Write data by name
|
||||
description: Write a value to the connected ADS device.
|
||||
|
||||
fields:
|
||||
adsvar:
|
||||
name: ADS variable
|
||||
description: The name of the variable to write to.
|
||||
required: true
|
||||
example: ".global_var"
|
||||
selector:
|
||||
text:
|
||||
adstype:
|
||||
name: ADS type
|
||||
description: The data type of the variable to write to.
|
||||
example: "int"
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- 'bool'
|
||||
- 'byte'
|
||||
- 'dint'
|
||||
- 'int'
|
||||
- 'udint'
|
||||
- 'uint'
|
||||
value:
|
||||
name: Value
|
||||
description: The value to write to the variable.
|
||||
example: 1
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 10000
|
||||
|
@ -70,7 +70,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
entities.append(AdvantageAirZone(instance, ac_key, zone_key))
|
||||
async_add_entities(entities)
|
||||
|
||||
platform = entity_platform.current_platform.get()
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
ADVANTAGE_AIR_SERVICE_SET_MYZONE,
|
||||
{},
|
||||
|
@ -22,7 +22,7 @@ class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config Advantage Air API connection."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
|
@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
|
||||
async_add_entities(entities)
|
||||
|
||||
platform = entity_platform.current_platform.get()
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
ADVANTAGE_AIR_SERVICE_SET_TIME_TO,
|
||||
{vol.Required("minutes"): cv.positive_int},
|
||||
|
@ -7,8 +7,15 @@ set_time_to:
|
||||
domain: sensor
|
||||
fields:
|
||||
minutes:
|
||||
name: Minutes
|
||||
description: Minutes until action
|
||||
example: "60"
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1440
|
||||
unit_of_measurement: minutes
|
||||
|
||||
set_myzone:
|
||||
name: Set MyZone
|
||||
description: Change which zone is set as the reference for temperature control
|
||||
|
@ -12,6 +12,7 @@
|
||||
"ip_address": "IP Adresse",
|
||||
"port": "Port"
|
||||
},
|
||||
"description": "Anschluss an die API Ihres Advantage Air Wandtabletts.",
|
||||
"title": "Verbinden"
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,13 @@ 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 DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, PLATFORMS
|
||||
from .const import (
|
||||
CONF_STATION_UPDATES,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -19,9 +25,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
station_updates = config_entry.options.get(CONF_STATION_UPDATES, True)
|
||||
|
||||
aemet = AEMET(api_key)
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude)
|
||||
weather_coordinator = WeatherUpdateCoordinator(
|
||||
hass, aemet, latitude, longitude, station_updates
|
||||
)
|
||||
|
||||
await weather_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@ -33,9 +42,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
|
||||
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
|
||||
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(async_update_options))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Update options."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
|
@ -1,58 +0,0 @@
|
||||
"""Abstraction form AEMET OpenData sensors."""
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
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, SensorEntity):
|
||||
"""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 extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
@ -4,16 +4,15 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
|
||||
|
||||
|
||||
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 = {}
|
||||
@ -49,6 +48,35 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle a option flow for AEMET."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle options flow."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_STATION_UPDATES,
|
||||
default=self.config_entry.options.get(CONF_STATION_UPDATES),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||
|
||||
|
||||
async def _is_aemet_api_online(hass, api_key):
|
||||
aemet = AEMET(api_key)
|
||||
|
@ -34,12 +34,12 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||
CONF_STATION_UPDATES = "station_updates"
|
||||
PLATFORMS = ["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"
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "AEMET OpenData",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"requirements": ["AEMET-OpenData==0.1.8"],
|
||||
"requirements": ["AEMET-OpenData==0.2.1"],
|
||||
"codeowners": ["@noltari"],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from .abstract_aemet_sensor import AbstractAemetSensor
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
@ -10,6 +14,9 @@ from .const import (
|
||||
FORECAST_MONITORED_CONDITIONS,
|
||||
FORECAST_SENSOR_TYPES,
|
||||
MONITORED_CONDITIONS,
|
||||
SENSOR_DEVICE_CLASS,
|
||||
SENSOR_NAME,
|
||||
SENSOR_UNIT,
|
||||
WEATHER_SENSOR_TYPES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
@ -56,6 +63,52 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
|
||||
"""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 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 extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
|
||||
|
||||
class AemetSensor(AbstractAemetSensor):
|
||||
"""Implementation of an AEMET OpenData sensor."""
|
||||
|
||||
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "Gather data from AEMET weather stations"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "Obt\u00e9 les dades de les estacions meteorol\u00f2giques d'AEMET"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "Gather data from AEMET weather stations"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "Obtener datos de las estaciones meteorol\u00f3gicas de AEMET"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "Koguandmeid AEMETi ilmajaamadest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "Raccogli i dati dalle stazioni meteorologiche AEMET"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "Verzamel gegevens van AEMET-weerstations"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "Samle inn data fra AEMET v\u00e6rstasjoner"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "\u0421\u0431\u043e\u0440 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0439 AEMET"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,5 +18,14 @@
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"station_updates": "\u81ea AEMET \u6c23\u8c61\u7ad9\u7372\u5f97\u8cc7\u6599"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -118,7 +118,7 @@ class TownNotFound(UpdateFailed):
|
||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Weather data update coordinator."""
|
||||
|
||||
def __init__(self, hass, aemet, latitude, longitude):
|
||||
def __init__(self, hass, aemet, latitude, longitude, station_updates):
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
||||
@ -129,6 +129,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
self._town = None
|
||||
self._latitude = latitude
|
||||
self._longitude = longitude
|
||||
self._station_updates = station_updates
|
||||
self._data = {
|
||||
"daily": None,
|
||||
"hourly": None,
|
||||
@ -210,7 +211,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
)
|
||||
|
||||
station = None
|
||||
if self._get_weather_station():
|
||||
if self._station_updates and self._get_weather_station():
|
||||
station = self._aemet.get_conventional_observation_station_data(
|
||||
self._station[AEMET_ATTR_IDEMA]
|
||||
)
|
||||
|
@ -1,2 +1,42 @@
|
||||
"""Constants for the Aftership integration."""
|
||||
DOMAIN = "aftership"
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN: Final = "aftership"
|
||||
|
||||
ATTRIBUTION: Final = "Information provided by AfterShip"
|
||||
ATTR_TRACKINGS: Final = "trackings"
|
||||
|
||||
BASE: Final = "https://track.aftership.com/"
|
||||
|
||||
CONF_SLUG: Final = "slug"
|
||||
CONF_TITLE: Final = "title"
|
||||
CONF_TRACKING_NUMBER: Final = "tracking_number"
|
||||
|
||||
DEFAULT_NAME: Final = "aftership"
|
||||
UPDATE_TOPIC: Final = f"{DOMAIN}_update"
|
||||
|
||||
ICON: Final = "mdi:package-variant-closed"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES: Final = timedelta(minutes=15)
|
||||
|
||||
SERVICE_ADD_TRACKING: Final = "add_tracking"
|
||||
SERVICE_REMOVE_TRACKING: Final = "remove_tracking"
|
||||
|
||||
ADD_TRACKING_SERVICE_SCHEMA: Final = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TRACKING_NUMBER): cv.string,
|
||||
vol.Optional(CONF_TITLE): cv.string,
|
||||
vol.Optional(CONF_SLUG): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
REMOVE_TRACKING_SERVICE_SCHEMA: Final = vol.Schema(
|
||||
{vol.Required(CONF_SLUG): cv.string, vol.Required(CONF_TRACKING_NUMBER): cv.string}
|
||||
)
|
||||
|
@ -1,53 +1,47 @@
|
||||
"""Support for non-delivered packages recorded in AfterShip."""
|
||||
from datetime import timedelta
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
|
||||
from pyaftership.tracker import Tracking
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, HTTP_OK
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.service import ServiceCall
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = "Information provided by AfterShip"
|
||||
ATTR_TRACKINGS = "trackings"
|
||||
|
||||
BASE = "https://track.aftership.com/"
|
||||
|
||||
CONF_SLUG = "slug"
|
||||
CONF_TITLE = "title"
|
||||
CONF_TRACKING_NUMBER = "tracking_number"
|
||||
|
||||
DEFAULT_NAME = "aftership"
|
||||
UPDATE_TOPIC = f"{DOMAIN}_update"
|
||||
|
||||
ICON = "mdi:package-variant-closed"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
SERVICE_ADD_TRACKING = "add_tracking"
|
||||
SERVICE_REMOVE_TRACKING = "remove_tracking"
|
||||
|
||||
ADD_TRACKING_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TRACKING_NUMBER): cv.string,
|
||||
vol.Optional(CONF_TITLE): cv.string,
|
||||
vol.Optional(CONF_SLUG): cv.string,
|
||||
}
|
||||
from .const import (
|
||||
ADD_TRACKING_SERVICE_SCHEMA,
|
||||
ATTR_TRACKINGS,
|
||||
ATTRIBUTION,
|
||||
BASE,
|
||||
CONF_SLUG,
|
||||
CONF_TITLE,
|
||||
CONF_TRACKING_NUMBER,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
ICON,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
REMOVE_TRACKING_SERVICE_SCHEMA,
|
||||
SERVICE_ADD_TRACKING,
|
||||
SERVICE_REMOVE_TRACKING,
|
||||
UPDATE_TOPIC,
|
||||
)
|
||||
|
||||
REMOVE_TRACKING_SERVICE_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_SLUG): cv.string, vol.Required(CONF_TRACKING_NUMBER): cv.string}
|
||||
)
|
||||
_LOGGER: Final = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@ -55,7 +49,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the AfterShip sensor platform."""
|
||||
apikey = config[CONF_API_KEY]
|
||||
name = config[CONF_NAME]
|
||||
@ -75,7 +74,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
|
||||
async_add_entities([instance], True)
|
||||
|
||||
async def handle_add_tracking(call):
|
||||
async def handle_add_tracking(call: ServiceCall) -> None:
|
||||
"""Call when a user adds a new Aftership tracking from Home Assistant."""
|
||||
title = call.data.get(CONF_TITLE)
|
||||
slug = call.data.get(CONF_SLUG)
|
||||
@ -91,7 +90,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
schema=ADD_TRACKING_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
async def handle_remove_tracking(call):
|
||||
async def handle_remove_tracking(call: ServiceCall) -> None:
|
||||
"""Call when a user removes an Aftership tracking from Home Assistant."""
|
||||
slug = call.data[CONF_SLUG]
|
||||
tracking_number = call.data[CONF_TRACKING_NUMBER]
|
||||
@ -110,39 +109,39 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
class AfterShipSensor(SensorEntity):
|
||||
"""Representation of a AfterShip sensor."""
|
||||
|
||||
def __init__(self, aftership, name):
|
||||
def __init__(self, aftership: Tracking, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._attributes = {}
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._attributes: dict[str, Any] = {}
|
||||
self._name: str = name
|
||||
self._state: int | None = None
|
||||
self.aftership = aftership
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return "packages"
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return attributes for the sensor."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str:
|
||||
"""Icon to use in the frontend."""
|
||||
return ICON
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self.async_on_remove(
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
@ -150,13 +149,13 @@ class AfterShipSensor(SensorEntity):
|
||||
)
|
||||
)
|
||||
|
||||
async def _force_update(self):
|
||||
async def _force_update(self) -> None:
|
||||
"""Force update of data."""
|
||||
await self.async_update(no_throttle=True)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def async_update(self, **kwargs):
|
||||
async def async_update(self, **kwargs: Any) -> None:
|
||||
"""Get the latest data from the AfterShip API."""
|
||||
await self.aftership.get_trackings()
|
||||
|
||||
@ -170,7 +169,7 @@ class AfterShipSensor(SensorEntity):
|
||||
return
|
||||
|
||||
status_to_ignore = {"delivered"}
|
||||
status_counts = {}
|
||||
status_counts: dict[str, int] = {}
|
||||
trackings = []
|
||||
not_delivered_count = 0
|
||||
|
||||
|
@ -1,24 +1,43 @@
|
||||
# Describes the format for available aftership services
|
||||
|
||||
add_tracking:
|
||||
description: Add new tracking to Aftership.
|
||||
name: Add tracking
|
||||
description: Add new tracking number to Aftership.
|
||||
fields:
|
||||
tracking_number:
|
||||
name: Tracking number
|
||||
description: Tracking number for the new tracking
|
||||
required: true
|
||||
example: "123456789"
|
||||
selector:
|
||||
text:
|
||||
title:
|
||||
name: Title
|
||||
description: A custom title for the new tracking
|
||||
example: "Laptop"
|
||||
selector:
|
||||
text:
|
||||
slug:
|
||||
name: Slug
|
||||
description: Slug (carrier) of the new tracking
|
||||
example: "USPS"
|
||||
selector:
|
||||
text:
|
||||
|
||||
remove_tracking:
|
||||
description: Remove a tracking from Aftership.
|
||||
name: Remove tracking
|
||||
description: Remove a tracking number from Aftership.
|
||||
fields:
|
||||
tracking_number:
|
||||
name: Tracking number
|
||||
description: Tracking number of the tracking to remove
|
||||
required: true
|
||||
example: "123456789"
|
||||
selector:
|
||||
text:
|
||||
slug:
|
||||
name: Slug
|
||||
description: Slug (carrier) of the tracking to remove
|
||||
example: "USPS"
|
||||
selector:
|
||||
text:
|
||||
|
@ -59,7 +59,7 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(cameras)
|
||||
|
||||
platform = entity_platform.current_platform.get()
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
for service, method in CAMERA_SERVICES.items():
|
||||
platform.async_register_entity_service(service, {}, method)
|
||||
|
||||
|
@ -1,34 +1,39 @@
|
||||
start_recording:
|
||||
name: Start recording
|
||||
description: Enable continuous recording.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the entity to start recording."
|
||||
example: "camera.camera_1"
|
||||
target:
|
||||
entity:
|
||||
integration: agent_dvr
|
||||
domain: camera
|
||||
|
||||
stop_recording:
|
||||
name: Stop recording
|
||||
description: Disable continuous recording.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the entity to stop recording."
|
||||
example: "camera.camera_1"
|
||||
target:
|
||||
entity:
|
||||
integration: agent_dvr
|
||||
domain: camera
|
||||
|
||||
enable_alerts:
|
||||
name: Enable alerts
|
||||
description: Enable alerts
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the entity to enable alerts."
|
||||
example: "camera.camera_1"
|
||||
target:
|
||||
entity:
|
||||
integration: agent_dvr
|
||||
domain: camera
|
||||
|
||||
disable_alerts:
|
||||
name: Disable alerts
|
||||
description: Disable alerts
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the entity to disable alerts."
|
||||
example: "camera.camera_1"
|
||||
target:
|
||||
entity:
|
||||
integration: agent_dvr
|
||||
domain: camera
|
||||
|
||||
snapshot:
|
||||
name: Snapshot
|
||||
description: Take a photo
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the entity to take a snapshot."
|
||||
example: "camera.camera_1"
|
||||
target:
|
||||
entity:
|
||||
integration: agent_dvr
|
||||
domain: camera
|
||||
|
@ -1,40 +1,45 @@
|
||||
"""Component for handling Air Quality data for your location."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import final
|
||||
from typing import Final, final
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType, StateType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER: Final = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AQI = "air_quality_index"
|
||||
ATTR_CO2 = "carbon_dioxide"
|
||||
ATTR_CO = "carbon_monoxide"
|
||||
ATTR_N2O = "nitrogen_oxide"
|
||||
ATTR_NO = "nitrogen_monoxide"
|
||||
ATTR_NO2 = "nitrogen_dioxide"
|
||||
ATTR_OZONE = "ozone"
|
||||
ATTR_PM_0_1 = "particulate_matter_0_1"
|
||||
ATTR_PM_10 = "particulate_matter_10"
|
||||
ATTR_PM_2_5 = "particulate_matter_2_5"
|
||||
ATTR_SO2 = "sulphur_dioxide"
|
||||
ATTR_AQI: Final = "air_quality_index"
|
||||
ATTR_CO2: Final = "carbon_dioxide"
|
||||
ATTR_CO: Final = "carbon_monoxide"
|
||||
ATTR_N2O: Final = "nitrogen_oxide"
|
||||
ATTR_NO: Final = "nitrogen_monoxide"
|
||||
ATTR_NO2: Final = "nitrogen_dioxide"
|
||||
ATTR_OZONE: Final = "ozone"
|
||||
ATTR_PM_0_1: Final = "particulate_matter_0_1"
|
||||
ATTR_PM_10: Final = "particulate_matter_10"
|
||||
ATTR_PM_2_5: Final = "particulate_matter_2_5"
|
||||
ATTR_SO2: Final = "sulphur_dioxide"
|
||||
|
||||
DOMAIN = "air_quality"
|
||||
DOMAIN: Final = "air_quality"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
PROP_TO_ATTR: Final[dict[str, str]] = {
|
||||
"air_quality_index": ATTR_AQI,
|
||||
"attribution": ATTR_ATTRIBUTION,
|
||||
"carbon_dioxide": ATTR_CO2,
|
||||
@ -50,7 +55,7 @@ PROP_TO_ATTR = {
|
||||
}
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the air quality component."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||
@ -59,84 +64,86 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
component: EntityComponent = hass.data[DOMAIN]
|
||||
return await component.async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
component: EntityComponent = hass.data[DOMAIN]
|
||||
return await component.async_unload_entry(entry)
|
||||
|
||||
|
||||
class AirQualityEntity(Entity):
|
||||
"""ABC for air quality data."""
|
||||
|
||||
@property
|
||||
def particulate_matter_2_5(self):
|
||||
def particulate_matter_2_5(self) -> StateType:
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self):
|
||||
def particulate_matter_10(self) -> StateType:
|
||||
"""Return the particulate matter 10 level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def particulate_matter_0_1(self):
|
||||
def particulate_matter_0_1(self) -> StateType:
|
||||
"""Return the particulate matter 0.1 level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def air_quality_index(self):
|
||||
def air_quality_index(self) -> StateType:
|
||||
"""Return the Air Quality Index (AQI)."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def ozone(self):
|
||||
def ozone(self) -> StateType:
|
||||
"""Return the O3 (ozone) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def carbon_monoxide(self):
|
||||
def carbon_monoxide(self) -> StateType:
|
||||
"""Return the CO (carbon monoxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def carbon_dioxide(self):
|
||||
def carbon_dioxide(self) -> StateType:
|
||||
"""Return the CO2 (carbon dioxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
def attribution(self) -> StateType:
|
||||
"""Return the attribution."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def sulphur_dioxide(self):
|
||||
def sulphur_dioxide(self) -> StateType:
|
||||
"""Return the SO2 (sulphur dioxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def nitrogen_oxide(self):
|
||||
def nitrogen_oxide(self) -> StateType:
|
||||
"""Return the N2O (nitrogen oxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def nitrogen_monoxide(self):
|
||||
def nitrogen_monoxide(self) -> StateType:
|
||||
"""Return the NO (nitrogen monoxide) level."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def nitrogen_dioxide(self):
|
||||
def nitrogen_dioxide(self) -> StateType:
|
||||
"""Return the NO2 (nitrogen dioxide) level."""
|
||||
return None
|
||||
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def state_attributes(self) -> dict[str, str | int | float]:
|
||||
"""Return the state attributes."""
|
||||
data = {}
|
||||
data: dict[str, str | int | float] = {}
|
||||
|
||||
for prop, attr in PROP_TO_ATTR.items():
|
||||
value = getattr(self, prop)
|
||||
@ -146,11 +153,11 @@ class AirQualityEntity(Entity):
|
||||
return data
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> StateType:
|
||||
"""Return the current state."""
|
||||
return self.particulate_matter_2_5
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
|
@ -1,15 +1,21 @@
|
||||
"""The Airly integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from math import ceil
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from airly import Airly
|
||||
from airly.exceptions import AirlyError
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import async_get_registry
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@ -30,7 +36,7 @@ PLATFORMS = ["air_quality", "sensor"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def set_update_interval(instances, requests_remaining):
|
||||
def set_update_interval(instances_count: int, requests_remaining: int) -> timedelta:
|
||||
"""
|
||||
Return data update interval.
|
||||
|
||||
@ -46,7 +52,7 @@ def set_update_interval(instances, requests_remaining):
|
||||
interval = timedelta(
|
||||
minutes=min(
|
||||
max(
|
||||
ceil(minutes_to_midnight / requests_remaining * instances),
|
||||
ceil(minutes_to_midnight / requests_remaining * instances_count),
|
||||
MIN_UPDATE_INTERVAL,
|
||||
),
|
||||
MAX_UPDATE_INTERVAL,
|
||||
@ -58,19 +64,39 @@ def set_update_interval(instances, requests_remaining):
|
||||
return interval
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Airly as config entry."""
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
use_nearest = config_entry.data.get(CONF_USE_NEAREST, False)
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
latitude = entry.data[CONF_LATITUDE]
|
||||
longitude = entry.data[CONF_LONGITUDE]
|
||||
use_nearest = entry.data.get(CONF_USE_NEAREST, False)
|
||||
|
||||
# For backwards compat, set unique ID
|
||||
if config_entry.unique_id is None:
|
||||
if entry.unique_id is None:
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=f"{latitude}-{longitude}"
|
||||
entry, unique_id=f"{latitude}-{longitude}"
|
||||
)
|
||||
|
||||
# identifiers in device_info should use tuple[str, str] type, but latitude and
|
||||
# longitude are float, so we convert old device entries to use correct types
|
||||
# We used to use a str 3-tuple here sometime, convert that to a 2-tuple too.
|
||||
device_registry = await async_get_registry(hass)
|
||||
old_ids = (DOMAIN, latitude, longitude)
|
||||
for old_ids in (
|
||||
(DOMAIN, latitude, longitude),
|
||||
(
|
||||
DOMAIN,
|
||||
str(latitude),
|
||||
str(longitude),
|
||||
),
|
||||
):
|
||||
device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type]
|
||||
if device_entry and entry.entry_id in device_entry.config_entries:
|
||||
new_ids = (DOMAIN, f"{latitude}-{longitude}")
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, new_identifiers={new_ids}
|
||||
)
|
||||
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
update_interval = timedelta(minutes=MIN_UPDATE_INTERVAL)
|
||||
@ -81,21 +107,19 @@ async def async_setup_entry(hass, config_entry):
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@ -105,14 +129,14 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
session,
|
||||
api_key,
|
||||
latitude,
|
||||
longitude,
|
||||
update_interval,
|
||||
use_nearest,
|
||||
):
|
||||
hass: HomeAssistant,
|
||||
session: ClientSession,
|
||||
api_key: str,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
update_interval: timedelta,
|
||||
use_nearest: bool,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
@ -121,9 +145,9 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||
|
||||
async def _async_update_data(self):
|
||||
async def _async_update_data(self) -> dict[str, str | float | int]:
|
||||
"""Update data via library."""
|
||||
data = {}
|
||||
data: dict[str, str | float | int] = {}
|
||||
if self.use_nearest:
|
||||
measurements = self.airly.create_measurements_session_nearest(
|
||||
self.latitude, self.longitude, max_distance_km=5
|
||||
|
@ -1,13 +1,22 @@
|
||||
"""Support for the Airly air_quality service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.air_quality import (
|
||||
ATTR_AQI,
|
||||
ATTR_PM_2_5,
|
||||
ATTR_PM_10,
|
||||
AirQualityEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirlyDataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_API_ADVICE,
|
||||
ATTR_API_CAQI,
|
||||
@ -36,80 +45,72 @@ LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Airly air_quality entity based on a config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
name = entry.data[CONF_NAME]
|
||||
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities([AirlyAirQuality(coordinator, name)], False)
|
||||
|
||||
|
||||
def round_state(func):
|
||||
"""Round state."""
|
||||
|
||||
def _decorator(self):
|
||||
res = func(self)
|
||||
if isinstance(res, float):
|
||||
return round(res)
|
||||
return res
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
class AirlyAirQuality(CoordinatorEntity, AirQualityEntity):
|
||||
"""Define an Airly air quality."""
|
||||
|
||||
def __init__(self, coordinator, name):
|
||||
coordinator: AirlyDataUpdateCoordinator
|
||||
|
||||
def __init__(self, coordinator: AirlyDataUpdateCoordinator, name: str) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._icon = "mdi:blur"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
@round_state
|
||||
def air_quality_index(self):
|
||||
def air_quality_index(self) -> float | None:
|
||||
"""Return the air quality index."""
|
||||
return self.coordinator.data[ATTR_API_CAQI]
|
||||
return round_state(self.coordinator.data[ATTR_API_CAQI])
|
||||
|
||||
@property
|
||||
@round_state
|
||||
def particulate_matter_2_5(self):
|
||||
def particulate_matter_2_5(self) -> float | None:
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
return self.coordinator.data.get(ATTR_API_PM25)
|
||||
return round_state(self.coordinator.data.get(ATTR_API_PM25))
|
||||
|
||||
@property
|
||||
@round_state
|
||||
def particulate_matter_10(self):
|
||||
def particulate_matter_10(self) -> float | None:
|
||||
"""Return the particulate matter 10 level."""
|
||||
return self.coordinator.data.get(ATTR_API_PM10)
|
||||
return round_state(self.coordinator.data.get(ATTR_API_PM10))
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
def attribution(self) -> str:
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique_id for this entity."""
|
||||
return f"{self.coordinator.latitude}-{self.coordinator.longitude}"
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return {
|
||||
"identifiers": {
|
||||
(DOMAIN, self.coordinator.latitude, self.coordinator.longitude)
|
||||
(
|
||||
DOMAIN,
|
||||
f"{self.coordinator.latitude}-{self.coordinator.longitude}",
|
||||
)
|
||||
},
|
||||
"name": DEFAULT_NAME,
|
||||
"manufacturer": MANUFACTURER,
|
||||
@ -117,7 +118,7 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity):
|
||||
}
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
attrs = {
|
||||
LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION],
|
||||
@ -135,3 +136,8 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity):
|
||||
self.coordinator.data[ATTR_API_PM10_PERCENT]
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
def round_state(state: float | None) -> float | None:
|
||||
"""Round state."""
|
||||
return round(state) if state else state
|
||||
|
@ -1,4 +1,9 @@
|
||||
"""Adds config flow for Airly."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from airly import Airly
|
||||
from airly.exceptions import AirlyError
|
||||
import async_timeout
|
||||
@ -13,6 +18,7 @@ from homeassistant.const import (
|
||||
HTTP_NOT_FOUND,
|
||||
HTTP_UNAUTHORIZED,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@ -23,9 +29,10 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Airly."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
use_nearest = False
|
||||
@ -85,7 +92,13 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
async def test_location(client, api_key, latitude, longitude, use_nearest=False):
|
||||
async def test_location(
|
||||
client: ClientSession,
|
||||
api_key: str,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
use_nearest: bool = False,
|
||||
) -> bool:
|
||||
"""Return true if location is valid."""
|
||||
airly = Airly(api_key, client)
|
||||
if use_nearest:
|
||||
|
@ -1,29 +1,72 @@
|
||||
"""Constants for Airly integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
ATTR_API_ADVICE = "ADVICE"
|
||||
ATTR_API_CAQI = "CAQI"
|
||||
ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION"
|
||||
ATTR_API_CAQI_LEVEL = "LEVEL"
|
||||
ATTR_API_HUMIDITY = "HUMIDITY"
|
||||
ATTR_API_PM1 = "PM1"
|
||||
ATTR_API_PM10 = "PM10"
|
||||
ATTR_API_PM10_LIMIT = "PM10_LIMIT"
|
||||
ATTR_API_PM10_PERCENT = "PM10_PERCENT"
|
||||
ATTR_API_PM25 = "PM25"
|
||||
ATTR_API_PM25_LIMIT = "PM25_LIMIT"
|
||||
ATTR_API_PM25_PERCENT = "PM25_PERCENT"
|
||||
ATTR_API_PRESSURE = "PRESSURE"
|
||||
ATTR_API_TEMPERATURE = "TEMPERATURE"
|
||||
from typing import Final
|
||||
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
PERCENTAGE,
|
||||
PRESSURE_HPA,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
|
||||
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_UPDATE_INTERVAL = 90
|
||||
MIN_UPDATE_INTERVAL = 5
|
||||
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."
|
||||
from .model import SensorDescription
|
||||
|
||||
ATTR_API_ADVICE: Final = "ADVICE"
|
||||
ATTR_API_CAQI: Final = "CAQI"
|
||||
ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION"
|
||||
ATTR_API_CAQI_LEVEL: Final = "LEVEL"
|
||||
ATTR_API_HUMIDITY: Final = "HUMIDITY"
|
||||
ATTR_API_PM10: Final = "PM10"
|
||||
ATTR_API_PM10_LIMIT: Final = "PM10_LIMIT"
|
||||
ATTR_API_PM10_PERCENT: Final = "PM10_PERCENT"
|
||||
ATTR_API_PM1: Final = "PM1"
|
||||
ATTR_API_PM25: Final = "PM25"
|
||||
ATTR_API_PM25_LIMIT: Final = "PM25_LIMIT"
|
||||
ATTR_API_PM25_PERCENT: Final = "PM25_PERCENT"
|
||||
ATTR_API_PRESSURE: Final = "PRESSURE"
|
||||
ATTR_API_TEMPERATURE: Final = "TEMPERATURE"
|
||||
ATTR_LABEL: Final = "label"
|
||||
ATTR_UNIT: Final = "unit"
|
||||
|
||||
ATTRIBUTION: Final = "Data provided by Airly"
|
||||
CONF_USE_NEAREST: Final = "use_nearest"
|
||||
DEFAULT_NAME: Final = "Airly"
|
||||
DOMAIN: Final = "airly"
|
||||
LABEL_ADVICE: Final = "advice"
|
||||
MANUFACTURER: Final = "Airly sp. z o.o."
|
||||
MAX_UPDATE_INTERVAL: Final = 90
|
||||
MIN_UPDATE_INTERVAL: Final = 5
|
||||
NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet."
|
||||
|
||||
SENSOR_TYPES: dict[str, SensorDescription] = {
|
||||
ATTR_API_PM1: {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
ATTR_ICON: "mdi:blur",
|
||||
ATTR_LABEL: ATTR_API_PM1,
|
||||
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
ATTR_API_HUMIDITY: {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
ATTR_ICON: None,
|
||||
ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_PRESSURE: {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||
ATTR_ICON: None,
|
||||
ATTR_LABEL: ATTR_API_PRESSURE.capitalize(),
|
||||
ATTR_UNIT: PRESSURE_HPA,
|
||||
},
|
||||
ATTR_API_TEMPERATURE: {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_ICON: None,
|
||||
ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(),
|
||||
ATTR_UNIT: TEMP_CELSIUS,
|
||||
},
|
||||
}
|
||||
|
13
homeassistant/components/airly/model.py
Normal file
13
homeassistant/components/airly/model.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Type definitions for Airly integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class SensorDescription(TypedDict):
|
||||
"""Sensor description class."""
|
||||
|
||||
device_class: str | None
|
||||
icon: str | None
|
||||
label: str
|
||||
unit: str
|
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