mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 16:17:20 +00:00
Merge pull request #57179 from home-assistant/rc
This commit is contained in:
commit
32889dbfbe
51
.coveragerc
51
.coveragerc
@ -36,6 +36,8 @@ omit =
|
||||
homeassistant/components/agent_dvr/helpers.py
|
||||
homeassistant/components/airnow/__init__.py
|
||||
homeassistant/components/airnow/sensor.py
|
||||
homeassistant/components/airthings/__init__.py
|
||||
homeassistant/components/airthings/sensor.py
|
||||
homeassistant/components/airtouch4/__init__.py
|
||||
homeassistant/components/airtouch4/climate.py
|
||||
homeassistant/components/airtouch4/const.py
|
||||
@ -49,6 +51,7 @@ omit =
|
||||
homeassistant/components/alarmdecoder/sensor.py
|
||||
homeassistant/components/alpha_vantage/sensor.py
|
||||
homeassistant/components/amazon_polly/*
|
||||
homeassistant/components/amberelectric/__init__.py
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/*
|
||||
homeassistant/components/amcrest/*
|
||||
@ -171,6 +174,13 @@ omit =
|
||||
homeassistant/components/coolmaster/const.py
|
||||
homeassistant/components/cppm_tracker/device_tracker.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/currencylayer/sensor.py
|
||||
homeassistant/components/daikin/*
|
||||
@ -203,7 +213,6 @@ omit =
|
||||
homeassistant/components/dlib_face_detect/image_processing.py
|
||||
homeassistant/components/dlib_face_identify/image_processing.py
|
||||
homeassistant/components/dlink/switch.py
|
||||
homeassistant/components/dlna_dmr/media_player.py
|
||||
homeassistant/components/dnsip/sensor.py
|
||||
homeassistant/components/dominos/*
|
||||
homeassistant/components/doods/*
|
||||
@ -368,7 +377,6 @@ omit =
|
||||
homeassistant/components/garages_amsterdam/sensor.py
|
||||
homeassistant/components/gc100/*
|
||||
homeassistant/components/geniushub/*
|
||||
homeassistant/components/generic_hygrostat/*
|
||||
homeassistant/components/github/sensor.py
|
||||
homeassistant/components/gitlab_ci/sensor.py
|
||||
homeassistant/components/gitter/sensor.py
|
||||
@ -687,7 +695,6 @@ omit =
|
||||
homeassistant/components/nad/media_player.py
|
||||
homeassistant/components/nanoleaf/__init__.py
|
||||
homeassistant/components/nanoleaf/light.py
|
||||
homeassistant/components/nanoleaf/util.py
|
||||
homeassistant/components/neato/__init__.py
|
||||
homeassistant/components/neato/api.py
|
||||
homeassistant/components/neato/camera.py
|
||||
@ -699,7 +706,10 @@ omit =
|
||||
homeassistant/components/nello/lock.py
|
||||
homeassistant/components/nest/legacy/*
|
||||
homeassistant/components/netdata/sensor.py
|
||||
homeassistant/components/netgear/__init__.py
|
||||
homeassistant/components/netgear/device_tracker.py
|
||||
homeassistant/components/netgear/router.py
|
||||
homeassistant/components/netgear/sensor.py
|
||||
homeassistant/components/netgear_lte/*
|
||||
homeassistant/components/netio/switch.py
|
||||
homeassistant/components/neurio_energy/sensor.py
|
||||
@ -754,6 +764,7 @@ omit =
|
||||
homeassistant/components/opencv/*
|
||||
homeassistant/components/openevse/sensor.py
|
||||
homeassistant/components/openexchangerates/sensor.py
|
||||
homeassistant/components/opengarage/__init__.py
|
||||
homeassistant/components/opengarage/cover.py
|
||||
homeassistant/components/openhome/__init__.py
|
||||
homeassistant/components/openhome/media_player.py
|
||||
@ -864,9 +875,6 @@ omit =
|
||||
homeassistant/components/rest/switch.py
|
||||
homeassistant/components/ring/camera.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/roomba/__init__.py
|
||||
homeassistant/components/roomba/binary_sensor.py
|
||||
@ -990,7 +998,6 @@ omit =
|
||||
homeassistant/components/squeezebox/__init__.py
|
||||
homeassistant/components/squeezebox/browse_media.py
|
||||
homeassistant/components/squeezebox/media_player.py
|
||||
homeassistant/components/ssdp/util.py
|
||||
homeassistant/components/starline/*
|
||||
homeassistant/components/starlingbank/sensor.py
|
||||
homeassistant/components/steam_online/sensor.py
|
||||
@ -1001,12 +1008,20 @@ omit =
|
||||
homeassistant/components/suez_water/*
|
||||
homeassistant/components/supervisord/sensor.py
|
||||
homeassistant/components/surepetcare/__init__.py
|
||||
homeassistant/components/surepetcare/entity.py
|
||||
homeassistant/components/surepetcare/binary_sensor.py
|
||||
homeassistant/components/surepetcare/sensor.py
|
||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||
homeassistant/components/swiss_public_transport/sensor.py
|
||||
homeassistant/components/swisscom/device_tracker.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/syncthing/__init__.py
|
||||
homeassistant/components/syncthing/sensor.py
|
||||
@ -1032,6 +1047,8 @@ omit =
|
||||
homeassistant/components/tank_utility/sensor.py
|
||||
homeassistant/components/tankerkoenig/*
|
||||
homeassistant/components/tapsaff/binary_sensor.py
|
||||
homeassistant/components/tautulli/const.py
|
||||
homeassistant/components/tautulli/coordinator.py
|
||||
homeassistant/components/tautulli/sensor.py
|
||||
homeassistant/components/ted5000/sensor.py
|
||||
homeassistant/components/telegram/notify.py
|
||||
@ -1047,14 +1064,6 @@ omit =
|
||||
homeassistant/components/telnet/switch.py
|
||||
homeassistant/components/temper/sensor.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/thermoworks_smoke/sensor.py
|
||||
homeassistant/components/thethingsnetwork/*
|
||||
@ -1088,16 +1097,15 @@ omit =
|
||||
homeassistant/components/totalconnect/binary_sensor.py
|
||||
homeassistant/components/totalconnect/const.py
|
||||
homeassistant/components/touchline/climate.py
|
||||
homeassistant/components/tplink/common.py
|
||||
homeassistant/components/tplink/switch.py
|
||||
homeassistant/components/tplink_lte/*
|
||||
homeassistant/components/traccar/device_tracker.py
|
||||
homeassistant/components/traccar/const.py
|
||||
homeassistant/components/trackr/device_tracker.py
|
||||
homeassistant/components/tractive/__init__.py
|
||||
homeassistant/components/tractive/binary_sensor.py
|
||||
homeassistant/components/tractive/device_tracker.py
|
||||
homeassistant/components/tractive/entity.py
|
||||
homeassistant/components/tractive/sensor.py
|
||||
homeassistant/components/tractive/switch.py
|
||||
homeassistant/components/tradfri/*
|
||||
homeassistant/components/trafikverket_train/sensor.py
|
||||
homeassistant/components/trafikverket_weatherstation/sensor.py
|
||||
@ -1107,9 +1115,9 @@ omit =
|
||||
homeassistant/components/transmission/errors.py
|
||||
homeassistant/components/travisci/sensor.py
|
||||
homeassistant/components/tuya/__init__.py
|
||||
homeassistant/components/tuya/base.py
|
||||
homeassistant/components/tuya/climate.py
|
||||
homeassistant/components/tuya/const.py
|
||||
homeassistant/components/tuya/cover.py
|
||||
homeassistant/components/tuya/fan.py
|
||||
homeassistant/components/tuya/light.py
|
||||
homeassistant/components/tuya/scene.py
|
||||
@ -1177,6 +1185,8 @@ omit =
|
||||
homeassistant/components/waterfurnace/*
|
||||
homeassistant/components/watson_iot/*
|
||||
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/helpers.py
|
||||
homeassistant/components/waze_travel_time/sensor.py
|
||||
@ -1283,5 +1293,6 @@ exclude_lines =
|
||||
raise AssertionError
|
||||
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:
|
||||
@overload
|
||||
|
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@ -133,7 +133,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2021.07.0
|
||||
uses: home-assistant/builder@2021.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@ -186,7 +186,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2021.07.0
|
||||
uses: home-assistant/builder@2021.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
|
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@ -10,7 +10,7 @@ on:
|
||||
pull_request: ~
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 2
|
||||
CACHE_VERSION: 3
|
||||
DEFAULT_PYTHON: 3.8
|
||||
PRE_COMMIT_CACHE: ~/.cache/pre-commit
|
||||
SQLALCHEMY_WARN_20: 1
|
||||
@ -580,7 +580,7 @@ jobs:
|
||||
|
||||
python -m venv venv
|
||||
. 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_test.txt
|
||||
pip install -e .
|
||||
@ -740,4 +740,4 @@ jobs:
|
||||
coverage report --fail-under=94
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2.0.3
|
||||
uses: codecov/codecov-action@v2.1.0
|
||||
|
10
.github/workflows/lock.yml
vendored
10
.github/workflows/lock.yml
vendored
@ -9,12 +9,12 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2.1.2
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||
issue-inactive-days: "30"
|
||||
exclude-issue-created-before: "2020-10-01T00:00:00Z"
|
||||
issue-lock-reason: ""
|
||||
pr-lock-inactive-days: "1"
|
||||
pr-exclude-created-before: "2020-11-01T00:00:00Z"
|
||||
pr-inactive-days: "1"
|
||||
exclude-pr-created-before: "2020-11-01T00:00:00Z"
|
||||
pr-lock-reason: ""
|
||||
|
6
.github/workflows/wheels.yml
vendored
6
.github/workflows/wheels.yml
vendored
@ -65,7 +65,6 @@ jobs:
|
||||
matrix:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
tag:
|
||||
- "3.9-alpine3.13"
|
||||
- "3.9-alpine3.14"
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
@ -90,7 +89,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
env-file: true
|
||||
apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev"
|
||||
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"
|
||||
skip-binary: aiohttp
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
@ -106,7 +105,6 @@ jobs:
|
||||
matrix:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
tag:
|
||||
- "3.9-alpine3.13"
|
||||
- "3.9-alpine3.14"
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
@ -160,7 +158,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
env-file: true
|
||||
apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev"
|
||||
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"
|
||||
skip-binary: aiohttp
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
config/*
|
||||
/config
|
||||
config2/*
|
||||
|
||||
tests/testing_config/deps
|
||||
|
@ -1,11 +1,11 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.23.3
|
||||
rev: v2.27.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.7b0
|
||||
rev: 21.9b0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
|
@ -27,9 +27,11 @@ homeassistant.components.calendar.*
|
||||
homeassistant.components.camera.*
|
||||
homeassistant.components.canary.*
|
||||
homeassistant.components.cover.*
|
||||
homeassistant.components.crownstone.*
|
||||
homeassistant.components.device_automation.*
|
||||
homeassistant.components.device_tracker.*
|
||||
homeassistant.components.devolo_home_control.*
|
||||
homeassistant.components.dlna_dmr.*
|
||||
homeassistant.components.dnsip.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.dunehd.*
|
||||
@ -54,6 +56,7 @@ homeassistant.components.huawei_lte.*
|
||||
homeassistant.components.hyperion.*
|
||||
homeassistant.components.image_processing.*
|
||||
homeassistant.components.integration.*
|
||||
homeassistant.components.iqvia.*
|
||||
homeassistant.components.knx.*
|
||||
homeassistant.components.kraken.*
|
||||
homeassistant.components.lcn.*
|
||||
@ -62,6 +65,7 @@ homeassistant.components.local_ip.*
|
||||
homeassistant.components.lock.*
|
||||
homeassistant.components.mailbox.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.modbus.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.nam.*
|
||||
homeassistant.components.neato.*
|
||||
@ -85,6 +89,7 @@ homeassistant.components.recorder.statistics
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.renault.*
|
||||
homeassistant.components.rituals_perfume_genie.*
|
||||
homeassistant.components.samsungtv.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.select.*
|
||||
homeassistant.components.sensor.*
|
||||
@ -95,18 +100,23 @@ homeassistant.components.sonos.media_player
|
||||
homeassistant.components.ssdp.*
|
||||
homeassistant.components.stream.*
|
||||
homeassistant.components.sun.*
|
||||
homeassistant.components.surepetcare.*
|
||||
homeassistant.components.switch.*
|
||||
homeassistant.components.switcher_kis.*
|
||||
homeassistant.components.synology_dsm.*
|
||||
homeassistant.components.systemmonitor.*
|
||||
homeassistant.components.tag.*
|
||||
homeassistant.components.tautulli.*
|
||||
homeassistant.components.tcp.*
|
||||
homeassistant.components.tile.*
|
||||
homeassistant.components.tplink.*
|
||||
homeassistant.components.tradfri.*
|
||||
homeassistant.components.tts.*
|
||||
homeassistant.components.upcloud.*
|
||||
homeassistant.components.uptime.*
|
||||
homeassistant.components.uptimerobot.*
|
||||
homeassistant.components.vacuum.*
|
||||
homeassistant.components.vallox.*
|
||||
homeassistant.components.water_heater.*
|
||||
homeassistant.components.weather.*
|
||||
homeassistant.components.websocket_api.*
|
||||
|
23
CODEOWNERS
23
CODEOWNERS
@ -29,6 +29,7 @@ homeassistant/components/aemet/* @noltari
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
homeassistant/components/airly/* @bieniu
|
||||
homeassistant/components/airnow/* @asymworks
|
||||
homeassistant/components/airthings/* @danielhiversen
|
||||
homeassistant/components/airtouch4/* @LonePurpleWolf
|
||||
homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarmdecoder/* @ajschmidt8
|
||||
@ -36,6 +37,7 @@ homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
|
||||
homeassistant/components/almond/* @gcampax @balloob
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
homeassistant/components/ambee/* @frenck
|
||||
homeassistant/components/amberelectric/* @madpilot
|
||||
homeassistant/components/ambiclimate/* @danielhiversen
|
||||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/amcrest/* @flacjacket
|
||||
@ -73,7 +75,7 @@ homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/blueprint/* @home-assistant/core
|
||||
homeassistant/components/bmp280/* @belidzs
|
||||
homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe
|
||||
homeassistant/components/bond/* @prystupa
|
||||
homeassistant/components/bond/* @prystupa @joshs85
|
||||
homeassistant/components/bosch_shc/* @tschamm
|
||||
homeassistant/components/braviatv/* @bieniu @Drafteed
|
||||
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
||||
@ -104,6 +106,7 @@ homeassistant/components/coronavirus/* @home-assistant/core
|
||||
homeassistant/components/counter/* @fabaff
|
||||
homeassistant/components/cover/* @home-assistant/core
|
||||
homeassistant/components/cpuspeed/* @fabaff
|
||||
homeassistant/components/crownstone/* @Crownstone @RicArch97
|
||||
homeassistant/components/cups/* @fabaff
|
||||
homeassistant/components/daikin/* @fredrike
|
||||
homeassistant/components/darksky/* @fabaff
|
||||
@ -120,6 +123,7 @@ homeassistant/components/dhcp/* @bdraco
|
||||
homeassistant/components/dht/* @thegardenmonkey
|
||||
homeassistant/components/digital_ocean/* @fabaff
|
||||
homeassistant/components/discogs/* @thibmaek
|
||||
homeassistant/components/dlna_dmr/* @StevenLooman @chishm
|
||||
homeassistant/components/doorbird/* @oblogic7 @bdraco
|
||||
homeassistant/components/dsmr/* @Robbie1221 @frenck
|
||||
homeassistant/components/dsmr_reader/* @depl0y
|
||||
@ -132,6 +136,7 @@ homeassistant/components/ecobee/* @marthoc
|
||||
homeassistant/components/econet/* @vangorra @w1ll1am23
|
||||
homeassistant/components/ecovacs/* @OverloadUT
|
||||
homeassistant/components/edl21/* @mtdcr
|
||||
homeassistant/components/efergy/* @tkdrob
|
||||
homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/elgato/* @frenck
|
||||
@ -202,7 +207,7 @@ homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/growatt_server/* @indykoning @muppet3000 @JasperPlant
|
||||
homeassistant/components/guardian/* @bachya
|
||||
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/heatmiser/* @andylockran
|
||||
homeassistant/components/heos/* @andrewsayre
|
||||
@ -248,7 +253,7 @@ homeassistant/components/integration/* @dgomes
|
||||
homeassistant/components/intent/* @home-assistant/core
|
||||
homeassistant/components/intesishome/* @jnimmo
|
||||
homeassistant/components/ios/* @robbiet480
|
||||
homeassistant/components/iotawatt/* @gtdiehl
|
||||
homeassistant/components/iotawatt/* @gtdiehl @jyavenard
|
||||
homeassistant/components/iperf3/* @rohankapoorcom
|
||||
homeassistant/components/ipma/* @dgomes @abmantis
|
||||
homeassistant/components/ipp/* @ctalkington
|
||||
@ -263,7 +268,7 @@ homeassistant/components/kaiterra/* @Michsior14
|
||||
homeassistant/components/keba/* @dannerph
|
||||
homeassistant/components/keenetic_ndms2/* @foxel
|
||||
homeassistant/components/kef/* @basnijholt
|
||||
homeassistant/components/keyboard_remote/* @bendavid
|
||||
homeassistant/components/keyboard_remote/* @bendavid @lanrat
|
||||
homeassistant/components/kmtronic/* @dgomes
|
||||
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
|
||||
homeassistant/components/kodi/* @OnFreund @cgtobi
|
||||
@ -312,6 +317,7 @@ homeassistant/components/minecraft_server/* @elmurato
|
||||
homeassistant/components/minio/* @tkislan
|
||||
homeassistant/components/mobile_app/* @robbiet480
|
||||
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
|
||||
homeassistant/components/modem_callerid/* @tkdrob
|
||||
homeassistant/components/modern_forms/* @wonderslug
|
||||
homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||
homeassistant/components/moon/* @fabaff
|
||||
@ -335,6 +341,7 @@ homeassistant/components/ness_alarm/* @nickw444
|
||||
homeassistant/components/nest/* @allenporter
|
||||
homeassistant/components/netatmo/* @cgtobi
|
||||
homeassistant/components/netdata/* @fabaff
|
||||
homeassistant/components/netgear/* @hacf-fr @Quentame @starkillerOG
|
||||
homeassistant/components/nexia/* @bdraco
|
||||
homeassistant/components/nextbus/* @vividboarder
|
||||
homeassistant/components/nextcloud/* @meichthys
|
||||
@ -500,7 +507,7 @@ homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/surepetcare/* @benleb @danielhiversen
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
homeassistant/components/switchbot/* @danielhiversen
|
||||
homeassistant/components/switchbot/* @danielhiversen @RenierM26
|
||||
homeassistant/components/switcher_kis/* @tomerfi @thecode
|
||||
homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/syncthing/* @zhulik
|
||||
@ -518,7 +525,6 @@ homeassistant/components/tasmota/* @emontnemery
|
||||
homeassistant/components/tautulli/* @ludeeus
|
||||
homeassistant/components/tellduslive/* @fredrike
|
||||
homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core
|
||||
homeassistant/components/tesla/* @zabuldon @alandtse
|
||||
homeassistant/components/tfiac/* @fredrike @mellado
|
||||
homeassistant/components/thethingsnetwork/* @fabaff
|
||||
homeassistant/components/threshold/* @fabaff
|
||||
@ -538,7 +544,7 @@ homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/trafikverket_weatherstation/* @endor-force
|
||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
homeassistant/components/tts/* @pvizeli
|
||||
homeassistant/components/tuya/* @ollo69
|
||||
homeassistant/components/tuya/* @Tuya @zlinoliver @METISU
|
||||
homeassistant/components/twentemilieu/* @frenck
|
||||
homeassistant/components/twinkly/* @dr1rrb
|
||||
homeassistant/components/ubus/* @noltari
|
||||
@ -553,6 +559,7 @@ homeassistant/components/uptimerobot/* @ludeeus
|
||||
homeassistant/components/usb/* @bdraco
|
||||
homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
||||
homeassistant/components/utility_meter/* @dgomes
|
||||
homeassistant/components/vallox/* @andre-richter
|
||||
homeassistant/components/velbus/* @Cereal2nd @brefra
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/vera/* @pavoni
|
||||
@ -571,10 +578,12 @@ homeassistant/components/wake_on_lan/* @ntilley905
|
||||
homeassistant/components/wallbox/* @hesselonline
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/watttime/* @bachya
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/webostv/* @bendavid @thecode
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @esev
|
||||
homeassistant/components/whirlpool/* @abmantis
|
||||
homeassistant/components/wiffi/* @mampfes
|
||||
homeassistant/components/wilight/* @leofig-rj
|
||||
homeassistant/components/wirelesstag/* @sergeymaysak
|
||||
|
@ -6,6 +6,8 @@ RUN \
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
# Additional library needed by some tests and accordingly by VScode Tests Discovery
|
||||
bluez \
|
||||
libudev-dev \
|
||||
libavformat-dev \
|
||||
libavcodec-dev \
|
||||
|
@ -118,14 +118,6 @@ homeassistant.util.pressure
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
homeassistant.util.ruamel\_yaml
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: homeassistant.util.ruamel_yaml
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
homeassistant.util.ssl
|
||||
----------------------
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import faulthandler
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
@ -10,6 +11,8 @@ import threading
|
||||
|
||||
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
|
||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate that the right Python version is running."""
|
||||
@ -132,16 +135,14 @@ def get_arguments() -> argparse.Namespace:
|
||||
def daemonize() -> None:
|
||||
"""Move current process to daemon process."""
|
||||
# Create first fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
if os.fork() > 0:
|
||||
sys.exit(0)
|
||||
|
||||
# Decouple fork
|
||||
os.setsid()
|
||||
|
||||
# Create second fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
if os.fork() > 0:
|
||||
sys.exit(0)
|
||||
|
||||
# redirect standard file descriptors to devnull
|
||||
@ -311,7 +312,15 @@ def main() -> int:
|
||||
open_ui=args.open_ui,
|
||||
)
|
||||
|
||||
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:
|
||||
try_to_restart()
|
||||
|
||||
|
@ -341,8 +341,7 @@ class AuthManager:
|
||||
"System generated users cannot enable multi-factor auth module."
|
||||
)
|
||||
|
||||
module = self.get_auth_mfa_module(mfa_module_id)
|
||||
if module is None:
|
||||
if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
|
||||
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
|
||||
|
||||
await module.async_setup_user(user.id, data)
|
||||
@ -356,8 +355,7 @@ class AuthManager:
|
||||
"System generated users cannot disable multi-factor auth module."
|
||||
)
|
||||
|
||||
module = self.get_auth_mfa_module(mfa_module_id)
|
||||
if module is None:
|
||||
if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
|
||||
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
|
||||
|
||||
await module.async_depose_user(user.id)
|
||||
@ -466,7 +464,7 @@ class AuthManager:
|
||||
},
|
||||
refresh_token.jwt_key,
|
||||
algorithm="HS256",
|
||||
).decode()
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_resolve_provider(
|
||||
@ -498,8 +496,7 @@ class AuthManager:
|
||||
|
||||
Will raise InvalidAuthError on errors.
|
||||
"""
|
||||
provider = self._async_resolve_provider(refresh_token)
|
||||
if provider:
|
||||
if provider := self._async_resolve_provider(refresh_token):
|
||||
provider.async_validate_refresh_token(refresh_token, remote_ip)
|
||||
|
||||
async def async_validate_access_token(
|
||||
@ -507,7 +504,9 @@ class AuthManager:
|
||||
) -> models.RefreshToken | None:
|
||||
"""Return refresh token if an access token is valid."""
|
||||
try:
|
||||
unverif_claims = jwt.decode(token, verify=False)
|
||||
unverif_claims = jwt.decode(
|
||||
token, algorithms=["HS256"], options={"verify_signature": False}
|
||||
)
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
|
@ -96,8 +96,7 @@ class AuthStore:
|
||||
|
||||
groups = []
|
||||
for group_id in group_ids or []:
|
||||
group = self._groups.get(group_id)
|
||||
if group is None:
|
||||
if (group := self._groups.get(group_id)) is None:
|
||||
raise ValueError(f"Invalid group specified {group_id}")
|
||||
groups.append(group)
|
||||
|
||||
@ -160,8 +159,7 @@ class AuthStore:
|
||||
if group_ids is not None:
|
||||
groups = []
|
||||
for grid in group_ids:
|
||||
group = self._groups.get(grid)
|
||||
if group is None:
|
||||
if (group := self._groups.get(grid)) is None:
|
||||
raise ValueError("Invalid group specified.")
|
||||
groups.append(group)
|
||||
|
||||
@ -446,16 +444,14 @@ class AuthStore:
|
||||
)
|
||||
continue
|
||||
|
||||
token_type = rt_dict.get("token_type")
|
||||
if token_type is None:
|
||||
if (token_type := rt_dict.get("token_type")) is None:
|
||||
if rt_dict["client_id"] is None:
|
||||
token_type = models.TOKEN_TYPE_SYSTEM
|
||||
else:
|
||||
token_type = models.TOKEN_TYPE_NORMAL
|
||||
|
||||
# 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:
|
||||
if last_used_at_str := rt_dict.get("last_used_at"):
|
||||
last_used_at = dt_util.parse_datetime(last_used_at_str)
|
||||
else:
|
||||
last_used_at = None
|
||||
|
@ -38,12 +38,12 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Validate login flow input data."""
|
||||
return vol.Schema({"pin": str})
|
||||
return vol.Schema({vol.Required("pin"): str})
|
||||
|
||||
@property
|
||||
def setup_schema(self) -> vol.Schema:
|
||||
"""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:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
@ -110,7 +110,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""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:
|
||||
"""Load stored data."""
|
||||
@ -118,9 +118,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
if self._user_settings is not None:
|
||||
return
|
||||
|
||||
data = await self._user_store.async_load()
|
||||
|
||||
if data is None:
|
||||
if (data := await self._user_store.async_load()) is None:
|
||||
data = {STORAGE_USERS: {}}
|
||||
|
||||
self._user_settings = {
|
||||
@ -207,8 +205,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
notify_setting = self._user_settings.get(user_id)
|
||||
if notify_setting is None:
|
||||
if (notify_setting := self._user_settings.get(user_id)) is None:
|
||||
return False
|
||||
|
||||
# user_input has been validate in caller
|
||||
@ -225,8 +222,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
notify_setting = self._user_settings.get(user_id)
|
||||
if notify_setting is None:
|
||||
if (notify_setting := self._user_settings.get(user_id)) is None:
|
||||
raise ValueError("Cannot find user_id")
|
||||
|
||||
def generate_secret_and_one_time_password() -> str:
|
||||
|
@ -84,7 +84,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""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:
|
||||
"""Load stored data."""
|
||||
@ -92,9 +92,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
if self._users is not None:
|
||||
return
|
||||
|
||||
data = await self._user_store.async_load()
|
||||
|
||||
if data is None:
|
||||
if (data := await self._user_store.async_load()) is None:
|
||||
data = {STORAGE_USERS: {}}
|
||||
|
||||
self._users = data.get(STORAGE_USERS, {})
|
||||
@ -163,8 +161,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
"""Validate two factor authentication code."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
|
||||
ota_secret = self._users.get(user_id) # type: ignore
|
||||
if ota_secret is None:
|
||||
if (ota_secret := self._users.get(user_id)) is None: # type: ignore
|
||||
# even we cannot find user, we still do verify
|
||||
# to make timing the same as if user was found.
|
||||
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -33,9 +34,7 @@ class AbstractPermissions:
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Check if we can access entity."""
|
||||
entity_func = self._cached_entity_func
|
||||
|
||||
if entity_func is None:
|
||||
if (entity_func := self._cached_entity_func) is None:
|
||||
entity_func = self._cached_entity_func = self._entity_func()
|
||||
|
||||
return entity_func(entity_id, key)
|
||||
|
@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -72,8 +72,7 @@ def compile_policy(
|
||||
def apply_policy_funcs(object_id: str, key: str) -> bool:
|
||||
"""Apply several policy functions."""
|
||||
for func in funcs:
|
||||
result = func(object_id, key)
|
||||
if result is not None:
|
||||
if (result := func(object_id, key)) is not None:
|
||||
return result
|
||||
return False
|
||||
|
||||
|
@ -169,9 +169,7 @@ async def load_auth_provider_module(
|
||||
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
||||
if processed is None:
|
||||
if (processed := hass.data.get(DATA_REQS)) is None:
|
||||
processed = hass.data[DATA_REQS] = set()
|
||||
elif provider in processed:
|
||||
return module
|
||||
|
@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
import os
|
||||
@ -148,10 +147,13 @@ class CommandLineLoginFlow(LoginFlow):
|
||||
user_input.pop("password")
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema: dict[str, type] = collections.OrderedDict()
|
||||
schema["username"] = str
|
||||
schema["password"] = str
|
||||
|
||||
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,
|
||||
)
|
||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
@ -82,9 +81,7 @@ class Data:
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load stored data."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
if data is None:
|
||||
if (data := await self._store.async_load()) is None:
|
||||
data = {"users": []}
|
||||
|
||||
seen: set[str] = set()
|
||||
@ -93,9 +90,7 @@ class Data:
|
||||
username = user["username"]
|
||||
|
||||
# check if we have duplicates
|
||||
folded = username.casefold()
|
||||
|
||||
if folded in seen:
|
||||
if (folded := username.casefold()) in seen:
|
||||
self.is_legacy = True
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
@ -339,10 +334,13 @@ class HassLoginFlow(LoginFlow):
|
||||
user_input.pop("password")
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema: dict[str, type] = OrderedDict()
|
||||
schema["username"] = str
|
||||
schema["password"] = str
|
||||
|
||||
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,
|
||||
)
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Example auth provider."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping
|
||||
import hmac
|
||||
from typing import Any, cast
|
||||
@ -117,10 +116,13 @@ class ExampleLoginFlow(LoginFlow):
|
||||
user_input.pop("password")
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema: dict[str, type] = OrderedDict()
|
||||
schema["username"] = str
|
||||
schema["password"] = str
|
||||
|
||||
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,
|
||||
)
|
||||
|
@ -102,5 +102,7 @@ class LegacyLoginFlow(LoginFlow):
|
||||
return await self.async_finish({})
|
||||
|
||||
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,
|
||||
)
|
||||
|
@ -244,5 +244,7 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
||||
|
||||
return self.async_show_form(
|
||||
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)}
|
||||
),
|
||||
)
|
||||
|
@ -109,9 +109,8 @@ async def async_setup_hass(
|
||||
|
||||
config_dict = None
|
||||
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)
|
||||
|
||||
try:
|
||||
@ -368,8 +367,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, "deps")
|
||||
lib_dir = await async_get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
if (lib_dir := await async_get_user_site(deps_dir)) not in sys.path:
|
||||
sys.path.insert(0, lib_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)
|
||||
|
||||
logging_domains = domains_to_setup & LOGGING_INTEGRATIONS
|
||||
|
||||
# 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)
|
||||
await async_setup_multi_components(hass, logging_domains, config)
|
||||
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
|
||||
|
||||
if debuggers:
|
||||
if debuggers := domains_to_setup & DEBUGGER_INTEGRATIONS:
|
||||
_LOGGER.debug("Setting up debuggers: %s", debuggers)
|
||||
await async_setup_multi_components(hass, debuggers, config)
|
||||
|
||||
@ -524,9 +518,7 @@ async def _async_set_up_integrations(
|
||||
|
||||
stage_1_domains.add(domain)
|
||||
|
||||
dep_itg = integration_cache.get(domain)
|
||||
|
||||
if dep_itg is None:
|
||||
if (dep_itg := integration_cache.get(domain)) is None:
|
||||
continue
|
||||
|
||||
deps_promotion.update(dep_itg.all_dependencies)
|
||||
@ -564,6 +556,14 @@ async def _async_set_up_integrations(
|
||||
except asyncio.TimeoutError:
|
||||
_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()
|
||||
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")
|
||||
|
@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"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": {
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
|
@ -14,7 +14,7 @@
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"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.",
|
||||
"title": "AccuWeather"
|
||||
|
@ -6,7 +6,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
|
||||
"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": {
|
||||
"user": {
|
||||
|
@ -1,9 +1,18 @@
|
||||
{
|
||||
"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": {
|
||||
"user": {
|
||||
"data": {
|
||||
"account_id": "ID de la cuenta"
|
||||
"account_id": "ID de la cuenta",
|
||||
"host": "Host",
|
||||
"password": "Contrase\u00f1a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"account_id": "Fi\u00f3k ID",
|
||||
"host": "Gazdag\u00e9p",
|
||||
"host": "C\u00edm",
|
||||
"password": "Jelsz\u00f3"
|
||||
}
|
||||
}
|
||||
|
20
homeassistant/components/adax/translations/id.json
Normal file
20
homeassistant/components/adax/translations/id.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,9 +17,9 @@
|
||||
"host": "H\u00f4te",
|
||||
"password": "Mot de passe",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home utilise un certificat SSL",
|
||||
"ssl": "Utilise un certificat SSL",
|
||||
"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."
|
||||
}
|
||||
|
@ -7,6 +7,9 @@
|
||||
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"title": "AdGuard Home \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05e8\u05d7\u05d1\u05ea Assistant Assistant"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u05de\u05d0\u05e8\u05d7",
|
||||
|
@ -9,12 +9,12 @@
|
||||
},
|
||||
"step": {
|
||||
"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"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Hoszt",
|
||||
"host": "C\u00edm",
|
||||
"password": "Jelsz\u00f3",
|
||||
"port": "Port",
|
||||
"ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
|
||||
|
@ -9,7 +9,7 @@
|
||||
},
|
||||
"step": {
|
||||
"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"
|
||||
},
|
||||
"user": {
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Constant values for the AEMET OpenData component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
@ -40,9 +42,6 @@ DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
ENTRY_NAME = "name"
|
||||
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_FORECAST_DAILY = "forecast-daily"
|
||||
@ -200,118 +199,145 @@ FORECAST_MODE_ATTR_API = {
|
||||
FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
|
||||
}
|
||||
|
||||
FORECAST_SENSOR_TYPES = {
|
||||
ATTR_FORECAST_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION: {
|
||||
SENSOR_NAME: "Precipitation",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: {
|
||||
SENSOR_NAME: "Precipitation probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP_LOW: {
|
||||
SENSOR_NAME: "Temperature Low",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TIME: {
|
||||
SENSOR_NAME: "Time",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_FORECAST_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_FORECAST_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
WEATHER_SENSOR_TYPES = {
|
||||
ATTR_API_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_API_HUMIDITY: {
|
||||
SENSOR_NAME: "Humidity",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
},
|
||||
ATTR_API_PRESSURE: {
|
||||
SENSOR_NAME: "Pressure",
|
||||
SENSOR_UNIT: PRESSURE_HPA,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||
},
|
||||
ATTR_API_RAIN: {
|
||||
SENSOR_NAME: "Rain",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_RAIN_PROB: {
|
||||
SENSOR_NAME: "Rain probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_SNOW: {
|
||||
SENSOR_NAME: "Snow",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_SNOW_PROB: {
|
||||
SENSOR_NAME: "Snow probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_STATION_ID: {
|
||||
SENSOR_NAME: "Station ID",
|
||||
},
|
||||
ATTR_API_STATION_NAME: {
|
||||
SENSOR_NAME: "Station name",
|
||||
},
|
||||
ATTR_API_STATION_TIMESTAMP: {
|
||||
SENSOR_NAME: "Station timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_STORM_PROB: {
|
||||
SENSOR_NAME: "Storm probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE_FEELING: {
|
||||
SENSOR_NAME: "Temperature feeling",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TOWN_ID: {
|
||||
SENSOR_NAME: "Town ID",
|
||||
},
|
||||
ATTR_API_TOWN_NAME: {
|
||||
SENSOR_NAME: "Town name",
|
||||
},
|
||||
ATTR_API_TOWN_TIMESTAMP: {
|
||||
SENSOR_NAME: "Town timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_API_WIND_MAX_SPEED: {
|
||||
SENSOR_NAME: "Wind max speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_CONDITION,
|
||||
name="Condition",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_PRECIPITATION,
|
||||
name="Precipitation",
|
||||
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
name="Precipitation probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_TEMP,
|
||||
name="Temperature",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_TEMP_LOW,
|
||||
name="Temperature Low",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_TIME,
|
||||
name="Time",
|
||||
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_WIND_BEARING,
|
||||
name="Wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_WIND_SPEED,
|
||||
name="Wind speed",
|
||||
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
|
||||
),
|
||||
)
|
||||
WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_CONDITION,
|
||||
name="Condition",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_HUMIDITY,
|
||||
name="Humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_PRESSURE,
|
||||
name="Pressure",
|
||||
native_unit_of_measurement=PRESSURE_HPA,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_RAIN,
|
||||
name="Rain",
|
||||
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_RAIN_PROB,
|
||||
name="Rain probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_SNOW,
|
||||
name="Snow",
|
||||
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_SNOW_PROB,
|
||||
name="Snow probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_STATION_ID,
|
||||
name="Station ID",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_STATION_NAME,
|
||||
name="Station name",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_STATION_TIMESTAMP,
|
||||
name="Station timestamp",
|
||||
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_STORM_PROB,
|
||||
name="Storm probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_TEMPERATURE,
|
||||
name="Temperature",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_TEMPERATURE_FEELING,
|
||||
name="Temperature feeling",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
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 = {
|
||||
"C": None,
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""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.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@ -14,9 +16,6 @@ from .const import (
|
||||
FORECAST_MONITORED_CONDITIONS,
|
||||
FORECAST_SENSOR_TYPES,
|
||||
MONITORED_CONDITIONS,
|
||||
SENSOR_DEVICE_CLASS,
|
||||
SENSOR_NAME,
|
||||
SENSOR_UNIT,
|
||||
WEATHER_SENSOR_TYPES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
@ -28,36 +27,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
name = domain_data[ENTRY_NAME]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
weather_sensor_types = WEATHER_SENSOR_TYPES
|
||||
forecast_sensor_types = FORECAST_SENSOR_TYPES
|
||||
|
||||
entities = []
|
||||
for sensor_type in MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetSensor(
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
weather_sensor_types[sensor_type],
|
||||
weather_coordinator,
|
||||
)
|
||||
)
|
||||
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
|
||||
for sensor_type in FORECAST_MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}"
|
||||
entities.append(
|
||||
unique_id = config_entry.unique_id
|
||||
entities: list[AbstractAemetSensor] = [
|
||||
AemetSensor(name, unique_id, weather_coordinator, description)
|
||||
for description in WEATHER_SENSOR_TYPES
|
||||
if description.key in MONITORED_CONDITIONS
|
||||
]
|
||||
entities.extend(
|
||||
[
|
||||
AemetForecastSensor(
|
||||
f"{name} Forecast",
|
||||
unique_id,
|
||||
sensor_type,
|
||||
forecast_sensor_types[sensor_type],
|
||||
name_prefix,
|
||||
unique_id_prefix,
|
||||
weather_coordinator,
|
||||
mode,
|
||||
description,
|
||||
)
|
||||
for mode in FORECAST_MODES
|
||||
if (
|
||||
(name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast")
|
||||
and (unique_id_prefix := f"{unique_id}-forecast-{mode}")
|
||||
)
|
||||
for description in FORECAST_SENSOR_TYPES
|
||||
if description.key in FORECAST_MONITORED_CONDITIONS
|
||||
]
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
@ -72,20 +64,14 @@ class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._sensor_type = sensor_type
|
||||
self._sensor_name = sensor_configuration[SENSOR_NAME]
|
||||
self._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)
|
||||
self.entity_description = description
|
||||
self._attr_name = f"{name} {description.name}"
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
|
||||
class AemetSensor(AbstractAemetSensor):
|
||||
@ -95,20 +81,21 @@ class AemetSensor(AbstractAemetSensor):
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
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
|
||||
def native_value(self):
|
||||
"""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):
|
||||
@ -118,16 +105,17 @@ class AemetForecastSensor(AbstractAemetSensor):
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
description: SensorEntityDescription,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
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._attr_entity_registry_enabled_default = (
|
||||
self._forecast_mode == FORECAST_MODE_DAILY
|
||||
@ -137,9 +125,9 @@ class AemetForecastSensor(AbstractAemetSensor):
|
||||
def native_value(self):
|
||||
"""Return the state of the device."""
|
||||
forecast = None
|
||||
forecasts = self._weather_coordinator.data.get(
|
||||
forecasts = self.coordinator.data.get(
|
||||
FORECAST_MODE_ATTR_API[self._forecast_mode]
|
||||
)
|
||||
if forecasts:
|
||||
forecast = forecasts[0].get(self._sensor_type)
|
||||
forecast = forecasts[0].get(self.entity_description.key)
|
||||
return forecast
|
||||
|
@ -4,13 +4,13 @@
|
||||
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Nom d'h\u00f4te ou adresse IP",
|
||||
"host": "H\u00f4te",
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "Configurer l'agent DVR"
|
||||
|
@ -4,13 +4,13 @@
|
||||
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Hoszt",
|
||||
"host": "C\u00edm",
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "\u00c1ll\u00edtsa be az Agent DVR-t"
|
||||
|
@ -3,23 +3,6 @@ from __future__ import annotations
|
||||
|
||||
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_CAQI: Final = "CAQI"
|
||||
ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION"
|
||||
@ -49,56 +32,3 @@ MANUFACTURER: Final = "Airly sp. z o.o."
|
||||
MAX_UPDATE_INTERVAL: Final = 90
|
||||
MIN_UPDATE_INTERVAL: Final = 5
|
||||
NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet."
|
||||
|
||||
SENSOR_TYPES: 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),
|
||||
),
|
||||
)
|
||||
|
@ -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
|
@ -1,11 +1,31 @@
|
||||
"""Support for the Airly sensor service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
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.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.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@ -18,8 +38,12 @@ from .const import (
|
||||
ATTR_API_CAQI,
|
||||
ATTR_API_CAQI_DESCRIPTION,
|
||||
ATTR_API_CAQI_LEVEL,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PM1,
|
||||
ATTR_API_PM10,
|
||||
ATTR_API_PM25,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_DESCRIPTION,
|
||||
ATTR_LEVEL,
|
||||
ATTR_LIMIT,
|
||||
@ -28,15 +52,74 @@ from .const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
SENSOR_TYPES,
|
||||
SUFFIX_LIMIT,
|
||||
SUFFIX_PERCENT,
|
||||
)
|
||||
from .model import AirlySensorEntityDescription
|
||||
|
||||
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(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"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": {
|
||||
"invalid_api_key": "Cl\u00e9 API invalide",
|
||||
@ -13,7 +13,7 @@
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"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.",
|
||||
"title": "Airly"
|
||||
|
@ -1,14 +1,19 @@
|
||||
"""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 (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirNowDataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_API_AQI,
|
||||
ATTR_API_AQI_DESCRIPTION,
|
||||
@ -22,69 +27,72 @@ from .const import (
|
||||
|
||||
ATTRIBUTION = "Data provided by AirNow"
|
||||
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SENSOR_TYPES = {
|
||||
ATTR_API_AQI: {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
ATTR_ICON: "mdi:blur",
|
||||
ATTR_LABEL: ATTR_API_AQI,
|
||||
ATTR_UNIT: "aqi",
|
||||
},
|
||||
ATTR_API_PM25: {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
ATTR_ICON: "mdi:blur",
|
||||
ATTR_LABEL: ATTR_API_PM25,
|
||||
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
ATTR_API_O3: {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
ATTR_ICON: "mdi:blur",
|
||||
ATTR_LABEL: ATTR_API_O3,
|
||||
ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION,
|
||||
},
|
||||
}
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_AQI,
|
||||
icon="mdi:blur",
|
||||
name=ATTR_API_AQI,
|
||||
native_unit_of_measurement="aqi",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_PM25,
|
||||
icon="mdi:blur",
|
||||
name=ATTR_API_PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_O3,
|
||||
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):
|
||||
"""Set up AirNow sensor entities based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
sensors = []
|
||||
for sensor in SENSOR_TYPES:
|
||||
sensors.append(AirNowSensor(coordinator, sensor))
|
||||
entities = [AirNowSensor(coordinator, description) for description in SENSOR_TYPES]
|
||||
|
||||
async_add_entities(sensors, False)
|
||||
async_add_entities(entities, False)
|
||||
|
||||
|
||||
class AirNowSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Define an AirNow sensor."""
|
||||
|
||||
def __init__(self, coordinator, kind):
|
||||
coordinator: AirNowDataUpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirNowDataUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self.kind = kind
|
||||
self.entity_description = description
|
||||
self._state = None
|
||||
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
self._attr_name = f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
|
||||
self._attr_icon = SENSOR_TYPES[self.kind][ATTR_ICON]
|
||||
self._attr_device_class = SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
|
||||
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()}"
|
||||
self._attr_name = f"AirNow {description.name}"
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.latitude}-{coordinator.longitude}-{description.key.lower()}"
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state."""
|
||||
self._state = self.coordinator.data[self.kind]
|
||||
self._state = self.coordinator.data[self.entity_description.key]
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""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[
|
||||
ATTR_API_AQI_DESCRIPTION
|
||||
]
|
||||
|
@ -4,7 +4,7 @@
|
||||
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec \u00e0 la connexion",
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"invalid_auth": "Authentification invalide",
|
||||
"invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement",
|
||||
"unknown": "Erreur inattendue"
|
||||
@ -12,7 +12,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Cl\u00e9 API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"radius": "Rayon d'action de la station (en miles, facultatif)"
|
||||
|
61
homeassistant/components/airthings/__init__.py
Normal file
61
homeassistant/components/airthings/__init__.py
Normal 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
|
67
homeassistant/components/airthings/config_flow.py
Normal file
67
homeassistant/components/airthings/config_flow.py
Normal 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
|
||||
)
|
6
homeassistant/components/airthings/const.py
Normal file
6
homeassistant/components/airthings/const.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Constants for the Airthings integration."""
|
||||
|
||||
DOMAIN = "airthings"
|
||||
|
||||
CONF_ID = "id"
|
||||
CONF_SECRET = "secret"
|
11
homeassistant/components/airthings/manifest.json
Normal file
11
homeassistant/components/airthings/manifest.json
Normal 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"
|
||||
}
|
164
homeassistant/components/airthings/sensor.py
Normal file
164
homeassistant/components/airthings/sensor.py
Normal 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]
|
21
homeassistant/components/airthings/strings.json
Normal file
21
homeassistant/components/airthings/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/ca.json
Normal file
21
homeassistant/components/airthings/translations/ca.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/de.json
Normal file
21
homeassistant/components/airthings/translations/de.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/en.json
Normal file
21
homeassistant/components/airthings/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/et.json
Normal file
21
homeassistant/components/airthings/translations/et.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/airthings/translations/he.json
Normal file
20
homeassistant/components/airthings/translations/he.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/hu.json
Normal file
21
homeassistant/components/airthings/translations/hu.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/it.json
Normal file
21
homeassistant/components/airthings/translations/it.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/nl.json
Normal file
21
homeassistant/components/airthings/translations/nl.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/no.json
Normal file
21
homeassistant/components/airthings/translations/no.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/ru.json
Normal file
21
homeassistant/components/airthings/translations/ru.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/airthings/translations/zh-Hant.json
Normal file
21
homeassistant/components/airthings/translations/zh-Hant.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
homeassistant/components/airtouch4/translations/el.json
Normal file
9
homeassistant/components/airtouch4/translations/el.json
Normal 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}."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
homeassistant/components/airtouch4/translations/es.json
Normal file
19
homeassistant/components/airtouch4/translations/es.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
homeassistant/components/airtouch4/translations/fr.json
Normal file
17
homeassistant/components/airtouch4/translations/fr.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Gazdag\u00e9p"
|
||||
"host": "C\u00edm"
|
||||
},
|
||||
"title": "\u00c1ll\u00edtsa be az AirTouch 4 csatlakoz\u00e1si adatait."
|
||||
}
|
||||
|
17
homeassistant/components/airtouch4/translations/id.json
Normal file
17
homeassistant/components/airtouch4/translations/id.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Perangkat sudah dikonfigurasi"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Gagal terhubung"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
"""Support for AirVisual air quality sensors."""
|
||||
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.const import (
|
||||
ATTR_LATITUDE,
|
||||
@ -76,6 +80,7 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = (
|
||||
name="Air Quality Index",
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
native_unit_of_measurement="AQI",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_POLLUTANT,
|
||||
@ -92,6 +97,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
|
||||
name="Air Quality Index",
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
native_unit_of_measurement="AQI",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_BATTERY_LEVEL,
|
||||
@ -104,6 +110,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
|
||||
name="C02",
|
||||
device_class=DEVICE_CLASS_CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_HUMIDITY,
|
||||
@ -116,30 +123,35 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
|
||||
name="PM 0.1",
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_PM_1_0,
|
||||
name="PM 1.0",
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_PM_2_5,
|
||||
name="PM 2.5",
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_TEMPERATURE,
|
||||
name="Temperature",
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_VOC,
|
||||
name="VOC",
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
"step": {
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Clef d'API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
},
|
||||
@ -22,7 +22,7 @@
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Clef d'API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"city": "Ville",
|
||||
"country": "Pays",
|
||||
"state": "Etat"
|
||||
|
@ -32,7 +32,7 @@
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Hoszt",
|
||||
"ip_address": "C\u00edm",
|
||||
"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.",
|
||||
|
@ -9,11 +9,11 @@
|
||||
"s2": "Di\u00f3xido de azufre"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "Bien",
|
||||
"hazardous": "Peligroso",
|
||||
"good": "Bueno",
|
||||
"hazardous": "Da\u00f1ino",
|
||||
"moderate": "Moderado",
|
||||
"unhealthy": "Insalubre",
|
||||
"unhealthy_sensitive": "Incorrecto para grupos sensibles",
|
||||
"unhealthy_sensitive": "Insalubre para grupos sensibles",
|
||||
"very_unhealthy": "Muy poco saludable"
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"state": {
|
||||
"airvisual__pollutant_label": {
|
||||
"co": "Tlenek w\u0119gla",
|
||||
"n2": "Dwutlenek azotu",
|
||||
"o3": "Ozon",
|
||||
"co": "tlenek w\u0119gla",
|
||||
"n2": "dwutlenek azotu",
|
||||
"o3": "ozon",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Dwutlenek siarki"
|
||||
"s2": "dwutlenek siarki"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "dobry",
|
||||
|
@ -11,7 +11,10 @@ from homeassistant.components.alarm_control_panel.const import (
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
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.homeassistant.triggers import state as state_trigger
|
||||
from homeassistant.const import (
|
||||
@ -129,7 +132,7 @@ async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: AutomationActionType,
|
||||
automation_info: dict,
|
||||
automation_info: AutomationTriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
if config[CONF_TYPE] == "triggered":
|
||||
|
@ -4,7 +4,7 @@
|
||||
"arm_away": "Schakel {entity_name} in voor vertrek",
|
||||
"arm_home": "Schakel {entity_name} in voor thuis",
|
||||
"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",
|
||||
"trigger": "Laat {entity_name} afgaan"
|
||||
},
|
||||
@ -12,7 +12,7 @@
|
||||
"is_armed_away": "{entity_name} ingeschakeld voor vertrek",
|
||||
"is_armed_home": "{entity_name} ingeschakeld voor thuis",
|
||||
"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_triggered": "{entity_name} gaat af"
|
||||
},
|
||||
@ -20,7 +20,7 @@
|
||||
"armed_away": "{entity_name} ingeschakeld voor vertrek",
|
||||
"armed_home": "{entity_name} ingeschakeld voor thuis",
|
||||
"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",
|
||||
"triggered": "{entity_name} afgegaan"
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
"data": {
|
||||
"device_baudrate": "Eszk\u00f6z \u00e1tviteli sebess\u00e9ge",
|
||||
"device_path": "Eszk\u00f6z el\u00e9r\u00e9si \u00fatja",
|
||||
"host": "Hoszt",
|
||||
"host": "C\u00edm",
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "Konfigur\u00e1lja a csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sokat"
|
||||
|
@ -48,6 +48,7 @@ from .const import (
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PRESET_MODE_NA,
|
||||
Inputs,
|
||||
)
|
||||
from .errors import UnsupportedProperty
|
||||
@ -391,6 +392,8 @@ class AlexaPowerController(AlexaCapability):
|
||||
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
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:
|
||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||
elif self.entity.domain == timer.DOMAIN:
|
||||
@ -1155,9 +1158,6 @@ class AlexaPowerLevelController(AlexaCapability):
|
||||
if name != "powerLevel":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.domain == fan.DOMAIN:
|
||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
|
||||
class AlexaSecurityPanelController(AlexaCapability):
|
||||
"""Implements Alexa.SecurityPanelController.
|
||||
@ -1354,10 +1354,17 @@ class AlexaModeController(AlexaCapability):
|
||||
self._resource = AlexaModeResource(
|
||||
[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(
|
||||
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()
|
||||
|
||||
# Cover Position Resources
|
||||
@ -1483,16 +1490,6 @@ class AlexaRangeController(AlexaCapability):
|
||||
if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, 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
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
|
||||
@ -1501,6 +1498,13 @@ class AlexaRangeController(AlexaCapability):
|
||||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
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
|
||||
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
return float(self.entity.state)
|
||||
@ -1527,28 +1531,16 @@ class AlexaRangeController(AlexaCapability):
|
||||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
|
||||
# Fan Speed Resources
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST]
|
||||
max_value = len(speed_list) - 1
|
||||
# Fan Speed Percentage Resources
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
percentage_step = self.entity.attributes.get(fan.ATTR_PERCENTAGE_STEP)
|
||||
self._resource = AlexaPresetResource(
|
||||
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||
labels=["Percentage", AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||
min_value=0,
|
||||
max_value=max_value,
|
||||
precision=1,
|
||||
max_value=100,
|
||||
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()
|
||||
|
||||
# Cover Position Resources
|
||||
@ -1661,6 +1653,20 @@ class AlexaRangeController(AlexaCapability):
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
@ -78,6 +78,9 @@ API_THERMOSTAT_MODES = OrderedDict(
|
||||
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
|
||||
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:
|
||||
"""Possible causes for property changes.
|
||||
|
@ -60,11 +60,9 @@ from .capabilities import (
|
||||
AlexaLockController,
|
||||
AlexaModeController,
|
||||
AlexaMotionSensor,
|
||||
AlexaPercentageController,
|
||||
AlexaPlaybackController,
|
||||
AlexaPlaybackStateReporter,
|
||||
AlexaPowerController,
|
||||
AlexaPowerLevelController,
|
||||
AlexaRangeController,
|
||||
AlexaSceneController,
|
||||
AlexaSecurityPanelController,
|
||||
@ -530,27 +528,32 @@ class FanCapabilities(AlexaEntity):
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
force_range_controller = True
|
||||
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:
|
||||
yield AlexaToggleController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
||||
)
|
||||
force_range_controller = False
|
||||
if supported & fan.SUPPORT_PRESET_MODE:
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
|
||||
)
|
||||
force_range_controller = False
|
||||
if supported & fan.SUPPORT_DIRECTION:
|
||||
yield AlexaModeController(
|
||||
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 Alexa(self.hass)
|
||||
|
@ -54,6 +54,8 @@ from .const import (
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_MODES_CUSTOM,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PRESET_MODE_NA,
|
||||
Cause,
|
||||
Inputs,
|
||||
)
|
||||
@ -122,6 +124,8 @@ async def async_api_turn_on(hass, config, directive, context):
|
||||
service = SERVICE_TURN_ON
|
||||
if domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
elif domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_TURN_ON
|
||||
elif domain == vacuum.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
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
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
elif domain == vacuum.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (
|
||||
@ -318,7 +324,7 @@ async def async_api_activate(hass, config, directive, context):
|
||||
|
||||
payload = {
|
||||
"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(
|
||||
@ -342,7 +348,7 @@ async def async_api_deactivate(hass, config, directive, context):
|
||||
|
||||
payload = {
|
||||
"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(
|
||||
@ -825,48 +831,6 @@ async def async_api_reportstate(hass, config, directive, context):
|
||||
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"))
|
||||
async def async_api_arm(hass, config, directive, context):
|
||||
"""Process a Security Panel Arm request."""
|
||||
@ -961,7 +925,9 @@ async def async_api_set_mode(hass, config, directive, context):
|
||||
# Fan preset_mode
|
||||
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
|
||||
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
|
||||
data[fan.ATTR_PRESET_MODE] = preset_mode
|
||||
else:
|
||||
@ -1091,24 +1057,8 @@ async def async_api_set_range(hass, config, directive, context):
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
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
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
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
|
||||
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
|
||||
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_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"])
|
||||
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
|
||||
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)
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||
@ -1237,6 +1179,25 @@ async def async_api_adjust_range(hass, config, directive, context):
|
||||
else:
|
||||
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
|
||||
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
range_delta = float(range_delta)
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers.significant_change import create_checker
|
||||
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 .messages import AlexaResponse
|
||||
|
||||
@ -252,7 +252,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
|
||||
namespace="Alexa.DoorbellEventSource",
|
||||
payload={
|
||||
"cause": {"type": Cause.PHYSICAL_INTERACTION},
|
||||
"timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z",
|
||||
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "Impossible de se connecter au serveur Almond",
|
||||
"missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond.",
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"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} )",
|
||||
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
|
||||
},
|
||||
|
@ -2,14 +2,14 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"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.",
|
||||
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
|
||||
},
|
||||
"step": {
|
||||
"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} ?",
|
||||
"title": "Almond a Supervisor kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl"
|
||||
"description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot Almondhoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?",
|
||||
"title": "Almond - Home Assistant kieg\u00e9sz\u00edt\u0151 \u00e1ltal"
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"step": {
|
||||
"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"
|
||||
},
|
||||
"pick_implementation": {
|
||||
|
@ -1,12 +1,26 @@
|
||||
{
|
||||
"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": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"description": "Vuelva a autenticarse con su cuenta de Ambee."
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud",
|
||||
"name": "Nombre"
|
||||
},
|
||||
"description": "Configure Ambee para que se integre con Home Assistant."
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "R\u00e9-authentification r\u00e9ussie"
|
||||
"reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"invalid_api_key": "Cl\u00e9 API non valide"
|
||||
"invalid_api_key": "Cl\u00e9 API invalide"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "cl\u00e9 API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"description": "R\u00e9-authentifiez-vous avec votre compte Ambee."
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "cl\u00e9 API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Nom"
|
||||
|
@ -21,7 +21,7 @@
|
||||
"longitude": "Hossz\u00fas\u00e1g",
|
||||
"name": "N\u00e9v"
|
||||
},
|
||||
"description": "\u00c1ll\u00edtsa be az Ambee-t a Homeassistanttal val\u00f3 integr\u00e1ci\u00f3hoz."
|
||||
"description": "Integr\u00e1lja \u00f6ssze Ambeet Home Assistanttal."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,8 @@
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Kunci API"
|
||||
"api_key": "Kunci API",
|
||||
"description": "Autentikasi ulang dengan akun Ambee Anda."
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@ -19,7 +20,8 @@
|
||||
"latitude": "Lintang",
|
||||
"longitude": "Bujur",
|
||||
"name": "Nama"
|
||||
}
|
||||
},
|
||||
"description": "Siapkan Ambee Anda untuk diintegrasikan dengan Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"ambee__risk": {
|
||||
"high": "Tinggi",
|
||||
"low": "Rendah",
|
||||
"moderate": "Sedang"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"state": {
|
||||
"ambee__risk": {
|
||||
"high": "Wysoki",
|
||||
"low": "Niski",
|
||||
"moderate": "Umiarkowany",
|
||||
"very high": "Bardzo wysoki"
|
||||
"high": "wysoki",
|
||||
"low": "niski",
|
||||
"moderate": "umiarkowany",
|
||||
"very high": "bardzo wysoki"
|
||||
}
|
||||
}
|
||||
}
|
32
homeassistant/components/amberelectric/__init__.py
Normal file
32
homeassistant/components/amberelectric/__init__.py
Normal 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
|
88
homeassistant/components/amberelectric/binary_sensor.py
Normal file
88
homeassistant/components/amberelectric/binary_sensor.py
Normal 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)
|
120
homeassistant/components/amberelectric/config_flow.py
Normal file
120
homeassistant/components/amberelectric/config_flow.py
Normal 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,
|
||||
)
|
13
homeassistant/components/amberelectric/const.py
Normal file
13
homeassistant/components/amberelectric/const.py
Normal 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"]
|
111
homeassistant/components/amberelectric/coordinator.py
Normal file
111
homeassistant/components/amberelectric/coordinator.py
Normal 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)
|
13
homeassistant/components/amberelectric/manifest.json
Normal file
13
homeassistant/components/amberelectric/manifest.json
Normal 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"
|
||||
}
|
234
homeassistant/components/amberelectric/sensor.py
Normal file
234
homeassistant/components/amberelectric/sensor.py
Normal 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)
|
22
homeassistant/components/amberelectric/strings.json
Normal file
22
homeassistant/components/amberelectric/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/amberelectric/translations/ca.json
Normal file
22
homeassistant/components/amberelectric/translations/ca.json
Normal 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
Loading…
x
Reference in New Issue
Block a user