Merge pull request #57179 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2021-10-06 16:36:51 +02:00 committed by GitHub
commit 32889dbfbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2553 changed files with 74294 additions and 37173 deletions

View File

@ -36,6 +36,8 @@ omit =
homeassistant/components/agent_dvr/helpers.py homeassistant/components/agent_dvr/helpers.py
homeassistant/components/airnow/__init__.py homeassistant/components/airnow/__init__.py
homeassistant/components/airnow/sensor.py homeassistant/components/airnow/sensor.py
homeassistant/components/airthings/__init__.py
homeassistant/components/airthings/sensor.py
homeassistant/components/airtouch4/__init__.py homeassistant/components/airtouch4/__init__.py
homeassistant/components/airtouch4/climate.py homeassistant/components/airtouch4/climate.py
homeassistant/components/airtouch4/const.py homeassistant/components/airtouch4/const.py
@ -49,6 +51,7 @@ omit =
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/* homeassistant/components/amazon_polly/*
homeassistant/components/amberelectric/__init__.py
homeassistant/components/ambiclimate/climate.py homeassistant/components/ambiclimate/climate.py
homeassistant/components/ambient_station/* homeassistant/components/ambient_station/*
homeassistant/components/amcrest/* homeassistant/components/amcrest/*
@ -171,6 +174,13 @@ omit =
homeassistant/components/coolmaster/const.py homeassistant/components/coolmaster/const.py
homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/cppm_tracker/device_tracker.py
homeassistant/components/cpuspeed/sensor.py homeassistant/components/cpuspeed/sensor.py
homeassistant/components/crownstone/__init__.py
homeassistant/components/crownstone/const.py
homeassistant/components/crownstone/listeners.py
homeassistant/components/crownstone/helpers.py
homeassistant/components/crownstone/devices.py
homeassistant/components/crownstone/entry_manager.py
homeassistant/components/crownstone/light.py
homeassistant/components/cups/sensor.py homeassistant/components/cups/sensor.py
homeassistant/components/currencylayer/sensor.py homeassistant/components/currencylayer/sensor.py
homeassistant/components/daikin/* homeassistant/components/daikin/*
@ -203,7 +213,6 @@ omit =
homeassistant/components/dlib_face_detect/image_processing.py homeassistant/components/dlib_face_detect/image_processing.py
homeassistant/components/dlib_face_identify/image_processing.py homeassistant/components/dlib_face_identify/image_processing.py
homeassistant/components/dlink/switch.py homeassistant/components/dlink/switch.py
homeassistant/components/dlna_dmr/media_player.py
homeassistant/components/dnsip/sensor.py homeassistant/components/dnsip/sensor.py
homeassistant/components/dominos/* homeassistant/components/dominos/*
homeassistant/components/doods/* homeassistant/components/doods/*
@ -368,7 +377,6 @@ omit =
homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/garages_amsterdam/sensor.py
homeassistant/components/gc100/* homeassistant/components/gc100/*
homeassistant/components/geniushub/* homeassistant/components/geniushub/*
homeassistant/components/generic_hygrostat/*
homeassistant/components/github/sensor.py homeassistant/components/github/sensor.py
homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py homeassistant/components/gitter/sensor.py
@ -687,7 +695,6 @@ omit =
homeassistant/components/nad/media_player.py homeassistant/components/nad/media_player.py
homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/__init__.py
homeassistant/components/nanoleaf/light.py homeassistant/components/nanoleaf/light.py
homeassistant/components/nanoleaf/util.py
homeassistant/components/neato/__init__.py homeassistant/components/neato/__init__.py
homeassistant/components/neato/api.py homeassistant/components/neato/api.py
homeassistant/components/neato/camera.py homeassistant/components/neato/camera.py
@ -699,7 +706,10 @@ omit =
homeassistant/components/nello/lock.py homeassistant/components/nello/lock.py
homeassistant/components/nest/legacy/* homeassistant/components/nest/legacy/*
homeassistant/components/netdata/sensor.py homeassistant/components/netdata/sensor.py
homeassistant/components/netgear/__init__.py
homeassistant/components/netgear/device_tracker.py homeassistant/components/netgear/device_tracker.py
homeassistant/components/netgear/router.py
homeassistant/components/netgear/sensor.py
homeassistant/components/netgear_lte/* homeassistant/components/netgear_lte/*
homeassistant/components/netio/switch.py homeassistant/components/netio/switch.py
homeassistant/components/neurio_energy/sensor.py homeassistant/components/neurio_energy/sensor.py
@ -754,6 +764,7 @@ omit =
homeassistant/components/opencv/* homeassistant/components/opencv/*
homeassistant/components/openevse/sensor.py homeassistant/components/openevse/sensor.py
homeassistant/components/openexchangerates/sensor.py homeassistant/components/openexchangerates/sensor.py
homeassistant/components/opengarage/__init__.py
homeassistant/components/opengarage/cover.py homeassistant/components/opengarage/cover.py
homeassistant/components/openhome/__init__.py homeassistant/components/openhome/__init__.py
homeassistant/components/openhome/media_player.py homeassistant/components/openhome/media_player.py
@ -864,9 +875,6 @@ omit =
homeassistant/components/rest/switch.py homeassistant/components/rest/switch.py
homeassistant/components/ring/camera.py homeassistant/components/ring/camera.py
homeassistant/components/ripple/sensor.py homeassistant/components/ripple/sensor.py
homeassistant/components/rituals_perfume_genie/binary_sensor.py
homeassistant/components/rituals_perfume_genie/number.py
homeassistant/components/rituals_perfume_genie/select.py
homeassistant/components/rocketchat/notify.py homeassistant/components/rocketchat/notify.py
homeassistant/components/roomba/__init__.py homeassistant/components/roomba/__init__.py
homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/binary_sensor.py
@ -990,7 +998,6 @@ omit =
homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/__init__.py
homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/browse_media.py
homeassistant/components/squeezebox/media_player.py homeassistant/components/squeezebox/media_player.py
homeassistant/components/ssdp/util.py
homeassistant/components/starline/* homeassistant/components/starline/*
homeassistant/components/starlingbank/sensor.py homeassistant/components/starlingbank/sensor.py
homeassistant/components/steam_online/sensor.py homeassistant/components/steam_online/sensor.py
@ -1001,12 +1008,20 @@ omit =
homeassistant/components/suez_water/* homeassistant/components/suez_water/*
homeassistant/components/supervisord/sensor.py homeassistant/components/supervisord/sensor.py
homeassistant/components/surepetcare/__init__.py homeassistant/components/surepetcare/__init__.py
homeassistant/components/surepetcare/entity.py
homeassistant/components/surepetcare/binary_sensor.py homeassistant/components/surepetcare/binary_sensor.py
homeassistant/components/surepetcare/sensor.py homeassistant/components/surepetcare/sensor.py
homeassistant/components/swiss_hydrological_data/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py
homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swiss_public_transport/sensor.py
homeassistant/components/swisscom/device_tracker.py homeassistant/components/swisscom/device_tracker.py
homeassistant/components/switchbot/switch.py homeassistant/components/switchbot/switch.py
homeassistant/components/switchbot/binary_sensor.py
homeassistant/components/switchbot/__init__.py
homeassistant/components/switchbot/const.py
homeassistant/components/switchbot/entity.py
homeassistant/components/switchbot/cover.py
homeassistant/components/switchbot/sensor.py
homeassistant/components/switchbot/coordinator.py
homeassistant/components/switchmate/switch.py homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/__init__.py
homeassistant/components/syncthing/sensor.py homeassistant/components/syncthing/sensor.py
@ -1032,6 +1047,8 @@ omit =
homeassistant/components/tank_utility/sensor.py homeassistant/components/tank_utility/sensor.py
homeassistant/components/tankerkoenig/* homeassistant/components/tankerkoenig/*
homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tapsaff/binary_sensor.py
homeassistant/components/tautulli/const.py
homeassistant/components/tautulli/coordinator.py
homeassistant/components/tautulli/sensor.py homeassistant/components/tautulli/sensor.py
homeassistant/components/ted5000/sensor.py homeassistant/components/ted5000/sensor.py
homeassistant/components/telegram/notify.py homeassistant/components/telegram/notify.py
@ -1047,14 +1064,6 @@ omit =
homeassistant/components/telnet/switch.py homeassistant/components/telnet/switch.py
homeassistant/components/temper/sensor.py homeassistant/components/temper/sensor.py
homeassistant/components/tensorflow/image_processing.py homeassistant/components/tensorflow/image_processing.py
homeassistant/components/tesla/__init__.py
homeassistant/components/tesla/binary_sensor.py
homeassistant/components/tesla/climate.py
homeassistant/components/tesla/const.py
homeassistant/components/tesla/device_tracker.py
homeassistant/components/tesla/lock.py
homeassistant/components/tesla/sensor.py
homeassistant/components/tesla/switch.py
homeassistant/components/tfiac/climate.py homeassistant/components/tfiac/climate.py
homeassistant/components/thermoworks_smoke/sensor.py homeassistant/components/thermoworks_smoke/sensor.py
homeassistant/components/thethingsnetwork/* homeassistant/components/thethingsnetwork/*
@ -1088,16 +1097,15 @@ omit =
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
homeassistant/components/tplink/common.py
homeassistant/components/tplink/switch.py
homeassistant/components/tplink_lte/* homeassistant/components/tplink_lte/*
homeassistant/components/traccar/device_tracker.py homeassistant/components/traccar/device_tracker.py
homeassistant/components/traccar/const.py homeassistant/components/traccar/const.py
homeassistant/components/trackr/device_tracker.py
homeassistant/components/tractive/__init__.py homeassistant/components/tractive/__init__.py
homeassistant/components/tractive/binary_sensor.py
homeassistant/components/tractive/device_tracker.py homeassistant/components/tractive/device_tracker.py
homeassistant/components/tractive/entity.py homeassistant/components/tractive/entity.py
homeassistant/components/tractive/sensor.py homeassistant/components/tractive/sensor.py
homeassistant/components/tractive/switch.py
homeassistant/components/tradfri/* homeassistant/components/tradfri/*
homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py
@ -1107,9 +1115,9 @@ omit =
homeassistant/components/transmission/errors.py homeassistant/components/transmission/errors.py
homeassistant/components/travisci/sensor.py homeassistant/components/travisci/sensor.py
homeassistant/components/tuya/__init__.py homeassistant/components/tuya/__init__.py
homeassistant/components/tuya/base.py
homeassistant/components/tuya/climate.py homeassistant/components/tuya/climate.py
homeassistant/components/tuya/const.py homeassistant/components/tuya/const.py
homeassistant/components/tuya/cover.py
homeassistant/components/tuya/fan.py homeassistant/components/tuya/fan.py
homeassistant/components/tuya/light.py homeassistant/components/tuya/light.py
homeassistant/components/tuya/scene.py homeassistant/components/tuya/scene.py
@ -1177,6 +1185,8 @@ omit =
homeassistant/components/waterfurnace/* homeassistant/components/waterfurnace/*
homeassistant/components/watson_iot/* homeassistant/components/watson_iot/*
homeassistant/components/watson_tts/tts.py homeassistant/components/watson_tts/tts.py
homeassistant/components/watttime/__init__.py
homeassistant/components/watttime/sensor.py
homeassistant/components/waze_travel_time/__init__.py homeassistant/components/waze_travel_time/__init__.py
homeassistant/components/waze_travel_time/helpers.py homeassistant/components/waze_travel_time/helpers.py
homeassistant/components/waze_travel_time/sensor.py homeassistant/components/waze_travel_time/sensor.py
@ -1283,5 +1293,6 @@ exclude_lines =
raise AssertionError raise AssertionError
raise NotImplementedError raise NotImplementedError
# TYPE_CHECKING block is never executed during pytest run # TYPE_CHECKING and @overload blocks are never executed during pytest run
if TYPE_CHECKING: if TYPE_CHECKING:
@overload

View File

@ -133,7 +133,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2021.07.0 uses: home-assistant/builder@2021.09.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -186,7 +186,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2021.07.0 uses: home-assistant/builder@2021.09.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \

View File

@ -10,7 +10,7 @@ on:
pull_request: ~ pull_request: ~
env: env:
CACHE_VERSION: 2 CACHE_VERSION: 3
DEFAULT_PYTHON: 3.8 DEFAULT_PYTHON: 3.8
PRE_COMMIT_CACHE: ~/.cache/pre-commit PRE_COMMIT_CACHE: ~/.cache/pre-commit
SQLALCHEMY_WARN_20: 1 SQLALCHEMY_WARN_20: 1
@ -580,7 +580,7 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
pip install -U "pip<20.3" "setuptools<58" wheel pip install -U "pip<20.3" setuptools wheel
pip install -r requirements_all.txt pip install -r requirements_all.txt
pip install -r requirements_test.txt pip install -r requirements_test.txt
pip install -e . pip install -e .
@ -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@v2.0.3 uses: codecov/codecov-action@v2.1.0

View File

@ -9,12 +9,12 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v2.1.2 - uses: dessant/lock-threads@v3
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-lock-inactive-days: "30" issue-inactive-days: "30"
issue-exclude-created-before: "2020-10-01T00:00:00Z" exclude-issue-created-before: "2020-10-01T00:00:00Z"
issue-lock-reason: "" issue-lock-reason: ""
pr-lock-inactive-days: "1" pr-inactive-days: "1"
pr-exclude-created-before: "2020-11-01T00:00:00Z" exclude-pr-created-before: "2020-11-01T00:00:00Z"
pr-lock-reason: "" pr-lock-reason: ""

View File

@ -65,7 +65,6 @@ jobs:
matrix: matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
tag: tag:
- "3.9-alpine3.13"
- "3.9-alpine3.14" - "3.9-alpine3.14"
steps: steps:
- name: Checkout the repository - name: Checkout the repository
@ -90,7 +89,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-user: wheels wheels-user: wheels
env-file: true env-file: true
apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev" apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;cargo"
pip: "Cython;numpy" pip: "Cython;numpy"
skip-binary: aiohttp skip-binary: aiohttp
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
@ -106,7 +105,6 @@ jobs:
matrix: matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
tag: tag:
- "3.9-alpine3.13"
- "3.9-alpine3.14" - "3.9-alpine3.14"
steps: steps:
- name: Checkout the repository - name: Checkout the repository
@ -160,7 +158,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-user: wheels wheels-user: wheels
env-file: true 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" 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;cargo"
pip: "Cython;numpy;scikit-build" pip: "Cython;numpy;scikit-build"
skip-binary: aiohttp skip-binary: aiohttp
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
config/* /config
config2/* config2/*
tests/testing_config/deps tests/testing_config/deps

View File

@ -1,11 +1,11 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.23.3 rev: v2.27.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.7b0 rev: 21.9b0
hooks: hooks:
- id: black - id: black
args: args:

View File

@ -27,9 +27,11 @@ homeassistant.components.calendar.*
homeassistant.components.camera.* homeassistant.components.camera.*
homeassistant.components.canary.* homeassistant.components.canary.*
homeassistant.components.cover.* homeassistant.components.cover.*
homeassistant.components.crownstone.*
homeassistant.components.device_automation.* homeassistant.components.device_automation.*
homeassistant.components.device_tracker.* homeassistant.components.device_tracker.*
homeassistant.components.devolo_home_control.* homeassistant.components.devolo_home_control.*
homeassistant.components.dlna_dmr.*
homeassistant.components.dnsip.* homeassistant.components.dnsip.*
homeassistant.components.dsmr.* homeassistant.components.dsmr.*
homeassistant.components.dunehd.* homeassistant.components.dunehd.*
@ -54,6 +56,7 @@ homeassistant.components.huawei_lte.*
homeassistant.components.hyperion.* homeassistant.components.hyperion.*
homeassistant.components.image_processing.* homeassistant.components.image_processing.*
homeassistant.components.integration.* homeassistant.components.integration.*
homeassistant.components.iqvia.*
homeassistant.components.knx.* homeassistant.components.knx.*
homeassistant.components.kraken.* homeassistant.components.kraken.*
homeassistant.components.lcn.* homeassistant.components.lcn.*
@ -62,6 +65,7 @@ homeassistant.components.local_ip.*
homeassistant.components.lock.* homeassistant.components.lock.*
homeassistant.components.mailbox.* homeassistant.components.mailbox.*
homeassistant.components.media_player.* homeassistant.components.media_player.*
homeassistant.components.modbus.*
homeassistant.components.mysensors.* homeassistant.components.mysensors.*
homeassistant.components.nam.* homeassistant.components.nam.*
homeassistant.components.neato.* homeassistant.components.neato.*
@ -85,6 +89,7 @@ homeassistant.components.recorder.statistics
homeassistant.components.remote.* homeassistant.components.remote.*
homeassistant.components.renault.* homeassistant.components.renault.*
homeassistant.components.rituals_perfume_genie.* homeassistant.components.rituals_perfume_genie.*
homeassistant.components.samsungtv.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.select.* homeassistant.components.select.*
homeassistant.components.sensor.* homeassistant.components.sensor.*
@ -95,18 +100,23 @@ homeassistant.components.sonos.media_player
homeassistant.components.ssdp.* homeassistant.components.ssdp.*
homeassistant.components.stream.* homeassistant.components.stream.*
homeassistant.components.sun.* homeassistant.components.sun.*
homeassistant.components.surepetcare.*
homeassistant.components.switch.* homeassistant.components.switch.*
homeassistant.components.switcher_kis.* homeassistant.components.switcher_kis.*
homeassistant.components.synology_dsm.* homeassistant.components.synology_dsm.*
homeassistant.components.systemmonitor.* homeassistant.components.systemmonitor.*
homeassistant.components.tag.* homeassistant.components.tag.*
homeassistant.components.tautulli.*
homeassistant.components.tcp.* homeassistant.components.tcp.*
homeassistant.components.tile.* homeassistant.components.tile.*
homeassistant.components.tplink.*
homeassistant.components.tradfri.*
homeassistant.components.tts.* homeassistant.components.tts.*
homeassistant.components.upcloud.* homeassistant.components.upcloud.*
homeassistant.components.uptime.* homeassistant.components.uptime.*
homeassistant.components.uptimerobot.* homeassistant.components.uptimerobot.*
homeassistant.components.vacuum.* homeassistant.components.vacuum.*
homeassistant.components.vallox.*
homeassistant.components.water_heater.* homeassistant.components.water_heater.*
homeassistant.components.weather.* homeassistant.components.weather.*
homeassistant.components.websocket_api.* homeassistant.components.websocket_api.*

View File

@ -29,6 +29,7 @@ homeassistant/components/aemet/* @noltari
homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/agent_dvr/* @ispysoftware
homeassistant/components/airly/* @bieniu homeassistant/components/airly/* @bieniu
homeassistant/components/airnow/* @asymworks homeassistant/components/airnow/* @asymworks
homeassistant/components/airthings/* @danielhiversen
homeassistant/components/airtouch4/* @LonePurpleWolf homeassistant/components/airtouch4/* @LonePurpleWolf
homeassistant/components/airvisual/* @bachya homeassistant/components/airvisual/* @bachya
homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alarmdecoder/* @ajschmidt8
@ -36,6 +37,7 @@ homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
homeassistant/components/almond/* @gcampax @balloob homeassistant/components/almond/* @gcampax @balloob
homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/ambee/* @frenck homeassistant/components/ambee/* @frenck
homeassistant/components/amberelectric/* @madpilot
homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambiclimate/* @danielhiversen
homeassistant/components/ambient_station/* @bachya homeassistant/components/ambient_station/* @bachya
homeassistant/components/amcrest/* @flacjacket homeassistant/components/amcrest/* @flacjacket
@ -73,7 +75,7 @@ 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 @joshs85
homeassistant/components/bosch_shc/* @tschamm homeassistant/components/bosch_shc/* @tschamm
homeassistant/components/braviatv/* @bieniu @Drafteed homeassistant/components/braviatv/* @bieniu @Drafteed
homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/broadlink/* @danielhiversen @felipediel
@ -104,6 +106,7 @@ homeassistant/components/coronavirus/* @home-assistant/core
homeassistant/components/counter/* @fabaff homeassistant/components/counter/* @fabaff
homeassistant/components/cover/* @home-assistant/core homeassistant/components/cover/* @home-assistant/core
homeassistant/components/cpuspeed/* @fabaff homeassistant/components/cpuspeed/* @fabaff
homeassistant/components/crownstone/* @Crownstone @RicArch97
homeassistant/components/cups/* @fabaff homeassistant/components/cups/* @fabaff
homeassistant/components/daikin/* @fredrike homeassistant/components/daikin/* @fredrike
homeassistant/components/darksky/* @fabaff homeassistant/components/darksky/* @fabaff
@ -120,6 +123,7 @@ homeassistant/components/dhcp/* @bdraco
homeassistant/components/dht/* @thegardenmonkey homeassistant/components/dht/* @thegardenmonkey
homeassistant/components/digital_ocean/* @fabaff homeassistant/components/digital_ocean/* @fabaff
homeassistant/components/discogs/* @thibmaek homeassistant/components/discogs/* @thibmaek
homeassistant/components/dlna_dmr/* @StevenLooman @chishm
homeassistant/components/doorbird/* @oblogic7 @bdraco homeassistant/components/doorbird/* @oblogic7 @bdraco
homeassistant/components/dsmr/* @Robbie1221 @frenck homeassistant/components/dsmr/* @Robbie1221 @frenck
homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dsmr_reader/* @depl0y
@ -132,6 +136,7 @@ homeassistant/components/ecobee/* @marthoc
homeassistant/components/econet/* @vangorra @w1ll1am23 homeassistant/components/econet/* @vangorra @w1ll1am23
homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/ecovacs/* @OverloadUT
homeassistant/components/edl21/* @mtdcr homeassistant/components/edl21/* @mtdcr
homeassistant/components/efergy/* @tkdrob
homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/egardia/* @jeroenterheerdt
homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/eight_sleep/* @mezz64
homeassistant/components/elgato/* @frenck homeassistant/components/elgato/* @frenck
@ -202,7 +207,7 @@ homeassistant/components/group/* @home-assistant/core
homeassistant/components/growatt_server/* @indykoning @muppet3000 @JasperPlant homeassistant/components/growatt_server/* @indykoning @muppet3000 @JasperPlant
homeassistant/components/guardian/* @bachya homeassistant/components/guardian/* @bachya
homeassistant/components/habitica/* @ASMfreaK @leikoilja homeassistant/components/habitica/* @ASMfreaK @leikoilja
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
homeassistant/components/hassio/* @home-assistant/supervisor homeassistant/components/hassio/* @home-assistant/supervisor
homeassistant/components/heatmiser/* @andylockran homeassistant/components/heatmiser/* @andylockran
homeassistant/components/heos/* @andrewsayre homeassistant/components/heos/* @andrewsayre
@ -248,7 +253,7 @@ homeassistant/components/integration/* @dgomes
homeassistant/components/intent/* @home-assistant/core homeassistant/components/intent/* @home-assistant/core
homeassistant/components/intesishome/* @jnimmo homeassistant/components/intesishome/* @jnimmo
homeassistant/components/ios/* @robbiet480 homeassistant/components/ios/* @robbiet480
homeassistant/components/iotawatt/* @gtdiehl homeassistant/components/iotawatt/* @gtdiehl @jyavenard
homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/iperf3/* @rohankapoorcom
homeassistant/components/ipma/* @dgomes @abmantis homeassistant/components/ipma/* @dgomes @abmantis
homeassistant/components/ipp/* @ctalkington homeassistant/components/ipp/* @ctalkington
@ -263,7 +268,7 @@ homeassistant/components/kaiterra/* @Michsior14
homeassistant/components/keba/* @dannerph homeassistant/components/keba/* @dannerph
homeassistant/components/keenetic_ndms2/* @foxel homeassistant/components/keenetic_ndms2/* @foxel
homeassistant/components/kef/* @basnijholt homeassistant/components/kef/* @basnijholt
homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/keyboard_remote/* @bendavid @lanrat
homeassistant/components/kmtronic/* @dgomes homeassistant/components/kmtronic/* @dgomes
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/kodi/* @OnFreund @cgtobi
@ -312,6 +317,7 @@ homeassistant/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan homeassistant/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/mobile_app/* @robbiet480
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
homeassistant/components/modem_callerid/* @tkdrob
homeassistant/components/modern_forms/* @wonderslug homeassistant/components/modern_forms/* @wonderslug
homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/monoprice/* @etsinko @OnFreund
homeassistant/components/moon/* @fabaff homeassistant/components/moon/* @fabaff
@ -335,6 +341,7 @@ homeassistant/components/ness_alarm/* @nickw444
homeassistant/components/nest/* @allenporter homeassistant/components/nest/* @allenporter
homeassistant/components/netatmo/* @cgtobi homeassistant/components/netatmo/* @cgtobi
homeassistant/components/netdata/* @fabaff homeassistant/components/netdata/* @fabaff
homeassistant/components/netgear/* @hacf-fr @Quentame @starkillerOG
homeassistant/components/nexia/* @bdraco homeassistant/components/nexia/* @bdraco
homeassistant/components/nextbus/* @vividboarder homeassistant/components/nextbus/* @vividboarder
homeassistant/components/nextcloud/* @meichthys homeassistant/components/nextcloud/* @meichthys
@ -500,7 +507,7 @@ homeassistant/components/supla/* @mwegrzynek
homeassistant/components/surepetcare/* @benleb @danielhiversen homeassistant/components/surepetcare/* @benleb @danielhiversen
homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_hydrological_data/* @fabaff
homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff
homeassistant/components/switchbot/* @danielhiversen homeassistant/components/switchbot/* @danielhiversen @RenierM26
homeassistant/components/switcher_kis/* @tomerfi @thecode homeassistant/components/switcher_kis/* @tomerfi @thecode
homeassistant/components/switchmate/* @danielhiversen homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthing/* @zhulik homeassistant/components/syncthing/* @zhulik
@ -518,7 +525,6 @@ homeassistant/components/tasmota/* @emontnemery
homeassistant/components/tautulli/* @ludeeus homeassistant/components/tautulli/* @ludeeus
homeassistant/components/tellduslive/* @fredrike homeassistant/components/tellduslive/* @fredrike
homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core
homeassistant/components/tesla/* @zabuldon @alandtse
homeassistant/components/tfiac/* @fredrike @mellado homeassistant/components/tfiac/* @fredrike @mellado
homeassistant/components/thethingsnetwork/* @fabaff homeassistant/components/thethingsnetwork/* @fabaff
homeassistant/components/threshold/* @fabaff homeassistant/components/threshold/* @fabaff
@ -538,7 +544,7 @@ homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force
homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/transmission/* @engrbm87 @JPHutchins
homeassistant/components/tts/* @pvizeli homeassistant/components/tts/* @pvizeli
homeassistant/components/tuya/* @ollo69 homeassistant/components/tuya/* @Tuya @zlinoliver @METISU
homeassistant/components/twentemilieu/* @frenck homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twinkly/* @dr1rrb homeassistant/components/twinkly/* @dr1rrb
homeassistant/components/ubus/* @noltari homeassistant/components/ubus/* @noltari
@ -553,6 +559,7 @@ homeassistant/components/uptimerobot/* @ludeeus
homeassistant/components/usb/* @bdraco homeassistant/components/usb/* @bdraco
homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/usgs_earthquakes_feed/* @exxamalte
homeassistant/components/utility_meter/* @dgomes homeassistant/components/utility_meter/* @dgomes
homeassistant/components/vallox/* @andre-richter
homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velbus/* @Cereal2nd @brefra
homeassistant/components/velux/* @Julius2342 homeassistant/components/velux/* @Julius2342
homeassistant/components/vera/* @pavoni homeassistant/components/vera/* @pavoni
@ -571,10 +578,12 @@ homeassistant/components/wake_on_lan/* @ntilley905
homeassistant/components/wallbox/* @hesselonline 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/watttime/* @bachya
homeassistant/components/weather/* @fabaff homeassistant/components/weather/* @fabaff
homeassistant/components/webostv/* @bendavid @thecode homeassistant/components/webostv/* @bendavid @thecode
homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wemo/* @esev homeassistant/components/wemo/* @esev
homeassistant/components/whirlpool/* @abmantis
homeassistant/components/wiffi/* @mampfes homeassistant/components/wiffi/* @mampfes
homeassistant/components/wilight/* @leofig-rj homeassistant/components/wilight/* @leofig-rj
homeassistant/components/wirelesstag/* @sergeymaysak homeassistant/components/wirelesstag/* @sergeymaysak

View File

@ -6,6 +6,8 @@ RUN \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& apt-get update \ && apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
# Additional library needed by some tests and accordingly by VScode Tests Discovery
bluez \
libudev-dev \ libudev-dev \
libavformat-dev \ libavformat-dev \
libavcodec-dev \ libavcodec-dev \

View File

@ -118,14 +118,6 @@ homeassistant.util.pressure
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
homeassistant.util.ruamel\_yaml
-------------------------------
.. automodule:: homeassistant.util.ruamel_yaml
:members:
:undoc-members:
:show-inheritance:
homeassistant.util.ssl homeassistant.util.ssl
---------------------- ----------------------

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import faulthandler
import os import os
import platform import platform
import subprocess import subprocess
@ -10,6 +11,8 @@ import threading
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
FAULT_LOG_FILENAME = "home-assistant.log.fault"
def validate_python() -> None: def validate_python() -> None:
"""Validate that the right Python version is running.""" """Validate that the right Python version is running."""
@ -132,16 +135,14 @@ def get_arguments() -> argparse.Namespace:
def daemonize() -> None: def daemonize() -> None:
"""Move current process to daemon process.""" """Move current process to daemon process."""
# Create first fork # Create first fork
pid = os.fork() if os.fork() > 0:
if pid > 0:
sys.exit(0) sys.exit(0)
# Decouple fork # Decouple fork
os.setsid() os.setsid()
# Create second fork # Create second fork
pid = os.fork() if os.fork() > 0:
if pid > 0:
sys.exit(0) sys.exit(0)
# redirect standard file descriptors to devnull # redirect standard file descriptors to devnull
@ -311,7 +312,15 @@ def main() -> int:
open_ui=args.open_ui, open_ui=args.open_ui,
) )
exit_code = runner.run(runtime_conf) fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
with open(fault_file_name, mode="a", encoding="utf8") as fault_file:
faulthandler.enable(fault_file)
exit_code = runner.run(runtime_conf)
faulthandler.disable()
if os.path.getsize(fault_file_name) == 0:
os.remove(fault_file_name)
if exit_code == RESTART_EXIT_CODE and not args.runner: if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart() try_to_restart()

View File

@ -341,8 +341,7 @@ class AuthManager:
"System generated users cannot enable multi-factor auth module." "System generated users cannot enable multi-factor auth module."
) )
module = self.get_auth_mfa_module(mfa_module_id) if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
if module is None:
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}") raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
await module.async_setup_user(user.id, data) await module.async_setup_user(user.id, data)
@ -356,8 +355,7 @@ class AuthManager:
"System generated users cannot disable multi-factor auth module." "System generated users cannot disable multi-factor auth module."
) )
module = self.get_auth_mfa_module(mfa_module_id) if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
if module is None:
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}") raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
await module.async_depose_user(user.id) await module.async_depose_user(user.id)
@ -466,7 +464,7 @@ class AuthManager:
}, },
refresh_token.jwt_key, refresh_token.jwt_key,
algorithm="HS256", algorithm="HS256",
).decode() )
@callback @callback
def _async_resolve_provider( def _async_resolve_provider(
@ -498,8 +496,7 @@ class AuthManager:
Will raise InvalidAuthError on errors. Will raise InvalidAuthError on errors.
""" """
provider = self._async_resolve_provider(refresh_token) if provider := self._async_resolve_provider(refresh_token):
if provider:
provider.async_validate_refresh_token(refresh_token, remote_ip) provider.async_validate_refresh_token(refresh_token, remote_ip)
async def async_validate_access_token( async def async_validate_access_token(
@ -507,7 +504,9 @@ class AuthManager:
) -> models.RefreshToken | None: ) -> models.RefreshToken | None:
"""Return refresh token if an access token is valid.""" """Return refresh token if an access token is valid."""
try: try:
unverif_claims = jwt.decode(token, verify=False) unverif_claims = jwt.decode(
token, algorithms=["HS256"], options={"verify_signature": False}
)
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return None return None

View File

@ -96,8 +96,7 @@ class AuthStore:
groups = [] groups = []
for group_id in group_ids or []: for group_id in group_ids or []:
group = self._groups.get(group_id) if (group := self._groups.get(group_id)) is None:
if group is None:
raise ValueError(f"Invalid group specified {group_id}") raise ValueError(f"Invalid group specified {group_id}")
groups.append(group) groups.append(group)
@ -160,8 +159,7 @@ class AuthStore:
if group_ids is not None: if group_ids is not None:
groups = [] groups = []
for grid in group_ids: for grid in group_ids:
group = self._groups.get(grid) if (group := self._groups.get(grid)) is None:
if group is None:
raise ValueError("Invalid group specified.") raise ValueError("Invalid group specified.")
groups.append(group) groups.append(group)
@ -446,16 +444,14 @@ class AuthStore:
) )
continue continue
token_type = rt_dict.get("token_type") if (token_type := rt_dict.get("token_type")) is None:
if token_type is None:
if rt_dict["client_id"] is None: if rt_dict["client_id"] is None:
token_type = models.TOKEN_TYPE_SYSTEM token_type = models.TOKEN_TYPE_SYSTEM
else: else:
token_type = models.TOKEN_TYPE_NORMAL token_type = models.TOKEN_TYPE_NORMAL
# old refresh_token don't have last_used_at (pre-0.78) # old refresh_token don't have last_used_at (pre-0.78)
last_used_at_str = rt_dict.get("last_used_at") if last_used_at_str := rt_dict.get("last_used_at"):
if last_used_at_str:
last_used_at = dt_util.parse_datetime(last_used_at_str) last_used_at = dt_util.parse_datetime(last_used_at_str)
else: else:
last_used_at = None last_used_at = None

View File

@ -38,12 +38,12 @@ class InsecureExampleModule(MultiFactorAuthModule):
@property @property
def input_schema(self) -> vol.Schema: def input_schema(self) -> vol.Schema:
"""Validate login flow input data.""" """Validate login flow input data."""
return vol.Schema({"pin": str}) return vol.Schema({vol.Required("pin"): str})
@property @property
def setup_schema(self) -> vol.Schema: def setup_schema(self) -> vol.Schema:
"""Validate async_setup_user input data.""" """Validate async_setup_user input data."""
return vol.Schema({"pin": str}) return vol.Schema({vol.Required("pin"): str})
async def async_setup_flow(self, user_id: str) -> SetupFlow: async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module. """Return a data entry flow handler for setup module.

View File

@ -110,7 +110,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
@property @property
def input_schema(self) -> vol.Schema: def input_schema(self) -> vol.Schema:
"""Validate login flow input data.""" """Validate login flow input data."""
return vol.Schema({INPUT_FIELD_CODE: str}) return vol.Schema({vol.Required(INPUT_FIELD_CODE): str})
async def _async_load(self) -> None: async def _async_load(self) -> None:
"""Load stored data.""" """Load stored data."""
@ -118,9 +118,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
if self._user_settings is not None: if self._user_settings is not None:
return return
data = await self._user_store.async_load() if (data := await self._user_store.async_load()) is None:
if data is None:
data = {STORAGE_USERS: {}} data = {STORAGE_USERS: {}}
self._user_settings = { self._user_settings = {
@ -207,8 +205,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
await self._async_load() await self._async_load()
assert self._user_settings is not None assert self._user_settings is not None
notify_setting = self._user_settings.get(user_id) if (notify_setting := self._user_settings.get(user_id)) is None:
if notify_setting is None:
return False return False
# user_input has been validate in caller # user_input has been validate in caller
@ -225,8 +222,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
await self._async_load() await self._async_load()
assert self._user_settings is not None assert self._user_settings is not None
notify_setting = self._user_settings.get(user_id) if (notify_setting := self._user_settings.get(user_id)) is None:
if notify_setting is None:
raise ValueError("Cannot find user_id") raise ValueError("Cannot find user_id")
def generate_secret_and_one_time_password() -> str: def generate_secret_and_one_time_password() -> str:

View File

@ -84,7 +84,7 @@ class TotpAuthModule(MultiFactorAuthModule):
@property @property
def input_schema(self) -> vol.Schema: def input_schema(self) -> vol.Schema:
"""Validate login flow input data.""" """Validate login flow input data."""
return vol.Schema({INPUT_FIELD_CODE: str}) return vol.Schema({vol.Required(INPUT_FIELD_CODE): str})
async def _async_load(self) -> None: async def _async_load(self) -> None:
"""Load stored data.""" """Load stored data."""
@ -92,9 +92,7 @@ class TotpAuthModule(MultiFactorAuthModule):
if self._users is not None: if self._users is not None:
return return
data = await self._user_store.async_load() if (data := await self._user_store.async_load()) is None:
if data is None:
data = {STORAGE_USERS: {}} data = {STORAGE_USERS: {}}
self._users = data.get(STORAGE_USERS, {}) self._users = data.get(STORAGE_USERS, {})
@ -163,8 +161,7 @@ class TotpAuthModule(MultiFactorAuthModule):
"""Validate two factor authentication code.""" """Validate two factor authentication code."""
import pyotp # pylint: disable=import-outside-toplevel import pyotp # pylint: disable=import-outside-toplevel
ota_secret = self._users.get(user_id) # type: ignore if (ota_secret := self._users.get(user_id)) is None: # type: ignore
if ota_secret is None:
# even we cannot find user, we still do verify # even we cannot find user, we still do verify
# to make timing the same as if user was found. # to make timing the same as if user was found.
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1) pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)

View File

@ -1,8 +1,9 @@
"""Permissions for Home Assistant.""" """Permissions for Home Assistant."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
import logging import logging
from typing import Any, Callable from typing import Any
import voluptuous as vol import voluptuous as vol
@ -33,9 +34,7 @@ class AbstractPermissions:
def check_entity(self, entity_id: str, key: str) -> bool: def check_entity(self, entity_id: str, key: str) -> bool:
"""Check if we can access entity.""" """Check if we can access entity."""
entity_func = self._cached_entity_func if (entity_func := self._cached_entity_func) is None:
if entity_func is None:
entity_func = self._cached_entity_func = self._entity_func() entity_func = self._cached_entity_func = self._entity_func()
return entity_func(entity_id, key) return entity_func(entity_id, key)

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections import OrderedDict from collections import OrderedDict
from typing import Callable from collections.abc import Callable
import voluptuous as vol import voluptuous as vol

View File

@ -72,8 +72,7 @@ def compile_policy(
def apply_policy_funcs(object_id: str, key: str) -> bool: def apply_policy_funcs(object_id: str, key: str) -> bool:
"""Apply several policy functions.""" """Apply several policy functions."""
for func in funcs: for func in funcs:
result = func(object_id, key) if (result := func(object_id, key)) is not None:
if result is not None:
return result return result
return False return False

View File

@ -169,9 +169,7 @@ async def load_auth_provider_module(
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
return module return module
processed = hass.data.get(DATA_REQS) if (processed := hass.data.get(DATA_REQS)) is None:
if processed is None:
processed = hass.data[DATA_REQS] = set() processed = hass.data[DATA_REQS] = set()
elif provider in processed: elif provider in processed:
return module return module

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import collections
from collections.abc import Mapping from collections.abc import Mapping
import logging import logging
import os import os
@ -148,10 +147,13 @@ class CommandLineLoginFlow(LoginFlow):
user_input.pop("password") user_input.pop("password")
return await self.async_finish(user_input) return await self.async_finish(user_input)
schema: dict[str, type] = collections.OrderedDict()
schema["username"] = str
schema["password"] = str
return self.async_show_form( return self.async_show_form(
step_id="init", data_schema=vol.Schema(schema), errors=errors step_id="init",
data_schema=vol.Schema(
{
vol.Required("username"): str,
vol.Required("password"): str,
}
),
errors=errors,
) )

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio import asyncio
import base64 import base64
from collections import OrderedDict
from collections.abc import Mapping from collections.abc import Mapping
import logging import logging
from typing import Any, cast from typing import Any, cast
@ -82,9 +81,7 @@ class Data:
async def async_load(self) -> None: async def async_load(self) -> None:
"""Load stored data.""" """Load stored data."""
data = await self._store.async_load() if (data := await self._store.async_load()) is None:
if data is None:
data = {"users": []} data = {"users": []}
seen: set[str] = set() seen: set[str] = set()
@ -93,9 +90,7 @@ class Data:
username = user["username"] username = user["username"]
# check if we have duplicates # check if we have duplicates
folded = username.casefold() if (folded := username.casefold()) in seen:
if folded in seen:
self.is_legacy = True self.is_legacy = True
logging.getLogger(__name__).warning( logging.getLogger(__name__).warning(
@ -339,10 +334,13 @@ class HassLoginFlow(LoginFlow):
user_input.pop("password") user_input.pop("password")
return await self.async_finish(user_input) return await self.async_finish(user_input)
schema: dict[str, type] = OrderedDict()
schema["username"] = str
schema["password"] = str
return self.async_show_form( return self.async_show_form(
step_id="init", data_schema=vol.Schema(schema), errors=errors step_id="init",
data_schema=vol.Schema(
{
vol.Required("username"): str,
vol.Required("password"): str,
}
),
errors=errors,
) )

View File

@ -1,7 +1,6 @@
"""Example auth provider.""" """Example auth provider."""
from __future__ import annotations from __future__ import annotations
from collections import OrderedDict
from collections.abc import Mapping from collections.abc import Mapping
import hmac import hmac
from typing import Any, cast from typing import Any, cast
@ -117,10 +116,13 @@ class ExampleLoginFlow(LoginFlow):
user_input.pop("password") user_input.pop("password")
return await self.async_finish(user_input) return await self.async_finish(user_input)
schema: dict[str, type] = OrderedDict()
schema["username"] = str
schema["password"] = str
return self.async_show_form( return self.async_show_form(
step_id="init", data_schema=vol.Schema(schema), errors=errors step_id="init",
data_schema=vol.Schema(
{
vol.Required("username"): str,
vol.Required("password"): str,
}
),
errors=errors,
) )

View File

@ -102,5 +102,7 @@ class LegacyLoginFlow(LoginFlow):
return await self.async_finish({}) return await self.async_finish({})
return self.async_show_form( return self.async_show_form(
step_id="init", data_schema=vol.Schema({"password": str}), errors=errors step_id="init",
data_schema=vol.Schema({vol.Required("password"): str}),
errors=errors,
) )

View File

@ -244,5 +244,7 @@ class TrustedNetworksLoginFlow(LoginFlow):
return self.async_show_form( return self.async_show_form(
step_id="init", step_id="init",
data_schema=vol.Schema({"user": vol.In(self._available_users)}), data_schema=vol.Schema(
{vol.Required("user"): vol.In(self._available_users)}
),
) )

View File

@ -109,9 +109,8 @@ async def async_setup_hass(
config_dict = None config_dict = None
basic_setup_success = False basic_setup_success = False
safe_mode = runtime_config.safe_mode
if not safe_mode: if not (safe_mode := runtime_config.safe_mode):
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
try: try:
@ -368,8 +367,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
This function is a coroutine. This function is a coroutine.
""" """
deps_dir = os.path.join(config_dir, "deps") deps_dir = os.path.join(config_dir, "deps")
lib_dir = await async_get_user_site(deps_dir) if (lib_dir := await async_get_user_site(deps_dir)) not in sys.path:
if lib_dir not in sys.path:
sys.path.insert(0, lib_dir) sys.path.insert(0, lib_dir)
return deps_dir return deps_dir
@ -494,17 +492,13 @@ async def _async_set_up_integrations(
_LOGGER.info("Domains to be set up: %s", domains_to_setup) _LOGGER.info("Domains to be set up: %s", domains_to_setup)
logging_domains = domains_to_setup & LOGGING_INTEGRATIONS
# Load logging as soon as possible # Load logging as soon as possible
if logging_domains: if logging_domains := domains_to_setup & LOGGING_INTEGRATIONS:
_LOGGER.info("Setting up logging: %s", logging_domains) _LOGGER.info("Setting up logging: %s", logging_domains)
await async_setup_multi_components(hass, logging_domains, config) await async_setup_multi_components(hass, logging_domains, config)
# Start up debuggers. Start these first in case they want to wait. # Start up debuggers. Start these first in case they want to wait.
debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS if debuggers := domains_to_setup & DEBUGGER_INTEGRATIONS:
if debuggers:
_LOGGER.debug("Setting up debuggers: %s", debuggers) _LOGGER.debug("Setting up debuggers: %s", debuggers)
await async_setup_multi_components(hass, debuggers, config) await async_setup_multi_components(hass, debuggers, config)
@ -524,9 +518,7 @@ async def _async_set_up_integrations(
stage_1_domains.add(domain) stage_1_domains.add(domain)
dep_itg = integration_cache.get(domain) if (dep_itg := integration_cache.get(domain)) is None:
if dep_itg is None:
continue continue
deps_promotion.update(dep_itg.all_dependencies) deps_promotion.update(dep_itg.all_dependencies)
@ -564,6 +556,14 @@ async def _async_set_up_integrations(
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 2 - moving forward") _LOGGER.warning("Setup timed out for stage 2 - moving forward")
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")
try:
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
await hass.async_block_till_done()
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
watch_task.cancel() watch_task.cancel()
async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, {}) async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, {})
@ -576,11 +576,3 @@ async def _async_set_up_integrations(
) )
}, },
) )
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")
try:
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
await hass.async_block_till_done()
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for bootstrap - moving forward")

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi",
"single_instance_allowed": "D\u00e9ja configur\u00e9. Une seule configuration possible." "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
}, },
"error": { "error": {
"cannot_connect": "\u00c9chec de connexion", "cannot_connect": "\u00c9chec de connexion",

View File

@ -14,7 +14,7 @@
"api_key": "Cl\u00e9 d'API", "api_key": "Cl\u00e9 d'API",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude", "longitude": "Longitude",
"name": "Nom de l'int\u00e9gration" "name": "Nom"
}, },
"description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration.", "description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration.",
"title": "AccuWeather" "title": "AccuWeather"

View File

@ -6,7 +6,7 @@
"error": { "error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s", "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs",
"requests_exceeded": "T\u00fall\u00e9pt\u00e9k az Accuweather API-hoz beny\u00fajtott k\u00e9relmek megengedett sz\u00e1m\u00e1t. Meg kell v\u00e1rnia vagy m\u00f3dos\u00edtania kell az API-kulcsot." "requests_exceeded": "Accuweather API-hoz enged\u00e9lyezett lek\u00e9r\u00e9sek sz\u00e1ma t\u00fal lett l\u00e9pve. Meg kell v\u00e1rnia m\u00edg a tilt\u00e1s lej\u00e1r vagy m\u00f3dos\u00edtania kell az API-kulcsot."
}, },
"step": { "step": {
"user": { "user": {

View File

@ -1,9 +1,18 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado"
},
"error": {
"cannot_connect": "No se pudo conectar",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
},
"step": { "step": {
"user": { "user": {
"data": { "data": {
"account_id": "ID de la cuenta" "account_id": "ID de la cuenta",
"host": "Host",
"password": "Contrase\u00f1a"
} }
} }
} }

View File

@ -11,7 +11,7 @@
"user": { "user": {
"data": { "data": {
"account_id": "Fi\u00f3k ID", "account_id": "Fi\u00f3k ID",
"host": "Gazdag\u00e9p", "host": "C\u00edm",
"password": "Jelsz\u00f3" "password": "Jelsz\u00f3"
} }
} }

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Perangkat sudah dikonfigurasi"
},
"error": {
"cannot_connect": "Gagal terhubung",
"invalid_auth": "Autentikasi tidak valid"
},
"step": {
"user": {
"data": {
"account_id": "ID Akun",
"host": "Host",
"password": "Kata Sandi"
}
}
}
}
}

View File

@ -17,9 +17,9 @@
"host": "H\u00f4te", "host": "H\u00f4te",
"password": "Mot de passe", "password": "Mot de passe",
"port": "Port", "port": "Port",
"ssl": "AdGuard Home utilise un certificat SSL", "ssl": "Utilise un certificat SSL",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"verify_ssl": "AdGuard Home utilise un certificat appropri\u00e9" "verify_ssl": "V\u00e9rifier le certificat SSL"
}, },
"description": "Configurez votre instance AdGuard Home pour permettre la surveillance et le contr\u00f4le." "description": "Configurez votre instance AdGuard Home pour permettre la surveillance et le contr\u00f4le."
} }

View File

@ -7,6 +7,9 @@
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
}, },
"step": { "step": {
"hassio_confirm": {
"title": "AdGuard Home \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05e8\u05d7\u05d1\u05ea Assistant Assistant"
},
"user": { "user": {
"data": { "data": {
"host": "\u05de\u05d0\u05e8\u05d7", "host": "\u05de\u05d0\u05e8\u05d7",

View File

@ -9,12 +9,12 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant-ot, hogy csatlakozzon az AdGuard Home-hoz, amelyet a kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot AdGuard Home-hoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?",
"title": "Az AdGuard Home a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" "title": "Az AdGuard Home a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel"
}, },
"user": { "user": {
"data": { "data": {
"host": "Hoszt", "host": "C\u00edm",
"password": "Jelsz\u00f3", "password": "Jelsz\u00f3",
"port": "Port", "port": "Port",
"ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",

View File

@ -9,7 +9,7 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke AdGuard Home yang disediakan oleh add-on Supervisor {addon}?", "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke AdGuard Home yang disediakan oleh add-on: {addon}?",
"title": "AdGuard Home melalui add-on Home Assistant" "title": "AdGuard Home melalui add-on Home Assistant"
}, },
"user": { "user": {

View File

@ -1,5 +1,7 @@
"""Constant values for the AEMET OpenData component.""" """Constant values for the AEMET OpenData component."""
from __future__ import annotations
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY, ATTR_CONDITION_CLOUDY,
@ -40,9 +42,6 @@ DEFAULT_NAME = "AEMET"
DOMAIN = "aemet" DOMAIN = "aemet"
ENTRY_NAME = "name" ENTRY_NAME = "name"
ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
SENSOR_NAME = "sensor_name"
SENSOR_UNIT = "sensor_unit"
SENSOR_DEVICE_CLASS = "sensor_device_class"
ATTR_API_CONDITION = "condition" ATTR_API_CONDITION = "condition"
ATTR_API_FORECAST_DAILY = "forecast-daily" ATTR_API_FORECAST_DAILY = "forecast-daily"
@ -200,118 +199,145 @@ FORECAST_MODE_ATTR_API = {
FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY, FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
} }
FORECAST_SENSOR_TYPES = { FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
ATTR_FORECAST_CONDITION: { SensorEntityDescription(
SENSOR_NAME: "Condition", key=ATTR_FORECAST_CONDITION,
}, name="Condition",
ATTR_FORECAST_PRECIPITATION: { ),
SENSOR_NAME: "Precipitation", SensorEntityDescription(
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, key=ATTR_FORECAST_PRECIPITATION,
}, name="Precipitation",
ATTR_FORECAST_PRECIPITATION_PROBABILITY: { native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
SENSOR_NAME: "Precipitation probability", ),
SENSOR_UNIT: PERCENTAGE, SensorEntityDescription(
}, key=ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP: { name="Precipitation probability",
SENSOR_NAME: "Temperature", native_unit_of_measurement=PERCENTAGE,
SENSOR_UNIT: TEMP_CELSIUS, ),
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, SensorEntityDescription(
}, key=ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW: { name="Temperature",
SENSOR_NAME: "Temperature Low", native_unit_of_measurement=TEMP_CELSIUS,
SENSOR_UNIT: TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE,
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ),
}, SensorEntityDescription(
ATTR_FORECAST_TIME: { key=ATTR_FORECAST_TEMP_LOW,
SENSOR_NAME: "Time", name="Temperature Low",
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, native_unit_of_measurement=TEMP_CELSIUS,
}, device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_FORECAST_WIND_BEARING: { ),
SENSOR_NAME: "Wind bearing", SensorEntityDescription(
SENSOR_UNIT: DEGREE, key=ATTR_FORECAST_TIME,
}, name="Time",
ATTR_FORECAST_WIND_SPEED: { device_class=DEVICE_CLASS_TIMESTAMP,
SENSOR_NAME: "Wind speed", ),
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, SensorEntityDescription(
}, key=ATTR_FORECAST_WIND_BEARING,
} name="Wind bearing",
WEATHER_SENSOR_TYPES = { native_unit_of_measurement=DEGREE,
ATTR_API_CONDITION: { ),
SENSOR_NAME: "Condition", SensorEntityDescription(
}, key=ATTR_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY: { name="Wind speed",
SENSOR_NAME: "Humidity", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
SENSOR_UNIT: PERCENTAGE, ),
SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, )
}, WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
ATTR_API_PRESSURE: { SensorEntityDescription(
SENSOR_NAME: "Pressure", key=ATTR_API_CONDITION,
SENSOR_UNIT: PRESSURE_HPA, name="Condition",
SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, ),
}, SensorEntityDescription(
ATTR_API_RAIN: { key=ATTR_API_HUMIDITY,
SENSOR_NAME: "Rain", name="Humidity",
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, native_unit_of_measurement=PERCENTAGE,
}, device_class=DEVICE_CLASS_HUMIDITY,
ATTR_API_RAIN_PROB: { ),
SENSOR_NAME: "Rain probability", SensorEntityDescription(
SENSOR_UNIT: PERCENTAGE, key=ATTR_API_PRESSURE,
}, name="Pressure",
ATTR_API_SNOW: { native_unit_of_measurement=PRESSURE_HPA,
SENSOR_NAME: "Snow", device_class=DEVICE_CLASS_PRESSURE,
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, ),
}, SensorEntityDescription(
ATTR_API_SNOW_PROB: { key=ATTR_API_RAIN,
SENSOR_NAME: "Snow probability", name="Rain",
SENSOR_UNIT: PERCENTAGE, native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
}, ),
ATTR_API_STATION_ID: { SensorEntityDescription(
SENSOR_NAME: "Station ID", key=ATTR_API_RAIN_PROB,
}, name="Rain probability",
ATTR_API_STATION_NAME: { native_unit_of_measurement=PERCENTAGE,
SENSOR_NAME: "Station name", ),
}, SensorEntityDescription(
ATTR_API_STATION_TIMESTAMP: { key=ATTR_API_SNOW,
SENSOR_NAME: "Station timestamp", name="Snow",
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
}, ),
ATTR_API_STORM_PROB: { SensorEntityDescription(
SENSOR_NAME: "Storm probability", key=ATTR_API_SNOW_PROB,
SENSOR_UNIT: PERCENTAGE, name="Snow probability",
}, native_unit_of_measurement=PERCENTAGE,
ATTR_API_TEMPERATURE: { ),
SENSOR_NAME: "Temperature", SensorEntityDescription(
SENSOR_UNIT: TEMP_CELSIUS, key=ATTR_API_STATION_ID,
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, name="Station ID",
}, ),
ATTR_API_TEMPERATURE_FEELING: { SensorEntityDescription(
SENSOR_NAME: "Temperature feeling", key=ATTR_API_STATION_NAME,
SENSOR_UNIT: TEMP_CELSIUS, name="Station name",
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ),
}, SensorEntityDescription(
ATTR_API_TOWN_ID: { key=ATTR_API_STATION_TIMESTAMP,
SENSOR_NAME: "Town ID", name="Station timestamp",
}, device_class=DEVICE_CLASS_TIMESTAMP,
ATTR_API_TOWN_NAME: { ),
SENSOR_NAME: "Town name", SensorEntityDescription(
}, key=ATTR_API_STORM_PROB,
ATTR_API_TOWN_TIMESTAMP: { name="Storm probability",
SENSOR_NAME: "Town timestamp", native_unit_of_measurement=PERCENTAGE,
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, ),
}, SensorEntityDescription(
ATTR_API_WIND_BEARING: { key=ATTR_API_TEMPERATURE,
SENSOR_NAME: "Wind bearing", name="Temperature",
SENSOR_UNIT: DEGREE, native_unit_of_measurement=TEMP_CELSIUS,
}, device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_API_WIND_MAX_SPEED: { ),
SENSOR_NAME: "Wind max speed", SensorEntityDescription(
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, key=ATTR_API_TEMPERATURE_FEELING,
}, name="Temperature feeling",
ATTR_API_WIND_SPEED: { native_unit_of_measurement=TEMP_CELSIUS,
SENSOR_NAME: "Wind speed", device_class=DEVICE_CLASS_TEMPERATURE,
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, ),
}, SensorEntityDescription(
} key=ATTR_API_TOWN_ID,
name="Town ID",
),
SensorEntityDescription(
key=ATTR_API_TOWN_NAME,
name="Town name",
),
SensorEntityDescription(
key=ATTR_API_TOWN_TIMESTAMP,
name="Town timestamp",
device_class=DEVICE_CLASS_TIMESTAMP,
),
SensorEntityDescription(
key=ATTR_API_WIND_BEARING,
name="Wind bearing",
native_unit_of_measurement=DEGREE,
),
SensorEntityDescription(
key=ATTR_API_WIND_MAX_SPEED,
name="Wind max speed",
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
),
SensorEntityDescription(
key=ATTR_API_WIND_SPEED,
name="Wind speed",
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
),
)
WIND_BEARING_MAP = { WIND_BEARING_MAP = {
"C": None, "C": None,

View File

@ -1,5 +1,7 @@
"""Support for the AEMET OpenData service.""" """Support for the AEMET OpenData service."""
from homeassistant.components.sensor import SensorEntity from __future__ import annotations
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -14,9 +16,6 @@ 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
@ -28,37 +27,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
name = domain_data[ENTRY_NAME] name = domain_data[ENTRY_NAME]
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
weather_sensor_types = WEATHER_SENSOR_TYPES unique_id = config_entry.unique_id
forecast_sensor_types = FORECAST_SENSOR_TYPES entities: list[AbstractAemetSensor] = [
AemetSensor(name, unique_id, weather_coordinator, description)
entities = [] for description in WEATHER_SENSOR_TYPES
for sensor_type in MONITORED_CONDITIONS: if description.key in MONITORED_CONDITIONS
unique_id = f"{config_entry.unique_id}-{sensor_type}" ]
entities.append( entities.extend(
AemetSensor( [
name, AemetForecastSensor(
unique_id, name_prefix,
sensor_type, unique_id_prefix,
weather_sensor_types[sensor_type],
weather_coordinator, weather_coordinator,
mode,
description,
) )
) for mode in FORECAST_MODES
if (
for mode in FORECAST_MODES: (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast")
name = f"{domain_data[ENTRY_NAME]} {mode}" and (unique_id_prefix := f"{unique_id}-forecast-{mode}")
for sensor_type in FORECAST_MONITORED_CONDITIONS:
unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}"
entities.append(
AemetForecastSensor(
f"{name} Forecast",
unique_id,
sensor_type,
forecast_sensor_types[sensor_type],
weather_coordinator,
mode,
)
) )
for description in FORECAST_SENSOR_TYPES
if description.key in FORECAST_MONITORED_CONDITIONS
]
)
async_add_entities(entities) async_add_entities(entities)
@ -72,20 +64,14 @@ class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
self, self,
name, name,
unique_id, unique_id,
sensor_type,
sensor_configuration,
coordinator: WeatherUpdateCoordinator, coordinator: WeatherUpdateCoordinator,
description: SensorEntityDescription,
): ):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self._name = name self.entity_description = description
self._unique_id = unique_id self._attr_name = f"{name} {description.name}"
self._sensor_type = sensor_type self._attr_unique_id = unique_id
self._sensor_name = sensor_configuration[SENSOR_NAME]
self._attr_name = f"{self._name} {self._sensor_name}"
self._attr_unique_id = self._unique_id
self._attr_device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
self._attr_native_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
class AemetSensor(AbstractAemetSensor): class AemetSensor(AbstractAemetSensor):
@ -95,20 +81,21 @@ class AemetSensor(AbstractAemetSensor):
self, self,
name, name,
unique_id, unique_id,
sensor_type,
sensor_configuration,
weather_coordinator: WeatherUpdateCoordinator, weather_coordinator: WeatherUpdateCoordinator,
description: SensorEntityDescription,
): ):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__( super().__init__(
name, unique_id, sensor_type, sensor_configuration, weather_coordinator name=name,
unique_id=f"{unique_id}-{description.key}",
coordinator=weather_coordinator,
description=description,
) )
self._weather_coordinator = weather_coordinator
@property @property
def native_value(self): def native_value(self):
"""Return the state of the device.""" """Return the state of the device."""
return self._weather_coordinator.data.get(self._sensor_type) return self.coordinator.data.get(self.entity_description.key)
class AemetForecastSensor(AbstractAemetSensor): class AemetForecastSensor(AbstractAemetSensor):
@ -118,16 +105,17 @@ class AemetForecastSensor(AbstractAemetSensor):
self, self,
name, name,
unique_id, unique_id,
sensor_type,
sensor_configuration,
weather_coordinator: WeatherUpdateCoordinator, weather_coordinator: WeatherUpdateCoordinator,
forecast_mode, forecast_mode,
description: SensorEntityDescription,
): ):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__( super().__init__(
name, unique_id, sensor_type, sensor_configuration, weather_coordinator name=name,
unique_id=f"{unique_id}-{description.key}",
coordinator=weather_coordinator,
description=description,
) )
self._weather_coordinator = weather_coordinator
self._forecast_mode = forecast_mode self._forecast_mode = forecast_mode
self._attr_entity_registry_enabled_default = ( self._attr_entity_registry_enabled_default = (
self._forecast_mode == FORECAST_MODE_DAILY self._forecast_mode == FORECAST_MODE_DAILY
@ -137,9 +125,9 @@ class AemetForecastSensor(AbstractAemetSensor):
def native_value(self): def native_value(self):
"""Return the state of the device.""" """Return the state of the device."""
forecast = None forecast = None
forecasts = self._weather_coordinator.data.get( forecasts = self.coordinator.data.get(
FORECAST_MODE_ATTR_API[self._forecast_mode] FORECAST_MODE_ATTR_API[self._forecast_mode]
) )
if forecasts: if forecasts:
forecast = forecasts[0].get(self._sensor_type) forecast = forecasts[0].get(self.entity_description.key)
return forecast return forecast

View File

@ -4,13 +4,13 @@
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
}, },
"error": { "error": {
"already_in_progress": "La configuration de l'appareil est d\u00e9j\u00e0 en cours.", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
"cannot_connect": "\u00c9chec de connexion" "cannot_connect": "\u00c9chec de connexion"
}, },
"step": { "step": {
"user": { "user": {
"data": { "data": {
"host": "Nom d'h\u00f4te ou adresse IP", "host": "H\u00f4te",
"port": "Port" "port": "Port"
}, },
"title": "Configurer l'agent DVR" "title": "Configurer l'agent DVR"

View File

@ -4,13 +4,13 @@
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
}, },
"error": { "error": {
"already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.",
"cannot_connect": "Sikertelen csatlakoz\u00e1s" "cannot_connect": "Sikertelen csatlakoz\u00e1s"
}, },
"step": { "step": {
"user": { "user": {
"data": { "data": {
"host": "Hoszt", "host": "C\u00edm",
"port": "Port" "port": "Port"
}, },
"title": "\u00c1ll\u00edtsa be az Agent DVR-t" "title": "\u00c1ll\u00edtsa be az Agent DVR-t"

View File

@ -3,23 +3,6 @@ from __future__ import annotations
from typing import Final from typing import Final
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
PRESSURE_HPA,
TEMP_CELSIUS,
)
from .model import AirlySensorEntityDescription
ATTR_API_ADVICE: Final = "ADVICE" ATTR_API_ADVICE: Final = "ADVICE"
ATTR_API_CAQI: Final = "CAQI" ATTR_API_CAQI: Final = "CAQI"
ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION" ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION"
@ -49,56 +32,3 @@ MANUFACTURER: Final = "Airly sp. z o.o."
MAX_UPDATE_INTERVAL: Final = 90 MAX_UPDATE_INTERVAL: Final = 90
MIN_UPDATE_INTERVAL: Final = 5 MIN_UPDATE_INTERVAL: Final = 5
NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet."
SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_CAQI,
device_class=DEVICE_CLASS_AQI,
name=ATTR_API_CAQI,
native_unit_of_measurement="CAQI",
),
AirlySensorEntityDescription(
key=ATTR_API_PM1,
device_class=DEVICE_CLASS_PM1,
name=ATTR_API_PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_PM25,
device_class=DEVICE_CLASS_PM25,
name="PM2.5",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_PM10,
device_class=DEVICE_CLASS_PM10,
name=ATTR_API_PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_HUMIDITY,
device_class=DEVICE_CLASS_HUMIDITY,
name=ATTR_API_HUMIDITY.capitalize(),
native_unit_of_measurement=PERCENTAGE,
state_class=STATE_CLASS_MEASUREMENT,
value=lambda value: round(value, 1),
),
AirlySensorEntityDescription(
key=ATTR_API_PRESSURE,
device_class=DEVICE_CLASS_PRESSURE,
name=ATTR_API_PRESSURE.capitalize(),
native_unit_of_measurement=PRESSURE_HPA,
state_class=STATE_CLASS_MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_TEMPERATURE,
device_class=DEVICE_CLASS_TEMPERATURE,
name=ATTR_API_TEMPERATURE.capitalize(),
native_unit_of_measurement=TEMP_CELSIUS,
state_class=STATE_CLASS_MEASUREMENT,
value=lambda value: round(value, 1),
),
)

View File

@ -1,14 +0,0 @@
"""Type definitions for Airly integration."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
from homeassistant.components.sensor import SensorEntityDescription
@dataclass
class AirlySensorEntityDescription(SensorEntityDescription):
"""Class describing Airly sensor entities."""
value: Callable = round

View File

@ -1,11 +1,31 @@
"""Support for the Airly sensor service.""" """Support for the Airly sensor service."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, cast from typing import Any, cast
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.const import (
ATTR_ATTRIBUTION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONF_NAME,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
PRESSURE_HPA,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
@ -18,8 +38,12 @@ from .const import (
ATTR_API_CAQI, ATTR_API_CAQI,
ATTR_API_CAQI_DESCRIPTION, ATTR_API_CAQI_DESCRIPTION,
ATTR_API_CAQI_LEVEL, ATTR_API_CAQI_LEVEL,
ATTR_API_HUMIDITY,
ATTR_API_PM1,
ATTR_API_PM10, ATTR_API_PM10,
ATTR_API_PM25, ATTR_API_PM25,
ATTR_API_PRESSURE,
ATTR_API_TEMPERATURE,
ATTR_DESCRIPTION, ATTR_DESCRIPTION,
ATTR_LEVEL, ATTR_LEVEL,
ATTR_LIMIT, ATTR_LIMIT,
@ -28,15 +52,74 @@ from .const import (
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
MANUFACTURER, MANUFACTURER,
SENSOR_TYPES,
SUFFIX_LIMIT, SUFFIX_LIMIT,
SUFFIX_PERCENT, SUFFIX_PERCENT,
) )
from .model import AirlySensorEntityDescription
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@dataclass
class AirlySensorEntityDescription(SensorEntityDescription):
"""Class describing Airly sensor entities."""
value: Callable = round
SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_CAQI,
device_class=DEVICE_CLASS_AQI,
name=ATTR_API_CAQI,
native_unit_of_measurement="CAQI",
),
AirlySensorEntityDescription(
key=ATTR_API_PM1,
device_class=DEVICE_CLASS_PM1,
name=ATTR_API_PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_PM25,
device_class=DEVICE_CLASS_PM25,
name="PM2.5",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_PM10,
device_class=DEVICE_CLASS_PM10,
name=ATTR_API_PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_HUMIDITY,
device_class=DEVICE_CLASS_HUMIDITY,
name=ATTR_API_HUMIDITY.capitalize(),
native_unit_of_measurement=PERCENTAGE,
state_class=STATE_CLASS_MEASUREMENT,
value=lambda value: round(value, 1),
),
AirlySensorEntityDescription(
key=ATTR_API_PRESSURE,
device_class=DEVICE_CLASS_PRESSURE,
name=ATTR_API_PRESSURE.capitalize(),
native_unit_of_measurement=PRESSURE_HPA,
state_class=STATE_CLASS_MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_TEMPERATURE,
device_class=DEVICE_CLASS_TEMPERATURE,
name=ATTR_API_TEMPERATURE.capitalize(),
native_unit_of_measurement=TEMP_CELSIUS,
state_class=STATE_CLASS_MEASUREMENT,
value=lambda value: round(value, 1),
),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:

View File

@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "L'int\u00e9gration des coordonn\u00e9es d'Airly est d\u00e9j\u00e0 configur\u00e9." "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9"
}, },
"error": { "error": {
"invalid_api_key": "Cl\u00e9 API invalide", "invalid_api_key": "Cl\u00e9 API invalide",
@ -13,7 +13,7 @@
"api_key": "Cl\u00e9 d'API", "api_key": "Cl\u00e9 d'API",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude", "longitude": "Longitude",
"name": "Nom de l'int\u00e9gration" "name": "Nom"
}, },
"description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.", "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.",
"title": "Airly" "title": "Airly"

View File

@ -1,14 +1,19 @@
"""Support for the AirNow sensor service.""" """Support for the AirNow sensor service."""
from homeassistant.components.sensor import SensorEntity from __future__ import annotations
from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
) )
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirNowDataUpdateCoordinator
from .const import ( from .const import (
ATTR_API_AQI, ATTR_API_AQI,
ATTR_API_AQI_DESCRIPTION, ATTR_API_AQI_DESCRIPTION,
@ -22,69 +27,72 @@ from .const import (
ATTRIBUTION = "Data provided by AirNow" ATTRIBUTION = "Data provided by AirNow"
ATTR_LABEL = "label"
ATTR_UNIT = "unit"
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
SENSOR_TYPES = { SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
ATTR_API_AQI: { SensorEntityDescription(
ATTR_DEVICE_CLASS: None, key=ATTR_API_AQI,
ATTR_ICON: "mdi:blur", icon="mdi:blur",
ATTR_LABEL: ATTR_API_AQI, name=ATTR_API_AQI,
ATTR_UNIT: "aqi", native_unit_of_measurement="aqi",
}, state_class=STATE_CLASS_MEASUREMENT,
ATTR_API_PM25: { ),
ATTR_DEVICE_CLASS: None, SensorEntityDescription(
ATTR_ICON: "mdi:blur", key=ATTR_API_PM25,
ATTR_LABEL: ATTR_API_PM25, icon="mdi:blur",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name=ATTR_API_PM25,
}, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_API_O3: { state_class=STATE_CLASS_MEASUREMENT,
ATTR_DEVICE_CLASS: None, ),
ATTR_ICON: "mdi:blur", SensorEntityDescription(
ATTR_LABEL: ATTR_API_O3, key=ATTR_API_O3,
ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, icon="mdi:blur",
}, name=ATTR_API_O3,
} native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=STATE_CLASS_MEASUREMENT,
),
)
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up AirNow sensor entities based on a config entry.""" """Set up AirNow sensor entities based on a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = hass.data[DOMAIN][config_entry.entry_id]
sensors = [] entities = [AirNowSensor(coordinator, description) for description in SENSOR_TYPES]
for sensor in SENSOR_TYPES:
sensors.append(AirNowSensor(coordinator, sensor))
async_add_entities(sensors, False) async_add_entities(entities, False)
class AirNowSensor(CoordinatorEntity, SensorEntity): class AirNowSensor(CoordinatorEntity, SensorEntity):
"""Define an AirNow sensor.""" """Define an AirNow sensor."""
def __init__(self, coordinator, kind): coordinator: AirNowDataUpdateCoordinator
def __init__(
self,
coordinator: AirNowDataUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self.kind = kind self.entity_description = description
self._state = None self._state = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._attr_name = f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" self._attr_name = f"AirNow {description.name}"
self._attr_icon = SENSOR_TYPES[self.kind][ATTR_ICON] self._attr_unique_id = (
self._attr_device_class = SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] f"{coordinator.latitude}-{coordinator.longitude}-{description.key.lower()}"
self._attr_native_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT] )
self._attr_unique_id = f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}"
@property @property
def native_value(self): def native_value(self):
"""Return the state.""" """Return the state."""
self._state = self.coordinator.data[self.kind] self._state = self.coordinator.data[self.entity_description.key]
return self._state return self._state
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
if self.kind == ATTR_API_AQI: if self.entity_description.key == ATTR_API_AQI:
self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[ self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[
ATTR_API_AQI_DESCRIPTION ATTR_API_AQI_DESCRIPTION
] ]

View File

@ -4,7 +4,7 @@
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
}, },
"error": { "error": {
"cannot_connect": "\u00c9chec \u00e0 la connexion", "cannot_connect": "\u00c9chec de connexion",
"invalid_auth": "Authentification invalide", "invalid_auth": "Authentification invalide",
"invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement", "invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement",
"unknown": "Erreur inattendue" "unknown": "Erreur inattendue"
@ -12,7 +12,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"api_key": "Cl\u00e9 API", "api_key": "Cl\u00e9 d'API",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude", "longitude": "Longitude",
"radius": "Rayon d'action de la station (en miles, facultatif)" "radius": "Rayon d'action de la station (en miles, facultatif)"

View File

@ -0,0 +1,61 @@
"""The Airthings integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_ID, CONF_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[str] = ["sensor"]
SCAN_INTERVAL = timedelta(minutes=6)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Airthings from a config entry."""
hass.data.setdefault(DOMAIN, {})
airthings = Airthings(
entry.data[CONF_ID],
entry.data[CONF_SECRET],
async_get_clientsession(hass),
)
async def _update_method():
"""Get the latest data from Airthings."""
try:
return await airthings.update_devices()
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,67 @@
"""Config flow for Airthings integration."""
from __future__ import annotations
import logging
from typing import Any
import airthings
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_ID, CONF_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_ID): str,
vol.Required(CONF_SECRET): str,
}
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Airthings."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
description_placeholders={
"url": "https://dashboard.airthings.com/integrations/api-integration",
},
)
errors = {}
try:
await airthings.get_token(
async_get_clientsession(self.hass),
user_input[CONF_ID],
user_input[CONF_SECRET],
)
except airthings.AirthingsConnectionError:
errors["base"] = "cannot_connect"
except airthings.AirthingsAuthError:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_ID])
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Airthings", data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@ -0,0 +1,6 @@
"""Constants for the Airthings integration."""
DOMAIN = "airthings"
CONF_ID = "id"
CONF_SECRET = "secret"

View File

@ -0,0 +1,11 @@
{
"domain": "airthings",
"name": "Airthings",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airthings",
"requirements": ["airthings_cloud==0.0.1"],
"codeowners": [
"@danielhiversen"
],
"iot_class": "cloud_polling"
}

View File

@ -0,0 +1,164 @@
"""Support for Airthings sensors."""
from __future__ import annotations
from airthings import AirthingsDevice
from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
SensorEntity,
SensorEntityDescription,
StateType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM25,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
PRESSURE_MBAR,
SIGNAL_STRENGTH_DECIBELS,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import DOMAIN
SENSORS: dict[str, SensorEntityDescription] = {
"radonShortTermAvg": SensorEntityDescription(
key="radonShortTermAvg",
native_unit_of_measurement="Bq/m³",
name="Radon",
),
"temp": SensorEntityDescription(
key="temp",
device_class=DEVICE_CLASS_TEMPERATURE,
native_unit_of_measurement=TEMP_CELSIUS,
name="Temperature",
),
"humidity": SensorEntityDescription(
key="humidity",
device_class=DEVICE_CLASS_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
name="Humidity",
),
"pressure": SensorEntityDescription(
key="pressure",
device_class=DEVICE_CLASS_PRESSURE,
native_unit_of_measurement=PRESSURE_MBAR,
name="Pressure",
),
"battery": SensorEntityDescription(
key="battery",
device_class=DEVICE_CLASS_BATTERY,
native_unit_of_measurement=PERCENTAGE,
name="Battery",
),
"co2": SensorEntityDescription(
key="co2",
device_class=DEVICE_CLASS_CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
name="CO2",
),
"voc": SensorEntityDescription(
key="voc",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
name="VOC",
),
"light": SensorEntityDescription(
key="light",
native_unit_of_measurement=PERCENTAGE,
name="Light",
),
"virusRisk": SensorEntityDescription(
key="virusRisk",
name="Virus Risk",
),
"mold": SensorEntityDescription(
key="mold",
name="Mold",
),
"rssi": SensorEntityDescription(
key="rssi",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
name="RSSI",
entity_registry_enabled_default=False,
),
"pm1": SensorEntityDescription(
key="pm1",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=DEVICE_CLASS_PM1,
name="PM1",
),
"pm25": SensorEntityDescription(
key="pm25",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=DEVICE_CLASS_PM25,
name="PM25",
),
}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Airthings sensor."""
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = [
AirthingsHeaterEnergySensor(
coordinator,
airthings_device,
SENSORS[sensor_types],
)
for airthings_device in coordinator.data.values()
for sensor_types in airthings_device.sensor_types
if sensor_types in SENSORS
]
async_add_entities(entities)
class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity):
"""Representation of a Airthings Sensor device."""
_attr_state_class = STATE_CLASS_MEASUREMENT
def __init__(
self,
coordinator: DataUpdateCoordinator,
airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_name = f"{airthings_device.name} {entity_description.name}"
self._attr_unique_id = f"{airthings_device.device_id}_{entity_description.key}"
self._id = airthings_device.device_id
self._attr_device_info = {
"identifiers": {(DOMAIN, airthings_device.device_id)},
"name": airthings_device.name,
"manufacturer": "Airthings",
}
@property
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
return self.coordinator.data[self._id].sensors[self.entity_description.key]

View File

@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"data": {
"id": "ID",
"secret": "Secret",
"description": "Login at {url} to find your credentials"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "El compte ja est\u00e0 configurat"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
"step": {
"user": {
"data": {
"description": "Inicia sessi\u00f3 a {url} per obtenir les credencials",
"id": "ID",
"secret": "Secret"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Konto wurde bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
"description": "Melde dich unter {url} an, um deine Zugangsdaten zu finden",
"id": "ID",
"secret": "Geheimnis"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"description": "Login at {url} to find your credentials",
"id": "ID",
"secret": "Secret"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Konto on juba h\u00e4\u00e4lestatud"
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
"invalid_auth": "Tuvastamise t\u00f5rge",
"unknown": "Ootamatu t\u00f5rge"
},
"step": {
"user": {
"data": {
"description": "Logi sisse aadressil {url}, et leida oma mandaadid",
"id": "Kasutajatunnus",
"secret": "Salas\u00f5na"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
},
"error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
},
"step": {
"user": {
"data": {
"id": "\u05de\u05d6\u05d4\u05d4",
"secret": "\u05e1\u05d5\u05d3"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
"data": {
"description": "Jelentkezzen be a {url} c\u00edmen hogy megkapja hiteles\u00edt\u0151 adatait",
"id": "Azonos\u00edt\u00f3",
"secret": "Titok"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
"cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
"step": {
"user": {
"data": {
"description": "Accedi a {url} per trovare le tue credenziali",
"id": "ID",
"secret": "Segreto"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Account is al geconfigureerd"
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
"description": "Log in op {url} om uw inloggegevens te vinden",
"id": "ID",
"secret": "Geheim"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Kontoen er allerede konfigurert"
},
"error": {
"cannot_connect": "Tilkobling mislyktes",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
"step": {
"user": {
"data": {
"description": "Logg p\u00e5 {url} \u00e5 finne legitimasjonen din",
"id": "ID",
"secret": "Hemmelig"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"description": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435: {url}",
"id": "ID",
"secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"user": {
"data": {
"description": "\u767b\u5165 {url} \u4ee5\u53d6\u5f97\u6191\u8b49",
"id": "ID",
"secret": "\u5bc6\u78bc"
}
}
}
}
}

View File

@ -0,0 +1,9 @@
{
"config": {
"step": {
"user": {
"title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 {intergration}."
}
}
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado"
},
"error": {
"cannot_connect": "No se pudo conectar",
"no_units": "No se pudo encontrar ning\u00fan grupo AirTouch 4."
},
"step": {
"user": {
"data": {
"host": "Host"
},
"title": "Configura los detalles de conexi\u00f3n de tu AirTouch 4."
}
}
}
}

View File

@ -0,0 +1,17 @@
{
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"cannot_connect": "\u00c9chec de connexion"
},
"step": {
"user": {
"data": {
"host": "H\u00f4te"
}
}
}
}
}

View File

@ -10,7 +10,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"host": "Gazdag\u00e9p" "host": "C\u00edm"
}, },
"title": "\u00c1ll\u00edtsa be az AirTouch 4 csatlakoz\u00e1si adatait." "title": "\u00c1ll\u00edtsa be az AirTouch 4 csatlakoz\u00e1si adatait."
} }

View File

@ -0,0 +1,17 @@
{
"config": {
"abort": {
"already_configured": "Perangkat sudah dikonfigurasi"
},
"error": {
"cannot_connect": "Gagal terhubung"
},
"step": {
"user": {
"data": {
"host": "Host"
}
}
}
}
}

View File

@ -1,7 +1,11 @@
"""Support for AirVisual air quality sensors.""" """Support for AirVisual air quality sensors."""
from __future__ import annotations from __future__ import annotations
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_LATITUDE, ATTR_LATITUDE,
@ -76,6 +80,7 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = (
name="Air Quality Index", name="Air Quality Index",
device_class=DEVICE_CLASS_AQI, device_class=DEVICE_CLASS_AQI,
native_unit_of_measurement="AQI", native_unit_of_measurement="AQI",
state_class=STATE_CLASS_MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=SENSOR_KIND_POLLUTANT, key=SENSOR_KIND_POLLUTANT,
@ -92,6 +97,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
name="Air Quality Index", name="Air Quality Index",
device_class=DEVICE_CLASS_AQI, device_class=DEVICE_CLASS_AQI,
native_unit_of_measurement="AQI", native_unit_of_measurement="AQI",
state_class=STATE_CLASS_MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=SENSOR_KIND_BATTERY_LEVEL, key=SENSOR_KIND_BATTERY_LEVEL,
@ -104,6 +110,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
name="C02", name="C02",
device_class=DEVICE_CLASS_CO2, device_class=DEVICE_CLASS_CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=STATE_CLASS_MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=SENSOR_KIND_HUMIDITY, key=SENSOR_KIND_HUMIDITY,
@ -116,30 +123,35 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
name="PM 0.1", name="PM 0.1",
device_class=DEVICE_CLASS_PM1, device_class=DEVICE_CLASS_PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=SENSOR_KIND_PM_1_0, key=SENSOR_KIND_PM_1_0,
name="PM 1.0", name="PM 1.0",
device_class=DEVICE_CLASS_PM10, device_class=DEVICE_CLASS_PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=SENSOR_KIND_PM_2_5, key=SENSOR_KIND_PM_2_5,
name="PM 2.5", name="PM 2.5",
device_class=DEVICE_CLASS_PM25, device_class=DEVICE_CLASS_PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=STATE_CLASS_MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=SENSOR_KIND_TEMPERATURE, key=SENSOR_KIND_TEMPERATURE,
name="Temperature", name="Temperature",
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
native_unit_of_measurement=TEMP_CELSIUS, native_unit_of_measurement=TEMP_CELSIUS,
state_class=STATE_CLASS_MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=SENSOR_KIND_VOC, key=SENSOR_KIND_VOC,
name="VOC", name="VOC",
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=STATE_CLASS_MEASUREMENT,
), ),
) )

View File

@ -13,7 +13,7 @@
"step": { "step": {
"geography_by_coords": { "geography_by_coords": {
"data": { "data": {
"api_key": "Clef d'API", "api_key": "Cl\u00e9 d'API",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude" "longitude": "Longitude"
}, },
@ -22,7 +22,7 @@
}, },
"geography_by_name": { "geography_by_name": {
"data": { "data": {
"api_key": "Clef d'API", "api_key": "Cl\u00e9 d'API",
"city": "Ville", "city": "Ville",
"country": "Pays", "country": "Pays",
"state": "Etat" "state": "Etat"

View File

@ -32,7 +32,7 @@
}, },
"node_pro": { "node_pro": {
"data": { "data": {
"ip_address": "Hoszt", "ip_address": "C\u00edm",
"password": "Jelsz\u00f3" "password": "Jelsz\u00f3"
}, },
"description": "Szem\u00e9lyes AirVisual egys\u00e9g figyel\u00e9se. A jelsz\u00f3 lek\u00e9rhet\u0151 a k\u00e9sz\u00fcl\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9r\u0151l.", "description": "Szem\u00e9lyes AirVisual egys\u00e9g figyel\u00e9se. A jelsz\u00f3 lek\u00e9rhet\u0151 a k\u00e9sz\u00fcl\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9r\u0151l.",

View File

@ -9,11 +9,11 @@
"s2": "Di\u00f3xido de azufre" "s2": "Di\u00f3xido de azufre"
}, },
"airvisual__pollutant_level": { "airvisual__pollutant_level": {
"good": "Bien", "good": "Bueno",
"hazardous": "Peligroso", "hazardous": "Da\u00f1ino",
"moderate": "Moderado", "moderate": "Moderado",
"unhealthy": "Insalubre", "unhealthy": "Insalubre",
"unhealthy_sensitive": "Incorrecto para grupos sensibles", "unhealthy_sensitive": "Insalubre para grupos sensibles",
"very_unhealthy": "Muy poco saludable" "very_unhealthy": "Muy poco saludable"
} }
} }

View File

@ -1,12 +1,12 @@
{ {
"state": { "state": {
"airvisual__pollutant_label": { "airvisual__pollutant_label": {
"co": "Tlenek w\u0119gla", "co": "tlenek w\u0119gla",
"n2": "Dwutlenek azotu", "n2": "dwutlenek azotu",
"o3": "Ozon", "o3": "ozon",
"p1": "PM10", "p1": "PM10",
"p2": "PM2.5", "p2": "PM2.5",
"s2": "Dwutlenek siarki" "s2": "dwutlenek siarki"
}, },
"airvisual__pollutant_level": { "airvisual__pollutant_level": {
"good": "dobry", "good": "dobry",

View File

@ -11,7 +11,10 @@ from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_NIGHT, SUPPORT_ALARM_ARM_NIGHT,
SUPPORT_ALARM_ARM_VACATION, SUPPORT_ALARM_ARM_VACATION,
) )
from homeassistant.components.automation import AutomationActionType from homeassistant.components.automation import (
AutomationActionType,
AutomationTriggerInfo,
)
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.components.homeassistant.triggers import state as state_trigger
from homeassistant.const import ( from homeassistant.const import (
@ -129,7 +132,7 @@ async def async_attach_trigger(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config: ConfigType,
action: AutomationActionType, action: AutomationActionType,
automation_info: dict, automation_info: AutomationTriggerInfo,
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Attach a trigger.""" """Attach a trigger."""
if config[CONF_TYPE] == "triggered": if config[CONF_TYPE] == "triggered":

View File

@ -4,7 +4,7 @@
"arm_away": "Schakel {entity_name} in voor vertrek", "arm_away": "Schakel {entity_name} in voor vertrek",
"arm_home": "Schakel {entity_name} in voor thuis", "arm_home": "Schakel {entity_name} in voor thuis",
"arm_night": "Schakel {entity_name} in voor 's nachts", "arm_night": "Schakel {entity_name} in voor 's nachts",
"arm_vacation": "Schakel {entity_name} in op vakantie", "arm_vacation": "Schakel {entity_name} in voor vakantie",
"disarm": "Schakel {entity_name} uit", "disarm": "Schakel {entity_name} uit",
"trigger": "Laat {entity_name} afgaan" "trigger": "Laat {entity_name} afgaan"
}, },
@ -12,7 +12,7 @@
"is_armed_away": "{entity_name} ingeschakeld voor vertrek", "is_armed_away": "{entity_name} ingeschakeld voor vertrek",
"is_armed_home": "{entity_name} ingeschakeld voor thuis", "is_armed_home": "{entity_name} ingeschakeld voor thuis",
"is_armed_night": "{entity_name} is ingeschakeld voor 's nachts", "is_armed_night": "{entity_name} is ingeschakeld voor 's nachts",
"is_armed_vacation": "{entity_name} is in vakantie geschakeld", "is_armed_vacation": "{entity_name} is ingeschakeld voor vakantie",
"is_disarmed": "{entity_name} is uitgeschakeld", "is_disarmed": "{entity_name} is uitgeschakeld",
"is_triggered": "{entity_name} gaat af" "is_triggered": "{entity_name} gaat af"
}, },
@ -20,7 +20,7 @@
"armed_away": "{entity_name} ingeschakeld voor vertrek", "armed_away": "{entity_name} ingeschakeld voor vertrek",
"armed_home": "{entity_name} ingeschakeld voor thuis", "armed_home": "{entity_name} ingeschakeld voor thuis",
"armed_night": "{entity_name} ingeschakeld voor 's nachts", "armed_night": "{entity_name} ingeschakeld voor 's nachts",
"armed_vacation": "{entity_name} schakelde vakantie in", "armed_vacation": "{entity_name} schakelde in voor vakantie",
"disarmed": "{entity_name} uitgeschakeld", "disarmed": "{entity_name} uitgeschakeld",
"triggered": "{entity_name} afgegaan" "triggered": "{entity_name} afgegaan"
} }
@ -40,5 +40,5 @@
"triggered": "Gaat af" "triggered": "Gaat af"
} }
}, },
"title": "Alarm bedieningspaneel" "title": "Alarmbedieningspaneel"
} }

View File

@ -14,7 +14,7 @@
"data": { "data": {
"device_baudrate": "Eszk\u00f6z \u00e1tviteli sebess\u00e9ge", "device_baudrate": "Eszk\u00f6z \u00e1tviteli sebess\u00e9ge",
"device_path": "Eszk\u00f6z el\u00e9r\u00e9si \u00fatja", "device_path": "Eszk\u00f6z el\u00e9r\u00e9si \u00fatja",
"host": "Hoszt", "host": "C\u00edm",
"port": "Port" "port": "Port"
}, },
"title": "Konfigur\u00e1lja a csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sokat" "title": "Konfigur\u00e1lja a csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sokat"

View File

@ -48,6 +48,7 @@ from .const import (
API_THERMOSTAT_MODES, API_THERMOSTAT_MODES,
API_THERMOSTAT_PRESETS, API_THERMOSTAT_PRESETS,
DATE_FORMAT, DATE_FORMAT,
PRESET_MODE_NA,
Inputs, Inputs,
) )
from .errors import UnsupportedProperty from .errors import UnsupportedProperty
@ -391,6 +392,8 @@ class AlexaPowerController(AlexaCapability):
if self.entity.domain == climate.DOMAIN: if self.entity.domain == climate.DOMAIN:
is_on = self.entity.state != climate.HVAC_MODE_OFF is_on = self.entity.state != climate.HVAC_MODE_OFF
elif self.entity.domain == fan.DOMAIN:
is_on = self.entity.state == fan.STATE_ON
elif self.entity.domain == vacuum.DOMAIN: elif self.entity.domain == vacuum.DOMAIN:
is_on = self.entity.state == vacuum.STATE_CLEANING is_on = self.entity.state == vacuum.STATE_CLEANING
elif self.entity.domain == timer.DOMAIN: elif self.entity.domain == timer.DOMAIN:
@ -1155,9 +1158,6 @@ class AlexaPowerLevelController(AlexaCapability):
if name != "powerLevel": if name != "powerLevel":
raise UnsupportedProperty(name) raise UnsupportedProperty(name)
if self.entity.domain == fan.DOMAIN:
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
class AlexaSecurityPanelController(AlexaCapability): class AlexaSecurityPanelController(AlexaCapability):
"""Implements Alexa.SecurityPanelController. """Implements Alexa.SecurityPanelController.
@ -1354,10 +1354,17 @@ class AlexaModeController(AlexaCapability):
self._resource = AlexaModeResource( self._resource = AlexaModeResource(
[AlexaGlobalCatalog.SETTING_PRESET], False [AlexaGlobalCatalog.SETTING_PRESET], False
) )
for preset_mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, []): preset_modes = self.entity.attributes.get(fan.ATTR_PRESET_MODES, [])
for preset_mode in preset_modes:
self._resource.add_mode( self._resource.add_mode(
f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode] f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode]
) )
# Fans with a single preset_mode completely break Alexa discovery, add a
# fake preset (see issue #53832).
if len(preset_modes) == 1:
self._resource.add_mode(
f"{fan.ATTR_PRESET_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
)
return self._resource.serialize_capability_resources() return self._resource.serialize_capability_resources()
# Cover Position Resources # Cover Position Resources
@ -1483,16 +1490,6 @@ class AlexaRangeController(AlexaCapability):
if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
return None return None
# Fan Speed
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
speed_list = self.entity.attributes.get(fan.ATTR_SPEED_LIST)
speed = self.entity.attributes.get(fan.ATTR_SPEED)
if speed_list is not None and speed is not None:
speed_index = next(
(i for i, v in enumerate(speed_list) if v == speed), None
)
return speed_index
# Cover Position # Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION) return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
@ -1501,6 +1498,13 @@ class AlexaRangeController(AlexaCapability):
if self.instance == f"{cover.DOMAIN}.tilt": if self.instance == f"{cover.DOMAIN}.tilt":
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
# Fan speed percentage
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported and fan.SUPPORT_SET_SPEED:
return self.entity.attributes.get(fan.ATTR_PERCENTAGE)
return 100 if self.entity.state == fan.STATE_ON else 0
# Input Number Value # Input Number Value
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
return float(self.entity.state) return float(self.entity.state)
@ -1527,28 +1531,16 @@ class AlexaRangeController(AlexaCapability):
def capability_resources(self): def capability_resources(self):
"""Return capabilityResources object.""" """Return capabilityResources object."""
# Fan Speed Resources # Fan Speed Percentage Resources
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST] percentage_step = self.entity.attributes.get(fan.ATTR_PERCENTAGE_STEP)
max_value = len(speed_list) - 1
self._resource = AlexaPresetResource( self._resource = AlexaPresetResource(
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], labels=["Percentage", AlexaGlobalCatalog.SETTING_FAN_SPEED],
min_value=0, min_value=0,
max_value=max_value, max_value=100,
precision=1, precision=percentage_step if percentage_step else 100,
unit=AlexaGlobalCatalog.UNIT_PERCENT,
) )
for index, speed in enumerate(speed_list):
labels = []
if isinstance(speed, str):
labels.append(speed.replace("_", " "))
if index == 1:
labels.append(AlexaGlobalCatalog.VALUE_MINIMUM)
if index == max_value:
labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM)
if len(labels) > 0:
self._resource.add_preset(value=index, labels=labels)
return self._resource.serialize_capability_resources() return self._resource.serialize_capability_resources()
# Cover Position Resources # Cover Position Resources
@ -1661,6 +1653,20 @@ class AlexaRangeController(AlexaCapability):
) )
return self._semantics.serialize_semantics() return self._semantics.serialize_semantics()
# Fan Speed Percentage
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
lower_labels = [AlexaSemantics.ACTION_LOWER]
raise_labels = [AlexaSemantics.ACTION_RAISE]
self._semantics = AlexaSemantics()
self._semantics.add_action_to_directive(
lower_labels, "SetRangeValue", {"rangeValue": 0}
)
self._semantics.add_action_to_directive(
raise_labels, "SetRangeValue", {"rangeValue": 100}
)
return self._semantics.serialize_semantics()
return None return None

View File

@ -78,6 +78,9 @@ API_THERMOSTAT_MODES = OrderedDict(
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
# AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode
PRESET_MODE_NA = "-"
class Cause: class Cause:
"""Possible causes for property changes. """Possible causes for property changes.

View File

@ -60,11 +60,9 @@ from .capabilities import (
AlexaLockController, AlexaLockController,
AlexaModeController, AlexaModeController,
AlexaMotionSensor, AlexaMotionSensor,
AlexaPercentageController,
AlexaPlaybackController, AlexaPlaybackController,
AlexaPlaybackStateReporter, AlexaPlaybackStateReporter,
AlexaPowerController, AlexaPowerController,
AlexaPowerLevelController,
AlexaRangeController, AlexaRangeController,
AlexaSceneController, AlexaSceneController,
AlexaSecurityPanelController, AlexaSecurityPanelController,
@ -530,27 +528,32 @@ class FanCapabilities(AlexaEntity):
def interfaces(self): def interfaces(self):
"""Yield the supported interfaces.""" """Yield the supported interfaces."""
yield AlexaPowerController(self.entity) yield AlexaPowerController(self.entity)
force_range_controller = True
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & fan.SUPPORT_SET_SPEED:
yield AlexaPercentageController(self.entity)
yield AlexaPowerLevelController(self.entity)
# The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
yield AlexaRangeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
)
if supported & fan.SUPPORT_OSCILLATE: if supported & fan.SUPPORT_OSCILLATE:
yield AlexaToggleController( yield AlexaToggleController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
) )
force_range_controller = False
if supported & fan.SUPPORT_PRESET_MODE: if supported & fan.SUPPORT_PRESET_MODE:
yield AlexaModeController( yield AlexaModeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}" self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
) )
force_range_controller = False
if supported & fan.SUPPORT_DIRECTION: if supported & fan.SUPPORT_DIRECTION:
yield AlexaModeController( yield AlexaModeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}" self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
) )
force_range_controller = False
# AlexaRangeController controls the Fan Speed Percentage.
# For fans which only support on/off, no controller is added. This makes the
# fan impossible to turn on or off through Alexa, most likely due to a bug in Alexa.
# As a workaround, we add a range controller which can only be set to 0% or 100%.
if force_range_controller or supported & fan.SUPPORT_SET_SPEED:
yield AlexaRangeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}"
)
yield AlexaEndpointHealth(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.hass) yield Alexa(self.hass)

View File

@ -54,6 +54,8 @@ from .const import (
API_THERMOSTAT_MODES, API_THERMOSTAT_MODES,
API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_MODES_CUSTOM,
API_THERMOSTAT_PRESETS, API_THERMOSTAT_PRESETS,
DATE_FORMAT,
PRESET_MODE_NA,
Cause, Cause,
Inputs, Inputs,
) )
@ -122,6 +124,8 @@ async def async_api_turn_on(hass, config, directive, context):
service = SERVICE_TURN_ON service = SERVICE_TURN_ON
if domain == cover.DOMAIN: if domain == cover.DOMAIN:
service = cover.SERVICE_OPEN_COVER service = cover.SERVICE_OPEN_COVER
elif domain == fan.DOMAIN:
service = fan.SERVICE_TURN_ON
elif domain == vacuum.DOMAIN: elif domain == vacuum.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if not supported & vacuum.SUPPORT_TURN_ON and supported & vacuum.SUPPORT_START: if not supported & vacuum.SUPPORT_TURN_ON and supported & vacuum.SUPPORT_START:
@ -156,6 +160,8 @@ async def async_api_turn_off(hass, config, directive, context):
service = SERVICE_TURN_OFF service = SERVICE_TURN_OFF
if entity.domain == cover.DOMAIN: if entity.domain == cover.DOMAIN:
service = cover.SERVICE_CLOSE_COVER service = cover.SERVICE_CLOSE_COVER
elif domain == fan.DOMAIN:
service = fan.SERVICE_TURN_OFF
elif domain == vacuum.DOMAIN: elif domain == vacuum.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if ( if (
@ -318,7 +324,7 @@ async def async_api_activate(hass, config, directive, context):
payload = { payload = {
"cause": {"type": Cause.VOICE_INTERACTION}, "cause": {"type": Cause.VOICE_INTERACTION},
"timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", "timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
} }
return directive.response( return directive.response(
@ -342,7 +348,7 @@ async def async_api_deactivate(hass, config, directive, context):
payload = { payload = {
"cause": {"type": Cause.VOICE_INTERACTION}, "cause": {"type": Cause.VOICE_INTERACTION},
"timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", "timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
} }
return directive.response( return directive.response(
@ -825,48 +831,6 @@ async def async_api_reportstate(hass, config, directive, context):
return directive.response(name="StateReport") return directive.response(name="StateReport")
@HANDLERS.register(("Alexa.PowerLevelController", "SetPowerLevel"))
async def async_api_set_power_level(hass, config, directive, context):
"""Process a SetPowerLevel request."""
entity = directive.entity
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_PERCENTAGE
percentage = int(directive.payload["powerLevel"])
data[fan.ATTR_PERCENTAGE] = percentage
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(("Alexa.PowerLevelController", "AdjustPowerLevel"))
async def async_api_adjust_power_level(hass, config, directive, context):
"""Process an AdjustPowerLevel request."""
entity = directive.entity
percentage_delta = int(directive.payload["powerLevelDelta"])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_PERCENTAGE
current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
# set percentage
percentage = min(100, max(0, percentage_delta + current))
data[fan.ATTR_PERCENTAGE] = percentage
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(("Alexa.SecurityPanelController", "Arm")) @HANDLERS.register(("Alexa.SecurityPanelController", "Arm"))
async def async_api_arm(hass, config, directive, context): async def async_api_arm(hass, config, directive, context):
"""Process a Security Panel Arm request.""" """Process a Security Panel Arm request."""
@ -961,7 +925,9 @@ async def async_api_set_mode(hass, config, directive, context):
# Fan preset_mode # Fan preset_mode
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
preset_mode = mode.split(".")[1] preset_mode = mode.split(".")[1]
if preset_mode in entity.attributes.get(fan.ATTR_PRESET_MODES): if preset_mode != PRESET_MODE_NA and preset_mode in entity.attributes.get(
fan.ATTR_PRESET_MODES
):
service = fan.SERVICE_SET_PRESET_MODE service = fan.SERVICE_SET_PRESET_MODE
data[fan.ATTR_PRESET_MODE] = preset_mode data[fan.ATTR_PRESET_MODE] = preset_mode
else: else:
@ -1091,24 +1057,8 @@ async def async_api_set_range(hass, config, directive, context):
data = {ATTR_ENTITY_ID: entity.entity_id} data = {ATTR_ENTITY_ID: entity.entity_id}
range_value = directive.payload["rangeValue"] range_value = directive.payload["rangeValue"]
# Fan Speed
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
range_value = int(range_value)
service = fan.SERVICE_SET_SPEED
speed_list = entity.attributes[fan.ATTR_SPEED_LIST]
speed = next((v for i, v in enumerate(speed_list) if i == range_value), None)
if not speed:
msg = "Entity does not support value"
raise AlexaInvalidValueError(msg)
if speed == fan.SPEED_OFF:
service = fan.SERVICE_TURN_OFF
data[fan.ATTR_SPEED] = speed
# Cover Position # Cover Position
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
range_value = int(range_value) range_value = int(range_value)
if range_value == 0: if range_value == 0:
service = cover.SERVICE_CLOSE_COVER service = cover.SERVICE_CLOSE_COVER
@ -1129,6 +1079,19 @@ async def async_api_set_range(hass, config, directive, context):
service = cover.SERVICE_SET_COVER_TILT_POSITION service = cover.SERVICE_SET_COVER_TILT_POSITION
data[cover.ATTR_TILT_POSITION] = range_value data[cover.ATTR_TILT_POSITION] = range_value
# Fan Speed
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
range_value = int(range_value)
if range_value == 0:
service = fan.SERVICE_TURN_OFF
else:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported and fan.SUPPORT_SET_SPEED:
service = fan.SERVICE_SET_PERCENTAGE
data[fan.ATTR_PERCENTAGE] = range_value
else:
service = fan.SERVICE_TURN_ON
# Input Number Value # Input Number Value
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
range_value = float(range_value) range_value = float(range_value)
@ -1184,29 +1147,8 @@ async def async_api_adjust_range(hass, config, directive, context):
range_delta_default = bool(directive.payload["rangeValueDeltaDefault"]) range_delta_default = bool(directive.payload["rangeValueDeltaDefault"])
response_value = 0 response_value = 0
# Fan Speed
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
range_delta = int(range_delta)
service = fan.SERVICE_SET_SPEED
speed_list = entity.attributes[fan.ATTR_SPEED_LIST]
current_speed = entity.attributes[fan.ATTR_SPEED]
current_speed_index = next(
(i for i, v in enumerate(speed_list) if v == current_speed), 0
)
new_speed_index = min(
len(speed_list) - 1, max(0, current_speed_index + range_delta)
)
speed = next(
(v for i, v in enumerate(speed_list) if i == new_speed_index), None
)
if speed == fan.SPEED_OFF:
service = fan.SERVICE_TURN_OFF
data[fan.ATTR_SPEED] = response_value = speed
# Cover Position # Cover Position
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
service = SERVICE_SET_COVER_POSITION service = SERVICE_SET_COVER_POSITION
current = entity.attributes.get(cover.ATTR_POSITION) current = entity.attributes.get(cover.ATTR_POSITION)
@ -1237,6 +1179,25 @@ async def async_api_adjust_range(hass, config, directive, context):
else: else:
data[cover.ATTR_TILT_POSITION] = tilt_position data[cover.ATTR_TILT_POSITION] = tilt_position
# Fan speed percentage
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
percentage_step = entity.attributes.get(fan.ATTR_PERCENTAGE_STEP) or 20
range_delta = (
int(range_delta * percentage_step)
if range_delta_default
else int(range_delta)
)
service = fan.SERVICE_SET_PERCENTAGE
current = entity.attributes.get(fan.ATTR_PERCENTAGE)
if not current:
msg = f"Unable to determine {entity.entity_id} current fan speed"
raise AlexaInvalidValueError(msg)
percentage = response_value = min(100, max(0, range_delta + current))
if percentage:
data[fan.ATTR_PERCENTAGE] = percentage
else:
service = fan.SERVICE_TURN_OFF
# Input Number Value # Input Number Value
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
range_delta = float(range_delta) range_delta = float(range_delta)

View File

@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers.significant_change import create_checker from homeassistant.helpers.significant_change import create_checker
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import API_CHANGE, DOMAIN, Cause from .const import API_CHANGE, DATE_FORMAT, DOMAIN, Cause
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
from .messages import AlexaResponse from .messages import AlexaResponse
@ -252,7 +252,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
namespace="Alexa.DoorbellEventSource", namespace="Alexa.DoorbellEventSource",
payload={ payload={
"cause": {"type": Cause.PHYSICAL_INTERACTION}, "cause": {"type": Cause.PHYSICAL_INTERACTION},
"timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", "timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
}, },
) )

View File

@ -1,8 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"cannot_connect": "Impossible de se connecter au serveur Almond", "cannot_connect": "\u00c9chec de connexion",
"missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.",
"no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )",
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
}, },

View File

@ -2,14 +2,14 @@
"config": { "config": {
"abort": { "abort": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s", "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.",
"no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.",
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Supervisor kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot Almondhoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?",
"title": "Almond a Supervisor kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl" "title": "Almond - Home Assistant kieg\u00e9sz\u00edt\u0151 \u00e1ltal"
}, },
"pick_implementation": { "pick_implementation": {
"title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"

View File

@ -8,7 +8,7 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on Supervisor {addon}?", "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on: {addon}?",
"title": "Almond melalui add-on Home Assistant" "title": "Almond melalui add-on Home Assistant"
}, },
"pick_implementation": { "pick_implementation": {

View File

@ -1,12 +1,26 @@
{ {
"config": { "config": {
"abort": {
"reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente"
},
"error": {
"cannot_connect": "No se pudo conectar",
"invalid_api_key": "Clave API no v\u00e1lida"
},
"step": { "step": {
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
"api_key": "Clave API",
"description": "Vuelva a autenticarse con su cuenta de Ambee." "description": "Vuelva a autenticarse con su cuenta de Ambee."
} }
}, },
"user": { "user": {
"data": {
"api_key": "Clave API",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Nombre"
},
"description": "Configure Ambee para que se integre con Home Assistant." "description": "Configure Ambee para que se integre con Home Assistant."
} }
} }

View File

@ -1,22 +1,22 @@
{ {
"config": { "config": {
"abort": { "abort": {
"reauth_successful": "R\u00e9-authentification r\u00e9ussie" "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
}, },
"error": { "error": {
"cannot_connect": "\u00c9chec de connexion", "cannot_connect": "\u00c9chec de connexion",
"invalid_api_key": "Cl\u00e9 API non valide" "invalid_api_key": "Cl\u00e9 API invalide"
}, },
"step": { "step": {
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
"api_key": "cl\u00e9 API", "api_key": "Cl\u00e9 d'API",
"description": "R\u00e9-authentifiez-vous avec votre compte Ambee." "description": "R\u00e9-authentifiez-vous avec votre compte Ambee."
} }
}, },
"user": { "user": {
"data": { "data": {
"api_key": "cl\u00e9 API", "api_key": "Cl\u00e9 d'API",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude", "longitude": "Longitude",
"name": "Nom" "name": "Nom"

View File

@ -21,7 +21,7 @@
"longitude": "Hossz\u00fas\u00e1g", "longitude": "Hossz\u00fas\u00e1g",
"name": "N\u00e9v" "name": "N\u00e9v"
}, },
"description": "\u00c1ll\u00edtsa be az Ambee-t a Homeassistanttal val\u00f3 integr\u00e1ci\u00f3hoz." "description": "Integr\u00e1lja \u00f6ssze Ambeet Home Assistanttal."
} }
} }
} }

View File

@ -10,7 +10,8 @@
"step": { "step": {
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
"api_key": "Kunci API" "api_key": "Kunci API",
"description": "Autentikasi ulang dengan akun Ambee Anda."
} }
}, },
"user": { "user": {
@ -19,7 +20,8 @@
"latitude": "Lintang", "latitude": "Lintang",
"longitude": "Bujur", "longitude": "Bujur",
"name": "Nama" "name": "Nama"
} },
"description": "Siapkan Ambee Anda untuk diintegrasikan dengan Home Assistant."
} }
} }
} }

View File

@ -0,0 +1,9 @@
{
"state": {
"ambee__risk": {
"high": "Tinggi",
"low": "Rendah",
"moderate": "Sedang"
}
}
}

View File

@ -1,10 +1,10 @@
{ {
"state": { "state": {
"ambee__risk": { "ambee__risk": {
"high": "Wysoki", "high": "wysoki",
"low": "Niski", "low": "niski",
"moderate": "Umiarkowany", "moderate": "umiarkowany",
"very high": "Bardzo wysoki" "very high": "bardzo wysoki"
} }
} }
} }

View File

@ -0,0 +1,32 @@
"""Support for Amber Electric."""
from amberelectric import Configuration
from amberelectric.api import amber_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, PLATFORMS
from .coordinator import AmberUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Amber Electric from a config entry."""
configuration = Configuration(access_token=entry.data[CONF_API_TOKEN])
api_instance = amber_api.AmberApi.create(configuration)
site_id = entry.data[CONF_SITE_ID]
coordinator = AmberUpdateCoordinator(hass, api_instance, site_id)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,88 @@
"""Amber Electric Binary Sensor definitions."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION, DOMAIN
from .coordinator import AmberUpdateCoordinator
PRICE_SPIKE_ICONS = {
"none": "mdi:power-plug",
"potential": "mdi:power-plug-outline",
"spike": "mdi:power-plug-off",
}
class AmberPriceGridSensor(CoordinatorEntity, BinarySensorEntity):
"""Sensor to show single grid binary values."""
def __init__(
self,
coordinator: AmberUpdateCoordinator,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the Sensor."""
super().__init__(coordinator)
self.site_id = coordinator.site_id
self.entity_description = description
self._attr_unique_id = f"{coordinator.site_id}-{description.key}"
self._attr_device_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self.coordinator.data["grid"][self.entity_description.key]
class AmberPriceSpikeBinarySensor(AmberPriceGridSensor):
"""Sensor to show single grid binary values."""
@property
def icon(self):
"""Return the sensor icon."""
status = self.coordinator.data["grid"]["price_spike"]
return PRICE_SPIKE_ICONS[status]
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self.coordinator.data["grid"]["price_spike"] == "spike"
@property
def device_state_attributes(self) -> Mapping[str, Any] | None:
"""Return additional pieces of information about the price spike."""
spike_status = self.coordinator.data["grid"]["price_spike"]
return {
"spike_status": spike_status,
ATTR_ATTRIBUTION: ATTRIBUTION,
}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list = []
price_spike_description = BinarySensorEntityDescription(
key="price_spike",
name=f"{entry.title} - Price Spike",
)
entities.append(AmberPriceSpikeBinarySensor(coordinator, price_spike_description))
async_add_entities(entities)

View File

@ -0,0 +1,120 @@
"""Config flow for the Amber Electric integration."""
from __future__ import annotations
from typing import Any
import amberelectric
from amberelectric.api import amber_api
from amberelectric.model.site import Site
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_TOKEN
from .const import CONF_SITE_ID, CONF_SITE_NAME, CONF_SITE_NMI, DOMAIN
API_URL = "https://app.amber.com.au/developers"
class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._errors: dict[str, str] = {}
self._sites: list[Site] | None = None
self._api_token: str | None = None
def _fetch_sites(self, token: str) -> list[Site] | None:
configuration = amberelectric.Configuration(access_token=token)
api = amber_api.AmberApi.create(configuration)
try:
sites = api.get_sites()
if len(sites) == 0:
self._errors[CONF_API_TOKEN] = "no_site"
return None
return sites
except amberelectric.ApiException as api_exception:
if api_exception.status == 403:
self._errors[CONF_API_TOKEN] = "invalid_api_token"
else:
self._errors[CONF_API_TOKEN] = "unknown_error"
return None
async def async_step_user(self, user_input: dict[str, Any] | None = None):
"""Step when user initializes a integration."""
self._errors = {}
self._sites = None
self._api_token = None
if user_input is not None:
token = user_input[CONF_API_TOKEN]
self._sites = await self.hass.async_add_executor_job(
self._fetch_sites, token
)
if self._sites is not None:
self._api_token = token
return await self.async_step_site()
else:
user_input = {CONF_API_TOKEN: ""}
return self.async_show_form(
step_id="user",
description_placeholders={"api_url": API_URL},
data_schema=vol.Schema(
{
vol.Required(
CONF_API_TOKEN, default=user_input[CONF_API_TOKEN]
): str,
}
),
errors=self._errors,
)
async def async_step_site(self, user_input: dict[str, Any] = None):
"""Step to select site."""
self._errors = {}
assert self._sites is not None
api_token = self._api_token
if user_input is not None:
site_nmi = user_input[CONF_SITE_NMI]
sites = [site for site in self._sites if site.nmi == site_nmi]
site = sites[0]
site_id = site.id
name = user_input.get(CONF_SITE_NAME, site_id)
return self.async_create_entry(
title=name,
data={
CONF_SITE_ID: site_id,
CONF_API_TOKEN: api_token,
CONF_SITE_NMI: site.nmi,
},
)
user_input = {
CONF_API_TOKEN: api_token,
CONF_SITE_NMI: "",
CONF_SITE_NAME: "",
}
return self.async_show_form(
step_id="site",
data_schema=vol.Schema(
{
vol.Required(
CONF_SITE_NMI, default=user_input[CONF_SITE_NMI]
): vol.In([site.nmi for site in self._sites]),
vol.Optional(
CONF_SITE_NAME, default=user_input[CONF_SITE_NAME]
): str,
}
),
errors=self._errors,
)

View File

@ -0,0 +1,13 @@
"""Amber Electric Constants."""
import logging
DOMAIN = "amberelectric"
CONF_API_TOKEN = "api_token"
CONF_SITE_NAME = "site_name"
CONF_SITE_ID = "site_id"
CONF_SITE_NMI = "site_nmi"
ATTRIBUTION = "Data provided by Amber Electric"
LOGGER = logging.getLogger(__package__)
PLATFORMS = ["sensor", "binary_sensor"]

View File

@ -0,0 +1,111 @@
"""Amber Electric Coordinator."""
from __future__ import annotations
from datetime import timedelta
from typing import Any
from amberelectric import ApiException
from amberelectric.api import amber_api
from amberelectric.model.actual_interval import ActualInterval
from amberelectric.model.channel import ChannelType
from amberelectric.model.current_interval import CurrentInterval
from amberelectric.model.forecast_interval import ForecastInterval
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
def is_current(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
"""Return true if the supplied interval is a CurrentInterval."""
return isinstance(interval, CurrentInterval)
def is_forecast(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
"""Return true if the supplied interval is a ForecastInterval."""
return isinstance(interval, ForecastInterval)
def is_general(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
"""Return true if the supplied interval is on the general channel."""
return interval.channel_type == ChannelType.GENERAL
def is_controlled_load(
interval: ActualInterval | CurrentInterval | ForecastInterval,
) -> bool:
"""Return true if the supplied interval is on the controlled load channel."""
return interval.channel_type == ChannelType.CONTROLLED_LOAD
def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
"""Return true if the supplied interval is on the feed in channel."""
return interval.channel_type == ChannelType.FEED_IN
class AmberUpdateCoordinator(DataUpdateCoordinator):
"""AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read."""
def __init__(
self, hass: HomeAssistant, api: amber_api.AmberApi, site_id: str
) -> None:
"""Initialise the data service."""
super().__init__(
hass,
LOGGER,
name="amberelectric",
update_interval=timedelta(minutes=1),
)
self._api = api
self.site_id = site_id
def update_price_data(self) -> dict[str, dict[str, Any]]:
"""Update callback."""
result: dict[str, dict[str, Any]] = {
"current": {},
"forecasts": {},
"grid": {},
}
try:
data = self._api.get_current_price(self.site_id, next=48)
except ApiException as api_exception:
raise UpdateFailed("Missing price data, skipping update") from api_exception
current = [interval for interval in data if is_current(interval)]
forecasts = [interval for interval in data if is_forecast(interval)]
general = [interval for interval in current if is_general(interval)]
if len(general) == 0:
raise UpdateFailed("No general channel configured")
result["current"]["general"] = general[0]
result["forecasts"]["general"] = [
interval for interval in forecasts if is_general(interval)
]
result["grid"]["renewables"] = round(general[0].renewables)
result["grid"]["price_spike"] = general[0].spike_status.value
controlled_load = [
interval for interval in current if is_controlled_load(interval)
]
if controlled_load:
result["current"]["controlled_load"] = controlled_load[0]
result["forecasts"]["controlled_load"] = [
interval for interval in forecasts if is_controlled_load(interval)
]
feed_in = [interval for interval in current if is_feed_in(interval)]
if feed_in:
result["current"]["feed_in"] = feed_in[0]
result["forecasts"]["feed_in"] = [
interval for interval in forecasts if is_feed_in(interval)
]
LOGGER.debug("Fetched new Amber data: %s", data)
return result
async def _async_update_data(self) -> dict[str, Any]:
"""Async update wrapper."""
return await self.hass.async_add_executor_job(self.update_price_data)

View File

@ -0,0 +1,13 @@
{
"domain": "amberelectric",
"name": "Amber Electric",
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
"config_flow": true,
"codeowners": [
"@madpilot"
],
"requirements": [
"amberelectric==1.0.3"
],
"iot_class": "cloud_polling"
}

View File

@ -0,0 +1,234 @@
"""Amber Electric Sensor definitions."""
# There are three types of sensor: Current, Forecast and Grid
# Current and forecast will create general, controlled load and feed in as required
# At the moment renewables in the only grid sensor.
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from amberelectric.model.channel import ChannelType
from amberelectric.model.current_interval import CurrentInterval
from amberelectric.model.forecast_interval import ForecastInterval
from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION, DOMAIN
from .coordinator import AmberUpdateCoordinator
ICONS = {
"general": "mdi:transmission-tower",
"controlled_load": "mdi:clock-outline",
"feed_in": "mdi:solar-power",
}
UNIT = f"{CURRENCY_DOLLAR}/{ENERGY_KILO_WATT_HOUR}"
def format_cents_to_dollars(cents: float) -> float:
"""Return a formatted conversion from cents to dollars."""
return round(cents / 100, 2)
def friendly_channel_type(channel_type: str) -> str:
"""Return a human readable version of the channel type."""
if channel_type == "controlled_load":
return "Controlled Load"
if channel_type == "feed_in":
return "Feed In"
return "General"
class AmberSensor(CoordinatorEntity, SensorEntity):
"""Amber Base Sensor."""
def __init__(
self,
coordinator: AmberUpdateCoordinator,
description: SensorEntityDescription,
channel_type: ChannelType,
) -> None:
"""Initialize the Sensor."""
super().__init__(coordinator)
self.site_id = coordinator.site_id
self.entity_description = description
self.channel_type = channel_type
self._attr_unique_id = (
f"{self.site_id}-{self.entity_description.key}-{self.channel_type}"
)
class AmberPriceSensor(AmberSensor):
"""Amber Price Sensor."""
@property
def native_value(self) -> float | None:
"""Return the current price in $/kWh."""
interval = self.coordinator.data[self.entity_description.key][self.channel_type]
if interval.channel_type == ChannelType.FEED_IN:
return format_cents_to_dollars(interval.per_kwh) * -1
return format_cents_to_dollars(interval.per_kwh)
@property
def device_state_attributes(self) -> Mapping[str, Any] | None:
"""Return additional pieces of information about the price."""
interval = self.coordinator.data[self.entity_description.key][self.channel_type]
data: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION}
if interval is None:
return data
data["duration"] = interval.duration
data["date"] = interval.date.isoformat()
data["per_kwh"] = format_cents_to_dollars(interval.per_kwh)
if interval.channel_type == ChannelType.FEED_IN:
data["per_kwh"] = data["per_kwh"] * -1
data["nem_date"] = interval.nem_time.isoformat()
data["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh)
data["start_time"] = interval.start_time.isoformat()
data["end_time"] = interval.end_time.isoformat()
data["renewables"] = round(interval.renewables)
data["estimate"] = interval.estimate
data["spike_status"] = interval.spike_status.value
data["channel_type"] = interval.channel_type.value
if interval.range is not None:
data["range_min"] = format_cents_to_dollars(interval.range.min)
data["range_max"] = format_cents_to_dollars(interval.range.max)
return data
class AmberForecastSensor(AmberSensor):
"""Amber Forecast Sensor."""
@property
def native_value(self) -> float | None:
"""Return the first forecast price in $/kWh."""
intervals = self.coordinator.data[self.entity_description.key].get(
self.channel_type
)
if not intervals:
return None
interval = intervals[0]
if interval.channel_type == ChannelType.FEED_IN:
return format_cents_to_dollars(interval.per_kwh) * -1
return format_cents_to_dollars(interval.per_kwh)
@property
def device_state_attributes(self) -> Mapping[str, Any] | None:
"""Return additional pieces of information about the price."""
intervals = self.coordinator.data[self.entity_description.key].get(
self.channel_type
)
if not intervals:
return None
data = {
"forecasts": [],
"channel_type": intervals[0].channel_type.value,
ATTR_ATTRIBUTION: ATTRIBUTION,
}
for interval in intervals:
datum = {}
datum["duration"] = interval.duration
datum["date"] = interval.date.isoformat()
datum["nem_date"] = interval.nem_time.isoformat()
datum["per_kwh"] = format_cents_to_dollars(interval.per_kwh)
if interval.channel_type == ChannelType.FEED_IN:
datum["per_kwh"] = datum["per_kwh"] * -1
datum["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh)
datum["start_time"] = interval.start_time.isoformat()
datum["end_time"] = interval.end_time.isoformat()
datum["renewables"] = round(interval.renewables)
datum["spike_status"] = interval.spike_status.value
if interval.range is not None:
datum["range_min"] = format_cents_to_dollars(interval.range.min)
datum["range_max"] = format_cents_to_dollars(interval.range.max)
data["forecasts"].append(datum)
return data
class AmberGridSensor(CoordinatorEntity, SensorEntity):
"""Sensor to show single grid specific values."""
def __init__(
self,
coordinator: AmberUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize the Sensor."""
super().__init__(coordinator)
self.site_id = coordinator.site_id
self.entity_description = description
self._attr_device_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._attr_unique_id = f"{coordinator.site_id}-{description.key}"
@property
def native_value(self) -> str | None:
"""Return the value of the sensor."""
return self.coordinator.data["grid"][self.entity_description.key]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
current: dict[str, CurrentInterval] = coordinator.data["current"]
forecasts: dict[str, list[ForecastInterval]] = coordinator.data["forecasts"]
entities: list = []
for channel_type in current:
description = SensorEntityDescription(
key="current",
name=f"{entry.title} - {friendly_channel_type(channel_type)} Price",
native_unit_of_measurement=UNIT,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICONS[channel_type],
)
entities.append(AmberPriceSensor(coordinator, description, channel_type))
for channel_type in forecasts:
description = SensorEntityDescription(
key="forecasts",
name=f"{entry.title} - {friendly_channel_type(channel_type)} Forecast",
native_unit_of_measurement=UNIT,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICONS[channel_type],
)
entities.append(AmberForecastSensor(coordinator, description, channel_type))
renewables_description = SensorEntityDescription(
key="renewables",
name=f"{entry.title} - Renewables",
native_unit_of_measurement="%",
state_class=STATE_CLASS_MEASUREMENT,
icon="mdi:solar-power",
)
entities.append(AmberGridSensor(coordinator, renewables_description))
async_add_entities(entities)

View File

@ -0,0 +1,22 @@
{
"config": {
"step": {
"user": {
"data": {
"api_token": "API Token",
"site_id": "Site ID"
},
"title": "Amber Electric",
"description": "Go to {api_url} to generate an API key"
},
"site": {
"data": {
"site_nmi": "Site NMI",
"site_name": "Site Name"
},
"title": "Amber Electric",
"description": "Select the NMI of the site you would like to add"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"step": {
"site": {
"data": {
"site_name": "Nom del lloc",
"site_nmi": "NMI del lloc"
},
"description": "Selecciona l'NMI del lloc que vulguis afegir",
"title": "Amber Electric"
},
"user": {
"data": {
"api_token": "Token d'API",
"site_id": "ID del lloc"
},
"description": "Ves a {api_url} per generar una clau API",
"title": "Amber Electric"
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More