Merge pull request #51370 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2021-06-02 18:31:46 +02:00 committed by GitHub
commit 51704d151d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3671 changed files with 67808 additions and 26385 deletions

View File

@ -8,8 +8,10 @@ omit =
homeassistant/scripts/*.py homeassistant/scripts/*.py
# omit pieces of code that rely on external devices being present # omit pieces of code that rely on external devices being present
homeassistant/components/acer_projector/switch.py homeassistant/components/acer_projector/*
homeassistant/components/actiontec/const.py
homeassistant/components/actiontec/device_tracker.py homeassistant/components/actiontec/device_tracker.py
homeassistant/components/actiontec/model.py
homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/__init__.py
homeassistant/components/acmeda/base.py homeassistant/components/acmeda/base.py
homeassistant/components/acmeda/const.py homeassistant/components/acmeda/const.py
@ -23,9 +25,8 @@ omit =
homeassistant/components/adguard/sensor.py homeassistant/components/adguard/sensor.py
homeassistant/components/adguard/switch.py homeassistant/components/adguard/switch.py
homeassistant/components/ads/* homeassistant/components/ads/*
homeassistant/components/aemet/abstract_aemet_sensor.py
homeassistant/components/aemet/weather_update_coordinator.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/__init__.py
homeassistant/components/agent_dvr/alarm_control_panel.py homeassistant/components/agent_dvr/alarm_control_panel.py
homeassistant/components/agent_dvr/camera.py homeassistant/components/agent_dvr/camera.py
@ -36,14 +37,14 @@ omit =
homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/__init__.py
homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/air_quality.py
homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual/sensor.py
homeassistant/components/aladdin_connect/cover.py homeassistant/components/aladdin_connect/*
homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/__init__.py
homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/alarm_control_panel.py
homeassistant/components/alarmdecoder/binary_sensor.py homeassistant/components/alarmdecoder/binary_sensor.py
homeassistant/components/alarmdecoder/const.py homeassistant/components/alarmdecoder/const.py
homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alarmdecoder/sensor.py
homeassistant/components/alpha_vantage/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/ambiclimate/climate.py
homeassistant/components/ambient_station/* homeassistant/components/ambient_station/*
homeassistant/components/amcrest/* homeassistant/components/amcrest/*
@ -113,6 +114,10 @@ omit =
homeassistant/components/bmw_connected_drive/lock.py homeassistant/components/bmw_connected_drive/lock.py
homeassistant/components/bmw_connected_drive/notify.py homeassistant/components/bmw_connected_drive/notify.py
homeassistant/components/bmw_connected_drive/sensor.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/__init__.py
homeassistant/components/braviatv/const.py homeassistant/components/braviatv/const.py
homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/media_player.py
@ -304,7 +309,7 @@ omit =
homeassistant/components/firmata/pin.py homeassistant/components/firmata/pin.py
homeassistant/components/firmata/sensor.py homeassistant/components/firmata/sensor.py
homeassistant/components/firmata/switch.py homeassistant/components/firmata/switch.py
homeassistant/components/fitbit/sensor.py homeassistant/components/fitbit/*
homeassistant/components/fixer/sensor.py homeassistant/components/fixer/sensor.py
homeassistant/components/fleetgo/device_tracker.py homeassistant/components/fleetgo/device_tracker.py
homeassistant/components/flexit/climate.py homeassistant/components/flexit/climate.py
@ -330,9 +335,12 @@ omit =
homeassistant/components/freebox/sensor.py homeassistant/components/freebox/sensor.py
homeassistant/components/freebox/switch.py homeassistant/components/freebox/switch.py
homeassistant/components/fritz/__init__.py homeassistant/components/fritz/__init__.py
homeassistant/components/fritz/binary_sensor.py
homeassistant/components/fritz/common.py homeassistant/components/fritz/common.py
homeassistant/components/fritz/const.py homeassistant/components/fritz/const.py
homeassistant/components/fritz/device_tracker.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/__init__.py
homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/const.py
homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/base.py
@ -342,6 +350,9 @@ omit =
homeassistant/components/frontier_silicon/media_player.py homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py homeassistant/components/futurenow/light.py
homeassistant/components/garadget/cover.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/__init__.py
homeassistant/components/garmin_connect/const.py homeassistant/components/garmin_connect/const.py
homeassistant/components/garmin_connect/sensor.py homeassistant/components/garmin_connect/sensor.py
@ -358,6 +369,7 @@ omit =
homeassistant/components/goalfeed/* homeassistant/components/goalfeed/*
homeassistant/components/goalzero/__init__.py homeassistant/components/goalzero/__init__.py
homeassistant/components/goalzero/binary_sensor.py homeassistant/components/goalzero/binary_sensor.py
homeassistant/components/goalzero/switch.py
homeassistant/components/google/* homeassistant/components/google/*
homeassistant/components/google_cloud/tts.py homeassistant/components/google_cloud/tts.py
homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_maps/device_tracker.py
@ -371,6 +383,7 @@ omit =
homeassistant/components/greenwave/light.py homeassistant/components/greenwave/light.py
homeassistant/components/group/notify.py homeassistant/components/group/notify.py
homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/sensor.py
homeassistant/components/growatt_server/__init__.py
homeassistant/components/gstreamer/media_player.py homeassistant/components/gstreamer/media_player.py
homeassistant/components/gtfs/sensor.py homeassistant/components/gtfs/sensor.py
homeassistant/components/guardian/__init__.py homeassistant/components/guardian/__init__.py
@ -545,7 +558,6 @@ omit =
homeassistant/components/life360/* homeassistant/components/life360/*
homeassistant/components/lifx/* homeassistant/components/lifx/*
homeassistant/components/lifx_cloud/scene.py homeassistant/components/lifx_cloud/scene.py
homeassistant/components/lifx_legacy/light.py
homeassistant/components/lightwave/* homeassistant/components/lightwave/*
homeassistant/components/limitlessled/light.py homeassistant/components/limitlessled/light.py
homeassistant/components/linksys_smart/device_tracker.py homeassistant/components/linksys_smart/device_tracker.py
@ -599,6 +611,9 @@ omit =
homeassistant/components/meteo_france/sensor.py homeassistant/components/meteo_france/sensor.py
homeassistant/components/meteo_france/weather.py homeassistant/components/meteo_france/weather.py
homeassistant/components/meteoalarm/* 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/sensor.py
homeassistant/components/metoffice/weather.py homeassistant/components/metoffice/weather.py
homeassistant/components/microsoft/tts.py homeassistant/components/microsoft/tts.py
@ -618,9 +633,6 @@ omit =
homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/camera.py
homeassistant/components/mochad/* homeassistant/components/mochad/*
homeassistant/components/modbus/climate.py 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/modem_callerid/sensor.py
homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/__init__.py
homeassistant/components/motion_blinds/const.py homeassistant/components/motion_blinds/const.py
@ -656,7 +668,7 @@ omit =
homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/binary_sensor.py
homeassistant/components/mystrom/light.py homeassistant/components/mystrom/light.py
homeassistant/components/mystrom/switch.py homeassistant/components/mystrom/switch.py
homeassistant/components/n26/* homeassistant/components/myq/__init__.py
homeassistant/components/nad/media_player.py homeassistant/components/nad/media_player.py
homeassistant/components/nanoleaf/light.py homeassistant/components/nanoleaf/light.py
homeassistant/components/neato/__init__.py homeassistant/components/neato/__init__.py
@ -703,6 +715,7 @@ omit =
homeassistant/components/omnilogic/__init__.py homeassistant/components/omnilogic/__init__.py
homeassistant/components/omnilogic/common.py homeassistant/components/omnilogic/common.py
homeassistant/components/omnilogic/sensor.py homeassistant/components/omnilogic/sensor.py
homeassistant/components/omnilogic/switch.py
homeassistant/components/ondilo_ico/__init__.py homeassistant/components/ondilo_ico/__init__.py
homeassistant/components/ondilo_ico/api.py homeassistant/components/ondilo_ico/api.py
homeassistant/components/ondilo_ico/const.py homeassistant/components/ondilo_ico/const.py
@ -862,6 +875,7 @@ omit =
homeassistant/components/russound_rnet/media_player.py homeassistant/components/russound_rnet/media_player.py
homeassistant/components/sabnzbd/* homeassistant/components/sabnzbd/*
homeassistant/components/saj/sensor.py homeassistant/components/saj/sensor.py
homeassistant/components/samsungtv/bridge.py
homeassistant/components/satel_integra/* homeassistant/components/satel_integra/*
homeassistant/components/schluter/* homeassistant/components/schluter/*
homeassistant/components/scrape/sensor.py homeassistant/components/scrape/sensor.py
@ -905,6 +919,11 @@ omit =
homeassistant/components/skybeacon/sensor.py homeassistant/components/skybeacon/sensor.py
homeassistant/components/skybell/* homeassistant/components/skybell/*
homeassistant/components/slack/notify.py homeassistant/components/slack/notify.py
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/sinch/*
homeassistant/components/slide/* homeassistant/components/slide/*
homeassistant/components/sma/__init__.py homeassistant/components/sma/__init__.py
@ -931,7 +950,12 @@ omit =
homeassistant/components/soma/__init__.py homeassistant/components/soma/__init__.py
homeassistant/components/soma/cover.py homeassistant/components/soma/cover.py
homeassistant/components/soma/sensor.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/__init__.py
homeassistant/components/somfy_mylink/cover.py homeassistant/components/somfy_mylink/cover.py
homeassistant/components/sonos/* homeassistant/components/sonos/*
@ -940,7 +964,6 @@ omit =
homeassistant/components/speedtestdotnet/* homeassistant/components/speedtestdotnet/*
homeassistant/components/spider/* homeassistant/components/spider/*
homeassistant/components/splunk/* homeassistant/components/splunk/*
homeassistant/components/spotcrime/sensor.py
homeassistant/components/spotify/__init__.py homeassistant/components/spotify/__init__.py
homeassistant/components/spotify/media_player.py homeassistant/components/spotify/media_player.py
homeassistant/components/spotify/system_health.py homeassistant/components/spotify/system_health.py
@ -964,6 +987,10 @@ omit =
homeassistant/components/switchbot/switch.py homeassistant/components/switchbot/switch.py
homeassistant/components/switcher_kis/switch.py homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/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/syncthru/sensor.py
homeassistant/components/synology_chat/notify.py homeassistant/components/synology_chat/notify.py
homeassistant/components/synology_dsm/__init__.py homeassistant/components/synology_dsm/__init__.py
@ -973,6 +1000,10 @@ omit =
homeassistant/components/synology_dsm/switch.py homeassistant/components/synology_dsm/switch.py
homeassistant/components/synology_srm/device_tracker.py homeassistant/components/synology_srm/device_tracker.py
homeassistant/components/syslog/notify.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/systemmonitor/sensor.py
homeassistant/components/tado/* homeassistant/components/tado/*
homeassistant/components/tado/device_tracker.py homeassistant/components/tado/device_tracker.py
@ -1033,7 +1064,6 @@ omit =
homeassistant/components/toon/switch.py homeassistant/components/toon/switch.py
homeassistant/components/torque/sensor.py homeassistant/components/torque/sensor.py
homeassistant/components/totalconnect/__init__.py homeassistant/components/totalconnect/__init__.py
homeassistant/components/totalconnect/alarm_control_panel.py
homeassistant/components/totalconnect/binary_sensor.py homeassistant/components/totalconnect/binary_sensor.py
homeassistant/components/totalconnect/const.py homeassistant/components/totalconnect/const.py
homeassistant/components/touchline/climate.py homeassistant/components/touchline/climate.py
@ -1206,7 +1236,6 @@ omit =
homeassistant/components/supla/* homeassistant/components/supla/*
homeassistant/components/zwave/util.py homeassistant/components/zwave/util.py
homeassistant/components/zwave_js/discovery.py homeassistant/components/zwave_js/discovery.py
homeassistant/components/zwave_js/light.py
homeassistant/components/zwave_js/sensor.py homeassistant/components/zwave_js/sensor.py
[report] [report]

View File

@ -36,19 +36,6 @@
- [ ] Breaking change (fix/feature causing existing functionality to break) - [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests - [ ] 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 ## Additional information
<!-- <!--
Details are important, and help maintainers processing your PR. Details are important, and help maintainers processing your PR.

View File

@ -23,7 +23,7 @@ jobs:
publish: ${{ steps.version.outputs.publish }} publish: ${{ steps.version.outputs.publish }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -54,7 +54,7 @@ jobs:
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
@ -84,7 +84,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
@ -102,20 +102,20 @@ jobs:
version="$(python setup.py -V)" version="$(python setup.py -V)"
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1.9.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v1.9.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2021.04.2 uses: home-assistant/builder@2021.05.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -151,23 +151,23 @@ jobs:
- tinker - tinker
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1.9.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v1.9.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2021.04.2 uses: home-assistant/builder@2021.05.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -182,7 +182,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master uses: home-assistant/actions/helpers/git-init@master
@ -214,16 +214,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1.9.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v1.9.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}

View File

@ -26,7 +26,7 @@ jobs:
pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
@ -84,7 +84,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -124,7 +124,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -164,7 +164,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -207,7 +207,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Register hadolint problem matcher - name: Register hadolint problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/hadolint.json" echo "::add-matcher::.github/workflows/matchers/hadolint.json"
@ -226,7 +226,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -269,7 +269,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -312,7 +312,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -352,7 +352,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -395,7 +395,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -436,7 +436,7 @@ jobs:
# needs: prepare-base # needs: prepare-base
# steps: # steps:
# - name: Check out code from GitHub # - name: Check out code from GitHub
# uses: actions/checkout@v2 # uses: actions/checkout@v2.3.4
# - name: Run ShellCheck # - name: Run ShellCheck
# uses: ludeeus/action-shellcheck@0.3.0 # uses: ludeeus/action-shellcheck@0.3.0
@ -446,7 +446,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -493,7 +493,7 @@ jobs:
container: homeassistant/ci-azure:${{ matrix.python-version }} container: homeassistant/ci-azure:${{ matrix.python-version }}
steps: steps:
- name: Check out code from GitHub - 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 - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v2.1.5 uses: actions/cache@v2.1.5
@ -517,7 +517,7 @@ jobs:
needs: prepare-base needs: prepare-base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2 uses: actions/setup-python@v2.2.2
id: python id: python
@ -551,7 +551,7 @@ jobs:
container: homeassistant/ci-azure:${{ matrix.python-version }} container: homeassistant/ci-azure:${{ matrix.python-version }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Generate partial Python venv restore key - name: Generate partial Python venv restore key
id: generate-python-key id: generate-python-key
run: >- run: >-
@ -595,7 +595,7 @@ jobs:
container: homeassistant/ci-azure:${{ matrix.python-version }} container: homeassistant/ci-azure:${{ matrix.python-version }}
steps: steps:
- name: Check out code from GitHub - 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 - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v2.1.5 uses: actions/cache@v2.1.5
@ -626,7 +626,7 @@ jobs:
container: homeassistant/ci-azure:${{ matrix.python-version }} container: homeassistant/ci-azure:${{ matrix.python-version }}
steps: steps:
- name: Check out code from GitHub - 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 - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v2.1.5 uses: actions/cache@v2.1.5
@ -660,7 +660,7 @@ jobs:
container: homeassistant/ci-azure:${{ matrix.python-version }} container: homeassistant/ci-azure:${{ matrix.python-version }}
steps: steps:
- name: Check out code from GitHub - 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 - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v2.1.5 uses: actions/cache@v2.1.5
@ -718,7 +718,7 @@ jobs:
container: homeassistant/ci-azure:${{ matrix.python-version }} container: homeassistant/ci-azure:${{ matrix.python-version }}
steps: steps:
- name: Check out code from GitHub - 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 - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v2.1.5 uses: actions/cache@v2.1.5
@ -740,4 +740,4 @@ jobs:
coverage report --fail-under=94 coverage report --fail-under=94
coverage xml coverage xml
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.4.1 uses: codecov/codecov-action@v1.5.0

View File

@ -16,7 +16,7 @@ jobs:
# - No PRs marked as no-stale # - No PRs marked as no-stale
# - No issues marked as no-stale or help-wanted # - No issues marked as no-stale or help-wanted
- name: 90 days stale issues & PRs policy - name: 90 days stale issues & PRs policy
uses: actions/stale@v3.0.18 uses: actions/stale@v3.0.19
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90 days-before-stale: 90
@ -53,7 +53,7 @@ jobs:
# - No PRs marked as no-stale or new-integrations # - No PRs marked as no-stale or new-integrations
# - No issues (-1) # - No issues (-1)
- name: 30 days stale PRs policy - name: 30 days stale PRs policy
uses: actions/stale@v3.0.18 uses: actions/stale@v3.0.19
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30 days-before-stale: 30
@ -78,7 +78,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted # - No Issues marked as no-stale or help-wanted
# - No PRs (-1) # - No PRs (-1)
- name: Needs more information stale issues policy - name: Needs more information stale issues policy
uses: actions/stale@v3.0.18 uses: actions/stale@v3.0.19
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "needs-more-information" only-labels: "needs-more-information"

64
.github/workflows/translations.yaml vendored Normal file
View 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
View 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"

View File

@ -1,11 +1,11 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.12.0 rev: v2.16.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.4b0 rev: 21.5b1
hooks: hooks:
- id: black - id: black
args: args:
@ -17,13 +17,13 @@ repos:
hooks: hooks:
- id: codespell - id: codespell
args: 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" - --skip="./.*,*.csv,*.json"
- --quiet-level=2 - --quiet-level=2
exclude_types: [csv, json] exclude_types: [csv, json]
exclude: ^tests/fixtures/ exclude: ^tests/fixtures/
- repo: https://gitlab.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 3.9.1 rev: 3.9.2
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
@ -31,7 +31,7 @@ repos:
- pyflakes==2.3.1 - pyflakes==2.3.1
- flake8-docstrings==1.6.0 - flake8-docstrings==1.6.0
- pydocstyle==6.0.0 - pydocstyle==6.0.0
- flake8-comprehensions==3.4.0 - flake8-comprehensions==3.5.0
- flake8-noqa==1.1.0 - flake8-noqa==1.1.0
- mccabe==0.6.1 - mccabe==0.6.1
files: ^(homeassistant|script|tests)/.+\.py$ files: ^(homeassistant|script|tests)/.+\.py$
@ -61,7 +61,7 @@ repos:
- --branch=master - --branch=master
- --branch=rc - --branch=rc
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.24.2 rev: v1.26.1
hooks: hooks:
- id: yamllint - id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier

View File

@ -3,14 +3,34 @@
# to enable strict mypy checks. # to enable strict mypy checks.
homeassistant.components 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.automation.*
homeassistant.components.binary_sensor.* homeassistant.components.binary_sensor.*
homeassistant.components.bluetooth_tracker.*
homeassistant.components.bond.* homeassistant.components.bond.*
homeassistant.components.brother.*
homeassistant.components.calendar.* homeassistant.components.calendar.*
homeassistant.components.camera.*
homeassistant.components.canary.*
homeassistant.components.cover.* homeassistant.components.cover.*
homeassistant.components.device_automation.* 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.frontend.*
homeassistant.components.geo_location.* homeassistant.components.geo_location.*
homeassistant.components.gios.*
homeassistant.components.group.* homeassistant.components.group.*
homeassistant.components.history.* homeassistant.components.history.*
homeassistant.components.http.* homeassistant.components.http.*
@ -19,16 +39,21 @@ homeassistant.components.hyperion.*
homeassistant.components.image_processing.* homeassistant.components.image_processing.*
homeassistant.components.integration.* homeassistant.components.integration.*
homeassistant.components.knx.* homeassistant.components.knx.*
homeassistant.components.kraken.*
homeassistant.components.light.* homeassistant.components.light.*
homeassistant.components.lock.* homeassistant.components.lock.*
homeassistant.components.mailbox.* homeassistant.components.mailbox.*
homeassistant.components.media_player.* homeassistant.components.media_player.*
homeassistant.components.nam.*
homeassistant.components.network.*
homeassistant.components.notify.* homeassistant.components.notify.*
homeassistant.components.number.* homeassistant.components.number.*
homeassistant.components.onewire.*
homeassistant.components.persistent_notification.* homeassistant.components.persistent_notification.*
homeassistant.components.proximity.* homeassistant.components.proximity.*
homeassistant.components.recorder.purge homeassistant.components.recorder.purge
homeassistant.components.recorder.repack homeassistant.components.recorder.repack
homeassistant.components.recorder.statistics
homeassistant.components.remote.* homeassistant.components.remote.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.sensor.* homeassistant.components.sensor.*
@ -36,8 +61,11 @@ homeassistant.components.slack.*
homeassistant.components.sonos.media_player homeassistant.components.sonos.media_player
homeassistant.components.sun.* homeassistant.components.sun.*
homeassistant.components.switch.* homeassistant.components.switch.*
homeassistant.components.synology_dsm.*
homeassistant.components.systemmonitor.* homeassistant.components.systemmonitor.*
homeassistant.components.tcp.*
homeassistant.components.tts.* homeassistant.components.tts.*
homeassistant.components.upcloud.*
homeassistant.components.vacuum.* homeassistant.components.vacuum.*
homeassistant.components.water_heater.* homeassistant.components.water_heater.*
homeassistant.components.weather.* homeassistant.components.weather.*

View File

@ -64,19 +64,20 @@ homeassistant/components/azure_service_bus/* @hfurubotten
homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/beewi_smartclim/* @alemuro
homeassistant/components/bitcoin/* @fabaff homeassistant/components/bitcoin/* @fabaff
homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/bizkaibus/* @UgaitzEtxebarria
homeassistant/components/blebox/* @gadgetmobile homeassistant/components/blebox/* @bbx-a @bbx-jp
homeassistant/components/blink/* @fronzbot homeassistant/components/blink/* @fronzbot
homeassistant/components/blueprint/* @home-assistant/core homeassistant/components/blueprint/* @home-assistant/core
homeassistant/components/bmp280/* @belidzs homeassistant/components/bmp280/* @belidzs
homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe
homeassistant/components/bond/* @prystupa homeassistant/components/bond/* @prystupa
homeassistant/components/bosch_shc/* @tschamm
homeassistant/components/braviatv/* @bieniu homeassistant/components/braviatv/* @bieniu
homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/broadlink/* @danielhiversen @felipediel
homeassistant/components/brother/* @bieniu homeassistant/components/brother/* @bieniu
homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/brunt/* @eavanvalkenburg
homeassistant/components/bsblan/* @liudger homeassistant/components/bsblan/* @liudger
homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/bt_smarthub/* @jxwolstenholme
homeassistant/components/buienradar/* @mjj4791 @ties homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221
homeassistant/components/cast/* @emontnemery homeassistant/components/cast/* @emontnemery
homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren
homeassistant/components/circuit/* @braam homeassistant/components/circuit/* @braam
@ -111,6 +112,7 @@ homeassistant/components/device_automation/* @home-assistant/core
homeassistant/components/devolo_home_control/* @2Fake @Shutgun homeassistant/components/devolo_home_control/* @2Fake @Shutgun
homeassistant/components/dexcom/* @gagebenne homeassistant/components/dexcom/* @gagebenne
homeassistant/components/dhcp/* @bdraco homeassistant/components/dhcp/* @bdraco
homeassistant/components/dht/* @thegardenmonkey
homeassistant/components/digital_ocean/* @fabaff homeassistant/components/digital_ocean/* @fabaff
homeassistant/components/directv/* @ctalkington homeassistant/components/directv/* @ctalkington
homeassistant/components/discogs/* @thibmaek homeassistant/components/discogs/* @thibmaek
@ -168,9 +170,11 @@ homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74
homeassistant/components/fritzbox/* @mib1185 homeassistant/components/fritzbox/* @mib1185
homeassistant/components/fronius/* @nielstron homeassistant/components/fronius/* @nielstron
homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/garages_amsterdam/* @klaasnicolaas
homeassistant/components/garmin_connect/* @cyberjunky homeassistant/components/garmin_connect/* @cyberjunky
homeassistant/components/gdacs/* @exxamalte homeassistant/components/gdacs/* @exxamalte
homeassistant/components/geniushub/* @zxdavb homeassistant/components/geniushub/* @zxdavb
homeassistant/components/geo_json_events/* @exxamalte
homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geo_rss_events/* @exxamalte
homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte
homeassistant/components/geonetnz_volcano/* @exxamalte homeassistant/components/geonetnz_volcano/* @exxamalte
@ -178,7 +182,7 @@ homeassistant/components/gios/* @bieniu
homeassistant/components/gitter/* @fabaff homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/glances/* @fabaff @engrbm87
homeassistant/components/goalzero/* @tkdrob homeassistant/components/goalzero/* @tkdrob
homeassistant/components/gogogate2/* @vangorra homeassistant/components/gogogate2/* @vangorra @bdraco
homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_assistant/* @home-assistant/cloud
homeassistant/components/google_cloud/* @lufton homeassistant/components/google_cloud/* @lufton
homeassistant/components/gpsd/* @fabaff homeassistant/components/gpsd/* @fabaff
@ -203,7 +207,7 @@ homeassistant/components/home_connect/* @DavidMStraub
homeassistant/components/home_plus_control/* @chemaaa homeassistant/components/home_plus_control/* @chemaaa
homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco homeassistant/components/homekit/* @bdraco
homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homekit_controller/* @Jc2k @bdraco
homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/homematic/* @pvizeli @danielperna84
homeassistant/components/http/* @home-assistant/core homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_lte/* @scop @fphammerle
@ -253,10 +257,12 @@ homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/kodi/* @OnFreund @cgtobi
homeassistant/components/konnected/* @heythisisnate @kit-klein homeassistant/components/konnected/* @heythisisnate @kit-klein
homeassistant/components/kostal_plenticore/* @stegm homeassistant/components/kostal_plenticore/* @stegm
homeassistant/components/kraken/* @eifinger
homeassistant/components/kulersky/* @emlove homeassistant/components/kulersky/* @emlove
homeassistant/components/lametric/* @robbiet480 homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus homeassistant/components/lcn/* @alengwenus
homeassistant/components/lg_netcast/* @Drafteed
homeassistant/components/life360/* @pnbruckner homeassistant/components/life360/* @pnbruckner
homeassistant/components/linux_battery/* @fabaff homeassistant/components/linux_battery/* @fabaff
homeassistant/components/litejet/* @joncar homeassistant/components/litejet/* @joncar
@ -284,6 +290,7 @@ homeassistant/components/met/* @danielhiversen @thimic
homeassistant/components/met_eireann/* @DylanGore homeassistant/components/met_eireann/* @DylanGore
homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame
homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/meteoclimatic/* @adrianmo
homeassistant/components/metoffice/* @MrHarcombe homeassistant/components/metoffice/* @MrHarcombe
homeassistant/components/miflora/* @danielhiversen @basnijholt homeassistant/components/miflora/* @danielhiversen @basnijholt
homeassistant/components/mikrotik/* @engrbm87 homeassistant/components/mikrotik/* @engrbm87
@ -306,6 +313,7 @@ homeassistant/components/my/* @home-assistant/core
homeassistant/components/myq/* @bdraco homeassistant/components/myq/* @bdraco
homeassistant/components/mysensors/* @MartinHjelmare @functionpointer homeassistant/components/mysensors/* @MartinHjelmare @functionpointer
homeassistant/components/mystrom/* @fabaff homeassistant/components/mystrom/* @fabaff
homeassistant/components/nam/* @bieniu
homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/neato/* @dshokouhi @Santobert
homeassistant/components/nederlandse_spoorwegen/* @YarmoM homeassistant/components/nederlandse_spoorwegen/* @YarmoM
homeassistant/components/nello/* @pschmitt homeassistant/components/nello/* @pschmitt
@ -327,7 +335,6 @@ homeassistant/components/notify_events/* @matrozov @papajojo
homeassistant/components/notion/* @bachya homeassistant/components/notion/* @bachya
homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_fuel_station/* @nickw444
homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
homeassistant/components/nuheat/* @bdraco
homeassistant/components/nuki/* @pschmitt @pvizeli @pree homeassistant/components/nuki/* @pschmitt @pvizeli @pree
homeassistant/components/numato/* @clssn homeassistant/components/numato/* @clssn
homeassistant/components/number/* @home-assistant/core @Shulyaka homeassistant/components/number/* @home-assistant/core @Shulyaka
@ -407,7 +414,7 @@ homeassistant/components/rpi_power/* @shenxn @swetoast
homeassistant/components/ruckus_unleashed/* @gabe565 homeassistant/components/ruckus_unleashed/* @gabe565
homeassistant/components/safe_mode/* @home-assistant/core homeassistant/components/safe_mode/* @home-assistant/core
homeassistant/components/saj/* @fredericvl homeassistant/components/saj/* @fredericvl
homeassistant/components/samsungtv/* @escoand homeassistant/components/samsungtv/* @escoand @chemelli74
homeassistant/components/scene/* @home-assistant/core homeassistant/components/scene/* @home-assistant/core
homeassistant/components/schluter/* @prairieapps homeassistant/components/schluter/* @prairieapps
homeassistant/components/scrape/* @fabaff homeassistant/components/scrape/* @fabaff
@ -425,6 +432,7 @@ homeassistant/components/shell_command/* @home-assistant/core
homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74 homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74
homeassistant/components/shiftr/* @fabaff homeassistant/components/shiftr/* @fabaff
homeassistant/components/shodan/* @fabaff homeassistant/components/shodan/* @fabaff
homeassistant/components/sia/* @eavanvalkenburg
homeassistant/components/sighthound/* @robmarkcole homeassistant/components/sighthound/* @robmarkcole
homeassistant/components/signal_messenger/* @bbernhard homeassistant/components/signal_messenger/* @bbernhard
homeassistant/components/simplisafe/* @bachya homeassistant/components/simplisafe/* @bachya
@ -450,7 +458,7 @@ homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne homeassistant/components/somfy/* @tetienne
homeassistant/components/sonarr/* @ctalkington homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti @shenxn homeassistant/components/songpal/* @rytilahti @shenxn
homeassistant/components/sonos/* @cgtobi homeassistant/components/sonos/* @cgtobi @jjlawren
homeassistant/components/spaceapi/* @fabaff homeassistant/components/spaceapi/* @fabaff
homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87 homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87
homeassistant/components/spider/* @peternijssen homeassistant/components/spider/* @peternijssen
@ -475,10 +483,12 @@ homeassistant/components/swiss_public_transport/* @fabaff
homeassistant/components/switchbot/* @danielhiversen homeassistant/components/switchbot/* @danielhiversen
homeassistant/components/switcher_kis/* @tomerfi homeassistant/components/switcher_kis/* @tomerfi
homeassistant/components/switchmate/* @danielhiversen homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthing/* @zhulik
homeassistant/components/syncthru/* @nielstron homeassistant/components/syncthru/* @nielstron
homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185 homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185
homeassistant/components/synology_srm/* @aerialls homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff homeassistant/components/syslog/* @fabaff
homeassistant/components/system_bridge/* @timmo001
homeassistant/components/tado/* @michaelarnauts @bdraco @noltari homeassistant/components/tado/* @michaelarnauts @bdraco @noltari
homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tag/* @balloob @dmulcahey
homeassistant/components/tahoma/* @philklei homeassistant/components/tahoma/* @philklei
@ -535,6 +545,7 @@ homeassistant/components/vlc_telnet/* @rodripf @dmcc
homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volkszaehler/* @fabaff
homeassistant/components/volumio/* @OnFreund homeassistant/components/volumio/* @OnFreund
homeassistant/components/wake_on_lan/* @ntilley905 homeassistant/components/wake_on_lan/* @ntilley905
homeassistant/components/wallbox/* @hesselonline
homeassistant/components/waqi/* @andrey-git homeassistant/components/waqi/* @andrey-git
homeassistant/components/watson_tts/* @rutkai homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff homeassistant/components/weather/* @fabaff
@ -554,6 +565,7 @@ homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG
homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xiaomi_tv/* @simse
homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yale_smart_alarm/* @gjohansson-ST
homeassistant/components/yamaha_musiccast/* @jalmeroth homeassistant/components/yamaha_musiccast/* @jalmeroth
homeassistant/components/yandex_transport/* @rishatik92 @devbis homeassistant/components/yandex_transport/* @rishatik92 @devbis
homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn

View File

@ -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"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]

View File

@ -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"

View File

@ -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'

View File

@ -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'

View File

@ -2,11 +2,11 @@
"image": "homeassistant/{arch}-homeassistant", "image": "homeassistant/{arch}-homeassistant",
"shadow_repository": "ghcr.io/home-assistant", "shadow_repository": "ghcr.io/home-assistant",
"build_from": { "build_from": {
"aarch64": "ghcr.io/home-assistant/aarch64-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.04.3", "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.05.0",
"armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.04.3", "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.05.0",
"amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.04.3", "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.05.0",
"i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.04.3" "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.05.0"
}, },
"labels": { "labels": {
"io.hass.type": "core", "io.hass.type": "core",

View File

@ -9,13 +9,12 @@ from typing import Any, Dict, Mapping, Optional, Tuple, cast
import jwt import jwt
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import auth_store, models 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 .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
from .providers import AuthProvider, LoginFlow, auth_provider_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): class AuthManagerFlowManager(data_entry_flow.FlowManager):
"""Manage authentication flows.""" """Manage authentication flows."""
def __init__(self, hass: HomeAssistant, auth_manager: AuthManager): def __init__(self, hass: HomeAssistant, auth_manager: AuthManager) -> None:
"""Init auth manager flows.""" """Init auth manager flows."""
super().__init__(hass) super().__init__(hass)
self.auth_manager = auth_manager self.auth_manager = auth_manager

View File

@ -95,31 +95,29 @@ class TrustedNetworksAuthProvider(AuthProvider):
user for user in users if not user.system_generated and user.is_active 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(): for ip_net, user_or_group_list in self.trusted_users.items():
if ip_addr in ip_net: if ip_addr not in ip_net:
user_list = [ continue
user_id
for user_id in user_or_group_list user_list = [
if isinstance(user_id, str) user_id for user_id in user_or_group_list if isinstance(user_id, str)
] ]
group_list = [ group_list = [
group[CONF_GROUP] group[CONF_GROUP]
for group in user_or_group_list for group in user_or_group_list
if isinstance(group, dict) if isinstance(group, dict)
] ]
flattened_group_list = [ flattened_group_list = [
group for sublist in group_list for group in sublist group for sublist in group_list for group in sublist
] ]
available_users = [ available_users = [
user user
for user in available_users for user in available_users
if ( if (
user.id in user_list user.id in user_list
or any( or any(group.id in flattened_group_list for group in user.groups)
group.id in flattened_group_list for group in user.groups )
) ]
) break
]
break
return TrustedNetworksLoginFlow( return TrustedNetworksLoginFlow(
self, self,
@ -136,13 +134,22 @@ class TrustedNetworksAuthProvider(AuthProvider):
users = await self.store.async_get_users() users = await self.store.async_get_users()
for user in users: for user in users:
if not user.system_generated and user.is_active and user.id == user_id: if user.id != user_id:
for credential in await self.async_credentials(): continue
if credential.data["user_id"] == user_id:
return credential if user.system_generated:
cred = self.async_create_credentials({"user_id": user_id}) continue
await self.store.async_link_user(user, cred)
return cred 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 # We only allow login as exist user
raise InvalidUserError raise InvalidUserError

View File

@ -45,15 +45,19 @@ ATTR_EVENT_BY = "event_by"
ATTR_VALUE = "value" ATTR_VALUE = "value"
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ vol.All(
DOMAIN: vol.Schema( # Deprecated in Home Assistant 2021.6
{ cv.deprecated(DOMAIN),
vol.Required(CONF_USERNAME): cv.string, {
vol.Required(CONF_PASSWORD): cv.string, DOMAIN: vol.Schema(
vol.Optional(CONF_POLLING, default=False): cv.boolean, {
} vol.Required(CONF_USERNAME): cv.string,
) vol.Required(CONF_PASSWORD): cv.string,
}, vol.Optional(CONF_POLLING, default=False): cv.boolean,
}
)
},
),
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )

View File

@ -18,7 +18,6 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Abode.""" """Config flow for Abode."""
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self): def __init__(self):
"""Initialize.""" """Initialize."""

View File

@ -1,21 +1,44 @@
capture_image: capture_image:
name: Capture image
description: Request a new image capture from a camera device. description: Request a new image capture from a camera device.
fields: fields:
entity_id: entity_id:
name: Entity
description: Entity id of the camera to request an image. 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: change_setting:
name: Change setting
description: Change an Abode system setting. description: Change an Abode system setting.
fields: fields:
setting: setting:
name: Setting
description: Setting to change. description: Setting to change.
required: true
example: beeper_mute example: beeper_mute
selector:
text:
value: value:
name: Value
description: Value of the setting. description: Value of the setting.
required: true
example: "1" example: "1"
selector:
text:
trigger_automation: trigger_automation:
name: Trigger automation
description: Trigger an Abode automation. description: Trigger an Abode automation.
fields: fields:
entity_id: entity_id:
name: Entity
description: Entity id of the automation to trigger. description: Entity id of the automation to trigger.
example: switch.my_automation required: true
selector:
entity:
integration: abode
domain: switch

View File

@ -1,12 +1,18 @@
"""The AccuWeather component.""" """The AccuWeather component."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any, Dict
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout from async_timeout import timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -23,11 +29,12 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "weather"] 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.""" """Set up AccuWeather as config entry."""
api_key = config_entry.data[CONF_API_KEY] api_key: str = entry.data[CONF_API_KEY]
location_key = config_entry.unique_id assert entry.unique_id is not None
forecast = config_entry.options.get(CONF_FORECAST, False) 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) _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() 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, COORDINATOR: coordinator,
UNDO_UPDATE_LISTENER: undo_listener, UNDO_UPDATE_LISTENER: undo_listener,
} }
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True 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 a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
config_entry, PLATFORMS
)
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok
async def update_listener(hass, config_entry): async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener.""" """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.""" """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.""" """Initialize."""
self.location_key = location_key self.location_key = location_key
self.forecast = forecast self.forecast = forecast
@ -91,7 +103,7 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) 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.""" """Update data via library."""
try: try:
async with timeout(10): async with timeout(10):
@ -108,5 +120,5 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
RequestsExceededError, RequestsExceededError,
) as error: ) as error:
raise UpdateFailed(error) from 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}} return {**current, **{ATTR_FORECAST: forecast}}

View File

@ -1,5 +1,8 @@
"""Adds config flow for AccuWeather.""" """Adds config flow for AccuWeather."""
from __future__ import annotations
import asyncio import asyncio
from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError from aiohttp import ClientError
@ -8,8 +11,10 @@ from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries 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.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -20,9 +25,10 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for AccuWeather.""" """Config flow for AccuWeather."""
VERSION = 1 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.""" """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 # 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. # the small number of requests allowed, we only allow one integration instance.
@ -78,7 +84,9 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: ConfigEntry,
) -> AccuWeatherOptionsFlowHandler:
"""Options callback for AccuWeather.""" """Options callback for AccuWeather."""
return AccuWeatherOptionsFlowHandler(config_entry) return AccuWeatherOptionsFlowHandler(config_entry)
@ -86,15 +94,19 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow): class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
"""Config flow options for AccuWeather.""" """Config flow options for AccuWeather."""
def __init__(self, config_entry): def __init__(self, entry: ConfigEntry) -> None:
"""Initialize AccuWeather options flow.""" """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.""" """Manage the options."""
return await self.async_step_user() 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.""" """Handle a flow initialized by the user."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)

View File

@ -1,4 +1,8 @@
"""Constants for AccuWeather integration.""" """Constants for AccuWeather integration."""
from __future__ import annotations
from typing import Final
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY, ATTR_CONDITION_CLOUDY,
@ -33,18 +37,25 @@ from homeassistant.const import (
UV_INDEX, UV_INDEX,
) )
ATTRIBUTION = "Data provided by AccuWeather" from .model import SensorDescription
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"
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_CLEAR_NIGHT: [33, 34, 37],
ATTR_CONDITION_CLOUDY: [7, 8, 38], ATTR_CONDITION_CLOUDY: [7, 8, 38],
ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31], ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31],
@ -61,15 +72,14 @@ CONDITION_CLASSES = {
ATTR_CONDITION_WINDY: [32], ATTR_CONDITION_WINDY: [32],
} }
FORECAST_DAYS = [0, 1, 2, 3, 4] FORECAST_SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
FORECAST_SENSOR_TYPES = {
"CloudCoverDay": { "CloudCoverDay": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-cloudy", ATTR_ICON: "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Day", ATTR_LABEL: "Cloud Cover Day",
ATTR_UNIT_METRIC: PERCENTAGE, ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, ATTR_UNIT_IMPERIAL: PERCENTAGE,
ATTR_ENABLED: False,
}, },
"CloudCoverNight": { "CloudCoverNight": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -77,6 +87,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Cloud Cover Night", ATTR_LABEL: "Cloud Cover Night",
ATTR_UNIT_METRIC: PERCENTAGE, ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, ATTR_UNIT_IMPERIAL: PERCENTAGE,
ATTR_ENABLED: False,
}, },
"Grass": { "Grass": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -84,6 +95,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Grass Pollen", ATTR_LABEL: "Grass Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED: False,
}, },
"HoursOfSun": { "HoursOfSun": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -91,6 +103,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Hours Of Sun", ATTR_LABEL: "Hours Of Sun",
ATTR_UNIT_METRIC: TIME_HOURS, ATTR_UNIT_METRIC: TIME_HOURS,
ATTR_UNIT_IMPERIAL: TIME_HOURS, ATTR_UNIT_IMPERIAL: TIME_HOURS,
ATTR_ENABLED: True,
}, },
"Mold": { "Mold": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -98,6 +111,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Mold Pollen", ATTR_LABEL: "Mold Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED: False,
}, },
"Ozone": { "Ozone": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -105,6 +119,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Ozone", ATTR_LABEL: "Ozone",
ATTR_UNIT_METRIC: None, ATTR_UNIT_METRIC: None,
ATTR_UNIT_IMPERIAL: None, ATTR_UNIT_IMPERIAL: None,
ATTR_ENABLED: False,
}, },
"Ragweed": { "Ragweed": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -112,6 +127,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Ragweed Pollen", ATTR_LABEL: "Ragweed Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED: False,
}, },
"RealFeelTemperatureMax": { "RealFeelTemperatureMax": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -119,6 +135,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "RealFeel Temperature Max", ATTR_LABEL: "RealFeel Temperature Max",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: True,
}, },
"RealFeelTemperatureMin": { "RealFeelTemperatureMin": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -126,6 +143,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "RealFeel Temperature Min", ATTR_LABEL: "RealFeel Temperature Min",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: True,
}, },
"RealFeelTemperatureShadeMax": { "RealFeelTemperatureShadeMax": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -133,6 +151,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "RealFeel Temperature Shade Max", ATTR_LABEL: "RealFeel Temperature Shade Max",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
}, },
"RealFeelTemperatureShadeMin": { "RealFeelTemperatureShadeMin": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -140,6 +159,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "RealFeel Temperature Shade Min", ATTR_LABEL: "RealFeel Temperature Shade Min",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
}, },
"ThunderstormProbabilityDay": { "ThunderstormProbabilityDay": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -147,6 +167,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Thunderstorm Probability Day", ATTR_LABEL: "Thunderstorm Probability Day",
ATTR_UNIT_METRIC: PERCENTAGE, ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, ATTR_UNIT_IMPERIAL: PERCENTAGE,
ATTR_ENABLED: True,
}, },
"ThunderstormProbabilityNight": { "ThunderstormProbabilityNight": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -154,6 +175,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Thunderstorm Probability Night", ATTR_LABEL: "Thunderstorm Probability Night",
ATTR_UNIT_METRIC: PERCENTAGE, ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, ATTR_UNIT_IMPERIAL: PERCENTAGE,
ATTR_ENABLED: True,
}, },
"Tree": { "Tree": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -161,6 +183,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Tree Pollen", ATTR_LABEL: "Tree Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED: False,
}, },
"UVIndex": { "UVIndex": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -168,6 +191,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "UV Index", ATTR_LABEL: "UV Index",
ATTR_UNIT_METRIC: UV_INDEX, ATTR_UNIT_METRIC: UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX, ATTR_UNIT_IMPERIAL: UV_INDEX,
ATTR_ENABLED: True,
}, },
"WindGustDay": { "WindGustDay": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -175,6 +199,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Wind Gust Day", ATTR_LABEL: "Wind Gust Day",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: False,
}, },
"WindGustNight": { "WindGustNight": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -182,6 +207,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Wind Gust Night", ATTR_LABEL: "Wind Gust Night",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: False,
}, },
"WindDay": { "WindDay": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -189,6 +215,7 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Wind Day", ATTR_LABEL: "Wind Day",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: True,
}, },
"WindNight": { "WindNight": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -196,37 +223,18 @@ FORECAST_SENSOR_TYPES = {
ATTR_LABEL: "Wind Night", ATTR_LABEL: "Wind Night",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: True,
}, },
} }
OPTIONAL_SENSORS = ( SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
"ApparentTemperature",
"CloudCover",
"CloudCoverDay",
"CloudCoverNight",
"DewPoint",
"Grass",
"Mold",
"Ozone",
"Ragweed",
"RealFeelTemperatureShade",
"RealFeelTemperatureShadeMax",
"RealFeelTemperatureShadeMin",
"Tree",
"WetBulbTemperature",
"WindChillTemperature",
"WindGust",
"WindGustDay",
"WindGustNight",
)
SENSOR_TYPES = {
"ApparentTemperature": { "ApparentTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, ATTR_ICON: None,
ATTR_LABEL: "Apparent Temperature", ATTR_LABEL: "Apparent Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
}, },
"Ceiling": { "Ceiling": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -234,6 +242,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "Cloud Ceiling", ATTR_LABEL: "Cloud Ceiling",
ATTR_UNIT_METRIC: LENGTH_METERS, ATTR_UNIT_METRIC: LENGTH_METERS,
ATTR_UNIT_IMPERIAL: LENGTH_FEET, ATTR_UNIT_IMPERIAL: LENGTH_FEET,
ATTR_ENABLED: True,
}, },
"CloudCover": { "CloudCover": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -241,6 +250,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "Cloud Cover", ATTR_LABEL: "Cloud Cover",
ATTR_UNIT_METRIC: PERCENTAGE, ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, ATTR_UNIT_IMPERIAL: PERCENTAGE,
ATTR_ENABLED: False,
}, },
"DewPoint": { "DewPoint": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -248,6 +258,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "Dew Point", ATTR_LABEL: "Dew Point",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
}, },
"RealFeelTemperature": { "RealFeelTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -255,6 +266,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "RealFeel Temperature", ATTR_LABEL: "RealFeel Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: True,
}, },
"RealFeelTemperatureShade": { "RealFeelTemperatureShade": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -262,6 +274,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "RealFeel Temperature Shade", ATTR_LABEL: "RealFeel Temperature Shade",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
}, },
"Precipitation": { "Precipitation": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -269,6 +282,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "Precipitation", ATTR_LABEL: "Precipitation",
ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, ATTR_UNIT_METRIC: LENGTH_MILLIMETERS,
ATTR_UNIT_IMPERIAL: LENGTH_INCHES, ATTR_UNIT_IMPERIAL: LENGTH_INCHES,
ATTR_ENABLED: True,
}, },
"PressureTendency": { "PressureTendency": {
ATTR_DEVICE_CLASS: "accuweather__pressure_tendency", ATTR_DEVICE_CLASS: "accuweather__pressure_tendency",
@ -276,6 +290,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "Pressure Tendency", ATTR_LABEL: "Pressure Tendency",
ATTR_UNIT_METRIC: None, ATTR_UNIT_METRIC: None,
ATTR_UNIT_IMPERIAL: None, ATTR_UNIT_IMPERIAL: None,
ATTR_ENABLED: True,
}, },
"UVIndex": { "UVIndex": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -283,6 +298,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "UV Index", ATTR_LABEL: "UV Index",
ATTR_UNIT_METRIC: UV_INDEX, ATTR_UNIT_METRIC: UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX, ATTR_UNIT_IMPERIAL: UV_INDEX,
ATTR_ENABLED: True,
}, },
"WetBulbTemperature": { "WetBulbTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -290,6 +306,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "Wet Bulb Temperature", ATTR_LABEL: "Wet Bulb Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
}, },
"WindChillTemperature": { "WindChillTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
@ -297,6 +314,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "Wind Chill Temperature", ATTR_LABEL: "Wind Chill Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
ATTR_ENABLED: False,
}, },
"Wind": { "Wind": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -304,6 +322,7 @@ SENSOR_TYPES = {
ATTR_LABEL: "Wind", ATTR_LABEL: "Wind",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: True,
}, },
"WindGust": { "WindGust": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -311,5 +330,6 @@ SENSOR_TYPES = {
ATTR_LABEL: "Wind Gust", ATTR_LABEL: "Wind Gust",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: False,
}, },
} }

View File

@ -2,7 +2,7 @@
"domain": "accuweather", "domain": "accuweather",
"name": "AccuWeather", "name": "AccuWeather",
"documentation": "https://www.home-assistant.io/integrations/accuweather/", "documentation": "https://www.home-assistant.io/integrations/accuweather/",
"requirements": ["accuweather==0.1.1"], "requirements": ["accuweather==0.2.0"],
"codeowners": ["@bieniu"], "codeowners": ["@bieniu"],
"config_flow": true, "config_flow": true,
"quality_scale": "platinum", "quality_scale": "platinum",

View 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

View File

@ -1,44 +1,62 @@
"""Support for the AccuWeather service.""" """Support for the AccuWeather service."""
from __future__ import annotations
from typing import Any, cast
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_ICON,
CONF_NAME, CONF_NAME,
DEVICE_CLASS_TEMPERATURE, 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherDataUpdateCoordinator
from .const import ( from .const import (
API_IMPERIAL,
API_METRIC,
ATTR_ENABLED,
ATTR_FORECAST, ATTR_FORECAST,
ATTR_ICON,
ATTR_LABEL, ATTR_LABEL,
ATTR_UNIT_IMPERIAL,
ATTR_UNIT_METRIC,
ATTRIBUTION, ATTRIBUTION,
COORDINATOR, COORDINATOR,
DOMAIN, DOMAIN,
FORECAST_DAYS,
FORECAST_SENSOR_TYPES, FORECAST_SENSOR_TYPES,
MANUFACTURER, MANUFACTURER,
MAX_FORECAST_DAYS,
NAME, NAME,
OPTIONAL_SENSORS,
SENSOR_TYPES, SENSOR_TYPES,
) )
PARALLEL_UPDATES = 1 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.""" """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: for sensor in SENSOR_TYPES:
sensors.append(AccuWeatherSensor(name, sensor, coordinator)) sensors.append(AccuWeatherSensor(name, sensor, coordinator))
if coordinator.forecast: if coordinator.forecast:
for sensor in FORECAST_SENSOR_TYPES: 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 # Some air quality/allergy sensors are only available for certain
# locations. # locations.
if sensor in coordinator.data[ATTR_FORECAST][0]: 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) AccuWeatherSensor(name, sensor, coordinator, forecast_day=day)
) )
async_add_entities(sensors, False) async_add_entities(sensors)
class AccuWeatherSensor(CoordinatorEntity, SensorEntity): class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
"""Define an AccuWeather entity.""" """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.""" """Initialize."""
super().__init__(coordinator) 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._name = name
self.kind = kind self.kind = kind
self._device_class = None self._device_class = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
self.forecast_day = forecast_day self.forecast_day = forecast_day
@property @property
def name(self): def name(self) -> str:
"""Return the name.""" """Return the name."""
if self.forecast_day is not None: 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} {self._description[ATTR_LABEL]} {self.forecast_day}d"
return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" return f"{self._name} {self._description[ATTR_LABEL]}"
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""
if self.forecast_day is not None: 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}-{self.forecast_day}".lower()
return f"{self.coordinator.location_key}-{self.kind}".lower() return f"{self.coordinator.location_key}-{self.kind}".lower()
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
return { return {
"identifiers": {(DOMAIN, self.coordinator.location_key)}, "identifiers": {(DOMAIN, self.coordinator.location_key)},
@ -87,72 +123,54 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
} }
@property @property
def state(self): def state(self) -> StateType:
"""Return the state.""" """Return the state."""
if self.forecast_day is not None: if self.forecast_day is not None:
if ( if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE:
FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] return cast(float, self._sensor_data["Value"])
== DEVICE_CLASS_TEMPERATURE if self.kind == "UVIndex":
): return cast(int, self._sensor_data["Value"])
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "Ozone"]:
self.kind return cast(int, self._sensor_data["Value"])
]["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.kind == "Ceiling": 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": if self.kind == "PressureTendency":
return self.coordinator.data[self.kind]["LocalizedText"].lower() return cast(str, self._sensor_data["LocalizedText"].lower())
if SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE: if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE:
return self.coordinator.data[self.kind][self._unit_system]["Value"] return cast(float, self._sensor_data[self._unit_system]["Value"])
if self.kind == "Precipitation": if self.kind == "Precipitation":
return self.coordinator.data["PrecipitationSummary"][self.kind][ return cast(float, self._sensor_data[self._unit_system]["Value"])
self._unit_system
]["Value"]
if self.kind in ["Wind", "WindGust"]: if self.kind in ["Wind", "WindGust"]:
return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"] return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"])
return self.coordinator.data[self.kind] if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
return cast(StateType, self._sensor_data["Speed"]["Value"])
return cast(StateType, self._sensor_data)
@property @property
def icon(self): def icon(self) -> str | None:
"""Return the icon.""" """Return the icon."""
if self.forecast_day is not None: return self._description[ATTR_ICON]
return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON]
return SENSOR_TYPES[self.kind][ATTR_ICON]
@property @property
def device_class(self): def device_class(self) -> str | None:
"""Return the device_class.""" """Return the device_class."""
if self.forecast_day is not None: return self._description[ATTR_DEVICE_CLASS]
return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
if self.forecast_day is not None: if self.coordinator.is_metric:
return FORECAST_SENSOR_TYPES[self.kind][self._unit_system] return self._description[ATTR_UNIT_METRIC]
return SENSOR_TYPES[self.kind][self._unit_system] return self._description[ATTR_UNIT_IMPERIAL]
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
if self.forecast_day is not None: if self.forecast_day is not None:
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][ self._attrs["direction"] = self._sensor_data["Direction"]["English"]
self.forecast_day
][self.kind]["Direction"]["English"]
elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]: elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
self._attrs["level"] = self.coordinator.data[ATTR_FORECAST][ self._attrs["level"] = self._sensor_data["Category"]
self.forecast_day
][self.kind]["Category"]
return self._attrs return self._attrs
if self.kind == "UVIndex": if self.kind == "UVIndex":
self._attrs["level"] = self.coordinator.data["UVIndexText"] self._attrs["level"] = self.coordinator.data["UVIndexText"]
@ -161,6 +179,6 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
return self._attrs return self._attrs
@property @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 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]

View File

@ -1,4 +1,8 @@
"""Provide info to system health.""" """Provide info to system health."""
from __future__ import annotations
from typing import Any
from accuweather.const import ENDPOINT from accuweather.const import ENDPOINT
from homeassistant.components import system_health from homeassistant.components import system_health
@ -15,7 +19,7 @@ def async_register(
register.async_register_info(system_health_info) 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.""" """Get info for the info page."""
remaining_requests = list(hass.data[DOMAIN].values())[0][ remaining_requests = list(hass.data[DOMAIN].values())[0][
COORDINATOR COORDINATOR

View File

@ -1,5 +1,8 @@
"""Support for the AccuWeather service.""" """Support for the AccuWeather service."""
from __future__ import annotations
from statistics import mean from statistics import mean
from typing import Any, cast
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
@ -12,11 +15,18 @@ from homeassistant.components.weather import (
ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_SPEED,
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT 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.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherDataUpdateCoordinator
from .const import ( from .const import (
API_IMPERIAL,
API_METRIC,
ATTR_FORECAST, ATTR_FORECAST,
ATTRIBUTION, ATTRIBUTION,
CONDITION_CLASSES, CONDITION_CLASSES,
@ -29,42 +39,49 @@ from .const import (
PARALLEL_UPDATES = 1 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.""" """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): class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
def __init__(self, name, coordinator): coordinator: AccuWeatherDataUpdateCoordinator
def __init__(
self, name: str, coordinator: AccuWeatherDataUpdateCoordinator
) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._name = name self._name = name
self._attrs = {} self._unit_system = API_METRIC if self.coordinator.is_metric else API_IMPERIAL
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
@property @property
def name(self): def name(self) -> str:
"""Return the name.""" """Return the name."""
return self._name return self._name
@property @property
def attribution(self): def attribution(self) -> str:
"""Return the attribution.""" """Return the attribution."""
return ATTRIBUTION return ATTRIBUTION
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""
return self.coordinator.location_key return self.coordinator.location_key
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
return { return {
"identifiers": {(DOMAIN, self.coordinator.location_key)}, "identifiers": {(DOMAIN, self.coordinator.location_key)},
@ -74,7 +91,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
} }
@property @property
def condition(self): def condition(self) -> str | None:
"""Return the current condition.""" """Return the current condition."""
try: try:
return [ return [
@ -86,52 +103,60 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
return None return None
@property @property
def temperature(self): def temperature(self) -> float:
"""Return the temperature.""" """Return the temperature."""
return self.coordinator.data["Temperature"][self._unit_system]["Value"] return cast(
float, self.coordinator.data["Temperature"][self._unit_system]["Value"]
)
@property @property
def temperature_unit(self): def temperature_unit(self) -> str:
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT
@property @property
def pressure(self): def pressure(self) -> float:
"""Return the pressure.""" """Return the pressure."""
return self.coordinator.data["Pressure"][self._unit_system]["Value"] return cast(
float, self.coordinator.data["Pressure"][self._unit_system]["Value"]
)
@property @property
def humidity(self): def humidity(self) -> int:
"""Return the humidity.""" """Return the humidity."""
return self.coordinator.data["RelativeHumidity"] return cast(int, self.coordinator.data["RelativeHumidity"])
@property @property
def wind_speed(self): def wind_speed(self) -> float:
"""Return the wind speed.""" """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 @property
def wind_bearing(self): def wind_bearing(self) -> int:
"""Return the wind bearing.""" """Return the wind bearing."""
return self.coordinator.data["Wind"]["Direction"]["Degrees"] return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"])
@property @property
def visibility(self): def visibility(self) -> float:
"""Return the visibility.""" """Return the visibility."""
return self.coordinator.data["Visibility"][self._unit_system]["Value"] return cast(
float, self.coordinator.data["Visibility"][self._unit_system]["Value"]
)
@property @property
def ozone(self): def ozone(self) -> int | None:
"""Return the ozone level.""" """Return the ozone level."""
# We only have ozone data for certain locations and only in the forecast data. # 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( if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get(
"Ozone" "Ozone"
): ):
return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"] return cast(int, self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"])
return None return None
@property @property
def forecast(self): def forecast(self) -> list[dict[str, Any]] | None:
"""Return the forecast array.""" """Return the forecast array."""
if not self.coordinator.forecast: if not self.coordinator.forecast:
return None return None
@ -161,7 +186,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
return forecast return forecast
@staticmethod @staticmethod
def _calc_precipitation(day: dict) -> float: def _calc_precipitation(day: dict[str, Any]) -> float:
"""Return sum of the precipitation.""" """Return sum of the precipitation."""
precip_sum = 0 precip_sum = 0
precip_types = ["Rain", "Snow", "Ice"] precip_types = ["Rain", "Snow", "Ice"]

View 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",
}

View File

@ -1,6 +1,9 @@
"""Use serial protocol of Acer projector to obtain state of the projector.""" """Use serial protocol of Acer projector to obtain state of the projector."""
from __future__ import annotations
import logging import logging
import re import re
from typing import Any
import serial import serial
import voluptuous as vol import voluptuous as vol
@ -14,39 +17,26 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv 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__) _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( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_FILENAME): cv.isdevice, 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.""" """Connect with serial port and return Acer Projector."""
serial_port = config[CONF_FILENAME] serial_port = config[CONF_FILENAME]
name = config[CONF_NAME] name = config[CONF_NAME]
@ -72,10 +67,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class AcerSwitch(SwitchEntity): class AcerSwitch(SwitchEntity):
"""Represents an Acer Projector as a switch.""" """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.""" """Init of the Acer projector."""
self.ser = serial.Serial( 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._serial_port = serial_port
self._name = name self._name = name
@ -87,7 +88,7 @@ class AcerSwitch(SwitchEntity):
ECO_MODE: STATE_UNKNOWN, ECO_MODE: STATE_UNKNOWN,
} }
def _write_read(self, msg): def _write_read(self, msg: str) -> str:
"""Write to the projector and read the return.""" """Write to the projector and read the return."""
ret = "" ret = ""
# Sometimes the projector won't answer for no reason or the projector # Sometimes the projector won't answer for no reason or the projector
@ -96,8 +97,7 @@ class AcerSwitch(SwitchEntity):
try: try:
if not self.ser.is_open: if not self.ser.is_open:
self.ser.open() self.ser.open()
msg = msg.encode("utf-8") self.ser.write(msg.encode("utf-8"))
self.ser.write(msg)
# Size is an experience value there is no real limit. # Size is an experience value there is no real limit.
# AFAIK there is no limit and no end character so we will usually # AFAIK there is no limit and no end character so we will usually
# need to wait for timeout # need to wait for timeout
@ -107,7 +107,7 @@ class AcerSwitch(SwitchEntity):
self.ser.close() self.ser.close()
return ret return ret
def _write_read_format(self, msg): def _write_read_format(self, msg: str) -> str:
"""Write msg, obtain answer and format output.""" """Write msg, obtain answer and format output."""
# answers are formatted as ***\answer\r*** # answers are formatted as ***\answer\r***
awns = self._write_read(msg) awns = self._write_read(msg)
@ -117,29 +117,33 @@ class AcerSwitch(SwitchEntity):
return STATE_UNKNOWN return STATE_UNKNOWN
@property @property
def available(self): def available(self) -> bool:
"""Return if projector is available.""" """Return if projector is available."""
return self._available return self._available
@property @property
def name(self): def name(self) -> str:
"""Return name of the projector.""" """Return name of the projector."""
return self._name return self._name
@property @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 if the projector is turned on."""
return self._state return self._state
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, str]:
"""Return state attributes.""" """Return state attributes."""
return self._attributes return self._attributes
def update(self): def update(self) -> None:
"""Get the latest state from the projector.""" """Get the latest state from the projector."""
msg = CMD_DICT[LAMP] awns = self._write_read_format(CMD_DICT[LAMP])
awns = self._write_read_format(msg)
if awns == "Lamp 1": if awns == "Lamp 1":
self._state = True self._state = True
self._available = True self._available = True
@ -155,14 +159,14 @@ class AcerSwitch(SwitchEntity):
awns = self._write_read_format(msg) awns = self._write_read_format(msg)
self._attributes[key] = awns self._attributes[key] = awns
def turn_on(self, **kwargs): def turn_on(self, **kwargs: Any) -> None:
"""Turn the projector on.""" """Turn the projector on."""
msg = CMD_DICT[STATE_ON] msg = CMD_DICT[STATE_ON]
self._write_read(msg) 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.""" """Turn the projector off."""
msg = CMD_DICT[STATE_OFF] msg = CMD_DICT[STATE_OFF]
self._write_read(msg) self._write_read(msg)
self._state = STATE_OFF self._state = False

View File

@ -13,7 +13,7 @@ from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER
class AcmedaBase(entity.Entity): class AcmedaBase(entity.Entity):
"""Base representation of an Acmeda roller.""" """Base representation of an Acmeda roller."""
def __init__(self, roller: aiopulse.Roller): def __init__(self, roller: aiopulse.Roller) -> None:
"""Initialize the roller.""" """Initialize the roller."""
self.roller = roller self.roller = roller

View File

@ -17,7 +17,6 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Acmeda config flow.""" """Handle a Acmeda config flow."""
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self): def __init__(self):
"""Initialize the config flow.""" """Initialize the config flow."""

View 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"
)

View File

@ -1,30 +1,28 @@
"""Support for Actiontec MI424WR (Verizon FIOS) routers.""" """Support for Actiontec MI424WR (Verizon FIOS) routers."""
from collections import namedtuple from __future__ import annotations
import logging import logging
import re
import telnetlib import telnetlib
from typing import Final
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, DOMAIN,
PLATFORM_SCHEMA, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
DeviceScanner, DeviceScanner,
) )
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv 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( _LOGGER: Final = logging.getLogger(__name__)
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"
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): 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.""" """Validate the configuration and return an Actiontec scanner."""
scanner = ActiontecDeviceScanner(config[DOMAIN]) scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "ip", "last_update"])
class ActiontecDeviceScanner(DeviceScanner): class ActiontecDeviceScanner(DeviceScanner):
"""This class queries an actiontec router for connected devices.""" """This class queries an actiontec router for connected devices."""
def __init__(self, config): def __init__(self, config: ConfigType) -> None:
"""Initialize the scanner.""" """Initialize the scanner."""
self.host = config[CONF_HOST] self.host: str = config[CONF_HOST]
self.username = config[CONF_USERNAME] self.username: str = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD] self.password: str = config[CONF_PASSWORD]
self.last_results = [] self.last_results: list[Device] = []
data = self.get_actiontec_data() data = self.get_actiontec_data()
self.success_init = data is not None self.success_init = data is not None
_LOGGER.info("Scanner initialized") _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.""" """Scan for new devices and return a list with found device IDs."""
self._update_info() 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.""" """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: for client in self.last_results:
if client.mac == device: if client.mac_address == device:
return client.ip return client.ip_address
return None return None
def _update_info(self): def _update_info(self) -> bool:
"""Ensure the information from the router is up to date. """Ensure the information from the router is up to date.
Return boolean if scanning successful. Return boolean if scanning successful.
@ -78,19 +71,16 @@ class ActiontecDeviceScanner(DeviceScanner):
if not self.success_init: if not self.success_init:
return False return False
now = dt_util.now()
actiontec_data = self.get_actiontec_data() actiontec_data = self.get_actiontec_data()
if not actiontec_data: if actiontec_data is None:
return False return False
self.last_results = [ self.last_results = [
Device(data["mac"], name, now) device for device in actiontec_data if device.timevalid > -60
for name, data in actiontec_data.items()
if data["timevalid"] > -60
] ]
_LOGGER.info("Scan successful") _LOGGER.info("Scan successful")
return True return True
def get_actiontec_data(self): def get_actiontec_data(self) -> list[Device] | None:
"""Retrieve data from Actiontec MI424WR and return parsed result.""" """Retrieve data from Actiontec MI424WR and return parsed result."""
try: try:
telnet = telnetlib.Telnet(self.host) telnet = telnetlib.Telnet(self.host)
@ -106,18 +96,20 @@ class ActiontecDeviceScanner(DeviceScanner):
telnet.write(b"exit\n") telnet.write(b"exit\n")
except EOFError: except EOFError:
_LOGGER.exception("Unexpected response from router") _LOGGER.exception("Unexpected response from router")
return return None
except ConnectionRefusedError: except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router. Telnet enabled?") _LOGGER.exception("Connection refused by router. Telnet enabled?")
return None return None
devices = {} devices: list[Device] = []
for lease in leases_result: 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: if match is not None:
devices[match.group("ip")] = { devices.append(
"ip": match.group("ip"), Device(
"mac": match.group("mac").upper(), match.group("ip"),
"timevalid": int(match.group("timevalid")), match.group("mac").upper(),
} int(match.group("timevalid")),
)
)
return devices return devices

View 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

View File

@ -2,22 +2,10 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
import voluptuous as vol 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.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -33,7 +21,19 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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__) _LOGGER = logging.getLogger(__name__)
@ -194,7 +194,7 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
"""Defines a AdGuard Home device entity.""" """Defines a AdGuard Home device entity."""
@property @property
def device_info(self) -> dict[str, Any]: def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance.""" """Return device information about this AdGuard Home instance."""
return { return {
"identifiers": { "identifiers": {

View File

@ -6,7 +6,6 @@ from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeConnectionError from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlow from homeassistant.config_entries import ConfigFlow
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -26,7 +25,6 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a AdGuard Home config flow.""" """Handle a AdGuard Home config flow."""
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
_hassio_discovery = None _hassio_discovery = None
@ -67,13 +65,9 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is None: if user_input is None:
return await self._show_setup_form(user_input) return await self._show_setup_form(user_input)
entries = self._async_current_entries() self._async_abort_entries_match(
for entry in entries: {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
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")
errors = {} errors = {}

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from typing import Callable
from adguardhome import AdGuardHome, AdGuardHomeConnectionError from adguardhome import AdGuardHome, AdGuardHomeConnectionError
@ -11,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdGuardHomeDeviceEntity from . import AdGuardHomeDeviceEntity
from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN
@ -23,7 +22,7 @@ PARALLEL_UPDATES = 4
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: ConfigEntry,
async_add_entities: Callable[[list[Entity], bool], None], async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdGuard Home sensor based on a config entry.""" """Set up AdGuard Home sensor based on a config entry."""
adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT]

View File

@ -1,37 +1,65 @@
add_url: add_url:
name: Add url
description: Add a new filter subscription to AdGuard Home. description: Add a new filter subscription to AdGuard Home.
fields: fields:
name: name:
name: Name
description: The name of the filter subscription. description: The name of the filter subscription.
required: true
example: Example example: Example
selector:
text:
url: url:
name: Url
description: The filter URL to subscribe to, containing the filter rules. description: The filter URL to subscribe to, containing the filter rules.
required: true
example: https://www.example.com/filter/1.txt example: https://www.example.com/filter/1.txt
selector:
text:
remove_url: remove_url:
name: Remove url
description: Removes a filter subscription from AdGuard Home. description: Removes a filter subscription from AdGuard Home.
fields: fields:
url: url:
name: Url
description: The filter subscription URL to remove. description: The filter subscription URL to remove.
required: true
example: https://www.example.com/filter/1.txt example: https://www.example.com/filter/1.txt
selector:
text:
enable_url: enable_url:
name: Enable url
description: Enables a filter subscription in AdGuard Home. description: Enables a filter subscription in AdGuard Home.
fields: fields:
url: url:
name: Url
description: The filter subscription URL to enable. description: The filter subscription URL to enable.
required: true
example: https://www.example.com/filter/1.txt example: https://www.example.com/filter/1.txt
selector:
text:
disable_url: disable_url:
name: Disable url
description: Disables a filter subscription in AdGuard Home. description: Disables a filter subscription in AdGuard Home.
fields: fields:
url: url:
name: Url
description: The filter subscription URL to disable. description: The filter subscription URL to disable.
required: true
example: https://www.example.com/filter/1.txt example: https://www.example.com/filter/1.txt
selector:
text:
refresh: refresh:
name: Refresh
description: Refresh all filter subscriptions in AdGuard Home. description: Refresh all filter subscriptions in AdGuard Home.
fields: fields:
force: force:
description: Force update (by passes AdGuard Home throttling). name: Force
example: '"true" to force, "false" or omit for a regular refresh.' description: Force update (bypasses AdGuard Home throttling). "true" to force, or "false" to omit for a regular refresh.
default: false
selector:
boolean:

View File

@ -3,7 +3,6 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Callable
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
@ -11,7 +10,7 @@ from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdGuardHomeDeviceEntity from . import AdGuardHomeDeviceEntity
from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN
@ -25,7 +24,7 @@ PARALLEL_UPDATES = 1
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: ConfigEntry,
async_add_entities: Callable[[list[Entity], bool], None], async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdGuard Home switch based on a config entry.""" """Set up AdGuard Home switch based on a config entry."""
adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT]

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "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.", "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."
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "El servei ja est\u00e0 configurat", "already_configured": "El servei ja est\u00e0 configurat",
"existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent."
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
}, },
"error": { "error": {
"cannot_connect": "Ha fallat la connexi\u00f3" "cannot_connect": "Ha fallat la connexi\u00f3"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Slu\u017eba je ji\u017e nastavena", "already_configured": "Slu\u017eba je ji\u017e nastavena",
"existing_instance_updated": "St\u00e1vaj\u00edc\u00ed nastaven\u00ed aktualizov\u00e1no.", "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."
}, },
"error": { "error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "Opdaterede eksisterende konfiguration.", "existing_instance_updated": "Opdaterede eksisterende konfiguration."
"single_instance_allowed": "Kun en enkelt konfiguration af AdGuard Home er tilladt."
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {

View File

@ -1,8 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", "already_configured": "Der Dienst ist bereits konfiguriert",
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert."
}, },
"error": { "error": {
"cannot_connect": "Verbindung fehlgeschlagen" "cannot_connect": "Verbindung fehlgeschlagen"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Service is already configured", "already_configured": "Service is already configured",
"existing_instance_updated": "Updated existing configuration.", "existing_instance_updated": "Updated existing configuration."
"single_instance_allowed": "Already configured. Only a single configuration possible."
}, },
"error": { "error": {
"cannot_connect": "Failed to connect" "cannot_connect": "Failed to connect"

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.", "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente."
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "El servicio ya est\u00e1 configurado", "already_configured": "El servicio ya est\u00e1 configurado",
"existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente."
"single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
}, },
"error": { "error": {
"cannot_connect": "No se pudo conectar" "cannot_connect": "No se pudo conectar"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Teenus on juba seadistatud", "already_configured": "Teenus on juba seadistatud",
"existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud.", "existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud."
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
}, },
"error": { "error": {
"cannot_connect": "\u00dchendamine nurjus" "cannot_connect": "\u00dchendamine nurjus"

View File

@ -1,8 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9",
"single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour."
}, },
"error": { "error": {
"cannot_connect": "\u00c9chec de connexion" "cannot_connect": "\u00c9chec de connexion"

View File

@ -1,8 +1,5 @@
{ {
"config": { "config": {
"abort": {
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"error": { "error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s" "cannot_connect": "Sikertelen csatlakoz\u00e1s"
}, },

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "Memperbarui konfigurasi yang ada.", "existing_instance_updated": "Memperbarui konfigurasi yang ada."
"single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
}, },
"error": { "error": {
"cannot_connect": "Gagal terhubung" "cannot_connect": "Gagal terhubung"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Il servizio \u00e8 gi\u00e0 configurato", "already_configured": "Il servizio \u00e8 gi\u00e0 configurato",
"existing_instance_updated": "Configurazione esistente aggiornata.", "existing_instance_updated": "Configurazione esistente aggiornata."
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
}, },
"error": { "error": {
"cannot_connect": "Impossibile connettersi" "cannot_connect": "Impossibile connettersi"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "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.", "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."
}, },
"error": { "error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.", "existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert."
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
}, },
"error": { "error": {
"cannot_connect": "Feeler beim verbannen" "cannot_connect": "Feeler beim verbannen"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Service is al geconfigureerd", "already_configured": "Service is al geconfigureerd",
"existing_instance_updated": "Bestaande configuratie bijgewerkt.", "existing_instance_updated": "Bestaande configuratie bijgewerkt."
"single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan."
}, },
"error": { "error": {
"cannot_connect": "Kan geen verbinding maken" "cannot_connect": "Kan geen verbinding maken"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Tjenesten er allerede konfigurert", "already_configured": "Tjenesten er allerede konfigurert",
"existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon."
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
}, },
"error": { "error": {
"cannot_connect": "Tilkobling mislyktes" "cannot_connect": "Tilkobling mislyktes"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana",
"existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119"
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
}, },
"error": { "error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada.", "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada."
"single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do AdGuard Home \u00e9 permitida."
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {

View File

@ -1,8 +1,5 @@
{ {
"config": { "config": {
"abort": {
"single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel."
},
"error": { "error": {
"cannot_connect": "Falha na liga\u00e7\u00e3o" "cannot_connect": "Falha na liga\u00e7\u00e3o"
}, },

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "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.", "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.", "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."
}, },
"error": { "error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.", "existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija."
"single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home."
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "Uppdaterade existerande konfiguration.", "existing_instance_updated": "Uppdaterade existerande konfiguration."
"single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten."
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {

View File

@ -1,8 +1,5 @@
{ {
"config": { "config": {
"abort": {
"single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
},
"error": { "error": {
"cannot_connect": "Ba\u011flanma hatas\u0131" "cannot_connect": "Ba\u011flanma hatas\u0131"
}, },

View File

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", "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."
}, },
"error": { "error": {
"cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"

View File

@ -2,8 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002"
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
}, },
"error": { "error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557" "cannot_connect": "\u9023\u7dda\u5931\u6557"

View File

@ -1,15 +1,34 @@
# Describes the format for available ADS services # Describes the format for available ADS services
write_data_by_name: write_data_by_name:
name: Write data by name
description: Write a value to the connected ADS device. description: Write a value to the connected ADS device.
fields: fields:
adsvar: adsvar:
name: ADS variable
description: The name of the variable to write to. description: The name of the variable to write to.
required: true
example: ".global_var" example: ".global_var"
selector:
text:
adstype: adstype:
name: ADS type
description: The data type of the variable to write to. description: The data type of the variable to write to.
example: "int" required: true
selector:
select:
options:
- 'bool'
- 'byte'
- 'dint'
- 'int'
- 'udint'
- 'uint'
value: value:
name: Value
description: The value to write to the variable. description: The value to write to the variable.
example: 1 required: true
selector:
number:
min: 0
max: 10000

View File

@ -70,7 +70,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(AdvantageAirZone(instance, ac_key, zone_key)) entities.append(AdvantageAirZone(instance, ac_key, zone_key))
async_add_entities(entities) async_add_entities(entities)
platform = entity_platform.current_platform.get() platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service( platform.async_register_entity_service(
ADVANTAGE_AIR_SERVICE_SET_MYZONE, ADVANTAGE_AIR_SERVICE_SET_MYZONE,
{}, {},

View File

@ -22,7 +22,7 @@ class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config Advantage Air API connection.""" """Config Advantage Air API connection."""
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
DOMAIN = DOMAIN DOMAIN = DOMAIN
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):

View File

@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
async_add_entities(entities) async_add_entities(entities)
platform = entity_platform.current_platform.get() platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service( platform.async_register_entity_service(
ADVANTAGE_AIR_SERVICE_SET_TIME_TO, ADVANTAGE_AIR_SERVICE_SET_TIME_TO,
{vol.Required("minutes"): cv.positive_int}, {vol.Required("minutes"): cv.positive_int},

View File

@ -7,8 +7,15 @@ set_time_to:
domain: sensor domain: sensor
fields: fields:
minutes: minutes:
name: Minutes
description: Minutes until action description: Minutes until action
example: "60" required: true
selector:
number:
min: 0
max: 1440
unit_of_measurement: minutes
set_myzone: set_myzone:
name: Set MyZone name: Set MyZone
description: Change which zone is set as the reference for temperature control description: Change which zone is set as the reference for temperature control

View File

@ -12,6 +12,7 @@
"ip_address": "IP Adresse", "ip_address": "IP Adresse",
"port": "Port" "port": "Port"
}, },
"description": "Anschluss an die API Ihres Advantage Air Wandtabletts.",
"title": "Verbinden" "title": "Verbinden"
} }
} }

View File

@ -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.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant 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 from .weather_update_coordinator import WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _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] api_key = config_entry.data[CONF_API_KEY]
latitude = config_entry.data[CONF_LATITUDE] latitude = config_entry.data[CONF_LATITUDE]
longitude = config_entry.data[CONF_LONGITUDE] longitude = config_entry.data[CONF_LONGITUDE]
station_updates = config_entry.options.get(CONF_STATION_UPDATES, True)
aemet = AEMET(api_key) 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() 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) hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
config_entry.async_on_unload(config_entry.add_update_listener(async_update_options))
return True 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): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(

View File

@ -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}

View File

@ -4,16 +4,15 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME 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 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): class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for AEMET OpenData.""" """Config flow for AEMET OpenData."""
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=None):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
errors = {} 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) 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): async def _is_aemet_api_online(hass, api_key):
aemet = AEMET(api_key) aemet = AEMET(api_key)

View File

@ -34,12 +34,12 @@ from homeassistant.const import (
) )
ATTRIBUTION = "Powered by AEMET OpenData" ATTRIBUTION = "Powered by AEMET OpenData"
CONF_STATION_UPDATES = "station_updates"
PLATFORMS = ["sensor", "weather"] PLATFORMS = ["sensor", "weather"]
DEFAULT_NAME = "AEMET" DEFAULT_NAME = "AEMET"
DOMAIN = "aemet" DOMAIN = "aemet"
ENTRY_NAME = "name" ENTRY_NAME = "name"
ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
UPDATE_LISTENER = "update_listener"
SENSOR_NAME = "sensor_name" SENSOR_NAME = "sensor_name"
SENSOR_UNIT = "sensor_unit" SENSOR_UNIT = "sensor_unit"
SENSOR_DEVICE_CLASS = "sensor_device_class" SENSOR_DEVICE_CLASS = "sensor_device_class"

View File

@ -3,7 +3,7 @@
"name": "AEMET OpenData", "name": "AEMET OpenData",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aemet", "documentation": "https://www.home-assistant.io/integrations/aemet",
"requirements": ["AEMET-OpenData==0.1.8"], "requirements": ["AEMET-OpenData==0.2.1"],
"codeowners": ["@noltari"], "codeowners": ["@noltari"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -1,6 +1,10 @@
"""Support for the AEMET OpenData service.""" """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 ( from .const import (
ATTRIBUTION,
DOMAIN, DOMAIN,
ENTRY_NAME, ENTRY_NAME,
ENTRY_WEATHER_COORDINATOR, ENTRY_WEATHER_COORDINATOR,
@ -10,6 +14,9 @@ from .const import (
FORECAST_MONITORED_CONDITIONS, FORECAST_MONITORED_CONDITIONS,
FORECAST_SENSOR_TYPES, FORECAST_SENSOR_TYPES,
MONITORED_CONDITIONS, MONITORED_CONDITIONS,
SENSOR_DEVICE_CLASS,
SENSOR_NAME,
SENSOR_UNIT,
WEATHER_SENSOR_TYPES, WEATHER_SENSOR_TYPES,
) )
from .weather_update_coordinator import WeatherUpdateCoordinator 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) 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): class AemetSensor(AbstractAemetSensor):
"""Implementation of an AEMET OpenData sensor.""" """Implementation of an AEMET OpenData sensor."""

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Gather data from AEMET weather stations"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Obt\u00e9 les dades de les estacions meteorol\u00f2giques d'AEMET"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Gather data from AEMET weather stations"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Obtener datos de las estaciones meteorol\u00f3gicas de AEMET"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Koguandmeid AEMETi ilmajaamadest"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Raccogli i dati dalle stazioni meteorologiche AEMET"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Verzamel gegevens van AEMET-weerstations"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Samle inn data fra AEMET v\u00e6rstasjoner"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "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"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "\u81ea AEMET \u6c23\u8c61\u7ad9\u7372\u5f97\u8cc7\u6599"
}
}
}
} }
} }

View File

@ -118,7 +118,7 @@ class TownNotFound(UpdateFailed):
class WeatherUpdateCoordinator(DataUpdateCoordinator): class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator.""" """Weather data update coordinator."""
def __init__(self, hass, aemet, latitude, longitude): def __init__(self, hass, aemet, latitude, longitude, station_updates):
"""Initialize coordinator.""" """Initialize coordinator."""
super().__init__( super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
@ -129,6 +129,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
self._town = None self._town = None
self._latitude = latitude self._latitude = latitude
self._longitude = longitude self._longitude = longitude
self._station_updates = station_updates
self._data = { self._data = {
"daily": None, "daily": None,
"hourly": None, "hourly": None,
@ -210,7 +211,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
) )
station = None station = None
if self._get_weather_station(): if self._station_updates and self._get_weather_station():
station = self._aemet.get_conventional_observation_station_data( station = self._aemet.get_conventional_observation_station_data(
self._station[AEMET_ATTR_IDEMA] self._station[AEMET_ATTR_IDEMA]
) )

View File

@ -1,2 +1,42 @@
"""Constants for the Aftership integration.""" """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}
)

View File

@ -1,53 +1,47 @@
"""Support for non-delivered packages recorded in AfterShip.""" """Support for non-delivered packages recorded in AfterShip."""
from datetime import timedelta from __future__ import annotations
import logging import logging
from typing import Any, Final
from pyaftership.tracker import Tracking from pyaftership.tracker import Tracking
import voluptuous as vol 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.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 from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send 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 homeassistant.util import Throttle
from .const import DOMAIN from .const import (
ADD_TRACKING_SERVICE_SCHEMA,
_LOGGER = logging.getLogger(__name__) ATTR_TRACKINGS,
ATTRIBUTION,
ATTRIBUTION = "Information provided by AfterShip" BASE,
ATTR_TRACKINGS = "trackings" CONF_SLUG,
CONF_TITLE,
BASE = "https://track.aftership.com/" CONF_TRACKING_NUMBER,
DEFAULT_NAME,
CONF_SLUG = "slug" DOMAIN,
CONF_TITLE = "title" ICON,
CONF_TRACKING_NUMBER = "tracking_number" MIN_TIME_BETWEEN_UPDATES,
REMOVE_TRACKING_SERVICE_SCHEMA,
DEFAULT_NAME = "aftership" SERVICE_ADD_TRACKING,
UPDATE_TOPIC = f"{DOMAIN}_update" SERVICE_REMOVE_TRACKING,
UPDATE_TOPIC,
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,
}
) )
REMOVE_TRACKING_SERVICE_SCHEMA = vol.Schema( _LOGGER: Final = logging.getLogger(__name__)
{vol.Required(CONF_SLUG): cv.string, vol.Required(CONF_TRACKING_NUMBER): cv.string}
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): 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.""" """Set up the AfterShip sensor platform."""
apikey = config[CONF_API_KEY] apikey = config[CONF_API_KEY]
name = config[CONF_NAME] 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_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.""" """Call when a user adds a new Aftership tracking from Home Assistant."""
title = call.data.get(CONF_TITLE) title = call.data.get(CONF_TITLE)
slug = call.data.get(CONF_SLUG) 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, 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.""" """Call when a user removes an Aftership tracking from Home Assistant."""
slug = call.data[CONF_SLUG] slug = call.data[CONF_SLUG]
tracking_number = call.data[CONF_TRACKING_NUMBER] 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): class AfterShipSensor(SensorEntity):
"""Representation of a AfterShip sensor.""" """Representation of a AfterShip sensor."""
def __init__(self, aftership, name): def __init__(self, aftership: Tracking, name: str) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self._attributes = {} self._attributes: dict[str, Any] = {}
self._name = name self._name: str = name
self._state = None self._state: int | None = None
self.aftership = aftership self.aftership = aftership
@property @property
def name(self): def name(self) -> str:
"""Return the name of the sensor.""" """Return the name of the sensor."""
return self._name return self._name
@property @property
def state(self): def state(self) -> int | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._state return self._state
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity, if any.""" """Return the unit of measurement of this entity, if any."""
return "packages" return "packages"
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, str]:
"""Return attributes for the sensor.""" """Return attributes for the sensor."""
return self._attributes return self._attributes
@property @property
def icon(self): def icon(self) -> str:
"""Icon to use in the frontend.""" """Icon to use in the frontend."""
return ICON return ICON
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
self.async_on_remove( self.async_on_remove(
self.hass.helpers.dispatcher.async_dispatcher_connect( 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.""" """Force update of data."""
await self.async_update(no_throttle=True) await self.async_update(no_throttle=True)
self.async_write_ha_state() self.async_write_ha_state()
@Throttle(MIN_TIME_BETWEEN_UPDATES) @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.""" """Get the latest data from the AfterShip API."""
await self.aftership.get_trackings() await self.aftership.get_trackings()
@ -170,7 +169,7 @@ class AfterShipSensor(SensorEntity):
return return
status_to_ignore = {"delivered"} status_to_ignore = {"delivered"}
status_counts = {} status_counts: dict[str, int] = {}
trackings = [] trackings = []
not_delivered_count = 0 not_delivered_count = 0

View File

@ -1,24 +1,43 @@
# Describes the format for available aftership services # Describes the format for available aftership services
add_tracking: add_tracking:
description: Add new tracking to Aftership. name: Add tracking
description: Add new tracking number to Aftership.
fields: fields:
tracking_number: tracking_number:
name: Tracking number
description: Tracking number for the new tracking description: Tracking number for the new tracking
required: true
example: "123456789" example: "123456789"
selector:
text:
title: title:
name: Title
description: A custom title for the new tracking description: A custom title for the new tracking
example: "Laptop" example: "Laptop"
selector:
text:
slug: slug:
name: Slug
description: Slug (carrier) of the new tracking description: Slug (carrier) of the new tracking
example: "USPS" example: "USPS"
selector:
text:
remove_tracking: remove_tracking:
description: Remove a tracking from Aftership. name: Remove tracking
description: Remove a tracking number from Aftership.
fields: fields:
tracking_number: tracking_number:
name: Tracking number
description: Tracking number of the tracking to remove description: Tracking number of the tracking to remove
required: true
example: "123456789" example: "123456789"
selector:
text:
slug: slug:
name: Slug
description: Slug (carrier) of the tracking to remove description: Slug (carrier) of the tracking to remove
example: "USPS" example: "USPS"
selector:
text:

View File

@ -59,7 +59,7 @@ async def async_setup_entry(
async_add_entities(cameras) async_add_entities(cameras)
platform = entity_platform.current_platform.get() platform = entity_platform.async_get_current_platform()
for service, method in CAMERA_SERVICES.items(): for service, method in CAMERA_SERVICES.items():
platform.async_register_entity_service(service, {}, method) platform.async_register_entity_service(service, {}, method)

View File

@ -1,34 +1,39 @@
start_recording: start_recording:
name: Start recording
description: Enable continuous recording. description: Enable continuous recording.
fields: target:
entity_id: entity:
description: "Name(s) of the entity to start recording." integration: agent_dvr
example: "camera.camera_1" domain: camera
stop_recording: stop_recording:
name: Stop recording
description: Disable continuous recording. description: Disable continuous recording.
fields: target:
entity_id: entity:
description: "Name(s) of the entity to stop recording." integration: agent_dvr
example: "camera.camera_1" domain: camera
enable_alerts: enable_alerts:
name: Enable alerts
description: Enable alerts description: Enable alerts
fields: target:
entity_id: entity:
description: "Name(s) of the entity to enable alerts." integration: agent_dvr
example: "camera.camera_1" domain: camera
disable_alerts: disable_alerts:
name: Disable alerts
description: Disable alerts description: Disable alerts
fields: target:
entity_id: entity:
description: "Name(s) of the entity to disable alerts." integration: agent_dvr
example: "camera.camera_1" domain: camera
snapshot: snapshot:
name: Snapshot
description: Take a photo description: Take a photo
fields: target:
entity_id: entity:
description: "Name(s) of the entity to take a snapshot." integration: agent_dvr
example: "camera.camera_1" domain: camera

View File

@ -1,40 +1,45 @@
"""Component for handling Air Quality data for your location.""" """Component for handling Air Quality data for your location."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import final from typing import Final, final
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
) )
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE, PLATFORM_SCHEMA_BASE,
) )
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent 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_AQI: Final = "air_quality_index"
ATTR_CO2 = "carbon_dioxide" ATTR_CO2: Final = "carbon_dioxide"
ATTR_CO = "carbon_monoxide" ATTR_CO: Final = "carbon_monoxide"
ATTR_N2O = "nitrogen_oxide" ATTR_N2O: Final = "nitrogen_oxide"
ATTR_NO = "nitrogen_monoxide" ATTR_NO: Final = "nitrogen_monoxide"
ATTR_NO2 = "nitrogen_dioxide" ATTR_NO2: Final = "nitrogen_dioxide"
ATTR_OZONE = "ozone" ATTR_OZONE: Final = "ozone"
ATTR_PM_0_1 = "particulate_matter_0_1" ATTR_PM_0_1: Final = "particulate_matter_0_1"
ATTR_PM_10 = "particulate_matter_10" ATTR_PM_10: Final = "particulate_matter_10"
ATTR_PM_2_5 = "particulate_matter_2_5" ATTR_PM_2_5: Final = "particulate_matter_2_5"
ATTR_SO2 = "sulphur_dioxide" 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, "air_quality_index": ATTR_AQI,
"attribution": ATTR_ATTRIBUTION, "attribution": ATTR_ATTRIBUTION,
"carbon_dioxide": ATTR_CO2, "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.""" """Set up the air quality component."""
component = hass.data[DOMAIN] = EntityComponent( component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL _LOGGER, DOMAIN, hass, SCAN_INTERVAL
@ -59,84 +64,86 @@ async def async_setup(hass, config):
return True return True
async def async_setup_entry(hass, entry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry.""" """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.""" """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): class AirQualityEntity(Entity):
"""ABC for air quality data.""" """ABC for air quality data."""
@property @property
def particulate_matter_2_5(self): def particulate_matter_2_5(self) -> StateType:
"""Return the particulate matter 2.5 level.""" """Return the particulate matter 2.5 level."""
raise NotImplementedError() raise NotImplementedError()
@property @property
def particulate_matter_10(self): def particulate_matter_10(self) -> StateType:
"""Return the particulate matter 10 level.""" """Return the particulate matter 10 level."""
return None return None
@property @property
def particulate_matter_0_1(self): def particulate_matter_0_1(self) -> StateType:
"""Return the particulate matter 0.1 level.""" """Return the particulate matter 0.1 level."""
return None return None
@property @property
def air_quality_index(self): def air_quality_index(self) -> StateType:
"""Return the Air Quality Index (AQI).""" """Return the Air Quality Index (AQI)."""
return None return None
@property @property
def ozone(self): def ozone(self) -> StateType:
"""Return the O3 (ozone) level.""" """Return the O3 (ozone) level."""
return None return None
@property @property
def carbon_monoxide(self): def carbon_monoxide(self) -> StateType:
"""Return the CO (carbon monoxide) level.""" """Return the CO (carbon monoxide) level."""
return None return None
@property @property
def carbon_dioxide(self): def carbon_dioxide(self) -> StateType:
"""Return the CO2 (carbon dioxide) level.""" """Return the CO2 (carbon dioxide) level."""
return None return None
@property @property
def attribution(self): def attribution(self) -> StateType:
"""Return the attribution.""" """Return the attribution."""
return None return None
@property @property
def sulphur_dioxide(self): def sulphur_dioxide(self) -> StateType:
"""Return the SO2 (sulphur dioxide) level.""" """Return the SO2 (sulphur dioxide) level."""
return None return None
@property @property
def nitrogen_oxide(self): def nitrogen_oxide(self) -> StateType:
"""Return the N2O (nitrogen oxide) level.""" """Return the N2O (nitrogen oxide) level."""
return None return None
@property @property
def nitrogen_monoxide(self): def nitrogen_monoxide(self) -> StateType:
"""Return the NO (nitrogen monoxide) level.""" """Return the NO (nitrogen monoxide) level."""
return None return None
@property @property
def nitrogen_dioxide(self): def nitrogen_dioxide(self) -> StateType:
"""Return the NO2 (nitrogen dioxide) level.""" """Return the NO2 (nitrogen dioxide) level."""
return None return None
@final @final
@property @property
def state_attributes(self): def state_attributes(self) -> dict[str, str | int | float]:
"""Return the state attributes.""" """Return the state attributes."""
data = {} data: dict[str, str | int | float] = {}
for prop, attr in PROP_TO_ATTR.items(): for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop) value = getattr(self, prop)
@ -146,11 +153,11 @@ class AirQualityEntity(Entity):
return data return data
@property @property
def state(self): def state(self) -> StateType:
"""Return the current state.""" """Return the current state."""
return self.particulate_matter_2_5 return self.particulate_matter_2_5
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity.""" """Return the unit of measurement of this entity."""
return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER

View File

@ -1,15 +1,21 @@
"""The Airly integration.""" """The Airly integration."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from math import ceil from math import ceil
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from airly import Airly from airly import Airly
from airly.exceptions import AirlyError from airly.exceptions import AirlyError
import async_timeout import async_timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE 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.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.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -30,7 +36,7 @@ PLATFORMS = ["air_quality", "sensor"]
_LOGGER = logging.getLogger(__name__) _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. Return data update interval.
@ -46,7 +52,7 @@ def set_update_interval(instances, requests_remaining):
interval = timedelta( interval = timedelta(
minutes=min( minutes=min(
max( max(
ceil(minutes_to_midnight / requests_remaining * instances), ceil(minutes_to_midnight / requests_remaining * instances_count),
MIN_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL,
), ),
MAX_UPDATE_INTERVAL, MAX_UPDATE_INTERVAL,
@ -58,19 +64,39 @@ def set_update_interval(instances, requests_remaining):
return interval 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.""" """Set up Airly as config entry."""
api_key = config_entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
latitude = config_entry.data[CONF_LATITUDE] latitude = entry.data[CONF_LATITUDE]
longitude = config_entry.data[CONF_LONGITUDE] longitude = entry.data[CONF_LONGITUDE]
use_nearest = config_entry.data.get(CONF_USE_NEAREST, False) use_nearest = entry.data.get(CONF_USE_NEAREST, False)
# For backwards compat, set unique ID # 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( 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) websession = async_get_clientsession(hass)
update_interval = timedelta(minutes=MIN_UPDATE_INTERVAL) 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() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {}) 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 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 a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
config_entry, PLATFORMS
)
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok
@ -105,14 +129,14 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
def __init__( def __init__(
self, self,
hass, hass: HomeAssistant,
session, session: ClientSession,
api_key, api_key: str,
latitude, latitude: float,
longitude, longitude: float,
update_interval, update_interval: timedelta,
use_nearest, use_nearest: bool,
): ) -> None:
"""Initialize.""" """Initialize."""
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
@ -121,9 +145,9 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) 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.""" """Update data via library."""
data = {} data: dict[str, str | float | int] = {}
if self.use_nearest: if self.use_nearest:
measurements = self.airly.create_measurements_session_nearest( measurements = self.airly.create_measurements_session_nearest(
self.latitude, self.longitude, max_distance_km=5 self.latitude, self.longitude, max_distance_km=5

View File

@ -1,13 +1,22 @@
"""Support for the Airly air_quality service.""" """Support for the Airly air_quality service."""
from __future__ import annotations
from typing import Any
from homeassistant.components.air_quality import ( from homeassistant.components.air_quality import (
ATTR_AQI, ATTR_AQI,
ATTR_PM_2_5, ATTR_PM_2_5,
ATTR_PM_10, ATTR_PM_10,
AirQualityEntity, AirQualityEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirlyDataUpdateCoordinator
from .const import ( from .const import (
ATTR_API_ADVICE, ATTR_API_ADVICE,
ATTR_API_CAQI, ATTR_API_CAQI,
@ -36,80 +45,72 @@ LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
PARALLEL_UPDATES = 1 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.""" """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) 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): class AirlyAirQuality(CoordinatorEntity, AirQualityEntity):
"""Define an Airly air quality.""" """Define an Airly air quality."""
def __init__(self, coordinator, name): coordinator: AirlyDataUpdateCoordinator
def __init__(self, coordinator: AirlyDataUpdateCoordinator, name: str) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._name = name self._name = name
self._icon = "mdi:blur" self._icon = "mdi:blur"
@property @property
def name(self): def name(self) -> str:
"""Return the name.""" """Return the name."""
return self._name return self._name
@property @property
def icon(self): def icon(self) -> str:
"""Return the icon.""" """Return the icon."""
return self._icon return self._icon
@property @property
@round_state def air_quality_index(self) -> float | None:
def air_quality_index(self):
"""Return the air quality index.""" """Return the air quality index."""
return self.coordinator.data[ATTR_API_CAQI] return round_state(self.coordinator.data[ATTR_API_CAQI])
@property @property
@round_state def particulate_matter_2_5(self) -> float | None:
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level.""" """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 @property
@round_state def particulate_matter_10(self) -> float | None:
def particulate_matter_10(self):
"""Return the particulate matter 10 level.""" """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 @property
def attribution(self): def attribution(self) -> str:
"""Return the attribution.""" """Return the attribution."""
return ATTRIBUTION return ATTRIBUTION
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""
return f"{self.coordinator.latitude}-{self.coordinator.longitude}" return f"{self.coordinator.latitude}-{self.coordinator.longitude}"
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
return { return {
"identifiers": { "identifiers": {
(DOMAIN, self.coordinator.latitude, self.coordinator.longitude) (
DOMAIN,
f"{self.coordinator.latitude}-{self.coordinator.longitude}",
)
}, },
"name": DEFAULT_NAME, "name": DEFAULT_NAME,
"manufacturer": MANUFACTURER, "manufacturer": MANUFACTURER,
@ -117,7 +118,7 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity):
} }
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
attrs = { attrs = {
LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], 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] self.coordinator.data[ATTR_API_PM10_PERCENT]
) )
return attrs return attrs
def round_state(state: float | None) -> float | None:
"""Round state."""
return round(state) if state else state

View File

@ -1,4 +1,9 @@
"""Adds config flow for Airly.""" """Adds config flow for Airly."""
from __future__ import annotations
from typing import Any
from aiohttp import ClientSession
from airly import Airly from airly import Airly
from airly.exceptions import AirlyError from airly.exceptions import AirlyError
import async_timeout import async_timeout
@ -13,6 +18,7 @@ from homeassistant.const import (
HTTP_NOT_FOUND, HTTP_NOT_FOUND,
HTTP_UNAUTHORIZED, HTTP_UNAUTHORIZED,
) )
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -23,9 +29,10 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Airly.""" """Config flow for Airly."""
VERSION = 1 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.""" """Handle a flow initialized by the user."""
errors = {} errors = {}
use_nearest = False 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.""" """Return true if location is valid."""
airly = Airly(api_key, client) airly = Airly(api_key, client)
if use_nearest: if use_nearest:

View File

@ -1,29 +1,72 @@
"""Constants for Airly integration.""" """Constants for Airly integration."""
from __future__ import annotations
ATTR_API_ADVICE = "ADVICE" from typing import Final
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"
ATTR_LABEL = "label" from homeassistant.const import (
ATTR_UNIT = "unit" 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" from .model import SensorDescription
CONF_USE_NEAREST = "use_nearest"
DEFAULT_NAME = "Airly" ATTR_API_ADVICE: Final = "ADVICE"
DOMAIN = "airly" ATTR_API_CAQI: Final = "CAQI"
LABEL_ADVICE = "advice" ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION"
MANUFACTURER = "Airly sp. z o.o." ATTR_API_CAQI_LEVEL: Final = "LEVEL"
MAX_UPDATE_INTERVAL = 90 ATTR_API_HUMIDITY: Final = "HUMIDITY"
MIN_UPDATE_INTERVAL = 5 ATTR_API_PM10: Final = "PM10"
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." 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,
},
}

View 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