This commit is contained in:
Franck Nijhof 2022-12-07 20:26:50 +01:00 committed by GitHub
commit 7b462e1b8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4091 changed files with 95366 additions and 33683 deletions

View File

@ -38,6 +38,7 @@ base_platforms: &base_platforms
- homeassistant/components/siren/**
- homeassistant/components/stt/**
- homeassistant/components/switch/**
- homeassistant/components/text/**
- homeassistant/components/tts/**
- homeassistant/components/update/**
- homeassistant/components/vacuum/**

View File

@ -35,6 +35,8 @@ omit =
homeassistant/components/agent_dvr/helpers.py
homeassistant/components/airnow/__init__.py
homeassistant/components/airnow/sensor.py
homeassistant/components/airq/__init__.py
homeassistant/components/airq/sensor.py
homeassistant/components/airthings/__init__.py
homeassistant/components/airthings/sensor.py
homeassistant/components/airthings_ble/__init__.py
@ -232,8 +234,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/dnsip/__init__.py
homeassistant/components/dnsip/sensor.py
homeassistant/components/dominos/*
homeassistant/components/doods/*
homeassistant/components/doorbird/__init__.py
@ -455,7 +455,7 @@ omit =
homeassistant/components/github/sensor.py
homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py
homeassistant/components/glances/__init__.py
homeassistant/components/glances/const.py
homeassistant/components/glances/sensor.py
homeassistant/components/goalfeed/*
homeassistant/components/goodwe/__init__.py
@ -483,11 +483,6 @@ omit =
homeassistant/components/habitica/__init__.py
homeassistant/components/habitica/const.py
homeassistant/components/habitica/sensor.py
homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
homeassistant/components/hangouts/intents.py
homeassistant/components/hangouts/notify.py
homeassistant/components/harman_kardon_avr/media_player.py
homeassistant/components/harmony/const.py
homeassistant/components/harmony/data.py
@ -676,8 +671,8 @@ omit =
homeassistant/components/lcn/services.py
homeassistant/components/led_ble/__init__.py
homeassistant/components/led_ble/light.py
homeassistant/components/led_ble/util.py
homeassistant/components/lg_netcast/media_player.py
homeassistant/components/lg_soundbar/__init__.py
homeassistant/components/lg_soundbar/media_player.py
homeassistant/components/lidarr/__init__.py
homeassistant/components/lidarr/coordinator.py
@ -729,6 +724,9 @@ omit =
homeassistant/components/map/*
homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/*
homeassistant/components/matter/__init__.py
homeassistant/components/matter/adapter.py
homeassistant/components/matter/entity.py
homeassistant/components/meater/__init__.py
homeassistant/components/meater/const.py
homeassistant/components/meater/sensor.py
@ -951,6 +949,8 @@ omit =
homeassistant/components/overkiz/sensor.py
homeassistant/components/overkiz/siren.py
homeassistant/components/overkiz/switch.py
homeassistant/components/overkiz/water_heater.py
homeassistant/components/overkiz/water_heater_entities/*
homeassistant/components/ovo_energy/__init__.py
homeassistant/components/ovo_energy/const.py
homeassistant/components/ovo_energy/sensor.py
@ -960,6 +960,7 @@ omit =
homeassistant/components/pencom/switch.py
homeassistant/components/philips_js/__init__.py
homeassistant/components/philips_js/diagnostics.py
homeassistant/components/philips_js/helpers.py
homeassistant/components/philips_js/light.py
homeassistant/components/philips_js/media_player.py
homeassistant/components/philips_js/remote.py
@ -999,6 +1000,7 @@ omit =
homeassistant/components/proxmoxve/*
homeassistant/components/proxy/camera.py
homeassistant/components/pulseaudio_loopback/switch.py
homeassistant/components/pushbullet/api.py
homeassistant/components/pushbullet/notify.py
homeassistant/components/pushbullet/sensor.py
homeassistant/components/pushover/notify.py
@ -1107,13 +1109,6 @@ omit =
homeassistant/components/sesame/lock.py
homeassistant/components/seven_segments/image_processing.py
homeassistant/components/seventeentrack/sensor.py
homeassistant/components/shelly/binary_sensor.py
homeassistant/components/shelly/climate.py
homeassistant/components/shelly/coordinator.py
homeassistant/components/shelly/entity.py
homeassistant/components/shelly/number.py
homeassistant/components/shelly/sensor.py
homeassistant/components/shelly/utils.py
homeassistant/components/shiftr/*
homeassistant/components/shodan/sensor.py
homeassistant/components/sia/__init__.py
@ -1147,6 +1142,7 @@ omit =
homeassistant/components/skybell/switch.py
homeassistant/components/slack/__init__.py
homeassistant/components/slack/notify.py
homeassistant/components/slack/sensor.py
homeassistant/components/slide/*
homeassistant/components/slimproto/__init__.py
homeassistant/components/slimproto/media_player.py
@ -1574,6 +1570,7 @@ omit =
homeassistant/components/yolink/coordinator.py
homeassistant/components/yolink/cover.py
homeassistant/components/yolink/entity.py
homeassistant/components/yolink/light.py
homeassistant/components/yolink/lock.py
homeassistant/components/yolink/sensor.py
homeassistant/components/yolink/siren.py

View File

@ -9,7 +9,6 @@ docs
.vscode
# Test related files
.tox
tests
# Other virtualization methods

13
.github/move.yml vendored
View File

@ -1,13 +0,0 @@
# Configuration for move-issues - https://github.com/dessant/move-issues
# Delete the command comment. Ignored when the comment also contains other content
deleteCommand: true
# Close the source issue after moving
closeSourceIssue: true
# Lock the source issue after moving
lockSourceIssue: false
# Set custom aliases for targets
# aliases:
# r: repo
# or: owner/repo

View File

@ -159,7 +159,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2022.09.0
uses: home-assistant/builder@2022.11.0
with:
args: |
$BUILD_ARGS \
@ -225,7 +225,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2022.09.0
uses: home-assistant/builder@2022.11.0
with:
args: |
$BUILD_ARGS \

View File

@ -22,7 +22,7 @@ on:
env:
CACHE_VERSION: 3
PIP_CACHE_VERSION: 3
HA_SHORT_VERSION: 2022.11
HA_SHORT_VERSION: 2022.12
DEFAULT_PYTHON: 3.9
ALL_PYTHON_VERSIONS: "['3.9', '3.10']"
PRE_COMMIT_CACHE: ~/.cache/pre-commit
@ -842,7 +842,6 @@ jobs:
python3 -X dev -m pytest \
-qq \
--timeout=9 \
--durations=10 \
-n auto \
--cov="homeassistant.components.${{ matrix.group }}" \
--cov-report=xml \
@ -936,7 +935,7 @@ jobs:
. venv/bin/activate
pip install mysqlclient sqlalchemy_utils
- name: Run pytest (partially)
timeout-minutes: 10
timeout-minutes: 15
shell: bash
run: |
. venv/bin/activate
@ -944,14 +943,13 @@ jobs:
python3 -X dev -m pytest \
-qq \
--timeout=9 \
--timeout=20 \
-n 1 \
--cov="homeassistant.components.recorder" \
--cov-report=xml \
--cov-report=term-missing \
-o console_output_style=count \
--durations=0 \
--durations-min=10 \
--durations=10 \
-p no:sugar \
--dburl=mysql://root:password@127.0.0.1/homeassistant-test \
tests/components/recorder

1
.gitignore vendored
View File

@ -58,7 +58,6 @@ pip-log.txt
# Unit test / coverage reports
.coverage
.tox
coverage.xml
nosetests.xml
htmlcov/

View File

@ -1,9 +1,16 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v3.1.0
rev: v3.2.2
hooks:
- id: pyupgrade
args: [--py39-plus]
- repo: https://github.com/PyCQA/autoflake
rev: v2.0.0
hooks:
- id: autoflake
args:
- --in-place
- --remove-all-unused-imports
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
@ -13,27 +20,27 @@ repos:
- --quiet
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v2.1.0
rev: v2.2.2
hooks:
- id: codespell
args:
- --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,iif,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa,pullrequests
- --ignore-words-list=additionals,alot,ba,bre,bund,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types: [csv, json]
exclude: ^tests/fixtures/|homeassistant/generated/
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies:
- pycodestyle==2.8.0
- pyflakes==2.4.0
- pycodestyle==2.10.0
- pyflakes==3.0.1
- flake8-docstrings==1.6.0
- pydocstyle==6.1.1
- flake8-comprehensions==3.10.0
- flake8-noqa==1.2.8
- mccabe==0.6.1
- flake8-comprehensions==3.10.1
- flake8-noqa==1.3.0
- mccabe==0.7.0
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/bandit
rev: 1.7.4
@ -65,7 +72,7 @@ repos:
hooks:
- id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.6.1
rev: v2.7.1
hooks:
- id: prettier
- repo: https://github.com/cdce8p/python-typing-update

View File

@ -5,14 +5,10 @@
# Strict typing is enabled by default for core files.
# Add it here to add 'disallow_any_generics'.
# --- Only for core file! ---
homeassistant.exceptions
homeassistant.core
homeassistant.loader
homeassistant.requirements
homeassistant.runner
homeassistant.setup
homeassistant.auth.auth_store
homeassistant.auth.providers.*
homeassistant.core
homeassistant.exceptions
homeassistant.helpers.area_registry
homeassistant.helpers.condition
homeassistant.helpers.debounce
@ -29,6 +25,10 @@ homeassistant.helpers.script_variables
homeassistant.helpers.singleton
homeassistant.helpers.sun
homeassistant.helpers.translation
homeassistant.loader
homeassistant.requirements
homeassistant.runner
homeassistant.setup
homeassistant.util.async_
homeassistant.util.color
homeassistant.util.decorator
@ -79,13 +79,14 @@ homeassistant.components.button.*
homeassistant.components.calendar.*
homeassistant.components.camera.*
homeassistant.components.canary.*
homeassistant.components.cover.*
homeassistant.components.clickatell.*
homeassistant.components.clicksend.*
homeassistant.components.cover.*
homeassistant.components.cpuspeed.*
homeassistant.components.crownstone.*
homeassistant.components.deconz.*
homeassistant.components.demo.*
homeassistant.components.derivative.*
homeassistant.components.device_automation.*
homeassistant.components.device_tracker.*
homeassistant.components.devolo_home_control.*
@ -151,6 +152,7 @@ homeassistant.components.http.*
homeassistant.components.huawei_lte.*
homeassistant.components.hyperion.*
homeassistant.components.ibeacon.*
homeassistant.components.image.*
homeassistant.components.image_processing.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
@ -173,17 +175,21 @@ homeassistant.components.litterrobot.*
homeassistant.components.local_ip.*
homeassistant.components.lock.*
homeassistant.components.logbook.*
homeassistant.components.logger.*
homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.mailbox.*
homeassistant.components.matter.*
homeassistant.components.media_player.*
homeassistant.components.media_source.*
homeassistant.components.metoffice.*
homeassistant.components.mikrotik.*
homeassistant.components.min_max.*
homeassistant.components.mjpeg.*
homeassistant.components.modbus.*
homeassistant.components.modem_callerid.*
homeassistant.components.moon.*
homeassistant.components.mqtt.*
homeassistant.components.mysensors.*
homeassistant.components.nam.*
homeassistant.components.nanoleaf.*
@ -191,6 +197,7 @@ homeassistant.components.neato.*
homeassistant.components.nest.*
homeassistant.components.netatmo.*
homeassistant.components.network.*
homeassistant.components.nextdns.*
homeassistant.components.nfandroidtv.*
homeassistant.components.nissan_leaf.*
homeassistant.components.no_ip.*
@ -213,9 +220,9 @@ homeassistant.components.prusalink.*
homeassistant.components.pure_energie.*
homeassistant.components.pvoutput.*
homeassistant.components.qnap_qsw.*
homeassistant.components.radarr.*
homeassistant.components.rainmachine.*
homeassistant.components.rdw.*
homeassistant.components.radarr.*
homeassistant.components.recollect_waste.*
homeassistant.components.recorder.*
homeassistant.components.remote.*
@ -228,12 +235,14 @@ homeassistant.components.rituals_perfume_genie.*
homeassistant.components.roku.*
homeassistant.components.rpi_power.*
homeassistant.components.rtsp_to_webrtc.*
homeassistant.components.ruuvitag_ble.*
homeassistant.components.samsungtv.*
homeassistant.components.scene.*
homeassistant.components.schedule.*
homeassistant.components.select.*
homeassistant.components.senseme.*
homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.*
homeassistant.components.sensor.*
homeassistant.components.senz.*
homeassistant.components.shelly.*
@ -283,6 +292,7 @@ homeassistant.components.vacuum.*
homeassistant.components.vallox.*
homeassistant.components.velbus.*
homeassistant.components.vlc_telnet.*
homeassistant.components.wake_on_lan.*
homeassistant.components.wallbox.*
homeassistant.components.water_heater.*
homeassistant.components.watttime.*

View File

@ -45,6 +45,8 @@ build.json @home-assistant/supervisor
/tests/components/airly/ @bieniu
/homeassistant/components/airnow/ @asymworks
/tests/components/airnow/ @asymworks
/homeassistant/components/airq/ @Sibgatulin @dl2080
/tests/components/airq/ @Sibgatulin @dl2080
/homeassistant/components/airthings/ @danielhiversen
/tests/components/airthings/ @danielhiversen
/homeassistant/components/airthings_ble/ @vincegio
@ -61,8 +63,8 @@ build.json @home-assistant/supervisor
/tests/components/alarm_control_panel/ @home-assistant/core
/homeassistant/components/alert/ @home-assistant/core @frenck
/tests/components/alert/ @home-assistant/core @frenck
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy
/tests/components/alexa/ @home-assistant/cloud @ochlocracy
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/almond/ @gcampax @balloob
/tests/components/almond/ @gcampax @balloob
/homeassistant/components/amberelectric/ @madpilot
@ -94,6 +96,8 @@ build.json @home-assistant/supervisor
/tests/components/apprise/ @caronc
/homeassistant/components/aprs/ @PhilRW
/tests/components/aprs/ @PhilRW
/homeassistant/components/aranet/ @aschmitz
/tests/components/aranet/ @aschmitz
/homeassistant/components/arcam_fmj/ @elupus
/tests/components/arcam_fmj/ @elupus
/homeassistant/components/arris_tg2492lg/ @vanbalken
@ -477,6 +481,8 @@ build.json @home-assistant/supervisor
/tests/components/homeassistant/ @home-assistant/core
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
/tests/components/homeassistant_alerts/ @home-assistant/core
/homeassistant/components/homeassistant_hardware/ @home-assistant/core
/tests/components/homeassistant_hardware/ @home-assistant/core
/homeassistant/components/homeassistant_sky_connect/ @home-assistant/core
/tests/components/homeassistant_sky_connect/ @home-assistant/core
/homeassistant/components/homeassistant_yellow/ @home-assistant/core
@ -631,6 +637,10 @@ build.json @home-assistant/supervisor
/tests/components/litejet/ @joncar
/homeassistant/components/litterrobot/ @natekspencer @tkdrob
/tests/components/litterrobot/ @natekspencer @tkdrob
/homeassistant/components/livisi/ @StefanIacobLivisi
/tests/components/livisi/ @StefanIacobLivisi
/homeassistant/components/local_calendar/ @allenporter
/tests/components/local_calendar/ @allenporter
/homeassistant/components/local_ip/ @issacg
/tests/components/local_ip/ @issacg
/homeassistant/components/lock/ @home-assistant/core
@ -649,13 +659,15 @@ build.json @home-assistant/supervisor
/homeassistant/components/luftdaten/ @fabaff @frenck
/tests/components/luftdaten/ @fabaff @frenck
/homeassistant/components/lupusec/ @majuss
/homeassistant/components/lutron/ @JonGilmore
/homeassistant/components/lutron/ @cdheiser
/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues
/tests/components/lutron_caseta/ @swails @bdraco @danaues
/homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001
/homeassistant/components/mastodon/ @fabaff
/homeassistant/components/matrix/ @tinloaf
/homeassistant/components/matter/ @MartinHjelmare @marcelveldt
/tests/components/matter/ @MartinHjelmare @marcelveldt
/homeassistant/components/mazda/ @bdr99
/tests/components/mazda/ @bdr99
/homeassistant/components/meater/ @Sotolotl @emontnemery
@ -687,8 +699,8 @@ build.json @home-assistant/supervisor
/tests/components/mikrotik/ @engrbm87
/homeassistant/components/mill/ @danielhiversen
/tests/components/mill/ @danielhiversen
/homeassistant/components/min_max/ @fabaff
/tests/components/min_max/ @fabaff
/homeassistant/components/min_max/ @gjohansson-ST
/tests/components/min_max/ @gjohansson-ST
/homeassistant/components/minecraft_server/ @elmurato
/tests/components/minecraft_server/ @elmurato
/homeassistant/components/minio/ @tkislan
@ -713,8 +725,8 @@ build.json @home-assistant/supervisor
/tests/components/motion_blinds/ @starkillerOG
/homeassistant/components/motioneye/ @dermotduffy
/tests/components/motioneye/ @dermotduffy
/homeassistant/components/mqtt/ @emontnemery
/tests/components/mqtt/ @emontnemery
/homeassistant/components/mqtt/ @emontnemery @jbouwh
/tests/components/mqtt/ @emontnemery @jbouwh
/homeassistant/components/msteams/ @peroyvind
/homeassistant/components/mullvad/ @meichthys
/tests/components/mullvad/ @meichthys
@ -776,6 +788,8 @@ build.json @home-assistant/supervisor
/tests/components/nsw_fuel_station/ @nickw444
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
/tests/components/nsw_rural_fire_service_feed/ @exxamalte
/homeassistant/components/nuheat/ @tstabrawa
/tests/components/nuheat/ @tstabrawa
/homeassistant/components/nuki/ @pschmitt @pvizeli @pree
/tests/components/nuki/ @pschmitt @pvizeli @pree
/homeassistant/components/numato/ @clssn
@ -878,6 +892,8 @@ build.json @home-assistant/supervisor
/tests/components/pure_energie/ @klaasnicolaas
/homeassistant/components/push/ @dgomes
/tests/components/push/ @dgomes
/homeassistant/components/pushbullet/ @engrbm87
/tests/components/pushbullet/ @engrbm87
/homeassistant/components/pushover/ @engrbm87
/tests/components/pushover/ @engrbm87
/homeassistant/components/pvoutput/ @frenck
@ -955,6 +971,8 @@ build.json @home-assistant/supervisor
/tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @gabe565
/tests/components/ruckus_unleashed/ @gabe565
/homeassistant/components/ruuvitag_ble/ @akx
/tests/components/ruuvitag_ble/ @akx
/homeassistant/components/sabnzbd/ @shaiu
/tests/components/sabnzbd/ @shaiu
/homeassistant/components/safe_mode/ @home-assistant/core
@ -985,6 +1003,8 @@ build.json @home-assistant/supervisor
/tests/components/senseme/ @mikelawrence @bdraco
/homeassistant/components/sensibo/ @andrey-git @gjohansson-ST
/tests/components/sensibo/ @andrey-git @gjohansson-ST
/homeassistant/components/sensirion_ble/ @akx
/tests/components/sensirion_ble/ @akx
/homeassistant/components/sensor/ @home-assistant/core
/tests/components/sensor/ @home-assistant/core
/homeassistant/components/sensorpro/ @bdraco
@ -1001,8 +1021,8 @@ build.json @home-assistant/supervisor
/tests/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10
/homeassistant/components/shell_command/ @home-assistant/core
/tests/components/shell_command/ @home-assistant/core
/homeassistant/components/shelly/ @balloob @bieniu @thecode @chemelli74
/tests/components/shelly/ @balloob @bieniu @thecode @chemelli74
/homeassistant/components/shelly/ @balloob @bieniu @thecode @chemelli74 @bdraco
/tests/components/shelly/ @balloob @bieniu @thecode @chemelli74 @bdraco
/homeassistant/components/shodan/ @fabaff
/homeassistant/components/sia/ @eavanvalkenburg
/tests/components/sia/ @eavanvalkenburg
@ -1121,8 +1141,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/synology_srm/ @aerialls
/homeassistant/components/system_bridge/ @timmo001
/tests/components/system_bridge/ @timmo001
/homeassistant/components/tado/ @michaelarnauts @north3221
/tests/components/tado/ @michaelarnauts @north3221
/homeassistant/components/tado/ @michaelarnauts
/tests/components/tado/ @michaelarnauts
/homeassistant/components/tag/ @balloob @dmulcahey
/tests/components/tag/ @balloob @dmulcahey
/homeassistant/components/tailscale/ @frenck
@ -1140,6 +1160,8 @@ build.json @home-assistant/supervisor
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
/homeassistant/components/tesla_wall_connector/ @einarhauks
/tests/components/tesla_wall_connector/ @einarhauks
/homeassistant/components/text/ @home-assistant/core
/tests/components/text/ @home-assistant/core
/homeassistant/components/tfiac/ @fredrike @mellado
/homeassistant/components/thermobeacon/ @bdraco
/tests/components/thermobeacon/ @bdraco

View File

@ -1,11 +1,11 @@
image: homeassistant/{arch}-homeassistant
shadow_repository: ghcr.io/home-assistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.10.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.10.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.10.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.10.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.10.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.11.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.11.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.11.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.11.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.11.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@ -89,11 +89,21 @@ def get_arguments() -> argparse.Namespace:
parser.add_argument(
"--open-ui", action="store_true", help="Open the webinterface in a browser"
)
parser.add_argument(
skip_pip_group = parser.add_mutually_exclusive_group()
skip_pip_group.add_argument(
"--skip-pip",
action="store_true",
help="Skips pip install of required packages on startup",
)
skip_pip_group.add_argument(
"--skip-pip-packages",
metavar="package_names",
type=lambda arg: arg.split(","),
default=[],
help="Skip pip install of specific packages on startup",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
)
@ -180,6 +190,7 @@ def main() -> int:
log_file=args.log_file,
log_no_color=args.log_no_color,
skip_pip=args.skip_pip,
skip_pip_packages=args.skip_pip_packages,
safe_mode=args.safe_mode,
debug=args.debug,
open_ui=args.open_ui,

View File

@ -356,8 +356,7 @@ class AuthManager:
provider = self._async_get_auth_provider(credentials)
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
# https://github.com/python/mypy/issues/1424
await provider.async_will_remove_credentials(credentials) # type: ignore[attr-defined]
await provider.async_will_remove_credentials(credentials)
await self._store.async_remove_credentials(credentials)

View File

@ -166,7 +166,6 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul
processed = hass.data[DATA_REQS] = set()
# https://github.com/python/mypy/issues/1424
await requirements.async_process_requirements(
hass, module_path, module.REQUIREMENTS
)

View File

@ -47,7 +47,7 @@ def _lookup_domain(
perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str
) -> ValueType | None:
"""Look up entity permissions by domain."""
return domains_dict.get(entity_id.split(".", 1)[0])
return domains_dict.get(entity_id.partition(".")[0])
def _lookup_area(

View File

@ -250,9 +250,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
auth_module, "async_initialize_login_mfa_step"
):
try:
await auth_module.async_initialize_login_mfa_step( # type: ignore[attr-defined]
self.user.id
)
await auth_module.async_initialize_login_mfa_step(self.user.id)
except HomeAssistantError:
_LOGGER.exception("Error initializing MFA step")
return self.async_abort(reason="unknown_error")

View File

@ -88,12 +88,12 @@ class CommandLineAuthProvider(AuthProvider):
for _line in stdout.splitlines():
try:
line = _line.decode().lstrip()
if line.startswith("#"):
continue
key, value = line.split("=", 1)
except ValueError:
# malformed line
continue
if line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip()
if key in self.ALLOWED_META_KEYS:

View File

@ -17,7 +17,7 @@ import voluptuous as vol
import yarl
from . import config as conf_util, config_entries, core, loader
from .components import http, persistent_notification
from .components import http
from .const import (
REQUIRED_NEXT_PYTHON_HA_RELEASE,
REQUIRED_NEXT_PYTHON_VER,
@ -53,6 +53,7 @@ ERROR_LOG_FILENAME = "home-assistant.log"
# hass.data key for logging information.
DATA_LOGGING = "logging"
DATA_REGISTRIES_LOADED = "bootstrap_registries_loaded"
LOG_SLOW_STARTUP_INTERVAL = 60
SLOW_STARTUP_CHECK_INTERVAL = 1
@ -117,7 +118,8 @@ async def async_setup_hass(
)
hass.config.skip_pip = runtime_config.skip_pip
if runtime_config.skip_pip:
hass.config.skip_pip_packages = runtime_config.skip_pip_packages
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
_LOGGER.warning(
"Skipping pip installation of required modules. This may cause issues"
)
@ -175,6 +177,7 @@ async def async_setup_hass(
if old_logging:
hass.data[DATA_LOGGING] = old_logging
hass.config.skip_pip = old_config.skip_pip
hass.config.skip_pip_packages = old_config.skip_pip_packages
hass.config.internal_url = old_config.internal_url
hass.config.external_url = old_config.external_url
hass.config.config_dir = old_config.config_dir
@ -216,6 +219,32 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
)
async def load_registries(hass: core.HomeAssistant) -> None:
"""Load the registries and cache the result of platform.uname().processor."""
if DATA_REGISTRIES_LOADED in hass.data:
return
hass.data[DATA_REGISTRIES_LOADED] = None
def _cache_uname_processor() -> None:
"""Cache the result of platform.uname().processor in the executor.
Multiple modules call this function at startup which
executes a blocking subprocess call. This is a problem for the
asyncio event loop. By primeing the cache of uname we can
avoid the blocking call in the event loop.
"""
platform.uname().processor # pylint: disable=expression-not-assigned
# Load the registries and cache the result of platform.uname().processor
await asyncio.gather(
area_registry.async_load(hass),
device_registry.async_load(hass),
entity_registry.async_load(hass),
issue_registry.async_load(hass),
hass.async_add_executor_job(_cache_uname_processor),
)
async def async_from_config_dict(
config: ConfigType, hass: core.HomeAssistant
) -> core.HomeAssistant | None:
@ -228,6 +257,7 @@ async def async_from_config_dict(
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_initialize()
await load_registries(hass)
# Set up core.
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
@ -268,16 +298,31 @@ async def async_from_config_dict(
REQUIRED_NEXT_PYTHON_HA_RELEASE
and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER
):
msg = (
"Support for the running Python version "
f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will "
f"be removed in Home Assistant {REQUIRED_NEXT_PYTHON_HA_RELEASE}. "
"Please upgrade Python to "
f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])}."
current_python_version = ".".join(str(x) for x in sys.version_info[:3])
required_python_version = ".".join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])
_LOGGER.warning(
(
"Support for the running Python version %s is deprecated and "
"will be removed in Home Assistant %s; "
"Please upgrade Python to %s"
),
current_python_version,
REQUIRED_NEXT_PYTHON_HA_RELEASE,
required_python_version,
)
_LOGGER.warning(msg)
persistent_notification.async_create(
hass, msg, "Python version", "python_version"
issue_registry.async_create_issue(
hass,
core.DOMAIN,
"python_version",
is_fixable=False,
severity=issue_registry.IssueSeverity.WARNING,
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,
translation_key="python_version",
translation_placeholders={
"current_python_version": current_python_version,
"required_python_version": required_python_version,
"breaks_in_ha_version": REQUIRED_NEXT_PYTHON_HA_RELEASE,
},
)
return hass
@ -404,7 +449,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
"""Get domains of components to set up."""
# Filter out the repeating and common config section [homeassistant]
domains = {key.split(" ")[0] for key in config if key != core.DOMAIN}
domains = {key.partition(" ")[0] for key in config if key != core.DOMAIN}
# Add config entry domains
if not hass.config.safe_mode:
@ -515,25 +560,6 @@ async def _async_set_up_integrations(
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
def _cache_uname_processor() -> None:
"""Cache the result of platform.uname().processor in the executor.
Multiple modules call this function at startup which
executes a blocking subprocess call. This is a problem for the
asyncio event loop. By primeing the cache of uname we can
avoid the blocking call in the event loop.
"""
platform.uname().processor # pylint: disable=expression-not-assigned
# Load the registries and cache the result of platform.uname().processor
await asyncio.gather(
area_registry.async_load(hass),
device_registry.async_load(hass),
entity_registry.async_load(hass),
issue_registry.async_load(hass),
hass.async_add_executor_job(_cache_uname_processor),
)
# Initialize recorder
if "recorder" in domains_to_setup:
recorder.async_initialize_recorder(hass)

View File

@ -14,7 +14,6 @@
"google",
"nest",
"cast",
"hangouts",
"dialogflow"
]
}

View File

@ -0,0 +1,5 @@
{
"domain": "yamaha",
"name": "Yamaha",
"integrations": ["yamaha", "yamaha_musiccast"]
}

View File

@ -1,4 +1,7 @@
"""Support for Abode Security System binary sensors."""
from __future__ import annotations
from contextlib import suppress
from typing import cast
from abodepy.devices.binary_sensor import AbodeBinarySensor as ABBinarySensor
@ -47,8 +50,10 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorEntity):
return cast(bool, self._device.is_on)
@property
def device_class(self) -> str:
def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the class of the binary sensor."""
if self._device.get_value("is_window") == "1":
return BinarySensorDeviceClass.WINDOW
return cast(str, self._device.generic_type)
with suppress(ValueError):
return BinarySensorDeviceClass(cast(str, self._device.generic_type))
return None

View File

@ -116,8 +116,3 @@ class AbodeLight(AbodeDevice, LightEntity):
if self._device.is_dimmable:
return {ColorMode.BRIGHTNESS}
return {ColorMode.ONOFF}
@property
def supported_features(self) -> int:
"""Flag supported features."""
return 0

View File

@ -1,7 +1,7 @@
{
"config": {
"abort": {
"reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e",
"reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430",
"single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode."
},
"error": {

View File

@ -1,21 +1,34 @@
{
"config": {
"abort": {
"reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9"
"reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9",
"single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia."
},
"error": {
"invalid_auth": "Neplatn\u00e9 overenie"
"cannot_connect": "Nepodarilo sa pripoji\u0165",
"invalid_auth": "Neplatn\u00e9 overenie",
"invalid_mfa_code": "Neplatn\u00fd k\u00f3d MFA"
},
"step": {
"mfa": {
"data": {
"mfa_code": "K\u00f3d MFA (6-miestny)"
},
"title": "Zadajte svoj k\u00f3d MFA pre Abode"
},
"reauth_confirm": {
"data": {
"password": "Heslo",
"username": "Email"
}
},
"title": "Vypl\u0148te svoje prihlasovacie \u00fadaje do slu\u017eby Abode"
},
"user": {
"data": {
"password": "Heslo",
"username": "Email"
}
},
"title": "Vypl\u0148te svoje prihlasovacie \u00fadaje Abode"
}
}
}

View File

@ -17,9 +17,22 @@ from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
)
from .const import CONF_FORECAST, DOMAIN
OPTIONS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_FORECAST, default=False): bool,
}
)
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
}
class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for AccuWeather."""
@ -84,41 +97,6 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> AccuWeatherOptionsFlowHandler:
def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler:
"""Options callback for AccuWeather."""
return AccuWeatherOptionsFlowHandler(config_entry)
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
"""Config flow options for AccuWeather."""
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize AccuWeather options flow."""
self.config_entry = entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Optional(
CONF_FORECAST,
default=self.config_entry.options.get(CONF_FORECAST, False),
): bool
}
),
)
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)

View File

@ -253,7 +253,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
name="Cloud ceiling",
state_class=SensorStateClass.MEASUREMENT,
unit_fn=lambda metric: LENGTH_METERS if metric else LENGTH_FEET,
value_fn=lambda data, unit: round(data[unit][ATTR_VALUE]),
value_fn=lambda data, unit: round(cast(float, data[unit][ATTR_VALUE])),
),
AccuWeatherSensorDescription(
key="CloudCover",

View File

@ -24,7 +24,7 @@
},
"options": {
"step": {
"user": {
"init": {
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.",
"data": {
"forecast": "Weather forecast"

View File

@ -20,6 +20,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e"
},
"description": "\u041f\u043e\u0440\u0430\u0434\u0438 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u0442\u0430 \u043d\u0430 \u0431\u0435\u0437\u043f\u043b\u0430\u0442\u043d\u0430\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 API \u043a\u043b\u044e\u0447\u0430 \u043d\u0430 AccuWeather, \u043a\u043e\u0433\u0430\u0442\u043e \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430\u0442\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438\u0442\u0435 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438 \u0449\u0435 \u0441\u0435 \u0438\u0437\u0432\u044a\u0440\u0448\u0432\u0430\u0442 \u043d\u0430 \u0432\u0441\u0435\u043a\u0438 80 \u043c\u0438\u043d\u0443\u0442\u0438 \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0430 \u0432\u0441\u0435\u043a\u0438 40 \u043c\u0438\u043d\u0443\u0442\u0438."
},
"user": {
"data": {
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e"

View File

@ -24,6 +24,11 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Previsi\u00f3 meteorol\u00f2gica"
}
},
"user": {
"data": {
"forecast": "Previsi\u00f3 meteorol\u00f2gica"

View File

@ -21,6 +21,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed"
},
"description": "Vzhledem k omezen\u00edm bezplatn\u00e9 verze kl\u00ed\u010de AccuWeather API, kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, aktualizace dat se budou prov\u00e1d\u011bt ka\u017ed\u00fdch 80 minut m\u00edsto ka\u017ed\u00fdch 40 minut."
},
"user": {
"data": {
"forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed"

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Wettervorhersage"
},
"description": "Aufgrund der Einschr\u00e4nkungen der kostenlosen Version des AccuWeather API-Schl\u00fcssels werden bei aktivierter Wettervorhersage Datenaktualisierungen alle 80 Minuten statt alle 40 Minuten durchgef\u00fchrt."
},
"user": {
"data": {
"forecast": "Wettervorhersage"

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "\u03a0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd"
},
"description": "\u039b\u03cc\u03b3\u03c9 \u03c4\u03c9\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ce\u03bd \u03c4\u03b7\u03c2 \u03b4\u03c9\u03c1\u03b5\u03ac\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd AccuWeather API, \u03cc\u03c4\u03b1\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd, \u03bf\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b8\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 80 \u03bb\u03b5\u03c0\u03c4\u03ac \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 40 \u03bb\u03b5\u03c0\u03c4\u03ac."
},
"user": {
"data": {
"forecast": "\u03a0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd"

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Weather forecast"
},
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes."
},
"user": {
"data": {
"forecast": "Weather forecast"

View File

@ -4,7 +4,7 @@
"single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puedes habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n."
"default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puedes habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\nLa previsi\u00f3n meteorol\u00f3gica no est\u00e1 habilitada de forma predeterminada. Puedes habilitarla en las opciones de integraci\u00f3n."
},
"error": {
"cannot_connect": "No se pudo conectar",
@ -24,11 +24,17 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Previsi\u00f3n meteorol\u00f3gica"
},
"description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas la previsi\u00f3n meteorol\u00f3gica, las actualizaciones de datos se realizar\u00e1n cada 80 minutos en lugar de cada 40 minutos."
},
"user": {
"data": {
"forecast": "Pron\u00f3stico del tiempo"
"forecast": "Previsi\u00f3n meteorol\u00f3gica"
},
"description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 80 minutos en lugar de cada 40 minutos."
"description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas la previsi\u00f3n meteorol\u00f3gica, las actualizaciones de datos se realizar\u00e1n cada 80 minutos en lugar de cada 40 minutos."
}
}
},

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Ilmateade"
},
"description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 80 minuti j\u00e4rel (muidu 40 minutit)."
},
"user": {
"data": {
"forecast": "Ilmateade"

View File

@ -24,6 +24,11 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques"
}
},
"user": {
"data": {
"forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques"

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Prakiraan cuaca"
},
"description": "Karena keterbatasan versi gratis kunci API AccuWeather, ketika Anda mengaktifkan prakiraan cuaca, pembaruan data akan dilakukan setiap 80 menit, bukan setiap 40 menit."
},
"user": {
"data": {
"forecast": "Prakiraan cuaca"

View File

@ -28,7 +28,7 @@
"data": {
"forecast": "\u5929\u6c17\u4e88\u5831"
},
"description": "\u5236\u9650\u306b\u3088\u308a\u7121\u6599\u30d0\u30fc\u30b8\u30e7\u30f3\u306eAccuWeather API\u30ad\u30fc\u3067\u306f\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u3067\u306f\u306a\u304f80\u5206\u3054\u3068\u3067\u3059\u3002"
"description": "\u7121\u6599\u7248\u306eAccuWeather API\u30ad\u30fc\u306e\u5236\u9650\u306b\u3088\u308a\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u305f\u5834\u5408\u3001\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u6bce\u3067\u306f\u306a\u304f80\u5206\u6bce\u306b\u5b9f\u884c\u3055\u308c\u307e\u3059\u3002"
}
}
},

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Weersverwachting"
},
"description": "Wanneer je de weersverwachting ingeschakeld zullen updates elke 80 minuten plaatsvinden i.p.v. elke 40 minuten, dit komt door de beperkingen van de gratis versie van de AccuWeather API sleutel."
},
"user": {
"data": {
"forecast": "Weervoorspelling"

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "V\u00e6rmelding"
},
"description": "P\u00e5 grunn av begrensningene til gratisversjonen av AccuWeather API-n\u00f8kkelen, n\u00e5r du aktiverer v\u00e6rmelding, vil dataoppdateringer utf\u00f8res hvert 80. minutt i stedet for hvert 40. minutt."
},
"user": {
"data": {
"forecast": "V\u00e6rmelding"

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Previs\u00e3o do tempo"
},
"description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave API AccuWeather, quando voc\u00ea ativa a previs\u00e3o do tempo, as atualiza\u00e7\u00f5es de dados s\u00e3o realizadas a cada 80 minutos em vez de 40 minutos."
},
"user": {
"data": {
"forecast": "Previs\u00e3o do Tempo"

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b"
},
"description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 80 \u043c\u0438\u043d\u0443\u0442, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 40 \u043c\u0438\u043d\u0443\u0442."
},
"user": {
"data": {
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b"

View File

@ -0,0 +1,8 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Klesaj\u00faci",
"rising": "Padaj\u00faci"
}
}
}

View File

@ -1,7 +1,12 @@
{
"config": {
"abort": {
"single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia."
},
"error": {
"invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d"
"cannot_connect": "Nepodarilo sa pripoji\u0165",
"invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d",
"requests_exceeded": "Povolen\u00fd po\u010det po\u017eiadaviek na rozhranie Accuweather API bol prekro\u010den\u00fd. Mus\u00edte po\u010dka\u0165 alebo zmeni\u0165 k\u013e\u00fa\u010d API."
},
"step": {
"user": {
@ -13,5 +18,25 @@
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"forecast": "Predpove\u010f po\u010dasia"
}
},
"user": {
"data": {
"forecast": "Predpove\u010f po\u010dasia"
}
}
}
},
"system_health": {
"info": {
"can_reach_server": "Oslovi\u0165 server AccuWeather",
"remaining_requests": "Zost\u00e1vaj\u00face povolen\u00e9 \u017eiadosti"
}
}
}

View File

@ -1,4 +1,14 @@
{
"options": {
"step": {
"init": {
"data": {
"forecast": "\u5929\u6c14\u9884\u62a5"
},
"description": "\u7531\u4e8e AccuWeather API \u5bc6\u94a5\u514d\u8d39\u7248\u672c\u7684\u9650\u5236\uff0c\u5f53\u60a8\u542f\u7528\u5929\u6c14\u9884\u62a5\u65f6\uff0c\u6570\u636e\u66f4\u65b0\u5c06\u6bcf 80 \u5206\u949f\u6267\u884c\u4e00\u6b21\uff0c\u800c\u4e0d\u662f\u6bcf 40 \u5206\u949f\u4e00\u6b21\u3002"
}
}
},
"system_health": {
"info": {
"can_reach_server": "\u53ef\u8bbf\u95ee AccuWeather \u670d\u52a1\u5668",

View File

@ -24,6 +24,12 @@
},
"options": {
"step": {
"init": {
"data": {
"forecast": "\u5929\u6c23\u9810\u5831"
},
"description": "\u7531\u65bc AccuWeather API \u91d1\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002"
},
"user": {
"data": {
"forecast": "\u5929\u6c23\u9810\u5831"

View File

@ -18,16 +18,11 @@ from homeassistant.components.weather import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
LENGTH_INCHES,
LENGTH_KILOMETERS,
LENGTH_MILES,
LENGTH_MILLIMETERS,
PRESSURE_HPA,
PRESSURE_INHG,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
UnitOfLength,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -72,19 +67,19 @@ class AccuWeatherEntity(
# converted, hence the weather entity's native units follow the configured unit
# system
if coordinator.hass.config.units is METRIC_SYSTEM:
self._attr_native_precipitation_unit = LENGTH_MILLIMETERS
self._attr_native_pressure_unit = PRESSURE_HPA
self._attr_native_temperature_unit = TEMP_CELSIUS
self._attr_native_visibility_unit = LENGTH_KILOMETERS
self._attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
self._attr_native_pressure_unit = UnitOfPressure.HPA
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_native_visibility_unit = UnitOfLength.KILOMETERS
self._attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
self._unit_system = API_METRIC
else:
self._unit_system = API_IMPERIAL
self._attr_native_precipitation_unit = LENGTH_INCHES
self._attr_native_pressure_unit = PRESSURE_INHG
self._attr_native_temperature_unit = TEMP_FAHRENHEIT
self._attr_native_visibility_unit = LENGTH_MILES
self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.INCHES
self._attr_native_pressure_unit = UnitOfPressure.INHG
self._attr_native_temperature_unit = UnitOfTemperature.FAHRENHEIT
self._attr_native_visibility_unit = UnitOfLength.MILES
self._attr_native_wind_speed_unit = UnitOfSpeed.MILES_PER_HOUR
self._attr_unique_id = coordinator.location_key
self._attr_attribution = ATTRIBUTION
self._attr_device_info = coordinator.device_info

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
from contextlib import suppress
from typing import Any
import aiopulse
import async_timeout
@ -10,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_ID
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
@ -19,11 +21,13 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self):
def __init__(self) -> None:
"""Initialize the config flow."""
self.discovered_hubs: dict[str, aiopulse.Hub] | None = None
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
if (
user_input is not None
@ -37,7 +41,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
entry.unique_id for entry in self._async_current_entries()
}
hubs = []
hubs: list[aiopulse.Hub] = []
with suppress(asyncio.TimeoutError):
async with async_timeout.timeout(5):
async for hub in aiopulse.Hub.discover():
@ -63,7 +67,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
),
)
async def async_create(self, hub):
async def async_create(self, hub: aiopulse.Hub) -> FlowResult:
"""Create the Acmeda Hub entry."""
await self.async_set_unique_id(hub.id, raise_on_progress=False)
return self.async_create_entry(title=hub.id, data={CONF_HOST: hub.host})

View File

@ -70,9 +70,9 @@ class AcmedaCover(AcmedaBase, CoverEntity):
return position
@property
def supported_features(self) -> int:
def supported_features(self) -> CoverEntityFeature:
"""Flag supported features."""
supported_features = 0
supported_features = CoverEntityFeature(0)
if self.current_cover_position is not None:
supported_features |= (
CoverEntityFeature.OPEN

View File

@ -1,7 +1,7 @@
{
"config": {
"abort": {
"no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea"
"no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea"
},
"step": {
"user": {

View File

@ -0,0 +1,14 @@
{
"config": {
"abort": {
"no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia"
},
"step": {
"user": {
"data": {
"id": "ID hostite\u013ea"
}
}
}
}
}

View File

@ -1,7 +1,33 @@
{
"config": {
"abort": {
"already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9",
"heater_not_available": "Ohrieva\u010d nie je k dispoz\u00edcii. Sk\u00faste resetova\u0165 ohrieva\u010d stla\u010den\u00edm + a OK na nieko\u013eko sek\u00fand.",
"heater_not_found": "Ohrieva\u010d sa nena\u0161iel. Sk\u00faste presun\u00fa\u0165 ohrieva\u010d bli\u017e\u0161ie k Home Assistant.",
"invalid_auth": "Neplatn\u00e9 overenie"
},
"error": {
"cannot_connect": "Nepodarilo sa pripoji\u0165"
},
"step": {
"cloud": {
"data": {
"account_id": "ID \u00fa\u010dtu",
"password": "Heslo"
}
},
"local": {
"data": {
"wifi_pswd": "Heslo Wi-Fi",
"wifi_ssid": "Wi-Fi SSID"
}
},
"user": {
"data": {
"connection_type": "Vyberte typ pripojenia"
},
"description": "Vyberte typ pripojenia. Miestne vy\u017eaduje ohrieva\u010de s bluetooth"
}
}
}
}

View File

@ -1,9 +1,25 @@
{
"config": {
"abort": {
"already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1",
"existing_instance_updated": "Aktualizovan\u00e1 existuj\u00faca konfigur\u00e1cia."
},
"error": {
"cannot_connect": "Nepodarilo sa pripoji\u0165"
},
"step": {
"hassio_confirm": {
"description": "Chcete nakonfigurova\u0165 Home Assistant na pripojenie k AdGuard Home poskytovan\u00e9mu doplnkom: {addon}?",
"title": "AdGuard Home cez doplnok Home Assistant"
},
"user": {
"data": {
"port": "Port"
"host": "Hostite\u013e",
"password": "Heslo",
"port": "Port",
"ssl": "Pou\u017e\u00edva SSL certifik\u00e1t",
"username": "Pou\u017e\u00edvate\u013esk\u00e9 meno",
"verify_ssl": "Overi\u0165 SSL certifik\u00e1t"
}
}
}

View File

@ -1,10 +1,18 @@
{
"config": {
"abort": {
"already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9"
},
"error": {
"cannot_connect": "Nepodarilo sa pripoji\u0165"
},
"step": {
"user": {
"data": {
"ip_address": "IP adresa",
"port": "Port"
}
},
"title": "Pripoji\u0165"
}
}
}

View File

@ -8,9 +8,22 @@ from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
)
from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATION_UPDATES): bool,
}
)
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
}
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for AEMET OpenData."""
@ -54,32 +67,9 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> OptionsFlowHandler:
) -> SchemaOptionsFlowHandler:
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for AEMET."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
data_schema = vol.Schema(
{
vol.Required(
CONF_STATION_UPDATES,
default=self.config_entry.options.get(CONF_STATION_UPDATES),
): bool,
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
async def _is_aemet_api_online(hass, api_key):

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9"
},
"error": {
"invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d"
},
@ -8,7 +11,18 @@
"data": {
"api_key": "API k\u013e\u00fa\u010d",
"latitude": "Zemepisn\u00e1 \u0161\u00edrka",
"longitude": "Zemepisn\u00e1 d\u013a\u017eka"
"longitude": "Zemepisn\u00e1 d\u013a\u017eka",
"name": "N\u00e1zov integr\u00e1cie"
},
"description": "Ak chcete vygenerova\u0165 k\u013e\u00fa\u010d API, prejdite na https://opendata.aemet.es/centrodedescargas/altaUsuario"
}
}
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Zhroma\u017edi\u0165 \u00fadaje z meteorologick\u00fdch stan\u00edc AEMET"
}
}
}

View File

@ -12,10 +12,10 @@ from homeassistant.components.weather import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
LENGTH_MILLIMETERS,
PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -91,10 +91,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
"""Implementation of an AEMET OpenData sensor."""
_attr_attribution = ATTRIBUTION
_attr_native_precipitation_unit = LENGTH_MILLIMETERS
_attr_native_pressure_unit = PRESSURE_HPA
_attr_native_temperature_unit = TEMP_CELSIUS
_attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
_attr_native_pressure_unit = UnitOfPressure.HPA
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
def __init__(
self,

View File

@ -1,11 +1,16 @@
{
"config": {
"abort": {
"already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9"
},
"error": {
"already_in_progress": "Konfigur\u00e1cia u\u017e prebieha"
"already_in_progress": "Konfigur\u00e1cia u\u017e prebieha",
"cannot_connect": "Nepodarilo sa pripoji\u0165"
},
"step": {
"user": {
"data": {
"host": "Hostite\u013e",
"port": "Port"
}
}

View File

@ -21,7 +21,8 @@
},
"system_health": {
"info": {
"can_reach_server": "Lze kontaktovat Airly server"
"can_reach_server": "Lze kontaktovat Airly server",
"requests_per_day": "Povolen\u00e9 po\u017eadavky za den"
}
}
}

View File

@ -1,7 +1,11 @@
{
"config": {
"abort": {
"already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9"
},
"error": {
"invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d"
"invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d",
"wrong_location": "V tejto oblasti nie s\u00fa \u017eiadne meracie stanice Airly."
},
"step": {
"user": {
@ -10,8 +14,16 @@
"latitude": "Zemepisn\u00e1 \u0161\u00edrka",
"longitude": "Zemepisn\u00e1 d\u013a\u017eka",
"name": "N\u00e1zov"
}
},
"description": "Ak chcete vygenerova\u0165 k\u013e\u00fa\u010d API, prejdite na https://developer.airly.eu/register"
}
}
},
"system_health": {
"info": {
"can_reach_server": "Dosta\u0148te sa na server Airly",
"requests_per_day": "Povolen\u00e9 po\u017eiadavky za de\u0148",
"requests_remaining": "Zost\u00e1vaj\u00face povolen\u00e9 \u017eiadosti"
}
}
}

View File

@ -1,15 +1,23 @@
{
"config": {
"abort": {
"already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9"
},
"error": {
"invalid_auth": "Neplatn\u00e9 overenie"
"cannot_connect": "Nepodarilo sa pripoji\u0165",
"invalid_auth": "Neplatn\u00e9 overenie",
"invalid_location": "Pre t\u00fato lokalitu sa nena\u0161li \u017eiadne v\u00fdsledky",
"unknown": "Neo\u010dak\u00e1van\u00e1 chyba"
},
"step": {
"user": {
"data": {
"api_key": "API k\u013e\u00fa\u010d",
"latitude": "Zemepisn\u00e1 \u0161\u00edrka",
"longitude": "Zemepisn\u00e1 d\u013a\u017eka"
}
"longitude": "Zemepisn\u00e1 d\u013a\u017eka",
"radius": "Polomer stanice (m\u00edle; volite\u013en\u00e9)"
},
"description": "Ak chcete vygenerova\u0165 k\u013e\u00fa\u010d API, prejdite na https://docs.airnowapi.org/account/request/"
}
}
}

View File

@ -0,0 +1,78 @@
"""The air-Q integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from aioairq import AirQ
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
class AirQCoordinator(DataUpdateCoordinator):
"""Coordinator is responsible for querying the device at a specified route."""
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
) -> None:
"""Initialise a custom coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
session = async_get_clientsession(hass)
self.airq = AirQ(
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], session
)
self.device_id = entry.unique_id
assert self.device_id is not None
self.device_info = DeviceInfo(
manufacturer=MANUFACTURER,
identifiers={(DOMAIN, self.device_id)},
)
self.device_info.update(entry.data["device_info"])
async def _async_update_data(self) -> dict:
"""Fetch the data from the device."""
data = await self.airq.get(TARGET_ROUTE)
return self.airq.drop_uncertainties_from_data(data)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up air-Q from a config entry."""
coordinator = AirQCoordinator(hass, entry)
# Query the device for the first time and initialise coordinator.data
await coordinator.async_config_entry_first_refresh()
# Record the coordinator in a global store
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,84 @@
"""Config flow for air-Q integration."""
from __future__ import annotations
import logging
from typing import Any
from aioairq import AirQ, InvalidAuth, InvalidInput
from aiohttp.client_exceptions import ClientConnectionError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_IP_ADDRESS): str,
vol.Required(CONF_PASSWORD): str,
}
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for air-Q."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial (authentication) configuration step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors: dict[str, str] = {}
session = async_get_clientsession(self.hass)
try:
airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session)
except InvalidInput:
_LOGGER.debug(
"%s does not appear to be a valid IP address or mDNS name",
user_input[CONF_IP_ADDRESS],
)
errors["base"] = "invalid_input"
else:
try:
await airq.validate()
except ClientConnectionError:
_LOGGER.debug(
"Failed to connect to device %s. Check the IP address / device ID "
"as well as whether the device is connected to power and the WiFi",
user_input[CONF_IP_ADDRESS],
)
errors["base"] = "cannot_connect"
except InvalidAuth:
_LOGGER.debug(
"Incorrect password for device %s", user_input[CONF_IP_ADDRESS]
)
errors["base"] = "invalid_auth"
else:
_LOGGER.debug(
"Successfully connected to %s", user_input[CONF_IP_ADDRESS]
)
device_info = await airq.fetch_device_info()
await self.async_set_unique_id(device_info.pop("id"))
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=device_info["name"],
data=user_input | {"device_info": device_info},
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@ -0,0 +1,9 @@
"""Constants for the air-Q integration."""
from typing import Final
DOMAIN: Final = "airq"
MANUFACTURER: Final = "CorantGmbH"
TARGET_ROUTE: Final = "average"
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
UPDATE_INTERVAL: float = 10.0

View File

@ -0,0 +1,11 @@
{
"domain": "airq",
"name": "air-Q",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airq",
"requirements": ["aioairq==0.2.4"],
"codeowners": ["@Sibgatulin", "@dl2080"],
"iot_class": "local_polling",
"loggers": ["aioairq"],
"integration_type": "hub"
}

View File

@ -0,0 +1,361 @@
"""Definition of air-Q sensor platform."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Literal
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
PRESSURE_HPA,
SOUND_PRESSURE_WEIGHTED_DBA,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirQCoordinator
from .const import (
ACTIVITY_BECQUEREL_PER_CUBIC_METER,
CONCENTRATION_GRAMS_PER_CUBIC_METER,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@dataclass
class AirQEntityDescriptionMixin:
"""Class for keys required by AirQ entity."""
value: Callable[[dict], float | int | None]
@dataclass
class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin):
"""Describes AirQ sensor entity."""
# Keys must match those in the data dictionary
SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="nh3_MR100",
name="Ammonia",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("nh3_MR100"),
),
AirQEntityDescription(
key="cl2_M20",
name="Chlorine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("cl2_M20"),
),
AirQEntityDescription(
key="co",
name="CO",
device_class=SensorDeviceClass.CO,
native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("co"),
),
AirQEntityDescription(
key="co2",
name="CO2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("co2"),
),
AirQEntityDescription(
key="dewpt",
name="Dew point",
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("dewpt"),
icon="mdi:water-thermometer",
),
AirQEntityDescription(
key="ethanol",
name="Ethanol",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ethanol"),
),
AirQEntityDescription(
key="ch2o_M10",
name="Formaldehyde",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch2o_M10"),
),
AirQEntityDescription(
key="h2s",
name="H2S",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2s"),
),
AirQEntityDescription(
key="health",
name="Health Index",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:heart-pulse",
value=lambda data: data.get("health", 0.0) / 10.0,
),
AirQEntityDescription(
key="humidity",
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity"),
),
AirQEntityDescription(
key="humidity_abs",
name="Absolute humidity",
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity_abs"),
icon="mdi:water",
),
AirQEntityDescription(
key="h2_M1000",
name="Hydrogen",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2_M1000"),
),
AirQEntityDescription(
key="ch4_MIPEX",
name="Methane",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch4_MIPEX"),
),
AirQEntityDescription(
key="n2o",
name="N2O",
device_class=SensorDeviceClass.NITROUS_OXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("n2o"),
),
AirQEntityDescription(
key="no_M250",
name="NO",
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("no_M250"),
),
AirQEntityDescription(
key="no2",
name="NO2",
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("no2"),
),
AirQEntityDescription(
key="o3",
name="Ozone",
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("o3"),
),
AirQEntityDescription(
key="oxygen",
name="Oxygen",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("oxygen"),
icon="mdi:leaf",
),
AirQEntityDescription(
key="performance",
name="Performance Index",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:head-check",
value=lambda data: data.get("performance", 0.0) / 10.0,
),
AirQEntityDescription(
key="pm1",
name="PM1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm1"),
icon="mdi:dots-hexagon",
),
AirQEntityDescription(
key="pm2_5",
name="PM2.5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm2_5"),
icon="mdi:dots-hexagon",
),
AirQEntityDescription(
key="pm10",
name="PM10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm10"),
icon="mdi:dots-hexagon",
),
AirQEntityDescription(
key="pressure",
name="Pressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=PRESSURE_HPA,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pressure"),
),
AirQEntityDescription(
key="pressure_rel",
name="Relative pressure",
native_unit_of_measurement=PRESSURE_HPA,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pressure_rel"),
icon="mdi:gauge",
),
AirQEntityDescription(
key="c3h8_MIPEX",
name="Propane",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c3h8_MIPEX"),
),
AirQEntityDescription(
key="so2",
name="SO2",
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("so2"),
),
AirQEntityDescription(
key="sound",
name="Noise",
native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("sound"),
icon="mdi:ear-hearing",
),
AirQEntityDescription(
key="sound_max",
name="Noise (Maximum)",
native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("sound_max"),
icon="mdi:ear-hearing",
),
AirQEntityDescription(
key="radon",
name="Radon",
native_unit_of_measurement=ACTIVITY_BECQUEREL_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("radon"),
icon="mdi:radioactive",
),
AirQEntityDescription(
key="temperature",
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("temperature"),
),
AirQEntityDescription(
key="tvoc",
name="VOC",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("tvoc"),
),
AirQEntityDescription(
key="tvoc_ionsc",
name="VOC (Industrial)",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("tvoc_ionsc"),
),
]
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensor entities based on a config entry."""
coordinator = hass.data[DOMAIN][config.entry_id]
entities: list[AirQSensor] = []
device_status: dict[str, str] | Literal["OK"] = coordinator.data["Status"]
for description in SENSOR_TYPES:
if description.key not in coordinator.data:
if isinstance(
device_status, dict
) and "sensor still in warm up phase" in device_status.get(
description.key, "OK"
):
# warming up sensors do not contribute keys to coordinator.data
# but still must be added
_LOGGER.debug("Following sensor is warming up: %s", description.key)
else:
continue
entities.append(AirQSensor(coordinator, description))
async_add_entities(entities)
class AirQSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AirQCoordinator,
description: AirQEntityDescription,
) -> None:
"""Initialize a single sensor."""
super().__init__(coordinator)
self.entity_description: AirQEntityDescription = description
self._attr_device_info = coordinator.device_info
self._attr_name = description.name
self._attr_unique_id = f"{coordinator.device_id}_{description.key}"
self._attr_native_value = description.value(coordinator.data)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._attr_native_value = self.entity_description.value(self.coordinator.data)
self.async_write_ha_state()

View File

@ -0,0 +1,22 @@
{
"config": {
"step": {
"user": {
"title": "Identify the device",
"description": "Provide the IP address or mDNS of the device and its password",
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_input": "[%key:common::config_flow::error::invalid_host%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e"
},
"error": {
"cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435",
"invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435",
"invalid_input": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441"
},
"step": {
"user": {
"data": {
"ip_address": "IP \u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u0430"
},
"description": "\u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u0438\u043b\u0438 mDNS \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438 \u043d\u0435\u0433\u043e\u0432\u0430\u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u0430",
"title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_input": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids"
},
"step": {
"user": {
"data": {
"ip_address": "Adre\u00e7a IP",
"password": "Contrasenya"
},
"description": "Proporciona l'adre\u00e7a IP o el mDNS del dispositiu i la seva contrasenya",
"title": "Identificaci\u00f3 del dispositiu"
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno"
},
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
"invalid_input": "Neplatn\u00fd hostitel nebo IP adresa"
},
"step": {
"user": {
"data": {
"ip_address": "IP adresa",
"password": "Heslo"
}
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"invalid_input": "Ung\u00fcltiger Hostname oder IP-Adresse"
},
"step": {
"user": {
"data": {
"ip_address": "IP-Adresse",
"password": "Passwort"
},
"description": "Gib die IP-Adresse oder den mDNS des Ger\u00e4ts und sein Passwort an",
"title": "Identifizieren des Ger\u00e4ts"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af"
},
"error": {
"cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2",
"invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2",
"invalid_input": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP"
},
"step": {
"user": {
"data": {
"ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP",
"password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2"
},
"description": "\u0394\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03c4\u03bf mDNS \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c4\u03b7\u03c2",
"title": "\u03a0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"invalid_input": "Invalid hostname or IP address"
},
"step": {
"user": {
"data": {
"ip_address": "IP Address",
"password": "Password"
},
"description": "Provide the IP address or mDNS of the device and its password",
"title": "Identify the device"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado"
},
"error": {
"cannot_connect": "No se pudo conectar",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_input": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos"
},
"step": {
"user": {
"data": {
"ip_address": "Direcci\u00f3n IP",
"password": "Contrase\u00f1a"
},
"description": "Proporciona la direcci\u00f3n IP o mDNS del dispositivo y su contrase\u00f1a",
"title": "Identificar el dispositivo"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Seade on juba h\u00e4\u00e4lestatud"
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
"invalid_auth": "Tuvastamine nurjus",
"invalid_input": "Hostinimi v\u00f5i IP aadress vigane"
},
"step": {
"user": {
"data": {
"ip_address": "IP aadress",
"password": "Salas\u00f5na"
},
"description": "Sisesta seadme IP-aadress v\u00f5i mDNS ja parool",
"title": "Seadme tuvastamine"
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
"invalid_auth": "Authentification non valide",
"invalid_input": "Nom d'h\u00f4te ou adresse IP non valide"
},
"step": {
"user": {
"data": {
"ip_address": "Adresse IP",
"password": "Mot de passe"
}
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Ure\u0111aj je ve\u0107 konfiguriran"
},
"error": {
"cannot_connect": "Povezivanje nije uspjelo",
"invalid_auth": "Neva\u017ee\u0107a provjera autenti\u010dnosti",
"invalid_input": "Neva\u017ee\u0107i naziv hosta ili IP adresa"
},
"step": {
"user": {
"data": {
"ip_address": "IP adresa",
"password": "Lozinka"
},
"description": "Navedite IP adresu ili mDNS ure\u0111aja i njegovu lozinku",
"title": "Identificirajte ure\u0111aj"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"invalid_input": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm"
},
"step": {
"user": {
"data": {
"ip_address": "IP c\u00edm",
"password": "Jelsz\u00f3"
},
"description": "Adja meg az eszk\u00f6z IP-c\u00edm\u00e9t vagy mDNS c\u00edm\u00e9t \u00e9s jelszav\u00e1t.",
"title": "Az eszk\u00f6z azonos\u00edt\u00e1sa"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Perangkat sudah dikonfigurasi"
},
"error": {
"cannot_connect": "Gagal terhubung",
"invalid_auth": "Autentikasi tidak valid",
"invalid_input": "Nama host atau alamat IP tidak valid"
},
"step": {
"user": {
"data": {
"ip_address": "Alamat IP",
"password": "Kata Sandi"
},
"description": "Berikan alamat IP atau mDNS perangkat dan kata sandinya",
"title": "Identifikasi perangkat"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
"cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"invalid_input": "Nome host o indirizzo IP non valido"
},
"step": {
"user": {
"data": {
"ip_address": "Indirizzo IP",
"password": "Password"
},
"description": "Fornire l'indirizzo IP o mDNS del dispositivo e la relativa password",
"title": "Identifica il dispositivo"
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059"
},
"error": {
"cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f",
"invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c",
"invalid_input": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9"
},
"step": {
"user": {
"data": {
"ip_address": "IP\u30a2\u30c9\u30ec\u30b9",
"password": "\u30d1\u30b9\u30ef\u30fc\u30c9"
}
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
"cannot_connect": "Verbinding mislukt",
"invalid_auth": "Ongeldige authenticatie poging",
"invalid_input": "Ongeldige hostnaam of IP adres"
},
"step": {
"user": {
"data": {
"ip_address": "IP adres",
"password": "Wachtwoord"
},
"description": "Geef het IP adress of mDNS van het apparaat en het bijbehorend wachtwoord",
"title": "Identificeer het apparaat"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
"cannot_connect": "Tilkobling mislyktes",
"invalid_auth": "Ugyldig godkjenning",
"invalid_input": "Ugyldig vertsnavn eller IP-adresse"
},
"step": {
"user": {
"data": {
"ip_address": "IP adresse",
"password": "Passord"
},
"description": "Oppgi IP-adressen eller mDNS til enheten og passordet",
"title": "Identifiser enheten"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_auth": "Niepoprawne uwierzytelnienie",
"invalid_input": "Nieprawid\u0142owa nazwa hosta lub adres IP"
},
"step": {
"user": {
"data": {
"ip_address": "Adres IP",
"password": "Has\u0142o"
},
"description": "Podaj adres IP lub mDNS urz\u0105dzenia i jego has\u0142o",
"title": "Zidentyfikuj urz\u0105dzenie"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Dispositivo j\u00e1 est\u00e1 configurado"
},
"error": {
"cannot_connect": "Falha ao conectar",
"invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
"invalid_input": "Nome de host ou endere\u00e7o IP inv\u00e1lido"
},
"step": {
"user": {
"data": {
"ip_address": "Endere\u00e7o IP",
"password": "Senha"
},
"description": "Forne\u00e7a o endere\u00e7o IP ou mDNS do dispositivo e sua senha",
"title": "Identifique o dispositivo"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \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.",
"invalid_input": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441."
},
"step": {
"user": {
"data": {
"ip_address": "IP-\u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
},
"description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 mDNS \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u0435\u0433\u043e \u043f\u0430\u0440\u043e\u043b\u044c.",
"title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9"
},
"error": {
"cannot_connect": "Nepodarilo sa pripoji\u0165",
"invalid_auth": "Neplatn\u00e9 overenie",
"invalid_input": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa"
},
"step": {
"user": {
"data": {
"ip_address": "IP adresa",
"password": "Heslo"
},
"description": "Zadajte IP adresu alebo mDNS zariadenia a jeho heslo",
"title": "Identifikujte zariadenie"
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"invalid_input": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740"
},
"step": {
"user": {
"data": {
"ip_address": "IP \u4f4d\u5740",
"password": "\u5bc6\u78bc"
},
"description": "\u63d0\u4f9b\u88dd\u7f6e\u4e4b IP \u4f4d\u5740\u6216 mDNS \u53ca\u5bc6\u78bc",
"title": "\u8b58\u5225\u88dd\u7f6e"
}
}
}
}

View File

@ -1,7 +1,21 @@
{
"config": {
"abort": {
"already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd"
},
"error": {
"invalid_auth": "Neplatn\u00e9 overenie"
"cannot_connect": "Nepodarilo sa pripoji\u0165",
"invalid_auth": "Neplatn\u00e9 overenie",
"unknown": "Neo\u010dak\u00e1van\u00e1 chyba"
},
"step": {
"user": {
"data": {
"description": "Ak chcete n\u00e1js\u0165 svoje poverenia, prihl\u00e1ste sa na adrese {url}",
"id": "ID",
"secret": "Tajn\u00e9"
}
}
}
}
}

View File

@ -4,7 +4,7 @@
"already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
"already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea",
"no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
},
"flow_title": "{name}",

View File

@ -0,0 +1,18 @@
{
"config": {
"abort": {
"cannot_connect": "\u0549\u0570\u0561\u057b\u0578\u0572\u057e\u0565\u0581 \u0574\u056b\u0561\u0576\u0561\u056c",
"no_devices_found": "\u0551\u0561\u0576\u0581\u0578\u0582\u0574 \u057d\u0561\u0580\u0584\u0565\u0580 \u0579\u0565\u0576 \u0563\u057f\u0576\u057e\u0565\u056c",
"unknown": "\u0531\u0576\u057d\u057a\u0561\u057d\u0565\u056c\u056b \u057d\u056d\u0561\u056c"
},
"flow_title": "{name}",
"step": {
"bluetooth_confirm": {
"description": "\u0551\u0561\u0576\u056f\u0561\u0576\u0578\u0582\u055e\u0574 \u0565\u0584 \u056f\u0561\u0580\u0563\u0561\u057e\u0578\u0580\u0565\u056c {name}-\u0568:"
},
"user": {
"description": "\u0538\u0576\u057f\u0580\u0565\u0584 \u057d\u0561\u0580\u0584\u0568 \u056f\u0561\u0580\u0563\u0561\u057e\u0578\u0580\u0565\u056c\u0578\u0582 \u0570\u0561\u0574\u0561\u0580"
}
}
}
}

View File

@ -5,18 +5,18 @@
"already_in_progress": "Nederlands",
"cannot_connect": "Nederlands",
"no_devices_found": "Nederlands",
"unknown": "Nederlands"
"unknown": "Onverwachte fout"
},
"flow_title": "Nederlands",
"step": {
"bluetooth_confirm": {
"description": "Nederlands"
"description": "Wilt u {name} instellen?"
},
"user": {
"data": {
"address": "Nederlands"
"address": "Apparaat"
},
"description": "Nederlands"
"description": "Kies een apparaat om in te stellen"
}
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9",
"already_in_progress": "Konfigur\u00e1cia u\u017e prebieha",
"cannot_connect": "Nepodarilo sa pripoji\u0165",
"no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia",
"unknown": "Neo\u010dak\u00e1van\u00e1 chyba"
},
"flow_title": "{name}",
"step": {
"bluetooth_confirm": {
"description": "Chcete nastavi\u0165 {name}?"
},
"user": {
"data": {
"address": "Zaradenie"
},
"description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165"
}
}
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9"
},
"error": {
"cannot_connect": "Nepodarilo sa pripoji\u0165",
"no_units": "Nepodarilo sa n\u00e1js\u0165 \u017eiadne skupiny AirTouch 4."
},
"step": {
"user": {
"data": {
"host": "Hostite\u013e"
},
"title": "Nastavte podrobnosti pripojenia AirTouch 4."
}
}
}
}

View File

@ -17,7 +17,7 @@ from pyairvisual.node import NodeProError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_IP_ADDRESS,
@ -30,6 +30,10 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
)
from . import async_get_geography_id
from .const import (
@ -66,6 +70,13 @@ PICK_INTEGRATION_TYPE_SCHEMA = vol.Schema(
}
)
OPTIONS_SCHEMA = vol.Schema(
{vol.Required(CONF_SHOW_ON_MAP): bool},
)
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
}
class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an AirVisual config flow."""
@ -165,9 +176,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler:
"""Define the config flow to handle options."""
return AirVisualOptionsFlowHandler(config_entry)
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
async def async_step_geography_by_coords(
self, user_input: dict[str, str] | None = None
@ -258,30 +269,3 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_NAME:
return await self.async_step_geography_by_name()
return await self.async_step_node_pro()
class AirVisualOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle an AirVisual options flow."""
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize."""
self.entry = entry
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_SHOW_ON_MAP,
default=self.entry.options.get(CONF_SHOW_ON_MAP),
): bool
}
),
)

View File

@ -1,7 +1,7 @@
{
"config": {
"abort": {
"reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e"
"reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430"
},
"error": {
"cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435",

View File

@ -1,7 +1,20 @@
{
"state": {
"airvisual__pollutant_label": {
"co": "Oxid uho\u013enat\u00fd",
"n2": "Oxid dusi\u010dit\u00fd",
"o3": "Oz\u00f3n",
"p1": "PM10",
"p2": "PM2,5",
"s2": "Oxid siri\u010dit\u00fd"
},
"airvisual__pollutant_level": {
"good": "Dobr\u00e9"
"good": "Dobr\u00e9",
"hazardous": "Nebezpe\u010dn\u00e9",
"moderate": "Mierne",
"unhealthy": "Nezdrav\u00e9",
"unhealthy_sensitive": "Nezdrav\u00e9 pre citliv\u00e9 skupiny",
"very_unhealthy": "Ve\u013emi nezdrav\u00e9"
}
}
}

View File

@ -1,10 +1,14 @@
{
"config": {
"abort": {
"already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9 alebo ID uzla/Pro je u\u017e zaregistrovan\u00e9.",
"reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9"
},
"error": {
"invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d"
"cannot_connect": "Nepodarilo sa pripoji\u0165",
"general_error": "Neo\u010dak\u00e1van\u00e1 chyba",
"invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d",
"location_not_found": "Poloha sa nena\u0161la"
},
"step": {
"geography_by_coords": {
@ -12,17 +16,42 @@
"api_key": "API k\u013e\u00fa\u010d",
"latitude": "Zemepisn\u00e1 \u0161\u00edrka",
"longitude": "Zemepisn\u00e1 d\u013a\u017eka"
}
},
"title": "Konfigur\u00e1cia geografie"
},
"geography_by_name": {
"data": {
"api_key": "API k\u013e\u00fa\u010d"
"api_key": "API k\u013e\u00fa\u010d",
"city": "Mesto",
"country": "Krajina",
"state": "stav"
},
"title": "Konfigur\u00e1cia geografie"
},
"node_pro": {
"data": {
"ip_address": "Hostite\u013e",
"password": "Heslo"
}
},
"reauth_confirm": {
"data": {
"api_key": "API k\u013e\u00fa\u010d"
}
},
"title": "Op\u00e4tovn\u00e9 overenie AirVisual"
},
"user": {
"title": "Nakonfigurujte AirVisual"
}
}
},
"options": {
"step": {
"init": {
"data": {
"show_on_map": "Zobrazte na mape sledovan\u00fa geografiu"
},
"title": "Nakonfigurujte AirVisual"
}
}
}

View File

@ -1,6 +1,7 @@
"""Config flow for Airzone."""
from __future__ import annotations
import logging
from typing import Any
from aioairzone.const import DEFAULT_PORT, DEFAULT_SYSTEM_ID
@ -9,13 +10,16 @@ from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import dhcp
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT
from homeassistant.data_entry_flow import FlowResult
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.device_registry import format_mac
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
@ -29,9 +33,17 @@ SYSTEM_ID_SCHEMA = CONFIG_SCHEMA.extend(
)
def short_mac(addr: str) -> str:
"""Convert MAC address to short address."""
return addr.replace(":", "")[-4:].upper()
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle config flow for an Airzone device."""
_discovered_ip: str | None = None
_discovered_mac: str | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@ -60,7 +72,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
else:
if mac:
await self.async_set_unique_id(format_mac(mac))
await self.async_set_unique_id(
format_mac(mac), raise_on_progress=False
)
self._abort_if_unique_id_configured(
updates={
CONF_HOST: user_input[CONF_HOST],
@ -76,3 +90,78 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=data_schema,
errors=errors,
)
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Handle DHCP discovery."""
self._discovered_ip = discovery_info.ip
self._discovered_mac = discovery_info.macaddress
_LOGGER.debug(
"DHCP discovery detected Airzone WebServer: %s", self._discovered_mac
)
self._async_abort_entries_match({CONF_HOST: self._discovered_ip})
await self.async_set_unique_id(format_mac(self._discovered_mac))
self._abort_if_unique_id_configured()
options = ConnectionOptions(self._discovered_ip)
airzone = AirzoneLocalApi(
aiohttp_client.async_get_clientsession(self.hass), options
)
try:
await airzone.get_version()
except AirzoneError as err:
raise AbortFlow("cannot_connect") from err
return await self.async_step_discovered_connection()
async def async_step_discovered_connection(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm discovery."""
assert self._discovered_ip is not None
assert self._discovered_mac is not None
errors = {}
base_schema = {vol.Required(CONF_PORT, default=DEFAULT_PORT): int}
if user_input is not None:
airzone = AirzoneLocalApi(
aiohttp_client.async_get_clientsession(self.hass),
ConnectionOptions(
self._discovered_ip,
user_input[CONF_PORT],
user_input.get(CONF_ID, DEFAULT_SYSTEM_ID),
),
)
try:
mac = await airzone.validate()
except InvalidSystem:
base_schema[vol.Required(CONF_ID, default=1)] = int
errors[CONF_ID] = "invalid_system_id"
except AirzoneError:
errors["base"] = "cannot_connect"
else:
user_input[CONF_HOST] = self._discovered_ip
if mac is None:
mac = self._discovered_mac
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured(
updates={
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
}
)
title = f"Airzone {short_mac(mac)}"
return self.async_create_entry(title=title, data=user_input)
return self.async_show_form(
step_id="discovered_connection",
data_schema=vol.Schema(base_schema),
errors=errors,
)

View File

@ -3,8 +3,13 @@
"name": "Airzone",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone",
"requirements": ["aioairzone==0.4.8"],
"requirements": ["aioairzone==0.5.1"],
"codeowners": ["@Noltari"],
"iot_class": "local_polling",
"loggers": ["aioairzone"]
"loggers": ["aioairzone"],
"dhcp": [
{
"macaddress": "E84F25*"
}
]
}

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