Merge pull request #35828 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2020-05-20 10:14:49 +02:00 committed by GitHub
commit 70b14518d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3356 changed files with 62581 additions and 15267 deletions

View File

@ -16,11 +16,16 @@ omit =
homeassistant/components/adguard/switch.py homeassistant/components/adguard/switch.py
homeassistant/components/ads/* homeassistant/components/ads/*
homeassistant/components/aftership/sensor.py homeassistant/components/aftership/sensor.py
homeassistant/components/agent_dvr/__init__.py
homeassistant/components/agent_dvr/camera.py
homeassistant/components/agent_dvr/const.py
homeassistant/components/agent_dvr/helpers.py
homeassistant/components/airly/__init__.py homeassistant/components/airly/__init__.py
homeassistant/components/airly/air_quality.py homeassistant/components/airly/air_quality.py
homeassistant/components/airly/sensor.py homeassistant/components/airly/sensor.py
homeassistant/components/airly/const.py homeassistant/components/airly/const.py
homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/__init__.py
homeassistant/components/airvisual/air_quality.py
homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual/sensor.py
homeassistant/components/aladdin_connect/cover.py homeassistant/components/aladdin_connect/cover.py
homeassistant/components/alarmdecoder/* homeassistant/components/alarmdecoder/*
@ -57,7 +62,7 @@ omit =
homeassistant/components/aten_pe/* homeassistant/components/aten_pe/*
homeassistant/components/atome/* homeassistant/components/atome/*
homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/aurora_abb_powerone/sensor.py
homeassistant/components/automatic/device_tracker.py homeassistant/components/automatic/*
homeassistant/components/avea/light.py homeassistant/components/avea/light.py
homeassistant/components/avion/light.py homeassistant/components/avion/light.py
homeassistant/components/avri/sensor.py homeassistant/components/avri/sensor.py
@ -89,12 +94,16 @@ omit =
homeassistant/components/braviatv/const.py homeassistant/components/braviatv/const.py
homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/media_player.py
homeassistant/components/broadlink/const.py homeassistant/components/broadlink/const.py
homeassistant/components/broadlink/device.py
homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/remote.py
homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/sensor.py
homeassistant/components/broadlink/switch.py homeassistant/components/broadlink/switch.py
homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/brottsplatskartan/sensor.py
homeassistant/components/browser/* homeassistant/components/browser/*
homeassistant/components/brunt/cover.py homeassistant/components/brunt/cover.py
homeassistant/components/bsblan/__init__.py
homeassistant/components/bsblan/climate.py
homeassistant/components/bsblan/const.py
homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_home_hub_5/device_tracker.py
homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py
homeassistant/components/buienradar/sensor.py homeassistant/components/buienradar/sensor.py
@ -142,6 +151,9 @@ omit =
homeassistant/components/denon/media_player.py homeassistant/components/denon/media_player.py
homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/media_player.py
homeassistant/components/deutsche_bahn/sensor.py homeassistant/components/deutsche_bahn/sensor.py
homeassistant/components/devolo_home_control/__init__.py
homeassistant/components/devolo_home_control/const.py
homeassistant/components/devolo_home_control/switch.py
homeassistant/components/dht/sensor.py homeassistant/components/dht/sensor.py
homeassistant/components/digital_ocean/* homeassistant/components/digital_ocean/*
homeassistant/components/digitalloggers/switch.py homeassistant/components/digitalloggers/switch.py
@ -225,6 +237,9 @@ omit =
homeassistant/components/fleetgo/device_tracker.py homeassistant/components/fleetgo/device_tracker.py
homeassistant/components/flexit/climate.py homeassistant/components/flexit/climate.py
homeassistant/components/flic/binary_sensor.py homeassistant/components/flic/binary_sensor.py
homeassistant/components/flick_electric/__init__.py
homeassistant/components/flick_electric/const.py
homeassistant/components/flick_electric/sensor.py
homeassistant/components/flock/notify.py homeassistant/components/flock/notify.py
homeassistant/components/flume/* homeassistant/components/flume/*
homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/__init__.py
@ -298,6 +313,7 @@ omit =
homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hitron_coda/device_tracker.py
homeassistant/components/hive/* homeassistant/components/hive/*
homeassistant/components/hlk_sw16/* homeassistant/components/hlk_sw16/*
homeassistant/components/home_connect/*
homeassistant/components/homematic/* homeassistant/components/homematic/*
homeassistant/components/homematic/climate.py homeassistant/components/homematic/climate.py
homeassistant/components/homematic/cover.py homeassistant/components/homematic/cover.py
@ -310,7 +326,11 @@ omit =
homeassistant/components/huawei_lte/* homeassistant/components/huawei_lte/*
homeassistant/components/huawei_router/device_tracker.py homeassistant/components/huawei_router/device_tracker.py
homeassistant/components/hue/light.py homeassistant/components/hue/light.py
homeassistant/components/hunterdouglas_powerview/__init__.py
homeassistant/components/hunterdouglas_powerview/scene.py homeassistant/components/hunterdouglas_powerview/scene.py
homeassistant/components/hunterdouglas_powerview/sensor.py
homeassistant/components/hunterdouglas_powerview/cover.py
homeassistant/components/hunterdouglas_powerview/entity.py
homeassistant/components/hydrawise/* homeassistant/components/hydrawise/*
homeassistant/components/hyperion/light.py homeassistant/components/hyperion/light.py
homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/ialarm/alarm_control_panel.py
@ -343,11 +363,27 @@ omit =
homeassistant/components/iqvia/* homeassistant/components/iqvia/*
homeassistant/components/irish_rail_transport/sensor.py homeassistant/components/irish_rail_transport/sensor.py
homeassistant/components/iss/binary_sensor.py homeassistant/components/iss/binary_sensor.py
homeassistant/components/isy994/* homeassistant/components/isy994/__init__.py
homeassistant/components/isy994/binary_sensor.py
homeassistant/components/isy994/climate.py
homeassistant/components/isy994/cover.py
homeassistant/components/isy994/entity.py
homeassistant/components/isy994/fan.py
homeassistant/components/isy994/helpers.py
homeassistant/components/isy994/light.py
homeassistant/components/isy994/lock.py
homeassistant/components/isy994/sensor.py
homeassistant/components/isy994/services.py
homeassistant/components/isy994/switch.py
homeassistant/components/itach/remote.py homeassistant/components/itach/remote.py
homeassistant/components/itunes/media_player.py homeassistant/components/itunes/media_player.py
homeassistant/components/joaoapps_join/* homeassistant/components/joaoapps_join/*
homeassistant/components/juicenet/* homeassistant/components/juicenet/__init__.py
homeassistant/components/juicenet/const.py
homeassistant/components/juicenet/device.py
homeassistant/components/juicenet/entity.py
homeassistant/components/juicenet/sensor.py
homeassistant/components/juicenet/switch.py
homeassistant/components/kaiterra/* homeassistant/components/kaiterra/*
homeassistant/components/kankun/switch.py homeassistant/components/kankun/switch.py
homeassistant/components/keba/* homeassistant/components/keba/*
@ -428,6 +464,7 @@ omit =
homeassistant/components/miflora/sensor.py homeassistant/components/miflora/sensor.py
homeassistant/components/mikrotik/hub.py homeassistant/components/mikrotik/hub.py
homeassistant/components/mikrotik/device_tracker.py homeassistant/components/mikrotik/device_tracker.py
homeassistant/components/mill/__init__.py
homeassistant/components/mill/climate.py homeassistant/components/mill/climate.py
homeassistant/components/mill/const.py homeassistant/components/mill/const.py
homeassistant/components/minecraft_server/__init__.py homeassistant/components/minecraft_server/__init__.py
@ -502,7 +539,14 @@ omit =
homeassistant/components/ombi/* homeassistant/components/ombi/*
homeassistant/components/onewire/sensor.py homeassistant/components/onewire/sensor.py
homeassistant/components/onkyo/media_player.py homeassistant/components/onkyo/media_player.py
homeassistant/components/onvif/__init__.py
homeassistant/components/onvif/base.py
homeassistant/components/onvif/binary_sensor.py
homeassistant/components/onvif/camera.py homeassistant/components/onvif/camera.py
homeassistant/components/onvif/device.py
homeassistant/components/onvif/event.py
homeassistant/components/onvif/parsers.py
homeassistant/components/onvif/sensor.py
homeassistant/components/opencv/* homeassistant/components/opencv/*
homeassistant/components/openevse/sensor.py homeassistant/components/openevse/sensor.py
homeassistant/components/openexchangerates/sensor.py homeassistant/components/openexchangerates/sensor.py
@ -527,7 +571,6 @@ omit =
homeassistant/components/osramlightify/light.py homeassistant/components/osramlightify/light.py
homeassistant/components/otp/sensor.py homeassistant/components/otp/sensor.py
homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_bluray/media_player.py
homeassistant/components/panasonic_viera/__init__.py
homeassistant/components/panasonic_viera/media_player.py homeassistant/components/panasonic_viera/media_player.py
homeassistant/components/pandora/media_player.py homeassistant/components/pandora/media_player.py
homeassistant/components/pcal9535a/* homeassistant/components/pcal9535a/*
@ -569,7 +612,6 @@ omit =
homeassistant/components/qrcode/image_processing.py homeassistant/components/qrcode/image_processing.py
homeassistant/components/quantum_gateway/device_tracker.py homeassistant/components/quantum_gateway/device_tracker.py
homeassistant/components/qvr_pro/* homeassistant/components/qvr_pro/*
homeassistant/components/qwikswitch/*
homeassistant/components/rachio/* homeassistant/components/rachio/*
homeassistant/components/radarr/sensor.py homeassistant/components/radarr/sensor.py
homeassistant/components/radiotherm/climate.py homeassistant/components/radiotherm/climate.py
@ -599,7 +641,6 @@ omit =
homeassistant/components/ring/camera.py homeassistant/components/ring/camera.py
homeassistant/components/ripple/sensor.py homeassistant/components/ripple/sensor.py
homeassistant/components/rocketchat/notify.py homeassistant/components/rocketchat/notify.py
homeassistant/components/roku/remote.py
homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/binary_sensor.py
homeassistant/components/roomba/braava.py homeassistant/components/roomba/braava.py
homeassistant/components/roomba/irobot_base.py homeassistant/components/roomba/irobot_base.py
@ -608,7 +649,7 @@ omit =
homeassistant/components/roomba/vacuum.py homeassistant/components/roomba/vacuum.py
homeassistant/components/route53/* homeassistant/components/route53/*
homeassistant/components/rova/sensor.py homeassistant/components/rova/sensor.py
homeassistant/components/rpi_camera/camera.py homeassistant/components/rpi_camera/*
homeassistant/components/rpi_gpio/* homeassistant/components/rpi_gpio/*
homeassistant/components/rpi_gpio/cover.py homeassistant/components/rpi_gpio/cover.py
homeassistant/components/rpi_gpio_pwm/light.py homeassistant/components/rpi_gpio_pwm/light.py
@ -672,7 +713,6 @@ omit =
homeassistant/components/somfy/* homeassistant/components/somfy/*
homeassistant/components/somfy_mylink/* homeassistant/components/somfy_mylink/*
homeassistant/components/sonarr/sensor.py homeassistant/components/sonarr/sensor.py
homeassistant/components/songpal/*
homeassistant/components/sonos/* homeassistant/components/sonos/*
homeassistant/components/sony_projector/switch.py homeassistant/components/sony_projector/switch.py
homeassistant/components/spc/* homeassistant/components/spc/*
@ -773,6 +813,10 @@ omit =
homeassistant/components/ubus/device_tracker.py homeassistant/components/ubus/device_tracker.py
homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ue_smart_radio/media_player.py
homeassistant/components/unifiled/* homeassistant/components/unifiled/*
homeassistant/components/upb/__init__.py
homeassistant/components/upb/const.py
homeassistant/components/upb/light.py
homeassistant/components/upb/scene.py
homeassistant/components/upcloud/* homeassistant/components/upcloud/*
homeassistant/components/upnp/* homeassistant/components/upnp/*
homeassistant/components/upc_connect/* homeassistant/components/upc_connect/*
@ -816,6 +860,7 @@ omit =
homeassistant/components/webostv/* homeassistant/components/webostv/*
homeassistant/components/wemo/* homeassistant/components/wemo/*
homeassistant/components/whois/sensor.py homeassistant/components/whois/sensor.py
homeassistant/components/wiffi/*
homeassistant/components/wink/* homeassistant/components/wink/*
homeassistant/components/wirelesstag/* homeassistant/components/wirelesstag/*
homeassistant/components/worldtidesinfo/sensor.py homeassistant/components/worldtidesinfo/sensor.py
@ -829,7 +874,17 @@ omit =
homeassistant/components/xfinity/device_tracker.py homeassistant/components/xfinity/device_tracker.py
homeassistant/components/xiaomi/camera.py homeassistant/components/xiaomi/camera.py
homeassistant/components/xiaomi_aqara/* homeassistant/components/xiaomi_aqara/*
homeassistant/components/xiaomi_miio/* homeassistant/components/xiaomi_miio/__init__.py
homeassistant/components/xiaomi_miio/air_quality.py
homeassistant/components/xiaomi_miio/alarm_control_panel.py
homeassistant/components/xiaomi_miio/device_tracker.py
homeassistant/components/xiaomi_miio/fan.py
homeassistant/components/xiaomi_miio/gateway.py
homeassistant/components/xiaomi_miio/light.py
homeassistant/components/xiaomi_miio/remote.py
homeassistant/components/xiaomi_miio/sensor.py
homeassistant/components/xiaomi_miio/switch.py
homeassistant/components/xiaomi_miio/vacuum.py
homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xiaomi_tv/media_player.py
homeassistant/components/xmpp/notify.py homeassistant/components/xmpp/notify.py
homeassistant/components/xs1/* homeassistant/components/xs1/*
@ -844,8 +899,9 @@ omit =
homeassistant/components/zamg/weather.py homeassistant/components/zamg/weather.py
homeassistant/components/zengge/light.py homeassistant/components/zengge/light.py
homeassistant/components/zeroconf/* homeassistant/components/zeroconf/*
homeassistant/components/zerproc/__init__.py
homeassistant/components/zerproc/const.py
homeassistant/components/zestimate/sensor.py homeassistant/components/zestimate/sensor.py
homeassistant/components/zha/__init__.py
homeassistant/components/zha/api.py homeassistant/components/zha/api.py
homeassistant/components/zha/core/channels/* homeassistant/components/zha/core/channels/*
homeassistant/components/zha/core/const.py homeassistant/components/zha/core/const.py
@ -864,6 +920,10 @@ omit =
homeassistant/components/zoneminder/* homeassistant/components/zoneminder/*
homeassistant/components/supla/* homeassistant/components/supla/*
homeassistant/components/zwave/util.py homeassistant/components/zwave/util.py
homeassistant/components/ozw/__init__.py
homeassistant/components/ozw/discovery.py
homeassistant/components/ozw/entity.py
homeassistant/components/ozw/services.py
[report] [report]
# Regexes for lines to exclude from consideration # Regexes for lines to exclude from consideration

5
.hadolint.yaml Normal file
View File

@ -0,0 +1,5 @@
ignored:
- DL3006
- DL3008
- DL3013
- DL3018

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.1.0 rev: v2.3.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
@ -18,9 +18,9 @@ repos:
- id: codespell - id: codespell
args: args:
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.json" - --skip="./.*,*.csv,*.json"
- --quiet-level=2 - --quiet-level=2
exclude_types: [json] exclude_types: [csv, json]
- repo: https://gitlab.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9 rev: 3.7.9
hooks: hooks:

View File

@ -11,6 +11,9 @@ addons:
- libswscale-dev - libswscale-dev
- libswresample-dev - libswresample-dev
- libavfilter-dev - libavfilter-dev
sources:
- sourceline: ppa:savoury1/ffmpeg4
matrix: matrix:
fast_finish: true fast_finish: true
include: include:

View File

@ -15,6 +15,7 @@ homeassistant/scripts/check_config.py @kellerza
# Integrations # Integrations
homeassistant/components/abode/* @shred86 homeassistant/components/abode/* @shred86
homeassistant/components/adguard/* @frenck homeassistant/components/adguard/* @frenck
homeassistant/components/agent_dvr/* @ispysoftware
homeassistant/components/airly/* @bieniu homeassistant/components/airly/* @bieniu
homeassistant/components/airvisual/* @bachya homeassistant/components/airvisual/* @bachya
homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alarmdecoder/* @ajschmidt8
@ -47,12 +48,13 @@ homeassistant/components/avea/* @pattyland
homeassistant/components/avri/* @timvancann homeassistant/components/avri/* @timvancann
homeassistant/components/awair/* @danielsjf homeassistant/components/awair/* @danielsjf
homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/aws/* @awarecan @robbiet480
homeassistant/components/axis/* @kane610 homeassistant/components/axis/* @Kane610
homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/azure_event_hub/* @eavanvalkenburg
homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/azure_service_bus/* @hfurubotten
homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/beewi_smartclim/* @alemuro
homeassistant/components/bitcoin/* @fabaff homeassistant/components/bitcoin/* @fabaff
homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/bizkaibus/* @UgaitzEtxebarria
homeassistant/components/blebox/* @gadgetmobile
homeassistant/components/blink/* @fronzbot homeassistant/components/blink/* @fronzbot
homeassistant/components/bmp280/* @belidzs homeassistant/components/bmp280/* @belidzs
homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/bmw_connected_drive/* @gerard33
@ -61,6 +63,7 @@ homeassistant/components/braviatv/* @robbiet480 @bieniu
homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/broadlink/* @danielhiversen @felipediel
homeassistant/components/brother/* @bieniu homeassistant/components/brother/* @bieniu
homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/brunt/* @eavanvalkenburg
homeassistant/components/bsblan/* @liudger
homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/bt_smarthub/* @jxwolstenholme
homeassistant/components/buienradar/* @mjj4791 @ties homeassistant/components/buienradar/* @mjj4791 @ties
homeassistant/components/cast/* @emontnemery homeassistant/components/cast/* @emontnemery
@ -82,12 +85,13 @@ homeassistant/components/cpuspeed/* @fabaff
homeassistant/components/cups/* @fabaff homeassistant/components/cups/* @fabaff
homeassistant/components/daikin/* @fredrike homeassistant/components/daikin/* @fredrike
homeassistant/components/darksky/* @fabaff homeassistant/components/darksky/* @fabaff
homeassistant/components/deconz/* @kane610 homeassistant/components/deconz/* @Kane610
homeassistant/components/delijn/* @bollewolle homeassistant/components/delijn/* @bollewolle
homeassistant/components/demo/* @home-assistant/core homeassistant/components/demo/* @home-assistant/core
homeassistant/components/denonavr/* @scarface-4711 @starkillerOG homeassistant/components/denonavr/* @scarface-4711 @starkillerOG
homeassistant/components/derivative/* @afaucogney homeassistant/components/derivative/* @afaucogney
homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/device_automation/* @home-assistant/core
homeassistant/components/devolo_home_control/* @2Fake @Shutgun
homeassistant/components/digital_ocean/* @fabaff homeassistant/components/digital_ocean/* @fabaff
homeassistant/components/directv/* @ctalkington homeassistant/components/directv/* @ctalkington
homeassistant/components/discogs/* @thibmaek homeassistant/components/discogs/* @thibmaek
@ -122,9 +126,11 @@ homeassistant/components/file/* @fabaff
homeassistant/components/filter/* @dgomes homeassistant/components/filter/* @dgomes
homeassistant/components/fitbit/* @robbiet480 homeassistant/components/fitbit/* @robbiet480
homeassistant/components/fixer/* @fabaff homeassistant/components/fixer/* @fabaff
homeassistant/components/flick_electric/* @ZephireNZ
homeassistant/components/flock/* @fabaff homeassistant/components/flock/* @fabaff
homeassistant/components/flume/* @ChrisMandich @bdraco homeassistant/components/flume/* @ChrisMandich @bdraco
homeassistant/components/flunearyou/* @bachya homeassistant/components/flunearyou/* @bachya
homeassistant/components/forked_daapd/* @uvjustin
homeassistant/components/fortigate/* @kifeo homeassistant/components/fortigate/* @kifeo
homeassistant/components/fortios/* @kimfrellsen homeassistant/components/fortios/* @kimfrellsen
homeassistant/components/foscam/* @skgsergio homeassistant/components/foscam/* @skgsergio
@ -163,6 +169,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl
homeassistant/components/hisense_aehw4a1/* @bannhead homeassistant/components/hisense_aehw4a1/* @bannhead
homeassistant/components/history/* @home-assistant/core homeassistant/components/history/* @home-assistant/core
homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/hive/* @Rendili @KJonline
homeassistant/components/home_connect/* @DavidMStraub
homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco homeassistant/components/homekit/* @bdraco
homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homekit_controller/* @Jc2k
@ -174,6 +181,7 @@ homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop homeassistant/components/huawei_lte/* @scop
homeassistant/components/huawei_router/* @abmantis homeassistant/components/huawei_router/* @abmantis
homeassistant/components/hue/* @balloob homeassistant/components/hue/* @balloob
homeassistant/components/hunterdouglas_powerview/* @bdraco
homeassistant/components/iammeter/* @lewei50 homeassistant/components/iammeter/* @lewei50
homeassistant/components/iaqualink/* @flz homeassistant/components/iaqualink/* @flz
homeassistant/components/icloud/* @Quentame homeassistant/components/icloud/* @Quentame
@ -195,7 +203,7 @@ homeassistant/components/ipp/* @ctalkington
homeassistant/components/iqvia/* @bachya homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/islamic_prayer_times/* @engrbm87 homeassistant/components/islamic_prayer_times/* @engrbm87
homeassistant/components/isy994/* @bdraco homeassistant/components/isy994/* @bdraco @shbatm
homeassistant/components/izone/* @Swamp-Ig homeassistant/components/izone/* @Swamp-Ig
homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/juicenet/* @jesserockz homeassistant/components/juicenet/* @jesserockz
@ -239,7 +247,7 @@ homeassistant/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan homeassistant/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/mobile_app/* @robbiet480
homeassistant/components/modbus/* @adamchengtkc @janiversen homeassistant/components/modbus/* @adamchengtkc @janiversen
homeassistant/components/monoprice/* @etsinko homeassistant/components/monoprice/* @etsinko @OnFreund
homeassistant/components/moon/* @fabaff homeassistant/components/moon/* @fabaff
homeassistant/components/mpd/* @fabaff homeassistant/components/mpd/* @fabaff
homeassistant/components/mqtt/* @home-assistant/core @emontnemery homeassistant/components/mqtt/* @home-assistant/core @emontnemery
@ -267,6 +275,7 @@ homeassistant/components/nsw_fuel_station/* @nickw444
homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
homeassistant/components/nuheat/* @bdraco homeassistant/components/nuheat/* @bdraco
homeassistant/components/nuki/* @pvizeli homeassistant/components/nuki/* @pvizeli
homeassistant/components/numato/* @clssn
homeassistant/components/nut/* @bdraco homeassistant/components/nut/* @bdraco
homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nws/* @MatthewFlamm
homeassistant/components/nzbget/* @chriscla homeassistant/components/nzbget/* @chriscla
@ -275,13 +284,16 @@ homeassistant/components/ohmconnect/* @robbiet480
homeassistant/components/ombi/* @larssont homeassistant/components/ombi/* @larssont
homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/onboarding/* @home-assistant/core
homeassistant/components/onewire/* @garbled1 homeassistant/components/onewire/* @garbled1
homeassistant/components/onvif/* @hunterjm
homeassistant/components/openerz/* @misialq homeassistant/components/openerz/* @misialq
homeassistant/components/opengarage/* @danielhiversen
homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/opentherm_gw/* @mvn23
homeassistant/components/openuv/* @bachya homeassistant/components/openuv/* @bachya
homeassistant/components/openweathermap/* @fabaff homeassistant/components/openweathermap/* @fabaff
homeassistant/components/opnsense/* @mtreinish homeassistant/components/opnsense/* @mtreinish
homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/orangepi_gpio/* @pascallj
homeassistant/components/oru/* @bvlaicu homeassistant/components/oru/* @bvlaicu
homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare
homeassistant/components/panasonic_viera/* @joogps homeassistant/components/panasonic_viera/* @joogps
homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_custom/* @home-assistant/frontend
homeassistant/components/panel_iframe/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend
@ -289,7 +301,7 @@ homeassistant/components/pcal9535a/* @Shulyaka
homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/persistent_notification/* @home-assistant/core
homeassistant/components/philips_js/* @elupus homeassistant/components/philips_js/* @elupus
homeassistant/components/pi4ioe5v9xxxx/* @antonverburg homeassistant/components/pi4ioe5v9xxxx/* @antonverburg
homeassistant/components/pi_hole/* @fabaff @johnluetke homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn
homeassistant/components/pilight/* @trekky12 homeassistant/components/pilight/* @trekky12
homeassistant/components/plaato/* @JohNan homeassistant/components/plaato/* @JohNan
homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plant/* @ChristianKuehnel
@ -357,13 +369,14 @@ homeassistant/components/solax/* @squishykid
homeassistant/components/soma/* @ratsept homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne homeassistant/components/somfy/* @tetienne
homeassistant/components/sonarr/* @ctalkington homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti homeassistant/components/songpal/* @rytilahti @shenxn
homeassistant/components/sonos/* @amelchio homeassistant/components/sonos/* @amelchio
homeassistant/components/spaceapi/* @fabaff homeassistant/components/spaceapi/* @fabaff
homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/speedtestdotnet/* @rohankapoorcom
homeassistant/components/spider/* @peternijssen homeassistant/components/spider/* @peternijssen
homeassistant/components/spotify/* @frenck homeassistant/components/spotify/* @frenck
homeassistant/components/sql/* @dgomes homeassistant/components/sql/* @dgomes
homeassistant/components/squeezebox/* @rajlaud
homeassistant/components/starline/* @anonym-tsk homeassistant/components/starline/* @anonym-tsk
homeassistant/components/statistics/* @fabaff homeassistant/components/statistics/* @fabaff
homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stiebel_eltron/* @fucm
@ -406,12 +419,14 @@ homeassistant/components/tradfri/* @ggravlingen
homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/transmission/* @engrbm87 @JPHutchins
homeassistant/components/tts/* @pvizeli homeassistant/components/tts/* @pvizeli
homeassistant/components/tuya/* @ollo69
homeassistant/components/twentemilieu/* @frenck homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twilio_call/* @robbiet480 homeassistant/components/twilio_call/* @robbiet480
homeassistant/components/twilio_sms/* @robbiet480 homeassistant/components/twilio_sms/* @robbiet480
homeassistant/components/ubee/* @mzdrale homeassistant/components/ubee/* @mzdrale
homeassistant/components/unifi/* @kane610 homeassistant/components/unifi/* @Kane610
homeassistant/components/unifiled/* @florisvdk homeassistant/components/unifiled/* @florisvdk
homeassistant/components/upb/* @gwww
homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upc_connect/* @pvizeli
homeassistant/components/upcloud/* @scop homeassistant/components/upcloud/* @scop
homeassistant/components/updater/* @home-assistant/core homeassistant/components/updater/* @home-assistant/core
@ -436,6 +451,7 @@ homeassistant/components/weather/* @fabaff
homeassistant/components/webostv/* @bendavid homeassistant/components/webostv/* @bendavid
homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wemo/* @sqldiablo homeassistant/components/wemo/* @sqldiablo
homeassistant/components/wiffi/* @mampfes
homeassistant/components/withings/* @vangorra homeassistant/components/withings/* @vangorra
homeassistant/components/wled/* @frenck homeassistant/components/wled/* @frenck
homeassistant/components/workday/* @fabaff homeassistant/components/workday/* @fabaff
@ -455,6 +471,7 @@ homeassistant/components/yessssms/* @flowolf
homeassistant/components/yi/* @bachya homeassistant/components/yi/* @bachya
homeassistant/components/yr/* @danielhiversen homeassistant/components/yr/* @danielhiversen
homeassistant/components/zeroconf/* @robbiet480 @Kane610 homeassistant/components/zeroconf/* @robbiet480 @Kane610
homeassistant/components/zerproc/* @emlove
homeassistant/components/zha/* @dmulcahey @adminiuga homeassistant/components/zha/* @dmulcahey @adminiuga
homeassistant/components/zone/* @home-assistant/core homeassistant/components/zone/* @home-assistant/core
homeassistant/components/zoneminder/* @rohankapoorcom homeassistant/components/zoneminder/* @rohankapoorcom

View File

@ -1,7 +1,7 @@
FROM python:3.7 FROM python:3.8
RUN apt-get update \ RUN \
&& apt-get install -y --no-install-recommends \ apt-get update && apt-get install -y --no-install-recommends \
libudev-dev \ libudev-dev \
libavformat-dev \ libavformat-dev \
libavcodec-dev \ libavcodec-dev \
@ -18,8 +18,7 @@ WORKDIR /usr/src
# Setup hass-release # Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
&& cd hass-release \ && pip3 install -e hass-release/
&& pip3 install -e .
WORKDIR /workspaces WORKDIR /workspaces

View File

@ -4,9 +4,9 @@ trigger:
batch: true batch: true
branches: branches:
include: include:
- rc - rc
- dev - dev
- master - master
pr: pr:
- rc - rc
- dev - dev
@ -14,208 +14,229 @@ pr:
resources: resources:
containers: containers:
- container: 37 - container: 37
image: homeassistant/ci-azure:3.7 image: homeassistant/ci-azure:3.7
- container: 38
image: homeassistant/ci-azure:3.8
repositories: repositories:
- repository: azure - repository: azure
type: github type: github
name: 'home-assistant/ci-azure' name: "home-assistant/ci-azure"
endpoint: 'home-assistant' endpoint: "home-assistant"
variables: variables:
- name: PythonMain - name: PythonMain
value: '37' value: "37"
- name: versionHadolint
value: "v1.17.6"
stages: stages:
- stage: "Overview"
jobs:
- job: "Lint"
pool:
vmImage: "ubuntu-latest"
container: $[ variables['PythonMain'] ]
steps:
- template: templates/azp-step-cache.yaml@azure
parameters:
keyfile: "requirements_test.txt | homeassistant/package_constraints.txt"
build: |
python -m venv venv
- stage: 'Overview' . venv/bin/activate
jobs: pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
- job: 'Lint' pre-commit install-hooks
pool: - script: |
vmImage: 'ubuntu-latest' . venv/bin/activate
container: $[ variables['PythonMain'] ] pre-commit run --hook-stage manual check-executables-have-shebangs --all-files
steps: displayName: "Run executables check"
- template: templates/azp-step-cache.yaml@azure - script: |
parameters: . venv/bin/activate
keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' pre-commit run codespell --all-files
build: | displayName: "Run codespell"
python -m venv venv - script: |
. venv/bin/activate
pre-commit run flake8 --all-files
displayName: "Run flake8"
- script: |
. venv/bin/activate
pre-commit run bandit --all-files
displayName: "Run bandit"
- script: |
. venv/bin/activate
pre-commit run isort --all-files --show-diff-on-failure
displayName: "Run isort"
- script: |
. venv/bin/activate
pre-commit run check-json --all-files
displayName: "Run check-json"
- script: |
. venv/bin/activate
pre-commit run yamllint --all-files
displayName: "Run yamllint"
- script: |
. venv/bin/activate
pre-commit run pyupgrade --all-files --show-diff-on-failure
displayName: "Run pyupgrade"
# Prettier seems to hang on Azure, unknown why yet.
# Temporarily disable the check to no block PRs
# - script: |
# . venv/bin/activate
# pre-commit run prettier --all-files --show-diff-on-failure
# displayName: 'Run prettier'
- job: "Validate"
pool:
vmImage: "ubuntu-latest"
container: $[ variables['PythonMain'] ]
steps:
- template: templates/azp-step-cache.yaml@azure
parameters:
keyfile: "homeassistant/package_constraints.txt"
build: |
python -m venv venv
. venv/bin/activate . venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt pip install -e .
pre-commit install-hooks - script: |
- script: | . venv/bin/activate
. venv/bin/activate python -m script.hassfest --action validate
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files displayName: "Validate manifests"
displayName: 'Run executables check' - script: |
- script: | . venv/bin/activate
. venv/bin/activate ./script/gen_requirements_all.py validate
pre-commit run codespell --all-files displayName: "requirements_all validate"
displayName: 'Run codespell' - job: "CheckFormat"
- script: | pool:
. venv/bin/activate vmImage: "ubuntu-latest"
pre-commit run flake8 --all-files container: $[ variables['PythonMain'] ]
displayName: 'Run flake8' steps:
- script: | - template: templates/azp-step-cache.yaml@azure
. venv/bin/activate parameters:
pre-commit run bandit --all-files keyfile: "requirements_test.txt | homeassistant/package_constraints.txt"
displayName: 'Run bandit' build: |
- script: | python -m venv venv
. venv/bin/activate
pre-commit run isort --all-files --show-diff-on-failure
displayName: 'Run isort'
- script: |
. venv/bin/activate
pre-commit run check-json --all-files
displayName: 'Run check-json'
- script: |
. venv/bin/activate
pre-commit run yamllint --all-files
displayName: 'Run yamllint'
- script: |
. venv/bin/activate
pre-commit run pyupgrade --all-files --show-diff-on-failure
displayName: 'Run pyupgrade'
# Prettier seems to hang on Azure, unknown why yet.
# Temporarily disable the check to no block PRs
# - script: |
# . venv/bin/activate
# pre-commit run prettier --all-files --show-diff-on-failure
# displayName: 'Run prettier'
- job: 'Validate'
pool:
vmImage: 'ubuntu-latest'
container: $[ variables['PythonMain'] ]
steps:
- template: templates/azp-step-cache.yaml@azure
parameters:
keyfile: 'homeassistant/package_constraints.txt'
build: |
python -m venv venv
. venv/bin/activate . venv/bin/activate
pip install -e . pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
- script: | pre-commit install-hooks
. venv/bin/activate - script: |
python -m script.hassfest --action validate . venv/bin/activate
displayName: 'Validate manifests' pre-commit run black --all-files --show-diff-on-failure
- script: | displayName: "Check Black formatting"
. venv/bin/activate - job: "Docker"
./script/gen_requirements_all.py validate pool:
displayName: 'requirements_all validate' vmImage: "ubuntu-latest"
- job: 'CheckFormat' steps:
pool: - script: sudo docker pull hadolint/hadolint:$(versionHadolint)
vmImage: 'ubuntu-latest' displayName: "Install Hadolint"
container: $[ variables['PythonMain'] ] - script: |
steps: set -e
- template: templates/azp-step-cache.yaml@azure for dockerfile in Dockerfile Dockerfile.dev
parameters: do
keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' echo "Linting: $dockerfile"
build: | docker run --rm -i \
python -m venv venv -v "$(pwd)/.hadolint.yaml:/.hadolint.yaml:ro" \
hadolint/hadolint:$(versionHadolint) < "$dockerfile"
done
displayName: "Run Hadolint"
. venv/bin/activate - stage: "Tests"
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt dependsOn:
pre-commit install-hooks - "Overview"
- script: | jobs:
. venv/bin/activate - job: "PyTest"
pre-commit run black --all-files --show-diff-on-failure pool:
displayName: 'Check Black formatting' vmImage: "ubuntu-latest"
strategy:
maxParallel: 3
matrix:
Python37:
python.container: "37"
Python38:
python.container: "38"
container: $[ variables['python.container'] ]
steps:
- template: templates/azp-step-cache.yaml@azure
parameters:
keyfile: "requirements_test_all.txt | homeassistant/package_constraints.txt"
build: |
set -e
python -m venv venv
- stage: 'Tests' . venv/bin/activate
dependsOn: pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt
- 'Overview' pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt
jobs: # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
- job: 'PyTest' # Find offending deps with `pipdeptree -r -p typing`
pool: pip uninstall -y typing
vmImage: 'ubuntu-latest' - script: |
strategy: . venv/bin/activate
maxParallel: 3 pip install -e .
matrix: displayName: "Install Home Assistant"
Python37: - script: |
python.container: '37' set -e
container: $[ variables['python.container'] ]
steps:
- template: templates/azp-step-cache.yaml@azure
parameters:
keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt'
build: |
set -e
python -m venv venv
. venv/bin/activate . venv/bin/activate
pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar tests
pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt script/check_dirty
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. displayName: "Run pytest for python $(python.container)"
# Find offending deps with `pipdeptree -r -p typing` condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain']))
pip uninstall -y typing - script: |
- script: | set -e
. venv/bin/activate
pip install -e .
displayName: 'Install Home Assistant'
- script: |
set -e
. venv/bin/activate . venv/bin/activate
pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar tests pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
script/check_dirty codecov --token $(codecovToken)
displayName: 'Run pytest for python $(python.container)' script/check_dirty
condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) displayName: "Run pytest for python $(python.container) / coverage"
- script: | condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain']))
set -e
. venv/bin/activate - stage: "FullCheck"
pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests dependsOn:
codecov --token $(codecovToken) - "Overview"
script/check_dirty jobs:
displayName: 'Run pytest for python $(python.container) / coverage' - job: "Pylint"
condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) pool:
vmImage: "ubuntu-latest"
container: $[ variables['PythonMain'] ]
steps:
- template: templates/azp-step-cache.yaml@azure
parameters:
keyfile: "requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt"
build: |
set -e
python -m venv venv
- stage: 'FullCheck' . venv/bin/activate
dependsOn: pip install -U pip setuptools wheel
- 'Overview' pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
jobs: pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
- job: 'Pylint' # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
pool: # Find offending deps with `pipdeptree -r -p typing`
vmImage: 'ubuntu-latest' pip uninstall -y typing
container: $[ variables['PythonMain'] ] - script: |
steps: . venv/bin/activate
- template: templates/azp-step-cache.yaml@azure pip install -e .
parameters: displayName: "Install Home Assistant"
keyfile: 'requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt' - script: |
build: | . venv/bin/activate
set -e pylint homeassistant
python -m venv venv displayName: "Run pylint"
- job: "Mypy"
pool:
vmImage: "ubuntu-latest"
container: $[ variables['PythonMain'] ]
steps:
- template: templates/azp-step-cache.yaml@azure
parameters:
keyfile: "requirements_test.txt | setup.py | homeassistant/package_constraints.txt"
build: |
python -m venv venv
. venv/bin/activate . venv/bin/activate
pip install -U pip setuptools wheel pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt pre-commit install-hooks
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: |
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. . venv/bin/activate
# Find offending deps with `pipdeptree -r -p typing` pre-commit run mypy --all-files
pip uninstall -y typing displayName: "Run mypy"
- script: |
. venv/bin/activate
pip install -e .
displayName: 'Install Home Assistant'
- script: |
. venv/bin/activate
pylint homeassistant
displayName: 'Run pylint'
- job: 'Mypy'
pool:
vmImage: 'ubuntu-latest'
container: $[ variables['PythonMain'] ]
steps:
- template: templates/azp-step-cache.yaml@azure
parameters:
keyfile: 'requirements_test.txt | setup.py | homeassistant/package_constraints.txt'
build: |
python -m venv venv
. venv/bin/activate
pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt
pre-commit install-hooks
- script: |
. venv/bin/activate
pre-commit run mypy --all-files
displayName: 'Run mypy'

View File

@ -8,6 +8,8 @@ import sys
import threading import threading
from typing import List from typing import List
import yarl
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
@ -256,10 +258,17 @@ async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
if hass is None: if hass is None:
return 1 return 1
if args.open_ui and hass.config.api is not None: if args.open_ui:
import webbrowser # pylint: disable=import-outside-toplevel import webbrowser # pylint: disable=import-outside-toplevel
hass.add_job(webbrowser.open, hass.config.api.base_url) if hass.config.api is not None:
scheme = "https" if hass.config.api.use_ssl else "http"
url = str(
yarl.URL.build(
scheme=scheme, host="127.0.0.1", port=hass.config.api.port
)
)
hass.add_job(webbrowser.open, url)
return await hass.async_run() return await hass.async_run()

View File

@ -61,8 +61,7 @@ class CommandLineAuthProvider(AuthProvider):
"""Validate a username and password.""" """Validate a username and password."""
env = {"username": username, "password": password} env = {"username": username, "password": password}
try: try:
# pylint: disable=no-member process = await asyncio.subprocess.create_subprocess_exec( # pylint: disable=no-member
process = await asyncio.subprocess.create_subprocess_exec(
self.config[CONF_COMMAND], self.config[CONF_COMMAND],
*self.config[CONF_ARGS], *self.config[CONF_ARGS],
env=env, env=env,

View File

@ -138,8 +138,9 @@ class Data:
if not bcrypt.checkpw(password.encode(), user_hash): if not bcrypt.checkpw(password.encode(), user_hash):
raise InvalidAuth raise InvalidAuth
# pylint: disable=no-self-use def hash_password( # pylint: disable=no-self-use
def hash_password(self, password: str, for_storage: bool = False) -> bytes: self, password: str, for_storage: bool = False
) -> bytes:
"""Encode a password.""" """Encode a password."""
hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

View File

@ -249,6 +249,10 @@ def async_enable_logging(
logging.getLogger("urllib3").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING) logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
sys.excepthook = lambda *args: logging.getLogger(None).exception(
"Uncaught exception", exc_info=args # type: ignore
)
# Log errors to a file if we have write access to file or config dir # Log errors to a file if we have write access to file or config dir
if log_file is None: if log_file is None:
err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_log_path = hass.config.path(ERROR_LOG_FILENAME)

View File

@ -25,7 +25,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) )
class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
"""An alarm_control_panel implementation for Abode.""" """An alarm_control_panel implementation for Abode."""
@property @property

View File

@ -3,7 +3,7 @@ import abodepy.helpers.constants as CONST
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
BinarySensorDevice, BinarySensorEntity,
) )
from . import AbodeDevice from . import AbodeDevice
@ -30,7 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities) async_add_entities(entities)
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): class AbodeBinarySensor(AbodeDevice, BinarySensorEntity):
"""A binary sensor implementation for Abode device.""" """A binary sensor implementation for Abode device."""
@property @property

View File

@ -1,7 +1,7 @@
"""Support for Abode Security System covers.""" """Support for Abode Security System covers."""
import abodepy.helpers.constants as CONST import abodepy.helpers.constants as CONST
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverEntity
from . import AbodeDevice from . import AbodeDevice
from .const import DOMAIN from .const import DOMAIN
@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities) async_add_entities(entities)
class AbodeCover(AbodeDevice, CoverDevice): class AbodeCover(AbodeDevice, CoverEntity):
"""Representation of an Abode cover.""" """Representation of an Abode cover."""
@property @property

View File

@ -10,7 +10,7 @@ from homeassistant.components.light import (
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_COLOR,
SUPPORT_COLOR_TEMP, SUPPORT_COLOR_TEMP,
Light, LightEntity,
) )
from homeassistant.util.color import ( from homeassistant.util.color import (
color_temperature_kelvin_to_mired, color_temperature_kelvin_to_mired,
@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities) async_add_entities(entities)
class AbodeLight(AbodeDevice, Light): class AbodeLight(AbodeDevice, LightEntity):
"""Representation of an Abode light.""" """Representation of an Abode light."""
def turn_on(self, **kwargs): def turn_on(self, **kwargs):

View File

@ -1,7 +1,7 @@
"""Support for the Abode Security System locks.""" """Support for the Abode Security System locks."""
import abodepy.helpers.constants as CONST import abodepy.helpers.constants as CONST
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockEntity
from . import AbodeDevice from . import AbodeDevice
from .const import DOMAIN from .const import DOMAIN
@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities) async_add_entities(entities)
class AbodeLock(AbodeDevice, LockDevice): class AbodeLock(AbodeDevice, LockEntity):
"""Representation of an Abode lock.""" """Representation of an Abode lock."""
def lock(self, **kwargs): def lock(self, **kwargs):

View File

@ -3,7 +3,10 @@
"step": { "step": {
"user": { "user": {
"title": "Fill in your Abode login information", "title": "Fill in your Abode login information",
"data": { "username": "Email Address", "password": "Password" } "data": {
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
} }
}, },
"error": { "error": {
@ -15,4 +18,4 @@
"single_instance_allowed": "Only a single configuration of Abode is allowed." "single_instance_allowed": "Only a single configuration of Abode is allowed."
} }
} }
} }

View File

@ -1,7 +1,7 @@
"""Support for Abode Security System switches.""" """Support for Abode Security System switches."""
import abodepy.helpers.constants as CONST import abodepy.helpers.constants as CONST
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import AbodeAutomation, AbodeDevice from . import AbodeAutomation, AbodeDevice
@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities) async_add_entities(entities)
class AbodeSwitch(AbodeDevice, SwitchDevice): class AbodeSwitch(AbodeDevice, SwitchEntity):
"""Representation of an Abode switch.""" """Representation of an Abode switch."""
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
@ -45,7 +45,7 @@ class AbodeSwitch(AbodeDevice, SwitchDevice):
return self._device.is_on return self._device.is_on
class AbodeAutomationSwitch(AbodeAutomation, SwitchDevice): class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity):
"""A switch implementation for Abode automations.""" """A switch implementation for Abode automations."""
async def async_added_to_hass(self): async def async_added_to_hass(self):

View File

@ -12,7 +12,7 @@
"user": { "user": {
"data": { "data": {
"password": "Password", "password": "Password",
"username": "Email Address" "username": "Email"
}, },
"title": "Fill in your Abode login information" "title": "Fill in your Abode login information"
} }

View File

@ -0,0 +1,7 @@
{
"config": {
"error": {
"connection_error": "Yhteytt\u00e4 Abodeen ei voi muodostaa."
}
}
}

View File

@ -12,9 +12,9 @@
"user": { "user": {
"data": { "data": {
"password": "\ube44\ubc00\ubc88\ud638", "password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc774\uba54\uc77c \uc8fc\uc18c" "username": "\uc774\uba54\uc77c"
}, },
"title": "Abode \uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" "title": "Abode \uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\ud558\uae30"
} }
} }
} }

View File

@ -6,7 +6,7 @@
"error": { "error": {
"connection_error": "Kan ikke koble til Abode.", "connection_error": "Kan ikke koble til Abode.",
"identifier_exists": "Kontoen er allerede registrert.", "identifier_exists": "Kontoen er allerede registrert.",
"invalid_credentials": "Ugyldig brukerinformasjon" "invalid_credentials": "Ugyldig legitimasjon"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -11,8 +11,8 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"password": "Has\u0142o", "password": "[%key_id:common::config_flow::data::password%]",
"username": "Adres e-mail" "username": "[%key_id:common::config_flow::data::email%]"
}, },
"title": "Wprowad\u017a informacje logowania Abode" "title": "Wprowad\u017a informacje logowania Abode"
} }

View File

@ -12,7 +12,7 @@
"user": { "user": {
"data": { "data": {
"password": "\u5bc6\u78bc", "password": "\u5bc6\u78bc",
"username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" "username": "\u96fb\u5b50\u90f5\u4ef6"
}, },
"title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a" "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a"
} }

View File

@ -2,6 +2,6 @@
"domain": "acer_projector", "domain": "acer_projector",
"name": "Acer Projector", "name": "Acer Projector",
"documentation": "https://www.home-assistant.io/integrations/acer_projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector",
"requirements": ["pyserial==3.1.1"], "requirements": ["pyserial==3.4"],
"codeowners": [] "codeowners": []
} }

View File

@ -5,7 +5,7 @@ import re
import serial import serial
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
from homeassistant.const import ( from homeassistant.const import (
CONF_FILENAME, CONF_FILENAME,
CONF_NAME, CONF_NAME,
@ -69,7 +69,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([AcerSwitch(serial_port, name, timeout, write_timeout)], True) add_entities([AcerSwitch(serial_port, name, timeout, write_timeout)], True)
class AcerSwitch(SwitchDevice): class AcerSwitch(SwitchEntity):
"""Represents an Acer Projector as a switch.""" """Represents an Acer Projector as a switch."""
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):

View File

@ -206,4 +206,5 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
"name": "AdGuard Home", "name": "AdGuard Home",
"manufacturer": "AdGuard Team", "manufacturer": "AdGuard Team",
"sw_version": self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION), "sw_version": self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION),
"entry_type": "service",
} }

View File

@ -4,10 +4,10 @@
"user": { "user": {
"description": "Set up your AdGuard Home instance to allow monitoring and control.", "description": "Set up your AdGuard Home instance to allow monitoring and control.",
"data": { "data": {
"host": "Host", "host": "[%key:common::config_flow::data::host%]",
"password": "Password", "password": "[%key:common::config_flow::data::password%]",
"port": "Port", "port": "[%key:common::config_flow::data::port%]",
"username": "Username", "username": "[%key:common::config_flow::data::username%]",
"ssl": "AdGuard Home uses a SSL certificate", "ssl": "AdGuard Home uses a SSL certificate",
"verify_ssl": "AdGuard Home uses a proper certificate" "verify_ssl": "AdGuard Home uses a proper certificate"
} }

View File

@ -10,7 +10,7 @@ from homeassistant.components.adguard.const import (
DATA_ADGUARD_VERION, DATA_ADGUARD_VERION,
DOMAIN, DOMAIN,
) )
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
@ -45,7 +45,7 @@ async def async_setup_entry(
async_add_entities(switches, True) async_add_entities(switches, True)
class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchDevice): class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
"""Defines a AdGuard Home switch.""" """Defines a AdGuard Home switch."""
def __init__( def __init__(

View File

@ -16,9 +16,7 @@
}, },
"user": { "user": {
"data": { "data": {
"host": "\u0410\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
"port": "\u041f\u043e\u0440\u0442",
"ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", "ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442",
"username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435",
"verify_ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0430\u0434\u0435\u0436\u0434\u0435\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" "verify_ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0430\u0434\u0435\u0436\u0434\u0435\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442"

View File

@ -16,11 +16,11 @@
}, },
"user": { "user": {
"data": { "data": {
"host": "Amfitri\u00f3", "host": "[%key::common::config_flow::data::host%]",
"password": "Contrasenya", "password": "[%key::common::config_flow::data::password%]",
"port": "Port", "port": "[%key::common::config_flow::data::port%]",
"ssl": "AdGuard Home utilitza un certificat SSL", "ssl": "AdGuard Home utilitza un certificat SSL",
"username": "Nom d'usuari", "username": "[%key::common::config_flow::data::username%]",
"verify_ssl": "AdGuard Home utilitza un certificat adequat" "verify_ssl": "AdGuard Home utilitza un certificat adequat"
}, },
"description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3.", "description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3.",

View File

@ -16,9 +16,7 @@
}, },
"user": { "user": {
"data": { "data": {
"host": "V\u00e6rt",
"password": "Adgangskode", "password": "Adgangskode",
"port": "Port",
"ssl": "AdGuard Home bruger et SSL-certifikat", "ssl": "AdGuard Home bruger et SSL-certifikat",
"username": "Brugernavn", "username": "Brugernavn",
"verify_ssl": "AdGuard Home bruger et korrekt certifikat" "verify_ssl": "AdGuard Home bruger et korrekt certifikat"

View File

@ -17,7 +17,6 @@
"user": { "user": {
"data": { "data": {
"password": "Contrase\u00f1a", "password": "Contrase\u00f1a",
"port": "Puerto",
"ssl": "AdGuard Home utiliza un certificado SSL", "ssl": "AdGuard Home utiliza un certificado SSL",
"username": "Nombre de usuario", "username": "Nombre de usuario",
"verify_ssl": "AdGuard Home utiliza un certificado adecuado" "verify_ssl": "AdGuard Home utiliza un certificado adecuado"

View File

@ -0,0 +1,15 @@
{
"config": {
"error": {
"connection_error": "Yhdist\u00e4minen ep\u00e4onnistui."
},
"step": {
"user": {
"data": {
"host": "Palvelin",
"port": "Portti"
}
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "Host",
"port": "\u05e4\u05d5\u05e8\u05d8"
}
}
}
}
}

View File

@ -6,8 +6,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"password": "Kata sandi", "password": "Kata sandi"
"port": "Port"
} }
} }
} }

View File

@ -24,7 +24,7 @@
"verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" "verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4"
}, },
"description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", "description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.",
"title": "AdGuard Home \uc5f0\uacb0" "title": "AdGuard Home \uc5f0\uacb0\ud558\uae30"
} }
} }
} }

View File

@ -16,7 +16,7 @@
}, },
"user": { "user": {
"data": { "data": {
"host": "Apparat", "host": "Host",
"password": "Passwuert", "password": "Passwuert",
"port": "Port", "port": "Port",
"ssl": "AdGuard Home benotzt een SSL Zertifikat", "ssl": "AdGuard Home benotzt een SSL Zertifikat",

View File

@ -16,9 +16,7 @@
}, },
"user": { "user": {
"data": { "data": {
"host": "Host",
"password": "Wachtwoord", "password": "Wachtwoord",
"port": "Poort",
"ssl": "AdGuard Home maakt gebruik van een SSL certificaat", "ssl": "AdGuard Home maakt gebruik van een SSL certificaat",
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
"verify_ssl": "AdGuard Home maakt gebruik van een goed certificaat" "verify_ssl": "AdGuard Home maakt gebruik van een goed certificaat"

View File

@ -17,10 +17,8 @@
"user": { "user": {
"data": { "data": {
"host": "Vert", "host": "Vert",
"password": "Passord",
"port": "", "port": "",
"ssl": "AdGuard Hjem bruker et SSL-sertifikat", "ssl": "AdGuard Hjem bruker et SSL-sertifikat",
"username": "Brukernavn",
"verify_ssl": "AdGuard Home bruker et riktig sertifikat" "verify_ssl": "AdGuard Home bruker et riktig sertifikat"
}, },
"description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll.", "description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll.",

View File

@ -7,7 +7,7 @@
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home."
}, },
"error": { "error": {
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]"
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
@ -16,11 +16,11 @@
}, },
"user": { "user": {
"data": { "data": {
"host": "Nazwa hosta lub adres IP", "host": "[%key_id:common::config_flow::data::host%]",
"password": "Has\u0142o", "password": "[%key_id:common::config_flow::data::password%]",
"port": "Port", "port": "[%key_id:common::config_flow::data::port%]",
"ssl": "AdGuard Home u\u017cywa certyfikatu SSL", "ssl": "AdGuard Home u\u017cywa certyfikatu SSL",
"username": "Nazwa u\u017cytkownika", "username": "[%key_id:common::config_flow::data::username%]",
"verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu." "verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu."
}, },
"description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.", "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.",

View File

@ -14,9 +14,7 @@
}, },
"user": { "user": {
"data": { "data": {
"host": "Host",
"password": "Senha", "password": "Senha",
"port": "Porta",
"ssl": "O AdGuard Home usa um certificado SSL", "ssl": "O AdGuard Home usa um certificado SSL",
"username": "Nome de usu\u00e1rio", "username": "Nome de usu\u00e1rio",
"verify_ssl": "O AdGuard Home usa um certificado apropriado" "verify_ssl": "O AdGuard Home usa um certificado apropriado"

View File

@ -3,9 +3,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"host": "Servidor",
"password": "Palavra-passe", "password": "Palavra-passe",
"port": "Porta",
"username": "Nome de Utilizador" "username": "Nome de Utilizador"
} }
} }

View File

@ -23,7 +23,7 @@
"username": "\u041b\u043e\u0433\u0438\u043d", "username": "\u041b\u043e\u0433\u0438\u043d",
"verify_ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" "verify_ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442"
}, },
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home.", "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home.",
"title": "AdGuard Home" "title": "AdGuard Home"
} }
} }

View File

@ -16,9 +16,7 @@
}, },
"user": { "user": {
"data": { "data": {
"host": "V\u00e4rd",
"password": "L\u00f6senord", "password": "L\u00f6senord",
"port": "Port",
"ssl": "AdGuard Home anv\u00e4nder ett SSL-certifikat", "ssl": "AdGuard Home anv\u00e4nder ett SSL-certifikat",
"username": "Anv\u00e4ndarnamn", "username": "Anv\u00e4ndarnamn",
"verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat" "verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat"

View File

@ -3,9 +3,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"host": "\u0110\u1ecba ch\u1ec9",
"password": "M\u1eadt kh\u1ea9u", "password": "M\u1eadt kh\u1ea9u",
"port": "C\u1ed5ng",
"username": "T\u00ean \u0111\u0103ng nh\u1eadp" "username": "T\u00ean \u0111\u0103ng nh\u1eadp"
} }
} }

View File

@ -7,7 +7,6 @@
"user": { "user": {
"data": { "data": {
"password": "\u5bc6\u7801", "password": "\u5bc6\u7801",
"port": "\u7aef\u53e3",
"username": "\u7528\u6237\u540d" "username": "\u7528\u6237\u540d"
} }
} }

View File

@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
BinarySensorDevice, BinarySensorEntity,
) )
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -37,7 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([ads_sensor]) add_entities([ads_sensor])
class AdsBinarySensor(AdsEntity, BinarySensorDevice): class AdsBinarySensor(AdsEntity, BinarySensorEntity):
"""Representation of ADS binary sensors.""" """Representation of ADS binary sensors."""
def __init__(self, ads_hub, name, ads_var, device_class): def __init__(self, ads_hub, name, ads_var, device_class):

View File

@ -11,7 +11,7 @@ from homeassistant.components.cover import (
SUPPORT_OPEN, SUPPORT_OPEN,
SUPPORT_SET_POSITION, SUPPORT_SET_POSITION,
SUPPORT_STOP, SUPPORT_STOP,
CoverDevice, CoverEntity,
) )
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -78,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
) )
class AdsCover(AdsEntity, CoverDevice): class AdsCover(AdsEntity, CoverEntity):
"""Representation of ADS cover.""" """Representation of ADS cover."""
def __init__( def __init__(

View File

@ -7,7 +7,7 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
Light, LightEntity,
) )
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)]) add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)])
class AdsLight(AdsEntity, Light): class AdsLight(AdsEntity, LightEntity):
"""Representation of ADS light.""" """Representation of ADS light."""
def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name): def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name):

View File

@ -3,7 +3,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([AdsSwitch(ads_hub, name, ads_var)]) add_entities([AdsSwitch(ads_hub, name, ads_var)])
class AdsSwitch(AdsEntity, SwitchDevice): class AdsSwitch(AdsEntity, SwitchEntity):
"""Representation of an ADS switch device.""" """Representation of an ADS switch device."""
async def async_added_to_hass(self): async def async_added_to_hass(self):

View File

@ -0,0 +1,82 @@
"""Support for Agent."""
import asyncio
import logging
from agent import AgentError
from agent.a import Agent
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN, SERVER_URL
ATTRIBUTION = "ispyconnect.com"
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
_LOGGER = logging.getLogger(__name__)
FORWARDS = ["camera"]
async def async_setup(hass, config):
"""Old way to set up integrations."""
return True
async def async_setup_entry(hass, config_entry):
"""Set up the Agent component."""
hass.data.setdefault(AGENT_DOMAIN, {})
server_origin = config_entry.data[SERVER_URL]
agent_client = Agent(server_origin, async_get_clientsession(hass))
try:
await agent_client.update()
except AgentError:
await agent_client.close()
raise ConfigEntryNotReady
if not agent_client.is_available:
raise ConfigEntryNotReady
await agent_client.get_devices()
hass.data[AGENT_DOMAIN][config_entry.entry_id] = {CONNECTION: agent_client}
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(AGENT_DOMAIN, agent_client.unique)},
manufacturer="iSpyConnect",
name=f"Agent {agent_client.name}",
model="Agent DVR",
sw_version=agent_client.version,
)
for forward in FORWARDS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, forward)
)
return True
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, forward)
for forward in FORWARDS
]
)
)
await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close()
if unload_ok:
hass.data[AGENT_DOMAIN].pop(config_entry.entry_id)
return unload_ok

View File

@ -0,0 +1,215 @@
"""Support for Agent camera streaming."""
from datetime import timedelta
import logging
from agent import AgentError
from homeassistant.components.camera import SUPPORT_ON_OFF
from homeassistant.components.mjpeg.camera import (
CONF_MJPEG_URL,
CONF_STILL_IMAGE_URL,
MjpegCamera,
filter_urllib3_logging,
)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
from homeassistant.helpers import entity_platform
from .const import (
ATTRIBUTION,
CAMERA_SCAN_INTERVAL_SECS,
CONNECTION,
DOMAIN as AGENT_DOMAIN,
)
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
_LOGGER = logging.getLogger(__name__)
_DEV_EN_ALT = "enable_alerts"
_DEV_DS_ALT = "disable_alerts"
_DEV_EN_REC = "start_recording"
_DEV_DS_REC = "stop_recording"
_DEV_SNAP = "snapshot"
CAMERA_SERVICES = {
_DEV_EN_ALT: "async_enable_alerts",
_DEV_DS_ALT: "async_disable_alerts",
_DEV_EN_REC: "async_start_recording",
_DEV_DS_REC: "async_stop_recording",
_DEV_SNAP: "async_snapshot",
}
async def async_setup_entry(
hass, config_entry, async_add_entities, discovery_info=None
):
"""Set up the Agent cameras."""
filter_urllib3_logging()
cameras = []
server = hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION]
if not server.devices:
_LOGGER.warning("Could not fetch cameras from Agent server")
return
for device in server.devices:
if device.typeID == 2:
camera = AgentCamera(device)
cameras.append(camera)
async_add_entities(cameras)
platform = entity_platform.current_platform.get()
for service, method in CAMERA_SERVICES.items():
platform.async_register_entity_service(service, {}, method)
class AgentCamera(MjpegCamera):
"""Representation of an Agent Device Stream."""
def __init__(self, device):
"""Initialize as a subclass of MjpegCamera."""
self._servername = device.client.name
self.server_url = device.client._server_url
device_info = {
CONF_NAME: device.name,
CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size=640x480",
CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size=640x480",
}
self.device = device
self._removed = False
self._name = f"{self._servername} {device.name}"
self._unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
super().__init__(device_info)
@property
def device_info(self):
"""Return the device info for adding the entity to the agent object."""
return {
"identifiers": {(AGENT_DOMAIN, self._unique_id)},
"name": self._name,
"manufacturer": "Agent",
"model": "Camera",
"sw_version": self.device.client.version,
}
async def async_update(self):
"""Update our state from the Agent API."""
try:
await self.device.update()
if self._removed:
_LOGGER.debug("%s reacquired", self._name)
self._removed = False
except AgentError:
if self.device.client.is_available: # server still available - camera error
if not self._removed:
_LOGGER.error("%s lost", self._name)
self._removed = True
@property
def device_state_attributes(self):
"""Return the Agent DVR camera state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
"editable": False,
"enabled": self.is_on,
"connected": self.connected,
"detected": self.is_detected,
"alerted": self.is_alerted,
"has_ptz": self.device.has_ptz,
"alerts_enabled": self.device.alerts_active,
}
@property
def should_poll(self) -> bool:
"""Update the state periodically."""
return True
@property
def is_recording(self) -> bool:
"""Return whether the monitor is recording."""
return self.device.recording
@property
def is_alerted(self) -> bool:
"""Return whether the monitor has alerted."""
return self.device.alerted
@property
def is_detected(self) -> bool:
"""Return whether the monitor has alerted."""
return self.device.detected
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.device.client.is_available
@property
def connected(self) -> bool:
"""Return True if entity is connected."""
return self.device.connected
@property
def supported_features(self) -> int:
"""Return supported features."""
return SUPPORT_ON_OFF
@property
def is_on(self) -> bool:
"""Return true if on."""
return self.device.online
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
if self.is_on:
return "mdi:camcorder"
return "mdi:camcorder-off"
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return self.device.detector_active
@property
def unique_id(self) -> str:
"""Return a unique identifier for this agent object."""
return self._unique_id
async def async_enable_alerts(self):
"""Enable alerts."""
await self.device.alerts_on()
async def async_disable_alerts(self):
"""Disable alerts."""
await self.device.alerts_off()
async def async_enable_motion_detection(self):
"""Enable motion detection."""
await self.device.detector_on()
async def async_disable_motion_detection(self):
"""Disable motion detection."""
await self.device.detector_off()
async def async_start_recording(self):
"""Start recording."""
await self.device.record()
async def async_stop_recording(self):
"""Stop recording."""
await self.device.record_stop()
async def async_turn_on(self):
"""Enable the camera."""
await self.device.enable()
async def async_snapshot(self):
"""Take a snapshot."""
await self.device.snapshot()
async def async_turn_off(self):
"""Disable the camera."""
await self.device.disable()

View File

@ -0,0 +1,81 @@
"""Config flow to configure Agent devices."""
import logging
from agent import AgentConnectionError, AgentError
from agent.a import Agent
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, SERVER_URL # pylint:disable=unused-import
from .helpers import generate_url
DEFAULT_PORT = 8090
_LOGGER = logging.getLogger(__name__)
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an Agent config flow."""
def __init__(self):
"""Initialize the Agent config flow."""
self.device_config = {}
async def async_step_user(self, info=None):
"""Handle an Agent config flow."""
errors = {}
if info is not None:
host = info[CONF_HOST]
port = info[CONF_PORT]
server_origin = generate_url(host, port)
agent_client = Agent(server_origin, async_get_clientsession(self.hass))
try:
await agent_client.update()
except AgentConnectionError:
pass
except AgentError:
pass
await agent_client.close()
if agent_client.is_available:
await self.async_set_unique_id(agent_client.unique)
self._abort_if_unique_id_configured(
updates={
CONF_HOST: info[CONF_HOST],
CONF_PORT: info[CONF_PORT],
SERVER_URL: server_origin,
}
)
self.device_config = {
CONF_HOST: host,
CONF_PORT: port,
SERVER_URL: server_origin,
}
return await self._create_entry(agent_client.name)
errors["base"] = "device_unavailable"
data = {
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
return self.async_show_form(
step_id="user",
description_placeholders=self.device_config,
data_schema=vol.Schema(data),
errors=errors,
)
async def _create_entry(self, server_name):
"""Create entry for device."""
return self.async_create_entry(title=server_name, data=self.device_config)

View File

@ -0,0 +1,11 @@
"""Constants for agent_dvr component."""
DOMAIN = "agent_dvr"
SERVERS = "servers"
DEVICES = "devices"
ENTITIES = "entities"
CAMERA_SCAN_INTERVAL_SECS = 5
SERVICE_UPDATE = "update"
SIGNAL_UPDATE_AGENT = "agent_update"
ATTRIBUTION = "Data provided by ispyconnect.com"
SERVER_URL = "server_url"
CONNECTION = "connection"

View File

@ -0,0 +1,13 @@
"""Helpers for Agent DVR component."""
def generate_url(host, port) -> str:
"""Create a URL from the host and port."""
server_origin = host
if "://" not in host:
server_origin = f"http://{host}"
if server_origin[-1] == "/":
server_origin = server_origin[:-1]
return f"{server_origin}:{port}/"

View File

@ -0,0 +1,8 @@
{
"domain": "agent_dvr",
"name": "Agent DVR",
"documentation": "https://www.home-assistant.io/integrations/agent_dvr/",
"requirements": ["agent-py==0.0.20"],
"config_flow": true,
"codeowners": ["@ispysoftware"]
}

View File

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

View File

@ -0,0 +1,21 @@
{
"title": "Agent DVR",
"config": {
"step": {
"user": {
"title": "Set up Agent DVR",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
}
}
},
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available"
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Dispositiu ja est\u00e0 configurat"
},
"error": {
"already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
"device_unavailable": "Dispositiu no est\u00e0 disponible"
},
"step": {
"user": {
"data": {
"host": "Amfitri\u00f3",
"port": "Port"
},
"title": "Configuraci\u00f3 de Agent DVR"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
"already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.",
"device_unavailable": "Ger\u00e4t ist nicht verf\u00fcgbar"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port"
},
"title": "Richten Sie den Agent DVR ein"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port"
},
"title": "Set up Agent DVR"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado"
},
"error": {
"already_in_progress": "La configuraci\u00f3n del flujo para el dispositivo ya est\u00e1 en marcha.",
"device_unavailable": "El dispositivo no est\u00e1 disponible"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Puerto"
},
"title": "Configurar el Agente de DVR"
}
}
},
"title": "Agente DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Laite on jo m\u00e4\u00e4ritetty"
},
"error": {
"already_in_progress": "Laitteen m\u00e4\u00e4ritysvirta on jo k\u00e4ynniss\u00e4.",
"device_unavailable": "Laite ei ole k\u00e4ytett\u00e4viss\u00e4"
},
"step": {
"user": {
"data": {
"host": "Palvelin",
"port": "Portti"
},
"title": "Asenna Agent DVR"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"already_in_progress": "La configuration de l'appareil est d\u00e9j\u00e0 en cours.",
"device_unavailable": "L'appareil n'est pas disponible"
},
"step": {
"user": {
"data": {
"host": "H\u00f4te",
"port": "Port"
}
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,18 @@
{
"config": {
"abort": {
"already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
},
"error": {
"device_unavailable": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "\u05e4\u05d5\u05e8\u05d8"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
"already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
"device_unavailable": "Il dispositivo non \u00e8 disponibile"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Porta"
},
"title": "Configurare Agent DVR"
}
}
},
"title": "Agente DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
"device_unavailable": "\uae30\uae30\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8"
},
"title": "Agent DVR \uc124\uc815\ud558\uae30"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Apparat ass scho konfigur\u00e9iert"
},
"error": {
"already_in_progress": "Konfiguratioun's Oflaf fir den Apparat ass schonn am gaangen.",
"device_unavailable": "Apparat ass net erreechbar"
},
"step": {
"user": {
"data": {
"host": "Apparat",
"port": "Port"
},
"title": "Agent DVR ariichten"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
"already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
"device_unavailable": "Enheten er ikke tilgjengelig"
},
"step": {
"user": {
"data": {
"host": "Vert",
"port": "Port"
},
"title": "Konfigurere Agent DVR"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]"
},
"error": {
"already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
"device_unavailable": "Urz\u0105dzenie nie jest dost\u0119pne."
},
"step": {
"user": {
"data": {
"host": "[%key_id:common::config_flow::data::host%]",
"port": "[%key_id:common::config_flow::data::port%]"
},
"title": "Konfiguracja Agent DVR"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"error": {
"already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
"device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e."
},
"step": {
"user": {
"data": {
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442"
},
"title": "Agent DVR"
}
}
},
"title": "Agent DVR"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad"
},
"error": {
"already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan.",
"device_unavailable": "Enheten \u00e4r inte tillg\u00e4nglig"
},
"step": {
"user": {
"data": {
"host": "V\u00e4rd",
"port": "Port"
},
"title": "Konfigurera DVR Agent"
}
}
},
"title": "DVR Agent"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
"already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
"device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528"
},
"step": {
"user": {
"data": {
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0"
},
"title": "\u8a2d\u5b9a Agent DVR"
}
}
},
"title": "Agent DVR"
}

View File

@ -69,18 +69,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, False) async_add_entities(sensors, False)
def round_state(func):
"""Round state."""
def _decorator(self):
res = func(self)
if isinstance(res, float):
return round(res)
return res
return _decorator
class AirlySensor(Entity): class AirlySensor(Entity):
"""Define an Airly sensor.""" """Define an Airly sensor."""

View File

@ -6,7 +6,7 @@
"description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register",
"data": { "data": {
"name": "Name of the integration", "name": "Name of the integration",
"api_key": "Airly API key", "api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude" "longitude": "Longitude"
} }
@ -20,4 +20,4 @@
"already_configured": "Airly integration for these coordinates is already configured." "already_configured": "Airly integration for these coordinates is already configured."
} }
} }
} }

View File

@ -15,7 +15,7 @@
"longitude": "Longitud", "longitude": "Longitud",
"name": "Nom de la integraci\u00f3" "name": "Nom de la integraci\u00f3"
}, },
"description": "Configura una integraci\u00f3 de qualitat d\u2019aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", "description": "Configura una integraci\u00f3 de qualitat d'aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register",
"title": "Airly" "title": "Airly"
} }
} }

View File

@ -10,7 +10,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"api_key": "Airly API key", "api_key": "API Key",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude", "longitude": "Longitude",
"name": "Name of the integration" "name": "Name of the integration"

View File

@ -10,7 +10,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"api_key": "Airly API \ud0a4", "api_key": "API \ud0a4",
"latitude": "\uc704\ub3c4", "latitude": "\uc704\ub3c4",
"longitude": "\uacbd\ub3c4", "longitude": "\uacbd\ub3c4",
"name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984" "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984"

View File

@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Airly integrering for disse koordinatene er allerede konfigurert." "already_configured": "Airly integrasjonen for disse koordinatene er allerede konfigurert."
}, },
"error": { "error": {
"auth": "API-n\u00f8kkelen er ikke korrekt.", "auth": "API-n\u00f8kkelen er ikke korrekt.",
@ -15,7 +15,7 @@
"longitude": "Lengdegrad", "longitude": "Lengdegrad",
"name": "Navn p\u00e5 integrasjonen" "name": "Navn p\u00e5 integrasjonen"
}, },
"description": "Sett opp Airly luftkvalitet integrering. For \u00e5 generere API-n\u00f8kkel g\u00e5 til https://developer.airly.eu/register", "description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)",
"title": "" "title": ""
} }
} }

View File

@ -10,7 +10,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"api_key": "Klucz API Airly", "api_key": "[%key_id:common::config_flow::data::api_key%] Airly",
"latitude": "Szeroko\u015b\u0107 geograficzna", "latitude": "Szeroko\u015b\u0107 geograficzna",
"longitude": "D\u0142ugo\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna",
"name": "Nazwa integracji" "name": "Nazwa integracji"

View File

@ -10,7 +10,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"api_key": "Airly API \u5bc6\u9470", "api_key": "API \u5bc6\u9470",
"latitude": "\u7def\u5ea6", "latitude": "\u7def\u5ea6",
"longitude": "\u7d93\u5ea6", "longitude": "\u7d93\u5ea6",
"name": "\u6574\u5408\u540d\u7a31" "name": "\u6574\u5408\u540d\u7a31"

View File

@ -1,38 +1,44 @@
"""The airvisual component.""" """The airvisual component."""
import logging import asyncio
from datetime import timedelta
from pyairvisual import Client from pyairvisual import Client
from pyairvisual.errors import AirVisualError, InvalidKeyError from pyairvisual.errors import AirVisualError, NodeProError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY, CONF_API_KEY,
CONF_IP_ADDRESS,
CONF_LATITUDE, CONF_LATITUDE,
CONF_LONGITUDE, CONF_LONGITUDE,
CONF_PASSWORD,
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
CONF_STATE, CONF_STATE,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import (
CONF_CITY, CONF_CITY,
CONF_COUNTRY, CONF_COUNTRY,
CONF_GEOGRAPHIES, CONF_GEOGRAPHIES,
DATA_CLIENT, CONF_INTEGRATION_TYPE,
DEFAULT_SCAN_INTERVAL, DATA_COORDINATOR,
DOMAIN, DOMAIN,
TOPIC_UPDATE, INTEGRATION_TYPE_GEOGRAPHY,
INTEGRATION_TYPE_NODE_PRO,
LOGGER,
) )
_LOGGER = logging.getLogger(__name__) PLATFORMS = ["air_quality", "sensor"]
DATA_LISTENER = "listener"
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
DEFAULT_GEOGRAPHY_SCAN_INTERVAL = timedelta(minutes=10)
DEFAULT_NODE_PRO_SCAN_INTERVAL = timedelta(minutes=1)
DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True} DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True}
GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema( GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema(
@ -66,6 +72,9 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: CLOUD_API_SCHEMA}, extra=vol.ALLOW_EXTRA)
@callback @callback
def async_get_geography_id(geography_dict): def async_get_geography_id(geography_dict):
"""Generate a unique ID from a geography dict.""" """Generate a unique ID from a geography dict."""
if not geography_dict:
return
if CONF_CITY in geography_dict: if CONF_CITY in geography_dict:
return ", ".join( return ", ".join(
( (
@ -81,7 +90,7 @@ def async_get_geography_id(geography_dict):
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the AirVisual component.""" """Set up the AirVisual component."""
hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} hass.data[DOMAIN] = {DATA_COORDINATOR: {}}
if DOMAIN not in config: if DOMAIN not in config:
return True return True
@ -103,44 +112,118 @@ async def async_setup(hass, config):
return True return True
async def async_setup_entry(hass, config_entry): @callback
"""Set up AirVisual as config entry.""" def _standardize_geography_config_entry(hass, config_entry):
"""Ensure that geography config entries have appropriate properties."""
entry_updates = {} entry_updates = {}
if not config_entry.unique_id: if not config_entry.unique_id:
# If the config entry doesn't already have a unique ID, set one: # If the config entry doesn't already have a unique ID, set one:
entry_updates["unique_id"] = config_entry.data[CONF_API_KEY] entry_updates["unique_id"] = config_entry.data[CONF_API_KEY]
if not config_entry.options: if not config_entry.options:
# If the config entry doesn't already have any options set, set defaults: # If the config entry doesn't already have any options set, set defaults:
entry_updates["options"] = DEFAULT_OPTIONS entry_updates["options"] = {CONF_SHOW_ON_MAP: True}
if CONF_INTEGRATION_TYPE not in config_entry.data:
# If the config entry data doesn't contain the integration type, add it:
entry_updates["data"] = {
**config_entry.data,
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
}
if entry_updates: if not entry_updates:
hass.config_entries.async_update_entry(config_entry, **entry_updates) return
hass.config_entries.async_update_entry(config_entry, **entry_updates)
@callback
def _standardize_node_pro_config_entry(hass, config_entry):
"""Ensure that Node/Pro config entries have appropriate properties."""
entry_updates = {}
if CONF_INTEGRATION_TYPE not in config_entry.data:
# If the config entry data doesn't contain the integration type, add it:
entry_updates["data"] = {
**config_entry.data,
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO,
}
if not entry_updates:
return
hass.config_entries.async_update_entry(config_entry, **entry_updates)
async def async_setup_entry(hass, config_entry):
"""Set up AirVisual as config entry."""
websession = aiohttp_client.async_get_clientsession(hass) websession = aiohttp_client.async_get_clientsession(hass)
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = AirVisualData( if CONF_API_KEY in config_entry.data:
hass, Client(websession, api_key=config_entry.data[CONF_API_KEY]), config_entry _standardize_geography_config_entry(hass, config_entry)
)
try: client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession)
await hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id].async_update()
except InvalidKeyError:
_LOGGER.error("Invalid API key provided")
raise ConfigEntryNotReady
hass.async_create_task( async def async_update_data():
hass.config_entries.async_forward_entry_setup(config_entry, "sensor") """Get new data from the API."""
) if CONF_CITY in config_entry.data:
api_coro = client.api.city(
config_entry.data[CONF_CITY],
config_entry.data[CONF_STATE],
config_entry.data[CONF_COUNTRY],
)
else:
api_coro = client.api.nearest_city(
config_entry.data[CONF_LATITUDE], config_entry.data[CONF_LONGITUDE],
)
async def refresh(event_time): try:
"""Refresh data from AirVisual.""" return await api_coro
await hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id].async_update() except AirVisualError as err:
raise UpdateFailed(f"Error while retrieving data: {err}")
hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval( coordinator = DataUpdateCoordinator(
hass, refresh, DEFAULT_SCAN_INTERVAL hass,
) LOGGER,
name="geography data",
update_interval=DEFAULT_GEOGRAPHY_SCAN_INTERVAL,
update_method=async_update_data,
)
config_entry.add_update_listener(async_update_options) # Only geography-based entries have options:
config_entry.add_update_listener(async_update_options)
else:
_standardize_node_pro_config_entry(hass, config_entry)
client = Client(session=websession)
async def async_update_data():
"""Get new data from the API."""
try:
return await client.node.from_samba(
config_entry.data[CONF_IP_ADDRESS],
config_entry.data[CONF_PASSWORD],
include_history=False,
include_trends=False,
)
except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}")
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
name="Node/Pro data",
update_interval=DEFAULT_NODE_PRO_SCAN_INTERVAL,
update_method=async_update_data,
)
await coordinator.async_refresh()
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
return True return True
@ -149,7 +232,7 @@ async def async_migrate_entry(hass, config_entry):
"""Migrate an old config entry.""" """Migrate an old config entry."""
version = config_entry.version version = config_entry.version
_LOGGER.debug("Migrating from version %s", version) LOGGER.debug("Migrating from version %s", version)
# 1 -> 2: One geography per config entry # 1 -> 2: One geography per config entry
if version == 1: if version == 1:
@ -178,65 +261,84 @@ async def async_migrate_entry(hass, config_entry):
) )
) )
_LOGGER.info("Migration to version %s successful", version) LOGGER.info("Migration to version %s successful", version)
return True return True
async def async_unload_entry(hass, config_entry): async def async_unload_entry(hass, config_entry):
"""Unload an AirVisual config entry.""" """Unload an AirVisual config entry."""
hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id)
remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) return unload_ok
remove_listener()
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
return True
async def async_update_options(hass, config_entry): async def async_update_options(hass, config_entry):
"""Handle an options update.""" """Handle an options update."""
airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
airvisual.async_update_options(config_entry.options) await coordinator.async_request_refresh()
class AirVisualData: class AirVisualEntity(Entity):
"""Define a class to manage data from the AirVisual cloud API.""" """Define a generic AirVisual entity."""
def __init__(self, hass, client, config_entry): def __init__(self, coordinator):
"""Initialize.""" """Initialize."""
self._client = client self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._hass = hass self._icon = None
self.data = {} self._unit = None
self.geography_data = config_entry.data self.coordinator = coordinator
self.geography_id = config_entry.unique_id
self.options = config_entry.options @property
def available(self):
"""Return if entity is available."""
return self.coordinator.last_update_success
@property
def device_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.update_from_latest_data()
self.async_write_ha_state()
self.async_on_remove(self.coordinator.async_add_listener(update))
self.update_from_latest_data()
async def async_update(self): async def async_update(self):
"""Get new data for all locations from the AirVisual cloud API.""" """Update the entity.
if CONF_CITY in self.geography_data:
api_coro = self._client.api.city(
self.geography_data[CONF_CITY],
self.geography_data[CONF_STATE],
self.geography_data[CONF_COUNTRY],
)
else:
api_coro = self._client.api.nearest_city(
self.geography_data[CONF_LATITUDE], self.geography_data[CONF_LONGITUDE],
)
try: Only used by the generic entity update service.
self.data[self.geography_id] = await api_coro """
except AirVisualError as err: await self.coordinator.async_request_refresh()
_LOGGER.error("Error while retrieving data: %s", err)
self.data[self.geography_id] = {}
_LOGGER.debug("Received new data")
async_dispatcher_send(self._hass, TOPIC_UPDATE)
@callback @callback
def async_update_options(self, options): def update_from_latest_data(self):
"""Update the data manager's options.""" """Update the entity from the latest data."""
self.options = options raise NotImplementedError
async_dispatcher_send(self._hass, TOPIC_UPDATE)

View File

@ -0,0 +1,112 @@
"""Support for AirVisual Node/Pro units."""
from homeassistant.components.air_quality import AirQualityEntity
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
from homeassistant.core import callback
from . import AirVisualEntity
from .const import (
CONF_INTEGRATION_TYPE,
DATA_COORDINATOR,
DOMAIN,
INTEGRATION_TYPE_GEOGRAPHY,
)
ATTR_HUMIDITY = "humidity"
ATTR_SENSOR_LIFE = "{0}_sensor_life"
ATTR_VOC = "voc"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up AirVisual air quality entities based on a config entry."""
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
# Geography-based AirVisual integrations don't utilize this platform:
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
return
async_add_entities([AirVisualNodeProSensor(coordinator)], True)
class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
"""Define a sensor for a AirVisual Node/Pro."""
def __init__(self, airvisual):
"""Initialize."""
super().__init__(airvisual)
self._icon = "mdi:chemical-weapon"
self._unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
if self.coordinator.data["current"]["settings"]["is_aqi_usa"]:
return self.coordinator.data["current"]["measurements"]["aqi_us"]
return self.coordinator.data["current"]["measurements"]["aqi_cn"]
@property
def available(self):
"""Return True if entity is available."""
return bool(self.coordinator.data)
@property
def carbon_dioxide(self):
"""Return the CO2 (carbon dioxide) level."""
return self.coordinator.data["current"]["measurements"].get("co2")
@property
def device_info(self):
"""Return device registry information for this entity."""
return {
"identifiers": {
(DOMAIN, self.coordinator.data["current"]["serial_number"])
},
"name": self.coordinator.data["current"]["settings"]["node_name"],
"manufacturer": "AirVisual",
"model": f'{self.coordinator.data["current"]["status"]["model"]}',
"sw_version": (
f'Version {self.coordinator.data["current"]["status"]["system_version"]}'
f'{self.coordinator.data["current"]["status"]["app_version"]}'
),
}
@property
def name(self):
"""Return the name."""
node_name = self.coordinator.data["current"]["settings"]["node_name"]
return f"{node_name} Node/Pro: Air Quality"
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
return self.coordinator.data["current"]["measurements"].get("pm2_5")
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
return self.coordinator.data["current"]["measurements"].get("pm1_0")
@property
def particulate_matter_0_1(self):
"""Return the particulate matter 0.1 level."""
return self.coordinator.data["current"]["measurements"].get("pm0_1")
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self.coordinator.data["current"]["serial_number"]
@callback
def update_from_latest_data(self):
"""Update the entity from the latest data."""
self._attrs.update(
{
ATTR_VOC: self.coordinator.data["current"]["measurements"].get("voc"),
**{
ATTR_SENSOR_LIFE.format(pollutant): lifespan
for pollutant, lifespan in self.coordinator.data["current"][
"status"
]["sensor_life"].items()
},
}
)

View File

@ -2,21 +2,30 @@
import asyncio import asyncio
from pyairvisual import Client from pyairvisual import Client
from pyairvisual.errors import InvalidKeyError from pyairvisual.errors import InvalidKeyError, NodeProError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_IP_ADDRESS,
CONF_LATITUDE, CONF_LATITUDE,
CONF_LONGITUDE, CONF_LONGITUDE,
CONF_PASSWORD,
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from . import async_get_geography_id from . import async_get_geography_id
from .const import CONF_GEOGRAPHIES, DOMAIN # pylint: disable=unused-import from .const import ( # pylint: disable=unused-import
CONF_GEOGRAPHIES,
CONF_INTEGRATION_TYPE,
DOMAIN,
INTEGRATION_TYPE_GEOGRAPHY,
INTEGRATION_TYPE_NODE_PRO,
LOGGER,
)
class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -26,7 +35,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
@property @property
def cloud_api_schema(self): def geography_schema(self):
"""Return the data schema for the cloud API.""" """Return the data schema for the cloud API."""
return vol.Schema( return vol.Schema(
{ {
@ -40,38 +49,47 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
} }
) )
@property
def pick_integration_type_schema(self):
"""Return the data schema for picking the integration type."""
return vol.Schema(
{
vol.Required("type"): vol.In(
[INTEGRATION_TYPE_GEOGRAPHY, INTEGRATION_TYPE_NODE_PRO]
)
}
)
@property
def node_pro_schema(self):
"""Return the data schema for a Node/Pro."""
return vol.Schema(
{vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): str}
)
async def _async_set_unique_id(self, unique_id): async def _async_set_unique_id(self, unique_id):
"""Set the unique ID of the config flow and abort if it already exists.""" """Set the unique ID of the config flow and abort if it already exists."""
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
@callback
async def _show_form(self, errors=None):
"""Show the form to the user."""
return self.async_show_form(
step_id="user", data_schema=self.cloud_api_schema, errors=errors or {},
)
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry):
"""Define the config flow to handle options.""" """Define the config flow to handle options."""
return AirVisualOptionsFlowHandler(config_entry) return AirVisualOptionsFlowHandler(config_entry)
async def async_step_import(self, import_config): async def async_step_geography(self, user_input=None):
"""Import a config entry from configuration.yaml.""" """Handle the initialization of the integration via the cloud API."""
return await self.async_step_user(import_config)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input: if not user_input:
return await self._show_form() return self.async_show_form(
step_id="geography", data_schema=self.geography_schema
)
geo_id = async_get_geography_id(user_input) geo_id = async_get_geography_id(user_input)
await self._async_set_unique_id(geo_id) await self._async_set_unique_id(geo_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
# Find older config entries without unique ID # Find older config entries without unique ID:
for entry in self._async_current_entries(): for entry in self._async_current_entries():
if entry.version != 1: if entry.version != 1:
continue continue
@ -83,7 +101,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
websession = aiohttp_client.async_get_clientsession(self.hass) websession = aiohttp_client.async_get_clientsession(self.hass)
client = Client(websession, api_key=user_input[CONF_API_KEY]) client = Client(session=websession, api_key=user_input[CONF_API_KEY])
# If this is the first (and only the first) time we've seen this API key, check # If this is the first (and only the first) time we've seen this API key, check
# that it's valid: # that it's valid:
@ -97,16 +115,66 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
await client.api.nearest_city() await client.api.nearest_city()
except InvalidKeyError: except InvalidKeyError:
return await self._show_form( return self.async_show_form(
errors={CONF_API_KEY: "invalid_api_key"} step_id="geography",
data_schema=self.geography_schema,
errors={CONF_API_KEY: "invalid_api_key"},
) )
checked_keys.add(user_input[CONF_API_KEY]) checked_keys.add(user_input[CONF_API_KEY])
return self.async_create_entry( return self.async_create_entry(
title=f"Cloud API ({geo_id})", data=user_input title=f"Cloud API ({geo_id})",
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
) )
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
return await self.async_step_geography(import_config)
async def async_step_node_pro(self, user_input=None):
"""Handle the initialization of the integration with a Node/Pro."""
if not user_input:
return self.async_show_form(
step_id="node_pro", data_schema=self.node_pro_schema
)
await self._async_set_unique_id(user_input[CONF_IP_ADDRESS])
websession = aiohttp_client.async_get_clientsession(self.hass)
client = Client(session=websession)
try:
await client.node.from_samba(
user_input[CONF_IP_ADDRESS],
user_input[CONF_PASSWORD],
include_history=False,
include_trends=False,
)
except NodeProError as err:
LOGGER.error("Error connecting to Node/Pro unit: %s", err)
return self.async_show_form(
step_id="node_pro",
data_schema=self.node_pro_schema,
errors={CONF_IP_ADDRESS: "unable_to_connect"},
)
return self.async_create_entry(
title=f"Node/Pro ({user_input[CONF_IP_ADDRESS]})",
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO},
)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input:
return self.async_show_form(
step_id="user", data_schema=self.pick_integration_type_schema
)
if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY:
return await self.async_step_geography()
return await self.async_step_node_pro()
class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): class AirVisualOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle an AirVisual options flow.""" """Handle an AirVisual options flow."""

View File

@ -1,14 +1,15 @@
"""Define AirVisual constants.""" """Define AirVisual constants."""
from datetime import timedelta import logging
DOMAIN = "airvisual" DOMAIN = "airvisual"
LOGGER = logging.getLogger(__package__)
INTEGRATION_TYPE_GEOGRAPHY = "Geographical Location"
INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro"
CONF_CITY = "city" CONF_CITY = "city"
CONF_COUNTRY = "country" CONF_COUNTRY = "country"
CONF_GEOGRAPHIES = "geographies" CONF_GEOGRAPHIES = "geographies"
CONF_INTEGRATION_TYPE = "integration_type"
DATA_CLIENT = "client" DATA_COORDINATOR = "coordinator"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
TOPIC_UPDATE = f"{DOMAIN}_update"

View File

@ -3,6 +3,6 @@
"name": "AirVisual", "name": "AirVisual",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual", "documentation": "https://www.home-assistant.io/integrations/airvisual",
"requirements": ["pyairvisual==3.0.1"], "requirements": ["pyairvisual==4.4.0"],
"codeowners": ["@bachya"] "codeowners": ["@bachya"]
} }

View File

@ -2,7 +2,6 @@
from logging import getLogger from logging import getLogger
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LATITUDE, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_LONGITUDE,
ATTR_STATE, ATTR_STATE,
@ -13,12 +12,23 @@ from homeassistant.const import (
CONF_LONGITUDE, CONF_LONGITUDE,
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
CONF_STATE, CONF_STATE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS,
UNIT_PERCENTAGE,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import CONF_CITY, CONF_COUNTRY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE from . import AirVisualEntity
from .const import (
CONF_CITY,
CONF_COUNTRY,
CONF_INTEGRATION_TYPE,
DATA_COORDINATOR,
DOMAIN,
INTEGRATION_TYPE_GEOGRAPHY,
)
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@ -28,8 +38,6 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
ATTR_POLLUTANT_UNIT = "pollutant_unit" ATTR_POLLUTANT_UNIT = "pollutant_unit"
ATTR_REGION = "region" ATTR_REGION = "region"
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
MASS_PARTS_PER_MILLION = "ppm" MASS_PARTS_PER_MILLION = "ppm"
MASS_PARTS_PER_BILLION = "ppb" MASS_PARTS_PER_BILLION = "ppb"
VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3" VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3"
@ -37,11 +45,22 @@ VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3"
SENSOR_KIND_LEVEL = "air_pollution_level" SENSOR_KIND_LEVEL = "air_pollution_level"
SENSOR_KIND_AQI = "air_quality_index" SENSOR_KIND_AQI = "air_quality_index"
SENSOR_KIND_POLLUTANT = "main_pollutant" SENSOR_KIND_POLLUTANT = "main_pollutant"
SENSORS = [ SENSOR_KIND_BATTERY_LEVEL = "battery_level"
SENSOR_KIND_HUMIDITY = "humidity"
SENSOR_KIND_TEMPERATURE = "temperature"
GEOGRAPHY_SENSORS = [
(SENSOR_KIND_LEVEL, "Air Pollution Level", "mdi:gauge", None), (SENSOR_KIND_LEVEL, "Air Pollution Level", "mdi:gauge", None),
(SENSOR_KIND_AQI, "Air Quality Index", "mdi:chart-line", "AQI"), (SENSOR_KIND_AQI, "Air Quality Index", "mdi:chart-line", "AQI"),
(SENSOR_KIND_POLLUTANT, "Main Pollutant", "mdi:chemical-weapon", None), (SENSOR_KIND_POLLUTANT, "Main Pollutant", "mdi:chemical-weapon", None),
] ]
GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
NODE_PRO_SENSORS = [
(SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE),
(SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, UNIT_PERCENTAGE),
(SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS),
]
POLLUTANT_LEVEL_MAPPING = [ POLLUTANT_LEVEL_MAPPING = [
{"label": "Good", "icon": "mdi:emoticon-excited", "minimum": 0, "maximum": 50}, {"label": "Good", "icon": "mdi:emoticon-excited", "minimum": 0, "maximum": 50},
@ -71,31 +90,43 @@ POLLUTANT_MAPPING = {
"s2": {"label": "Sulfur Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION}, "s2": {"label": "Sulfur Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION},
} }
SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up AirVisual sensors based on a config entry.""" """Set up AirVisual sensors based on a config entry."""
airvisual = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
async_add_entities( if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
[ sensors = [
AirVisualSensor(airvisual, kind, name, icon, unit, locale, geography_id) AirVisualGeographySensor(
for geography_id in airvisual.data coordinator, config_entry, kind, name, icon, unit, locale,
for locale in SENSOR_LOCALES )
for kind, name, icon, unit in SENSORS for locale in GEOGRAPHY_SENSOR_LOCALES
], for kind, name, icon, unit in GEOGRAPHY_SENSORS
True, ]
) else:
sensors = [
AirVisualNodeProSensor(coordinator, kind, name, device_class, unit)
for kind, name, device_class, unit in NODE_PRO_SENSORS
]
async_add_entities(sensors, True)
class AirVisualSensor(Entity): class AirVisualGeographySensor(AirVisualEntity):
"""Define an AirVisual sensor.""" """Define an AirVisual sensor related to geography data via the Cloud API."""
def __init__(self, airvisual, kind, name, icon, unit, locale, geography_id): def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale):
"""Initialize.""" """Initialize."""
self._airvisual = airvisual super().__init__(coordinator)
self._geography_id = geography_id
self._attrs.update(
{
ATTR_CITY: config_entry.data.get(CONF_CITY),
ATTR_STATE: config_entry.data.get(CONF_STATE),
ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY),
}
)
self._config_entry = config_entry
self._icon = icon self._icon = icon
self._kind = kind self._kind = kind
self._locale = locale self._locale = locale
@ -103,37 +134,20 @@ class AirVisualSensor(Entity):
self._state = None self._state = None
self._unit = unit self._unit = unit
self._attrs = {
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION,
ATTR_CITY: airvisual.data[geography_id].get(CONF_CITY),
ATTR_STATE: airvisual.data[geography_id].get(CONF_STATE),
ATTR_COUNTRY: airvisual.data[geography_id].get(CONF_COUNTRY),
}
@property @property
def available(self): def available(self):
"""Return True if entity is available.""" """Return True if entity is available."""
try: try:
return bool( return self.coordinator.last_update_success and bool(
self._airvisual.data[self._geography_id]["current"]["pollution"] self.coordinator.data["current"]["pollution"]
) )
except KeyError: except KeyError:
return False return False
@property
def device_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
@property
def icon(self):
"""Return the icon."""
return self._icon
@property @property
def name(self): def name(self):
"""Return the name.""" """Return the name."""
return f"{SENSOR_LOCALES[self._locale]} {self._name}" return f"{GEOGRAPHY_SENSOR_LOCALES[self._locale]} {self._name}"
@property @property
def state(self): def state(self):
@ -143,27 +157,13 @@ class AirVisualSensor(Entity):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity.""" """Return a unique, Home Assistant friendly identifier for this entity."""
return f"{self._geography_id}_{self._locale}_{self._kind}" return f"{self._config_entry.unique_id}_{self._locale}_{self._kind}"
@property @callback
def unit_of_measurement(self): def update_from_latest_data(self):
"""Return the unit the value is expressed in.""" """Update the entity from the latest data."""
return self._unit
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)
self.async_on_remove(async_dispatcher_connect(self.hass, TOPIC_UPDATE, update))
async def async_update(self):
"""Update the sensor."""
try: try:
data = self._airvisual.data[self._geography_id]["current"]["pollution"] data = self.coordinator.data["current"]["pollution"]
except KeyError: except KeyError:
return return
@ -188,18 +188,79 @@ class AirVisualSensor(Entity):
} }
) )
if CONF_LATITUDE in self._airvisual.geography_data: if CONF_LATITUDE in self._config_entry.data:
if self._airvisual.options[CONF_SHOW_ON_MAP]: if self._config_entry.options[CONF_SHOW_ON_MAP]:
self._attrs[ATTR_LATITUDE] = self._airvisual.geography_data[ self._attrs[ATTR_LATITUDE] = self._config_entry.data[CONF_LATITUDE]
CONF_LATITUDE self._attrs[ATTR_LONGITUDE] = self._config_entry.data[CONF_LONGITUDE]
]
self._attrs[ATTR_LONGITUDE] = self._airvisual.geography_data[
CONF_LONGITUDE
]
self._attrs.pop("lati", None) self._attrs.pop("lati", None)
self._attrs.pop("long", None) self._attrs.pop("long", None)
else: else:
self._attrs["lati"] = self._airvisual.geography_data[CONF_LATITUDE] self._attrs["lati"] = self._config_entry.data[CONF_LATITUDE]
self._attrs["long"] = self._airvisual.geography_data[CONF_LONGITUDE] self._attrs["long"] = self._config_entry.data[CONF_LONGITUDE]
self._attrs.pop(ATTR_LATITUDE, None) self._attrs.pop(ATTR_LATITUDE, None)
self._attrs.pop(ATTR_LONGITUDE, None) self._attrs.pop(ATTR_LONGITUDE, None)
class AirVisualNodeProSensor(AirVisualEntity):
"""Define an AirVisual sensor related to a Node/Pro unit."""
def __init__(self, coordinator, kind, name, device_class, unit):
"""Initialize."""
super().__init__(coordinator)
self._device_class = device_class
self._kind = kind
self._name = name
self._state = None
self._unit = unit
@property
def device_class(self):
"""Return the device class."""
return self._device_class
@property
def device_info(self):
"""Return device registry information for this entity."""
return {
"identifiers": {
(DOMAIN, self.coordinator.data["current"]["serial_number"])
},
"name": self.coordinator.data["current"]["settings"]["node_name"],
"manufacturer": "AirVisual",
"model": f'{self.coordinator.data["current"]["status"]["model"]}',
"sw_version": (
f'Version {self.coordinator.data["current"]["status"]["system_version"]}'
f'{self.coordinator.data["current"]["status"]["app_version"]}'
),
}
@property
def name(self):
"""Return the name."""
node_name = self.coordinator.data["current"]["settings"]["node_name"]
return f"{node_name} Node/Pro: {self._name}"
@property
def state(self):
"""Return the state."""
return self._state
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return f"{self.coordinator.data['current']['serial_number']}_{self._kind}"
@callback
def update_from_latest_data(self):
"""Update the entity from the latest data."""
if self._kind == SENSOR_KIND_BATTERY_LEVEL:
self._state = self.coordinator.data["current"]["status"]["battery"]
elif self._kind == SENSOR_KIND_HUMIDITY:
self._state = self.coordinator.data["current"]["measurements"].get(
"humidity"
)
elif self._kind == SENSOR_KIND_TEMPERATURE:
self._state = self.coordinator.data["current"]["measurements"].get(
"temperature_C"
)

View File

@ -1,28 +1,50 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "geography": {
"title": "Configure AirVisual", "title": "Configure a Geography",
"description": "Monitor air quality in a geographical location.", "description": "Use the AirVisual cloud API to monitor a geographical location.",
"data": { "data": {
"api_key": "API Key", "api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude" "longitude": "Longitude"
} }
},
"node_pro": {
"title": "Configure an AirVisual Node/Pro",
"description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.",
"data": {
"ip_address": "Unit IP Address/Hostname",
"password": "[%key:common::config_flow::data::password%]"
}
},
"user": {
"title": "Configure AirVisual",
"description": "Pick what type of AirVisual data you want to monitor.",
"data": {
"cloud_api": "Geographical Location",
"node_pro": "AirVisual Node Pro",
"type": "Integration Type"
}
} }
}, },
"error": { "invalid_api_key": "Invalid API key" }, "error": {
"general_error": "There was an unknown error.",
"invalid_api_key": "Invalid API key provided.",
"unable_to_connect": "Unable to connect to Node/Pro unit."
},
"abort": { "abort": {
"already_configured": "These coordinates have already been registered." "already_configured": "These coordinates or Node/Pro ID are already registered."
} }
}, },
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "Configure AirVisual", "title": "Configure AirVisual",
"description": "Set various options for the AirVisual integration.", "data": {
"data": { "show_on_map": "Show monitored geography on the map" } "show_on_map": "Show monitored geography on the map"
}
} }
} }
} }
} }

View File

@ -5,13 +5,13 @@
}, },
"error": { "error": {
"general_error": "S'ha produ\u00eft un error desconegut.", "general_error": "S'ha produ\u00eft un error desconegut.",
"invalid_api_key": "Clau API inv\u00e0lida", "invalid_api_key": "Clau API proporiconada no v\u00e0lida.",
"unable_to_connect": "No s'ha pogut connectar a la unitat Node/Pro." "unable_to_connect": "No s'ha pogut connectar a la unitat Node/Pro."
}, },
"step": { "step": {
"geography": { "geography": {
"data": { "data": {
"api_key": "Clau API", "api_key": "[%key::common::config_flow::data::api_key%]",
"latitude": "Latitud", "latitude": "Latitud",
"longitude": "Longitud" "longitude": "Longitud"
}, },

View File

@ -14,7 +14,8 @@
"api_key": "API-Schl\u00fcssel", "api_key": "API-Schl\u00fcssel",
"latitude": "Breitengrad", "latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad" "longitude": "L\u00e4ngengrad"
} },
"title": "Konfigurieren Sie eine Geografie"
}, },
"node_pro": { "node_pro": {
"data": { "data": {

View File

@ -1,13 +1,15 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "These coordinates have already been registered." "already_configured": "These coordinates or Node/Pro ID are already registered."
}, },
"error": { "error": {
"invalid_api_key": "Invalid API key" "general_error": "There was an unknown error.",
"invalid_api_key": "Invalid API key provided.",
"unable_to_connect": "Unable to connect to Node/Pro unit."
}, },
"step": { "step": {
"user": { "geography": {
"data": { "data": {
"api_key": "API Key", "api_key": "API Key",
"latitude": "Latitude", "latitude": "Latitude",
@ -19,7 +21,7 @@
"node_pro": { "node_pro": {
"data": { "data": {
"ip_address": "Unit IP Address/Hostname", "ip_address": "Unit IP Address/Hostname",
"password": "Unit Password" "password": "Password"
}, },
"description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.", "description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.",
"title": "Configure an AirVisual Node/Pro" "title": "Configure an AirVisual Node/Pro"
@ -49,4 +51,4 @@
} }
} }
} }
} }

View File

@ -4,14 +4,34 @@
"already_configured": "Estas coordenadas ya han sido registradas." "already_configured": "Estas coordenadas ya han sido registradas."
}, },
"error": { "error": {
"invalid_api_key": "Clave de API inv\u00e1lida" "general_error": "Se ha producido un error desconocido.",
"unable_to_connect": "No se puede conectar a la unidad Node/Pro."
}, },
"step": { "step": {
"geography": {
"data": {
"latitude": "Latitud",
"longitude": "Longitud"
},
"description": "Use la API de AirVisual para monitorear una ubicaci\u00f3n geogr\u00e1fica.",
"title": "Configurar una geograf\u00eda"
},
"node_pro": {
"data": {
"ip_address": "Direcci\u00f3n IP/nombre de host de la unidad",
"password": "Contrase\u00f1a de la unidad"
},
"description": "Monitoree una unidad AirVisual personal. La contrase\u00f1a se puede recuperar de la interfaz de usuario de la unidad.",
"title": "Configurar un AirVisual Node/Pro"
},
"user": { "user": {
"data": { "data": {
"api_key": "Clave API", "api_key": "Clave API",
"cloud_api": "Localizaci\u00f3n geogr\u00e1fica",
"latitude": "Latitud", "latitude": "Latitud",
"longitude": "Longitud" "longitude": "Longitud",
"node_pro": "AirVisual Node Pro",
"type": "Tipo de integraci\u00f3n"
}, },
"description": "Monitoree la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.", "description": "Monitoree la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.",
"title": "Configurar AirVisual" "title": "Configurar AirVisual"

View File

@ -1,11 +1,11 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Esta clave API ya est\u00e1 en uso." "already_configured": "Estas coordenadas o Nodo/Pro ID ya est\u00e1n registradas."
}, },
"error": { "error": {
"general_error": "Se ha producido un error desconocido.", "general_error": "Se ha producido un error desconocido.",
"invalid_api_key": "Clave API inv\u00e1lida", "invalid_api_key": "Se proporciona una clave API no v\u00e1lida.",
"unable_to_connect": "No se puede conectar a la unidad Node/Pro." "unable_to_connect": "No se puede conectar a la unidad Node/Pro."
}, },
"step": { "step": {
@ -35,7 +35,7 @@
"node_pro": "AirVisual Node Pro", "node_pro": "AirVisual Node Pro",
"type": "Tipo de Integraci\u00f3n" "type": "Tipo de Integraci\u00f3n"
}, },
"description": "Monitorizar la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.", "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres monitorear.",
"title": "Configurar AirVisual" "title": "Configurar AirVisual"
} }
} }

View File

@ -0,0 +1,28 @@
{
"config": {
"error": {
"general_error": "Tapahtui tuntematon virhe."
},
"step": {
"geography": {
"data": {
"api_key": "API-avain",
"latitude": "Leveysaste",
"longitude": "Pituusaste"
}
},
"node_pro": {
"data": {
"password": "Salasana"
}
},
"user": {
"data": {
"cloud_api": "Maantieteellinen sijainti",
"node_pro": "AirVisual Node Pro",
"type": "Integrointityyppi"
}
}
}
}
}

View File

@ -4,21 +4,36 @@
"already_configured": "Cette cl\u00e9 API est d\u00e9j\u00e0 utilis\u00e9e." "already_configured": "Cette cl\u00e9 API est d\u00e9j\u00e0 utilis\u00e9e."
}, },
"error": { "error": {
"invalid_api_key": "Cl\u00e9 API invalide" "general_error": "Une erreur inconnue est survenue.",
"invalid_api_key": "La cl\u00e9 API fournie n'est pas valide.",
"unable_to_connect": "Impossible de se connecter \u00e0 l'unit\u00e9 Node / Pro."
}, },
"step": { "step": {
"geography": { "geography": {
"data": { "data": {
"api_key": "Cl\u00e9 d'API", "api_key": "Cl\u00e9 API",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude" "longitude": "Longitude"
} },
"description": "Utilisez l'API cloud AirVisual pour surveiller une position g\u00e9ographique.",
"title": "Configurer une g\u00e9ographie"
},
"node_pro": {
"data": {
"ip_address": "Adresse IP / nom d'h\u00f4te de l'unit\u00e9",
"password": "Mot de passe de l'unit\u00e9"
},
"description": "Surveillez une unit\u00e9 AirVisual personnelle. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.",
"title": "Configurer un AirVisual Node/Pro"
}, },
"user": { "user": {
"data": { "data": {
"api_key": "Cl\u00e9 API", "api_key": "Cl\u00e9 API",
"cloud_api": "Localisation g\u00e9ographique",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude" "longitude": "Longitude",
"node_pro": "AirVisual Node Pro",
"type": "Type d'int\u00e9gration"
}, },
"description": "Surveiller la qualit\u00e9 de l\u2019air dans un emplacement g\u00e9ographique.", "description": "Surveiller la qualit\u00e9 de l\u2019air dans un emplacement g\u00e9ographique.",
"title": "Configurer AirVisual" "title": "Configurer AirVisual"
@ -28,6 +43,9 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"data": {
"show_on_map": "Afficher la g\u00e9ographie surveill\u00e9e sur la carte"
},
"description": "D\u00e9finissez diverses options pour l'int\u00e9gration d'AirVisual.", "description": "D\u00e9finissez diverses options pour l'int\u00e9gration d'AirVisual.",
"title": "Configurer AirVisual" "title": "Configurer AirVisual"
} }

View File

@ -0,0 +1,14 @@
{
"config": {
"error": {
"invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7"
},
"step": {
"geography": {
"data": {
"api_key": "\u05de\u05e4\u05ea\u05d7 API"
}
}
}
}
}

View File

@ -7,7 +7,6 @@
"step": { "step": {
"geography": { "geography": {
"data": { "data": {
"api_key": "\u090f\u092a\u0940\u0906\u0908 \u0915\u0941\u0902\u091c\u0940",
"latitude": "\u0905\u0915\u094d\u0937\u093e\u0902\u0936", "latitude": "\u0905\u0915\u094d\u0937\u093e\u0902\u0936",
"longitude": "\u0926\u0947\u0936\u093e\u0928\u094d\u0924\u0930" "longitude": "\u0926\u0947\u0936\u093e\u0928\u094d\u0924\u0930"
}, },

View File

@ -0,0 +1,16 @@
{
"config": {
"error": {
"general_error": "Ismeretlen hiba t\u00f6rt\u00e9nt."
},
"step": {
"geography": {
"data": {
"api_key": "API Kulcs",
"latitude": "Sz\u00e9less\u00e9g",
"longitude": "Hossz\u00fas\u00e1g"
}
}
}
}
}

View File

@ -1,19 +1,41 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Queste coordinate sono gi\u00e0 state registrate." "already_configured": "Queste coordinate o Node/Pro ID sono gi\u00e0 registrate."
}, },
"error": { "error": {
"invalid_api_key": "Chiave API non valida" "general_error": "Si \u00e8 verificato un errore sconosciuto.",
"invalid_api_key": "Chiave API non valida fornita.",
"unable_to_connect": "Impossibile connettersi all'unit\u00e0 Node/Pro."
}, },
"step": { "step": {
"user": { "geography": {
"data": { "data": {
"api_key": "Chiave API", "api_key": "Chiave API",
"latitude": "Latitudine", "latitude": "Latitudine",
"longitude": "Logitudine" "longitude": "Logitudine"
}, },
"description": "Monitorare la qualit\u00e0 dell'aria in una posizione geografica.", "description": "Utilizzare l'API di AirVisual cloud per monitorare una posizione geografica.",
"title": "Configurare una Geografia"
},
"node_pro": {
"data": {
"ip_address": "Indirizzo IP/Nome host dell'unit\u00e0",
"password": "Password dell'unit\u00e0"
},
"description": "Monitorare un'unit\u00e0 AirVisual personale. La password pu\u00f2 essere recuperata dall'interfaccia utente dell'unit\u00e0.",
"title": "Configurare un AirVisual Node/Pro"
},
"user": {
"data": {
"api_key": "Chiave API",
"cloud_api": "Posizione geografica",
"latitude": "Latitudine",
"longitude": "Logitudine",
"node_pro": "AirVisual Node Pro",
"type": "Tipo di integrazione"
},
"description": "Scegliere il tipo di dati AirVisual che si desidera monitorare.",
"title": "Configura AirVisual" "title": "Configura AirVisual"
} }
} }

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