mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 23:27:37 +00:00
Merge pull request #35828 from home-assistant/rc
This commit is contained in:
commit
70b14518d0
80
.coveragerc
80
.coveragerc
@ -16,11 +16,16 @@ omit =
|
||||
homeassistant/components/adguard/switch.py
|
||||
homeassistant/components/ads/*
|
||||
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/air_quality.py
|
||||
homeassistant/components/airly/sensor.py
|
||||
homeassistant/components/airly/const.py
|
||||
homeassistant/components/airvisual/__init__.py
|
||||
homeassistant/components/airvisual/air_quality.py
|
||||
homeassistant/components/airvisual/sensor.py
|
||||
homeassistant/components/aladdin_connect/cover.py
|
||||
homeassistant/components/alarmdecoder/*
|
||||
@ -57,7 +62,7 @@ omit =
|
||||
homeassistant/components/aten_pe/*
|
||||
homeassistant/components/atome/*
|
||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||
homeassistant/components/automatic/device_tracker.py
|
||||
homeassistant/components/automatic/*
|
||||
homeassistant/components/avea/light.py
|
||||
homeassistant/components/avion/light.py
|
||||
homeassistant/components/avri/sensor.py
|
||||
@ -89,12 +94,16 @@ omit =
|
||||
homeassistant/components/braviatv/const.py
|
||||
homeassistant/components/braviatv/media_player.py
|
||||
homeassistant/components/broadlink/const.py
|
||||
homeassistant/components/broadlink/device.py
|
||||
homeassistant/components/broadlink/remote.py
|
||||
homeassistant/components/broadlink/sensor.py
|
||||
homeassistant/components/broadlink/switch.py
|
||||
homeassistant/components/brottsplatskartan/sensor.py
|
||||
homeassistant/components/browser/*
|
||||
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_smarthub/device_tracker.py
|
||||
homeassistant/components/buienradar/sensor.py
|
||||
@ -142,6 +151,9 @@ omit =
|
||||
homeassistant/components/denon/media_player.py
|
||||
homeassistant/components/denonavr/media_player.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/digital_ocean/*
|
||||
homeassistant/components/digitalloggers/switch.py
|
||||
@ -225,6 +237,9 @@ omit =
|
||||
homeassistant/components/fleetgo/device_tracker.py
|
||||
homeassistant/components/flexit/climate.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/flume/*
|
||||
homeassistant/components/flunearyou/__init__.py
|
||||
@ -298,6 +313,7 @@ omit =
|
||||
homeassistant/components/hitron_coda/device_tracker.py
|
||||
homeassistant/components/hive/*
|
||||
homeassistant/components/hlk_sw16/*
|
||||
homeassistant/components/home_connect/*
|
||||
homeassistant/components/homematic/*
|
||||
homeassistant/components/homematic/climate.py
|
||||
homeassistant/components/homematic/cover.py
|
||||
@ -310,7 +326,11 @@ omit =
|
||||
homeassistant/components/huawei_lte/*
|
||||
homeassistant/components/huawei_router/device_tracker.py
|
||||
homeassistant/components/hue/light.py
|
||||
homeassistant/components/hunterdouglas_powerview/__init__.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/hyperion/light.py
|
||||
homeassistant/components/ialarm/alarm_control_panel.py
|
||||
@ -343,11 +363,27 @@ omit =
|
||||
homeassistant/components/iqvia/*
|
||||
homeassistant/components/irish_rail_transport/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/itunes/media_player.py
|
||||
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/kankun/switch.py
|
||||
homeassistant/components/keba/*
|
||||
@ -428,6 +464,7 @@ omit =
|
||||
homeassistant/components/miflora/sensor.py
|
||||
homeassistant/components/mikrotik/hub.py
|
||||
homeassistant/components/mikrotik/device_tracker.py
|
||||
homeassistant/components/mill/__init__.py
|
||||
homeassistant/components/mill/climate.py
|
||||
homeassistant/components/mill/const.py
|
||||
homeassistant/components/minecraft_server/__init__.py
|
||||
@ -502,7 +539,14 @@ omit =
|
||||
homeassistant/components/ombi/*
|
||||
homeassistant/components/onewire/sensor.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/device.py
|
||||
homeassistant/components/onvif/event.py
|
||||
homeassistant/components/onvif/parsers.py
|
||||
homeassistant/components/onvif/sensor.py
|
||||
homeassistant/components/opencv/*
|
||||
homeassistant/components/openevse/sensor.py
|
||||
homeassistant/components/openexchangerates/sensor.py
|
||||
@ -527,7 +571,6 @@ omit =
|
||||
homeassistant/components/osramlightify/light.py
|
||||
homeassistant/components/otp/sensor.py
|
||||
homeassistant/components/panasonic_bluray/media_player.py
|
||||
homeassistant/components/panasonic_viera/__init__.py
|
||||
homeassistant/components/panasonic_viera/media_player.py
|
||||
homeassistant/components/pandora/media_player.py
|
||||
homeassistant/components/pcal9535a/*
|
||||
@ -569,7 +612,6 @@ omit =
|
||||
homeassistant/components/qrcode/image_processing.py
|
||||
homeassistant/components/quantum_gateway/device_tracker.py
|
||||
homeassistant/components/qvr_pro/*
|
||||
homeassistant/components/qwikswitch/*
|
||||
homeassistant/components/rachio/*
|
||||
homeassistant/components/radarr/sensor.py
|
||||
homeassistant/components/radiotherm/climate.py
|
||||
@ -599,7 +641,6 @@ omit =
|
||||
homeassistant/components/ring/camera.py
|
||||
homeassistant/components/ripple/sensor.py
|
||||
homeassistant/components/rocketchat/notify.py
|
||||
homeassistant/components/roku/remote.py
|
||||
homeassistant/components/roomba/binary_sensor.py
|
||||
homeassistant/components/roomba/braava.py
|
||||
homeassistant/components/roomba/irobot_base.py
|
||||
@ -608,7 +649,7 @@ omit =
|
||||
homeassistant/components/roomba/vacuum.py
|
||||
homeassistant/components/route53/*
|
||||
homeassistant/components/rova/sensor.py
|
||||
homeassistant/components/rpi_camera/camera.py
|
||||
homeassistant/components/rpi_camera/*
|
||||
homeassistant/components/rpi_gpio/*
|
||||
homeassistant/components/rpi_gpio/cover.py
|
||||
homeassistant/components/rpi_gpio_pwm/light.py
|
||||
@ -672,7 +713,6 @@ omit =
|
||||
homeassistant/components/somfy/*
|
||||
homeassistant/components/somfy_mylink/*
|
||||
homeassistant/components/sonarr/sensor.py
|
||||
homeassistant/components/songpal/*
|
||||
homeassistant/components/sonos/*
|
||||
homeassistant/components/sony_projector/switch.py
|
||||
homeassistant/components/spc/*
|
||||
@ -773,6 +813,10 @@ omit =
|
||||
homeassistant/components/ubus/device_tracker.py
|
||||
homeassistant/components/ue_smart_radio/media_player.py
|
||||
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/upnp/*
|
||||
homeassistant/components/upc_connect/*
|
||||
@ -816,6 +860,7 @@ omit =
|
||||
homeassistant/components/webostv/*
|
||||
homeassistant/components/wemo/*
|
||||
homeassistant/components/whois/sensor.py
|
||||
homeassistant/components/wiffi/*
|
||||
homeassistant/components/wink/*
|
||||
homeassistant/components/wirelesstag/*
|
||||
homeassistant/components/worldtidesinfo/sensor.py
|
||||
@ -829,7 +874,17 @@ omit =
|
||||
homeassistant/components/xfinity/device_tracker.py
|
||||
homeassistant/components/xiaomi/camera.py
|
||||
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/xmpp/notify.py
|
||||
homeassistant/components/xs1/*
|
||||
@ -844,8 +899,9 @@ omit =
|
||||
homeassistant/components/zamg/weather.py
|
||||
homeassistant/components/zengge/light.py
|
||||
homeassistant/components/zeroconf/*
|
||||
homeassistant/components/zerproc/__init__.py
|
||||
homeassistant/components/zerproc/const.py
|
||||
homeassistant/components/zestimate/sensor.py
|
||||
homeassistant/components/zha/__init__.py
|
||||
homeassistant/components/zha/api.py
|
||||
homeassistant/components/zha/core/channels/*
|
||||
homeassistant/components/zha/core/const.py
|
||||
@ -864,6 +920,10 @@ omit =
|
||||
homeassistant/components/zoneminder/*
|
||||
homeassistant/components/supla/*
|
||||
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]
|
||||
# Regexes for lines to exclude from consideration
|
||||
|
5
.hadolint.yaml
Normal file
5
.hadolint.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
ignored:
|
||||
- DL3006
|
||||
- DL3008
|
||||
- DL3013
|
||||
- DL3018
|
@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.1.0
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
@ -18,9 +18,9 @@ repos:
|
||||
- id: codespell
|
||||
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
|
||||
- --skip="./.*,*.json"
|
||||
- --skip="./.*,*.csv,*.json"
|
||||
- --quiet-level=2
|
||||
exclude_types: [json]
|
||||
exclude_types: [csv, json]
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.7.9
|
||||
hooks:
|
||||
|
@ -11,6 +11,9 @@ addons:
|
||||
- libswscale-dev
|
||||
- libswresample-dev
|
||||
- libavfilter-dev
|
||||
sources:
|
||||
- sourceline: ppa:savoury1/ffmpeg4
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
|
31
CODEOWNERS
31
CODEOWNERS
@ -15,6 +15,7 @@ homeassistant/scripts/check_config.py @kellerza
|
||||
# Integrations
|
||||
homeassistant/components/abode/* @shred86
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
homeassistant/components/airly/* @bieniu
|
||||
homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarmdecoder/* @ajschmidt8
|
||||
@ -47,12 +48,13 @@ homeassistant/components/avea/* @pattyland
|
||||
homeassistant/components/avri/* @timvancann
|
||||
homeassistant/components/awair/* @danielsjf
|
||||
homeassistant/components/aws/* @awarecan @robbiet480
|
||||
homeassistant/components/axis/* @kane610
|
||||
homeassistant/components/axis/* @Kane610
|
||||
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
||||
homeassistant/components/azure_service_bus/* @hfurubotten
|
||||
homeassistant/components/beewi_smartclim/* @alemuro
|
||||
homeassistant/components/bitcoin/* @fabaff
|
||||
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||
homeassistant/components/blebox/* @gadgetmobile
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/bmp280/* @belidzs
|
||||
homeassistant/components/bmw_connected_drive/* @gerard33
|
||||
@ -61,6 +63,7 @@ homeassistant/components/braviatv/* @robbiet480 @bieniu
|
||||
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
||||
homeassistant/components/brother/* @bieniu
|
||||
homeassistant/components/brunt/* @eavanvalkenburg
|
||||
homeassistant/components/bsblan/* @liudger
|
||||
homeassistant/components/bt_smarthub/* @jxwolstenholme
|
||||
homeassistant/components/buienradar/* @mjj4791 @ties
|
||||
homeassistant/components/cast/* @emontnemery
|
||||
@ -82,12 +85,13 @@ homeassistant/components/cpuspeed/* @fabaff
|
||||
homeassistant/components/cups/* @fabaff
|
||||
homeassistant/components/daikin/* @fredrike
|
||||
homeassistant/components/darksky/* @fabaff
|
||||
homeassistant/components/deconz/* @kane610
|
||||
homeassistant/components/deconz/* @Kane610
|
||||
homeassistant/components/delijn/* @bollewolle
|
||||
homeassistant/components/demo/* @home-assistant/core
|
||||
homeassistant/components/denonavr/* @scarface-4711 @starkillerOG
|
||||
homeassistant/components/derivative/* @afaucogney
|
||||
homeassistant/components/device_automation/* @home-assistant/core
|
||||
homeassistant/components/devolo_home_control/* @2Fake @Shutgun
|
||||
homeassistant/components/digital_ocean/* @fabaff
|
||||
homeassistant/components/directv/* @ctalkington
|
||||
homeassistant/components/discogs/* @thibmaek
|
||||
@ -122,9 +126,11 @@ homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
homeassistant/components/fitbit/* @robbiet480
|
||||
homeassistant/components/fixer/* @fabaff
|
||||
homeassistant/components/flick_electric/* @ZephireNZ
|
||||
homeassistant/components/flock/* @fabaff
|
||||
homeassistant/components/flume/* @ChrisMandich @bdraco
|
||||
homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/forked_daapd/* @uvjustin
|
||||
homeassistant/components/fortigate/* @kifeo
|
||||
homeassistant/components/fortios/* @kimfrellsen
|
||||
homeassistant/components/foscam/* @skgsergio
|
||||
@ -163,6 +169,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl
|
||||
homeassistant/components/hisense_aehw4a1/* @bannhead
|
||||
homeassistant/components/history/* @home-assistant/core
|
||||
homeassistant/components/hive/* @Rendili @KJonline
|
||||
homeassistant/components/home_connect/* @DavidMStraub
|
||||
homeassistant/components/homeassistant/* @home-assistant/core
|
||||
homeassistant/components/homekit/* @bdraco
|
||||
homeassistant/components/homekit_controller/* @Jc2k
|
||||
@ -174,6 +181,7 @@ homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/huawei_lte/* @scop
|
||||
homeassistant/components/huawei_router/* @abmantis
|
||||
homeassistant/components/hue/* @balloob
|
||||
homeassistant/components/hunterdouglas_powerview/* @bdraco
|
||||
homeassistant/components/iammeter/* @lewei50
|
||||
homeassistant/components/iaqualink/* @flz
|
||||
homeassistant/components/icloud/* @Quentame
|
||||
@ -195,7 +203,7 @@ homeassistant/components/ipp/* @ctalkington
|
||||
homeassistant/components/iqvia/* @bachya
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
homeassistant/components/islamic_prayer_times/* @engrbm87
|
||||
homeassistant/components/isy994/* @bdraco
|
||||
homeassistant/components/isy994/* @bdraco @shbatm
|
||||
homeassistant/components/izone/* @Swamp-Ig
|
||||
homeassistant/components/jewish_calendar/* @tsvi
|
||||
homeassistant/components/juicenet/* @jesserockz
|
||||
@ -239,7 +247,7 @@ homeassistant/components/minecraft_server/* @elmurato
|
||||
homeassistant/components/minio/* @tkislan
|
||||
homeassistant/components/mobile_app/* @robbiet480
|
||||
homeassistant/components/modbus/* @adamchengtkc @janiversen
|
||||
homeassistant/components/monoprice/* @etsinko
|
||||
homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||
homeassistant/components/moon/* @fabaff
|
||||
homeassistant/components/mpd/* @fabaff
|
||||
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/nuheat/* @bdraco
|
||||
homeassistant/components/nuki/* @pvizeli
|
||||
homeassistant/components/numato/* @clssn
|
||||
homeassistant/components/nut/* @bdraco
|
||||
homeassistant/components/nws/* @MatthewFlamm
|
||||
homeassistant/components/nzbget/* @chriscla
|
||||
@ -275,13 +284,16 @@ homeassistant/components/ohmconnect/* @robbiet480
|
||||
homeassistant/components/ombi/* @larssont
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/onewire/* @garbled1
|
||||
homeassistant/components/onvif/* @hunterjm
|
||||
homeassistant/components/openerz/* @misialq
|
||||
homeassistant/components/opengarage/* @danielhiversen
|
||||
homeassistant/components/opentherm_gw/* @mvn23
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/openweathermap/* @fabaff
|
||||
homeassistant/components/opnsense/* @mtreinish
|
||||
homeassistant/components/orangepi_gpio/* @pascallj
|
||||
homeassistant/components/oru/* @bvlaicu
|
||||
homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare
|
||||
homeassistant/components/panasonic_viera/* @joogps
|
||||
homeassistant/components/panel_custom/* @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/philips_js/* @elupus
|
||||
homeassistant/components/pi4ioe5v9xxxx/* @antonverburg
|
||||
homeassistant/components/pi_hole/* @fabaff @johnluetke
|
||||
homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn
|
||||
homeassistant/components/pilight/* @trekky12
|
||||
homeassistant/components/plaato/* @JohNan
|
||||
homeassistant/components/plant/* @ChristianKuehnel
|
||||
@ -357,13 +369,14 @@ homeassistant/components/solax/* @squishykid
|
||||
homeassistant/components/soma/* @ratsept
|
||||
homeassistant/components/somfy/* @tetienne
|
||||
homeassistant/components/sonarr/* @ctalkington
|
||||
homeassistant/components/songpal/* @rytilahti
|
||||
homeassistant/components/songpal/* @rytilahti @shenxn
|
||||
homeassistant/components/sonos/* @amelchio
|
||||
homeassistant/components/spaceapi/* @fabaff
|
||||
homeassistant/components/speedtestdotnet/* @rohankapoorcom
|
||||
homeassistant/components/spider/* @peternijssen
|
||||
homeassistant/components/spotify/* @frenck
|
||||
homeassistant/components/sql/* @dgomes
|
||||
homeassistant/components/squeezebox/* @rajlaud
|
||||
homeassistant/components/starline/* @anonym-tsk
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
@ -406,12 +419,14 @@ homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
homeassistant/components/tts/* @pvizeli
|
||||
homeassistant/components/tuya/* @ollo69
|
||||
homeassistant/components/twentemilieu/* @frenck
|
||||
homeassistant/components/twilio_call/* @robbiet480
|
||||
homeassistant/components/twilio_sms/* @robbiet480
|
||||
homeassistant/components/ubee/* @mzdrale
|
||||
homeassistant/components/unifi/* @kane610
|
||||
homeassistant/components/unifi/* @Kane610
|
||||
homeassistant/components/unifiled/* @florisvdk
|
||||
homeassistant/components/upb/* @gwww
|
||||
homeassistant/components/upc_connect/* @pvizeli
|
||||
homeassistant/components/upcloud/* @scop
|
||||
homeassistant/components/updater/* @home-assistant/core
|
||||
@ -436,6 +451,7 @@ homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/webostv/* @bendavid
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @sqldiablo
|
||||
homeassistant/components/wiffi/* @mampfes
|
||||
homeassistant/components/withings/* @vangorra
|
||||
homeassistant/components/wled/* @frenck
|
||||
homeassistant/components/workday/* @fabaff
|
||||
@ -455,6 +471,7 @@ homeassistant/components/yessssms/* @flowolf
|
||||
homeassistant/components/yi/* @bachya
|
||||
homeassistant/components/yr/* @danielhiversen
|
||||
homeassistant/components/zeroconf/* @robbiet480 @Kane610
|
||||
homeassistant/components/zerproc/* @emlove
|
||||
homeassistant/components/zha/* @dmulcahey @adminiuga
|
||||
homeassistant/components/zone/* @home-assistant/core
|
||||
homeassistant/components/zoneminder/* @rohankapoorcom
|
||||
|
@ -1,7 +1,7 @@
|
||||
FROM python:3.7
|
||||
FROM python:3.8
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
RUN \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
libudev-dev \
|
||||
libavformat-dev \
|
||||
libavcodec-dev \
|
||||
@ -18,8 +18,7 @@ WORKDIR /usr/src
|
||||
|
||||
# Setup hass-release
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||
&& cd hass-release \
|
||||
&& pip3 install -e .
|
||||
&& pip3 install -e hass-release/
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
|
@ -4,9 +4,9 @@ trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- rc
|
||||
- dev
|
||||
- master
|
||||
- rc
|
||||
- dev
|
||||
- master
|
||||
pr:
|
||||
- rc
|
||||
- dev
|
||||
@ -14,208 +14,229 @@ pr:
|
||||
|
||||
resources:
|
||||
containers:
|
||||
- container: 37
|
||||
image: homeassistant/ci-azure:3.7
|
||||
- container: 37
|
||||
image: homeassistant/ci-azure:3.7
|
||||
- container: 38
|
||||
image: homeassistant/ci-azure:3.8
|
||||
repositories:
|
||||
- repository: azure
|
||||
type: github
|
||||
name: 'home-assistant/ci-azure'
|
||||
endpoint: 'home-assistant'
|
||||
name: "home-assistant/ci-azure"
|
||||
endpoint: "home-assistant"
|
||||
variables:
|
||||
- name: PythonMain
|
||||
value: '37'
|
||||
value: "37"
|
||||
- name: versionHadolint
|
||||
value: "v1.17.6"
|
||||
|
||||
stages:
|
||||
- stage: "Overview"
|
||||
jobs:
|
||||
- job: "Lint"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "requirements_test.txt | homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
python -m venv venv
|
||||
|
||||
- stage: 'Overview'
|
||||
jobs:
|
||||
- job: 'Lint'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt'
|
||||
build: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
pre-commit install-hooks
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files
|
||||
displayName: "Run executables check"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run codespell --all-files
|
||||
displayName: "Run codespell"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run flake8 --all-files
|
||||
displayName: "Run flake8"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run bandit --all-files
|
||||
displayName: "Run bandit"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run isort --all-files --show-diff-on-failure
|
||||
displayName: "Run isort"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run check-json --all-files
|
||||
displayName: "Run check-json"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run yamllint --all-files
|
||||
displayName: "Run yamllint"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run pyupgrade --all-files --show-diff-on-failure
|
||||
displayName: "Run pyupgrade"
|
||||
# Prettier seems to hang on Azure, unknown why yet.
|
||||
# Temporarily disable the check to no block PRs
|
||||
# - script: |
|
||||
# . venv/bin/activate
|
||||
# pre-commit run prettier --all-files --show-diff-on-failure
|
||||
# displayName: 'Run prettier'
|
||||
- job: "Validate"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
pre-commit install-hooks
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files
|
||||
displayName: 'Run executables check'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run codespell --all-files
|
||||
displayName: 'Run codespell'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run flake8 --all-files
|
||||
displayName: 'Run flake8'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run bandit --all-files
|
||||
displayName: 'Run bandit'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run isort --all-files --show-diff-on-failure
|
||||
displayName: 'Run isort'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run check-json --all-files
|
||||
displayName: 'Run check-json'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run yamllint --all-files
|
||||
displayName: 'Run yamllint'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run pyupgrade --all-files --show-diff-on-failure
|
||||
displayName: 'Run pyupgrade'
|
||||
# Prettier seems to hang on Azure, unknown why yet.
|
||||
# Temporarily disable the check to no block PRs
|
||||
# - script: |
|
||||
# . venv/bin/activate
|
||||
# pre-commit run prettier --all-files --show-diff-on-failure
|
||||
# displayName: 'Run prettier'
|
||||
- job: 'Validate'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: 'homeassistant/package_constraints.txt'
|
||||
build: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
python -m script.hassfest --action validate
|
||||
displayName: "Validate manifests"
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
./script/gen_requirements_all.py validate
|
||||
displayName: "requirements_all validate"
|
||||
- job: "CheckFormat"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "requirements_test.txt | homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
python -m script.hassfest --action validate
|
||||
displayName: 'Validate manifests'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
./script/gen_requirements_all.py validate
|
||||
displayName: 'requirements_all validate'
|
||||
- job: 'CheckFormat'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt'
|
||||
build: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
pre-commit install-hooks
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run black --all-files --show-diff-on-failure
|
||||
displayName: "Check Black formatting"
|
||||
- job: "Docker"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- script: sudo docker pull hadolint/hadolint:$(versionHadolint)
|
||||
displayName: "Install Hadolint"
|
||||
- script: |
|
||||
set -e
|
||||
for dockerfile in Dockerfile Dockerfile.dev
|
||||
do
|
||||
echo "Linting: $dockerfile"
|
||||
docker run --rm -i \
|
||||
-v "$(pwd)/.hadolint.yaml:/.hadolint.yaml:ro" \
|
||||
hadolint/hadolint:$(versionHadolint) < "$dockerfile"
|
||||
done
|
||||
displayName: "Run Hadolint"
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
pre-commit install-hooks
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run black --all-files --show-diff-on-failure
|
||||
displayName: 'Check Black formatting'
|
||||
- stage: "Tests"
|
||||
dependsOn:
|
||||
- "Overview"
|
||||
jobs:
|
||||
- job: "PyTest"
|
||||
pool:
|
||||
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'
|
||||
dependsOn:
|
||||
- 'Overview'
|
||||
jobs:
|
||||
- job: 'PyTest'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
matrix:
|
||||
Python37:
|
||||
python.container: '37'
|
||||
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
|
||||
pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt
|
||||
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
|
||||
# Find offending deps with `pipdeptree -r -p typing`
|
||||
pip uninstall -y typing
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: "Install Home Assistant"
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt
|
||||
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
|
||||
# Find offending deps with `pipdeptree -r -p typing`
|
||||
pip uninstall -y typing
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Install Home Assistant'
|
||||
- script: |
|
||||
set -e
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar tests
|
||||
script/check_dirty
|
||||
displayName: "Run pytest for python $(python.container)"
|
||||
condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain']))
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar tests
|
||||
script/check_dirty
|
||||
displayName: 'Run pytest for python $(python.container)'
|
||||
condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain']))
|
||||
- script: |
|
||||
set -e
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
|
||||
codecov --token $(codecovToken)
|
||||
script/check_dirty
|
||||
displayName: "Run pytest for python $(python.container) / coverage"
|
||||
condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain']))
|
||||
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
|
||||
codecov --token $(codecovToken)
|
||||
script/check_dirty
|
||||
displayName: 'Run pytest for python $(python.container) / coverage'
|
||||
condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain']))
|
||||
- stage: "FullCheck"
|
||||
dependsOn:
|
||||
- "Overview"
|
||||
jobs:
|
||||
- job: "Pylint"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: "requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt"
|
||||
build: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
- stage: 'FullCheck'
|
||||
dependsOn:
|
||||
- 'Overview'
|
||||
jobs:
|
||||
- job: 'Pylint'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- template: templates/azp-step-cache.yaml@azure
|
||||
parameters:
|
||||
keyfile: 'requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt'
|
||||
build: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools wheel
|
||||
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
|
||||
# Find offending deps with `pipdeptree -r -p typing`
|
||||
pip uninstall -y typing
|
||||
- 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 -U pip setuptools wheel
|
||||
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
|
||||
# Find offending deps with `pipdeptree -r -p typing`
|
||||
pip uninstall -y typing
|
||||
- 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'
|
||||
. 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"
|
||||
|
@ -8,6 +8,8 @@ import sys
|
||||
import threading
|
||||
from typing import List
|
||||
|
||||
import yarl
|
||||
|
||||
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:
|
||||
return 1
|
||||
|
||||
if args.open_ui and hass.config.api is not None:
|
||||
if args.open_ui:
|
||||
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()
|
||||
|
||||
|
@ -61,8 +61,7 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
"""Validate a username and password."""
|
||||
env = {"username": username, "password": password}
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
process = await asyncio.subprocess.create_subprocess_exec(
|
||||
process = await asyncio.subprocess.create_subprocess_exec( # pylint: disable=no-member
|
||||
self.config[CONF_COMMAND],
|
||||
*self.config[CONF_ARGS],
|
||||
env=env,
|
||||
|
@ -138,8 +138,9 @@ class Data:
|
||||
if not bcrypt.checkpw(password.encode(), user_hash):
|
||||
raise InvalidAuth
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
def hash_password( # pylint: disable=no-self-use
|
||||
self, password: str, for_storage: bool = False
|
||||
) -> bytes:
|
||||
"""Encode a password."""
|
||||
hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
||||
|
||||
|
@ -249,6 +249,10 @@ def async_enable_logging(
|
||||
logging.getLogger("urllib3").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
|
||||
if log_file is None:
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
|
@ -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."""
|
||||
|
||||
@property
|
||||
|
@ -3,7 +3,7 @@ import abodepy.helpers.constants as CONST
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_WINDOW,
|
||||
BinarySensorDevice,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
|
||||
from . import AbodeDevice
|
||||
@ -30,7 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
|
||||
class AbodeBinarySensor(AbodeDevice, BinarySensorEntity):
|
||||
"""A binary sensor implementation for Abode device."""
|
||||
|
||||
@property
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Support for Abode Security System covers."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.cover import CoverEntity
|
||||
|
||||
from . import AbodeDevice
|
||||
from .const import DOMAIN
|
||||
@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AbodeCover(AbodeDevice, CoverDevice):
|
||||
class AbodeCover(AbodeDevice, CoverEntity):
|
||||
"""Representation of an Abode cover."""
|
||||
|
||||
@property
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.light import (
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
Light,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_kelvin_to_mired,
|
||||
@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AbodeLight(AbodeDevice, Light):
|
||||
class AbodeLight(AbodeDevice, LightEntity):
|
||||
"""Representation of an Abode light."""
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Support for the Abode Security System locks."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.components.lock import LockEntity
|
||||
|
||||
from . import AbodeDevice
|
||||
from .const import DOMAIN
|
||||
@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AbodeLock(AbodeDevice, LockDevice):
|
||||
class AbodeLock(AbodeDevice, LockEntity):
|
||||
"""Representation of an Abode lock."""
|
||||
|
||||
def lock(self, **kwargs):
|
||||
|
@ -3,7 +3,10 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"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": {
|
||||
@ -15,4 +18,4 @@
|
||||
"single_instance_allowed": "Only a single configuration of Abode is allowed."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
"""Support for Abode Security System switches."""
|
||||
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 . import AbodeAutomation, AbodeDevice
|
||||
@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AbodeSwitch(AbodeDevice, SwitchDevice):
|
||||
class AbodeSwitch(AbodeDevice, SwitchEntity):
|
||||
"""Representation of an Abode switch."""
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
@ -45,7 +45,7 @@ class AbodeSwitch(AbodeDevice, SwitchDevice):
|
||||
return self._device.is_on
|
||||
|
||||
|
||||
class AbodeAutomationSwitch(AbodeAutomation, SwitchDevice):
|
||||
class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity):
|
||||
"""A switch implementation for Abode automations."""
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
|
@ -12,7 +12,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Email Address"
|
||||
"username": "Email"
|
||||
},
|
||||
"title": "Fill in your Abode login information"
|
||||
}
|
||||
|
7
homeassistant/components/abode/translations/fi.json
Normal file
7
homeassistant/components/abode/translations/fi.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"connection_error": "Yhteytt\u00e4 Abodeen ei voi muodostaa."
|
||||
}
|
||||
}
|
||||
}
|
@ -12,9 +12,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"error": {
|
||||
"connection_error": "Kan ikke koble til Abode.",
|
||||
"identifier_exists": "Kontoen er allerede registrert.",
|
||||
"invalid_credentials": "Ugyldig brukerinformasjon"
|
||||
"invalid_credentials": "Ugyldig legitimasjon"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
@ -11,8 +11,8 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Has\u0142o",
|
||||
"username": "Adres e-mail"
|
||||
"password": "[%key_id:common::config_flow::data::password%]",
|
||||
"username": "[%key_id:common::config_flow::data::email%]"
|
||||
},
|
||||
"title": "Wprowad\u017a informacje logowania Abode"
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "\u5bc6\u78bc",
|
||||
"username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740"
|
||||
"username": "\u96fb\u5b50\u90f5\u4ef6"
|
||||
},
|
||||
"title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a"
|
||||
}
|
||||
|
@ -2,6 +2,6 @@
|
||||
"domain": "acer_projector",
|
||||
"name": "Acer Projector",
|
||||
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
|
||||
"requirements": ["pyserial==3.1.1"],
|
||||
"requirements": ["pyserial==3.4"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import re
|
||||
import serial
|
||||
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_FILENAME,
|
||||
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)
|
||||
|
||||
|
||||
class AcerSwitch(SwitchDevice):
|
||||
class AcerSwitch(SwitchEntity):
|
||||
"""Represents an Acer Projector as a switch."""
|
||||
|
||||
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
|
||||
|
@ -206,4 +206,5 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
|
||||
"name": "AdGuard Home",
|
||||
"manufacturer": "AdGuard Team",
|
||||
"sw_version": self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION),
|
||||
"entry_type": "service",
|
||||
}
|
||||
|
@ -4,10 +4,10 @@
|
||||
"user": {
|
||||
"description": "Set up your AdGuard Home instance to allow monitoring and control.",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"ssl": "AdGuard Home uses a SSL certificate",
|
||||
"verify_ssl": "AdGuard Home uses a proper certificate"
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.adguard.const import (
|
||||
DATA_ADGUARD_VERION,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
@ -45,7 +45,7 @@ async def async_setup_entry(
|
||||
async_add_entities(switches, True)
|
||||
|
||||
|
||||
class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchDevice):
|
||||
class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
|
||||
"""Defines a AdGuard Home switch."""
|
||||
|
||||
def __init__(
|
||||
|
@ -16,9 +16,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u0410\u0434\u0440\u0435\u0441",
|
||||
"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",
|
||||
"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"
|
||||
|
@ -16,11 +16,11 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Amfitri\u00f3",
|
||||
"password": "Contrasenya",
|
||||
"port": "Port",
|
||||
"host": "[%key::common::config_flow::data::host%]",
|
||||
"password": "[%key::common::config_flow::data::password%]",
|
||||
"port": "[%key::common::config_flow::data::port%]",
|
||||
"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"
|
||||
},
|
||||
"description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3.",
|
||||
|
@ -16,9 +16,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "V\u00e6rt",
|
||||
"password": "Adgangskode",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home bruger et SSL-certifikat",
|
||||
"username": "Brugernavn",
|
||||
"verify_ssl": "AdGuard Home bruger et korrekt certifikat"
|
||||
|
@ -17,7 +17,6 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Contrase\u00f1a",
|
||||
"port": "Puerto",
|
||||
"ssl": "AdGuard Home utiliza un certificado SSL",
|
||||
"username": "Nombre de usuario",
|
||||
"verify_ssl": "AdGuard Home utiliza un certificado adecuado"
|
||||
|
15
homeassistant/components/adguard/translations/fi.json
Normal file
15
homeassistant/components/adguard/translations/fi.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"connection_error": "Yhdist\u00e4minen ep\u00e4onnistui."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Palvelin",
|
||||
"port": "Portti"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
homeassistant/components/adguard/translations/he.json
Normal file
12
homeassistant/components/adguard/translations/he.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "\u05e4\u05d5\u05e8\u05d8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,8 +6,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Kata sandi",
|
||||
"port": "Port"
|
||||
"password": "Kata sandi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@
|
||||
"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.",
|
||||
"title": "AdGuard Home \uc5f0\uacb0"
|
||||
"title": "AdGuard Home \uc5f0\uacb0\ud558\uae30"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Apparat",
|
||||
"host": "Host",
|
||||
"password": "Passwuert",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home benotzt een SSL Zertifikat",
|
||||
|
@ -16,9 +16,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Wachtwoord",
|
||||
"port": "Poort",
|
||||
"ssl": "AdGuard Home maakt gebruik van een SSL certificaat",
|
||||
"username": "Gebruikersnaam",
|
||||
"verify_ssl": "AdGuard Home maakt gebruik van een goed certificaat"
|
||||
|
@ -17,10 +17,8 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"password": "Passord",
|
||||
"port": "",
|
||||
"ssl": "AdGuard Hjem bruker et SSL-sertifikat",
|
||||
"username": "Brukernavn",
|
||||
"verify_ssl": "AdGuard Home bruker et riktig sertifikat"
|
||||
},
|
||||
"description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll.",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
|
||||
"connection_error": "[%key_id:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
@ -16,11 +16,11 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Nazwa hosta lub adres IP",
|
||||
"password": "Has\u0142o",
|
||||
"port": "Port",
|
||||
"host": "[%key_id:common::config_flow::data::host%]",
|
||||
"password": "[%key_id:common::config_flow::data::password%]",
|
||||
"port": "[%key_id:common::config_flow::data::port%]",
|
||||
"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."
|
||||
},
|
||||
"description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.",
|
||||
|
@ -14,9 +14,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Senha",
|
||||
"port": "Porta",
|
||||
"ssl": "O AdGuard Home usa um certificado SSL",
|
||||
"username": "Nome de usu\u00e1rio",
|
||||
"verify_ssl": "O AdGuard Home usa um certificado apropriado"
|
||||
|
@ -3,9 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Servidor",
|
||||
"password": "Palavra-passe",
|
||||
"port": "Porta",
|
||||
"username": "Nome de Utilizador"
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "V\u00e4rd",
|
||||
"password": "L\u00f6senord",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home anv\u00e4nder ett SSL-certifikat",
|
||||
"username": "Anv\u00e4ndarnamn",
|
||||
"verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat"
|
||||
|
@ -3,9 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u0110\u1ecba ch\u1ec9",
|
||||
"password": "M\u1eadt kh\u1ea9u",
|
||||
"port": "C\u1ed5ng",
|
||||
"username": "T\u00ean \u0111\u0103ng nh\u1eadp"
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "\u5bc6\u7801",
|
||||
"port": "\u7aef\u53e3",
|
||||
"username": "\u7528\u6237\u540d"
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import voluptuous as vol
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
BinarySensorDevice,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
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])
|
||||
|
||||
|
||||
class AdsBinarySensor(AdsEntity, BinarySensorDevice):
|
||||
class AdsBinarySensor(AdsEntity, BinarySensorEntity):
|
||||
"""Representation of ADS binary sensors."""
|
||||
|
||||
def __init__(self, ads_hub, name, ads_var, device_class):
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.components.cover import (
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_SET_POSITION,
|
||||
SUPPORT_STOP,
|
||||
CoverDevice,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
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."""
|
||||
|
||||
def __init__(
|
||||
|
@ -7,7 +7,7 @@ from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
PLATFORM_SCHEMA,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
Light,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
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)])
|
||||
|
||||
|
||||
class AdsLight(AdsEntity, Light):
|
||||
class AdsLight(AdsEntity, LightEntity):
|
||||
"""Representation of ADS light."""
|
||||
|
||||
def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name):
|
||||
|
@ -3,7 +3,7 @@ import logging
|
||||
|
||||
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
|
||||
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)])
|
||||
|
||||
|
||||
class AdsSwitch(AdsEntity, SwitchDevice):
|
||||
class AdsSwitch(AdsEntity, SwitchEntity):
|
||||
"""Representation of an ADS switch device."""
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
|
82
homeassistant/components/agent_dvr/__init__.py
Normal file
82
homeassistant/components/agent_dvr/__init__.py
Normal 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
|
215
homeassistant/components/agent_dvr/camera.py
Normal file
215
homeassistant/components/agent_dvr/camera.py
Normal 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()
|
81
homeassistant/components/agent_dvr/config_flow.py
Normal file
81
homeassistant/components/agent_dvr/config_flow.py
Normal 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)
|
11
homeassistant/components/agent_dvr/const.py
Normal file
11
homeassistant/components/agent_dvr/const.py
Normal 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"
|
13
homeassistant/components/agent_dvr/helpers.py
Normal file
13
homeassistant/components/agent_dvr/helpers.py
Normal 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}/"
|
8
homeassistant/components/agent_dvr/manifest.json
Normal file
8
homeassistant/components/agent_dvr/manifest.json
Normal 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"]
|
||||
}
|
34
homeassistant/components/agent_dvr/services.yaml
Normal file
34
homeassistant/components/agent_dvr/services.yaml
Normal 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"
|
21
homeassistant/components/agent_dvr/strings.json
Normal file
21
homeassistant/components/agent_dvr/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/ca.json
Normal file
21
homeassistant/components/agent_dvr/translations/ca.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/de.json
Normal file
21
homeassistant/components/agent_dvr/translations/de.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/en.json
Normal file
21
homeassistant/components/agent_dvr/translations/en.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/es.json
Normal file
21
homeassistant/components/agent_dvr/translations/es.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/fi.json
Normal file
21
homeassistant/components/agent_dvr/translations/fi.json
Normal 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"
|
||||
}
|
20
homeassistant/components/agent_dvr/translations/fr.json
Normal file
20
homeassistant/components/agent_dvr/translations/fr.json
Normal 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"
|
||||
}
|
18
homeassistant/components/agent_dvr/translations/he.json
Normal file
18
homeassistant/components/agent_dvr/translations/he.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/it.json
Normal file
21
homeassistant/components/agent_dvr/translations/it.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/ko.json
Normal file
21
homeassistant/components/agent_dvr/translations/ko.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/lb.json
Normal file
21
homeassistant/components/agent_dvr/translations/lb.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/no.json
Normal file
21
homeassistant/components/agent_dvr/translations/no.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/pl.json
Normal file
21
homeassistant/components/agent_dvr/translations/pl.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/ru.json
Normal file
21
homeassistant/components/agent_dvr/translations/ru.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/sv.json
Normal file
21
homeassistant/components/agent_dvr/translations/sv.json
Normal 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"
|
||||
}
|
21
homeassistant/components/agent_dvr/translations/zh-Hant.json
Normal file
21
homeassistant/components/agent_dvr/translations/zh-Hant.json
Normal 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"
|
||||
}
|
@ -69,18 +69,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
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):
|
||||
"""Define an Airly sensor."""
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
"description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register",
|
||||
"data": {
|
||||
"name": "Name of the integration",
|
||||
"api_key": "Airly API key",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
}
|
||||
@ -20,4 +20,4 @@
|
||||
"already_configured": "Airly integration for these coordinates is already configured."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
"longitude": "Longitud",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Airly API key",
|
||||
"api_key": "API Key",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Name of the integration"
|
||||
|
@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Airly API \ud0a4",
|
||||
"api_key": "API \ud0a4",
|
||||
"latitude": "\uc704\ub3c4",
|
||||
"longitude": "\uacbd\ub3c4",
|
||||
"name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Airly integrering for disse koordinatene er allerede konfigurert."
|
||||
"already_configured": "Airly integrasjonen for disse koordinatene er allerede konfigurert."
|
||||
},
|
||||
"error": {
|
||||
"auth": "API-n\u00f8kkelen er ikke korrekt.",
|
||||
@ -15,7 +15,7 @@
|
||||
"longitude": "Lengdegrad",
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Klucz API Airly",
|
||||
"api_key": "[%key_id:common::config_flow::data::api_key%] Airly",
|
||||
"latitude": "Szeroko\u015b\u0107 geograficzna",
|
||||
"longitude": "D\u0142ugo\u015b\u0107 geograficzna",
|
||||
"name": "Nazwa integracji"
|
||||
|
@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Airly API \u5bc6\u9470",
|
||||
"api_key": "API \u5bc6\u9470",
|
||||
"latitude": "\u7def\u5ea6",
|
||||
"longitude": "\u7d93\u5ea6",
|
||||
"name": "\u6574\u5408\u540d\u7a31"
|
||||
|
@ -1,38 +1,44 @@
|
||||
"""The airvisual component."""
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
from pyairvisual import Client
|
||||
from pyairvisual.errors import AirVisualError, InvalidKeyError
|
||||
from pyairvisual.errors import AirVisualError, NodeProError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONF_API_KEY,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_PASSWORD,
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
CONF_CITY,
|
||||
CONF_COUNTRY,
|
||||
CONF_GEOGRAPHIES,
|
||||
DATA_CLIENT,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN,
|
||||
TOPIC_UPDATE,
|
||||
INTEGRATION_TYPE_GEOGRAPHY,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_LISTENER = "listener"
|
||||
PLATFORMS = ["air_quality", "sensor"]
|
||||
|
||||
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}
|
||||
|
||||
GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema(
|
||||
@ -66,6 +72,9 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: CLOUD_API_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
||||
@callback
|
||||
def async_get_geography_id(geography_dict):
|
||||
"""Generate a unique ID from a geography dict."""
|
||||
if not geography_dict:
|
||||
return
|
||||
|
||||
if CONF_CITY in geography_dict:
|
||||
return ", ".join(
|
||||
(
|
||||
@ -81,7 +90,7 @@ def async_get_geography_id(geography_dict):
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the AirVisual component."""
|
||||
hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}}
|
||||
hass.data[DOMAIN] = {DATA_COORDINATOR: {}}
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
@ -103,44 +112,118 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up AirVisual as config entry."""
|
||||
@callback
|
||||
def _standardize_geography_config_entry(hass, config_entry):
|
||||
"""Ensure that geography config entries have appropriate properties."""
|
||||
entry_updates = {}
|
||||
|
||||
if not config_entry.unique_id:
|
||||
# If the config entry doesn't already have a unique ID, set one:
|
||||
entry_updates["unique_id"] = config_entry.data[CONF_API_KEY]
|
||||
if not config_entry.options:
|
||||
# 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:
|
||||
hass.config_entries.async_update_entry(config_entry, **entry_updates)
|
||||
if not 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)
|
||||
|
||||
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = AirVisualData(
|
||||
hass, Client(websession, api_key=config_entry.data[CONF_API_KEY]), config_entry
|
||||
)
|
||||
if CONF_API_KEY in config_entry.data:
|
||||
_standardize_geography_config_entry(hass, config_entry)
|
||||
|
||||
try:
|
||||
await hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id].async_update()
|
||||
except InvalidKeyError:
|
||||
_LOGGER.error("Invalid API key provided")
|
||||
raise ConfigEntryNotReady
|
||||
client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
|
||||
)
|
||||
async def async_update_data():
|
||||
"""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):
|
||||
"""Refresh data from AirVisual."""
|
||||
await hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id].async_update()
|
||||
try:
|
||||
return await api_coro
|
||||
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(
|
||||
hass, refresh, DEFAULT_SCAN_INTERVAL
|
||||
)
|
||||
coordinator = DataUpdateCoordinator(
|
||||
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
|
||||
|
||||
@ -149,7 +232,7 @@ async def async_migrate_entry(hass, config_entry):
|
||||
"""Migrate an old config entry."""
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
async def async_unload_entry(hass, 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)
|
||||
remove_listener()
|
||||
|
||||
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
|
||||
|
||||
return True
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_update_options(hass, config_entry):
|
||||
"""Handle an options update."""
|
||||
airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
|
||||
airvisual.async_update_options(config_entry.options)
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
|
||||
await coordinator.async_request_refresh()
|
||||
|
||||
|
||||
class AirVisualData:
|
||||
"""Define a class to manage data from the AirVisual cloud API."""
|
||||
class AirVisualEntity(Entity):
|
||||
"""Define a generic AirVisual entity."""
|
||||
|
||||
def __init__(self, hass, client, config_entry):
|
||||
def __init__(self, coordinator):
|
||||
"""Initialize."""
|
||||
self._client = client
|
||||
self._hass = hass
|
||||
self.data = {}
|
||||
self.geography_data = config_entry.data
|
||||
self.geography_id = config_entry.unique_id
|
||||
self.options = config_entry.options
|
||||
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||
self._icon = None
|
||||
self._unit = None
|
||||
self.coordinator = coordinator
|
||||
|
||||
@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):
|
||||
"""Get new data for all locations from the AirVisual cloud API."""
|
||||
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],
|
||||
)
|
||||
"""Update the entity.
|
||||
|
||||
try:
|
||||
self.data[self.geography_id] = await api_coro
|
||||
except AirVisualError as err:
|
||||
_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)
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def async_update_options(self, options):
|
||||
"""Update the data manager's options."""
|
||||
self.options = options
|
||||
async_dispatcher_send(self._hass, TOPIC_UPDATE)
|
||||
def update_from_latest_data(self):
|
||||
"""Update the entity from the latest data."""
|
||||
raise NotImplementedError
|
||||
|
112
homeassistant/components/airvisual/air_quality.py
Normal file
112
homeassistant/components/airvisual/air_quality.py
Normal 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()
|
||||
},
|
||||
}
|
||||
)
|
@ -2,21 +2,30 @@
|
||||
import asyncio
|
||||
|
||||
from pyairvisual import Client
|
||||
from pyairvisual.errors import InvalidKeyError
|
||||
from pyairvisual.errors import InvalidKeyError, NodeProError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_PASSWORD,
|
||||
CONF_SHOW_ON_MAP,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
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):
|
||||
@ -26,7 +35,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
@property
|
||||
def cloud_api_schema(self):
|
||||
def geography_schema(self):
|
||||
"""Return the data schema for the cloud API."""
|
||||
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):
|
||||
"""Set the unique ID of the config flow and abort if it already exists."""
|
||||
await self.async_set_unique_id(unique_id)
|
||||
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
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Define the config flow to handle options."""
|
||||
return AirVisualOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
return await self.async_step_user(import_config)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the start of the config flow."""
|
||||
async def async_step_geography(self, user_input=None):
|
||||
"""Handle the initialization of the integration via the cloud API."""
|
||||
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)
|
||||
await self._async_set_unique_id(geo_id)
|
||||
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():
|
||||
if entry.version != 1:
|
||||
continue
|
||||
@ -83,7 +101,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
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
|
||||
# that it's valid:
|
||||
@ -97,16 +115,66 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
await client.api.nearest_city()
|
||||
except InvalidKeyError:
|
||||
return await self._show_form(
|
||||
errors={CONF_API_KEY: "invalid_api_key"}
|
||||
return self.async_show_form(
|
||||
step_id="geography",
|
||||
data_schema=self.geography_schema,
|
||||
errors={CONF_API_KEY: "invalid_api_key"},
|
||||
)
|
||||
|
||||
checked_keys.add(user_input[CONF_API_KEY])
|
||||
|
||||
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):
|
||||
"""Handle an AirVisual options flow."""
|
||||
|
@ -1,14 +1,15 @@
|
||||
"""Define AirVisual constants."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
DOMAIN = "airvisual"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
INTEGRATION_TYPE_GEOGRAPHY = "Geographical Location"
|
||||
INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro"
|
||||
|
||||
CONF_CITY = "city"
|
||||
CONF_COUNTRY = "country"
|
||||
CONF_GEOGRAPHIES = "geographies"
|
||||
CONF_INTEGRATION_TYPE = "integration_type"
|
||||
|
||||
DATA_CLIENT = "client"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
TOPIC_UPDATE = f"{DOMAIN}_update"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
|
@ -3,6 +3,6 @@
|
||||
"name": "AirVisual",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airvisual",
|
||||
"requirements": ["pyairvisual==3.0.1"],
|
||||
"requirements": ["pyairvisual==4.4.0"],
|
||||
"codeowners": ["@bachya"]
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
from logging import getLogger
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
ATTR_STATE,
|
||||
@ -13,12 +12,23 @@ from homeassistant.const import (
|
||||
CONF_LONGITUDE,
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
TEMP_CELSIUS,
|
||||
UNIT_PERCENTAGE,
|
||||
)
|
||||
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__)
|
||||
|
||||
@ -28,8 +38,6 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
|
||||
ATTR_POLLUTANT_UNIT = "pollutant_unit"
|
||||
ATTR_REGION = "region"
|
||||
|
||||
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
||||
|
||||
MASS_PARTS_PER_MILLION = "ppm"
|
||||
MASS_PARTS_PER_BILLION = "ppb"
|
||||
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_AQI = "air_quality_index"
|
||||
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_AQI, "Air Quality Index", "mdi:chart-line", "AQI"),
|
||||
(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 = [
|
||||
{"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},
|
||||
}
|
||||
|
||||
SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""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(
|
||||
[
|
||||
AirVisualSensor(airvisual, kind, name, icon, unit, locale, geography_id)
|
||||
for geography_id in airvisual.data
|
||||
for locale in SENSOR_LOCALES
|
||||
for kind, name, icon, unit in SENSORS
|
||||
],
|
||||
True,
|
||||
)
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||
sensors = [
|
||||
AirVisualGeographySensor(
|
||||
coordinator, config_entry, kind, name, icon, unit, locale,
|
||||
)
|
||||
for locale in GEOGRAPHY_SENSOR_LOCALES
|
||||
for kind, name, icon, unit in GEOGRAPHY_SENSORS
|
||||
]
|
||||
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):
|
||||
"""Define an AirVisual sensor."""
|
||||
class AirVisualGeographySensor(AirVisualEntity):
|
||||
"""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."""
|
||||
self._airvisual = airvisual
|
||||
self._geography_id = geography_id
|
||||
super().__init__(coordinator)
|
||||
|
||||
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._kind = kind
|
||||
self._locale = locale
|
||||
@ -103,37 +134,20 @@ class AirVisualSensor(Entity):
|
||||
self._state = None
|
||||
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
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
try:
|
||||
return bool(
|
||||
self._airvisual.data[self._geography_id]["current"]["pollution"]
|
||||
return self.coordinator.last_update_success and bool(
|
||||
self.coordinator.data["current"]["pollution"]
|
||||
)
|
||||
except KeyError:
|
||||
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
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return f"{SENSOR_LOCALES[self._locale]} {self._name}"
|
||||
return f"{GEOGRAPHY_SENSOR_LOCALES[self._locale]} {self._name}"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@ -143,27 +157,13 @@ class AirVisualSensor(Entity):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""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
|
||||
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.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."""
|
||||
@callback
|
||||
def update_from_latest_data(self):
|
||||
"""Update the entity from the latest data."""
|
||||
try:
|
||||
data = self._airvisual.data[self._geography_id]["current"]["pollution"]
|
||||
data = self.coordinator.data["current"]["pollution"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
@ -188,18 +188,79 @@ class AirVisualSensor(Entity):
|
||||
}
|
||||
)
|
||||
|
||||
if CONF_LATITUDE in self._airvisual.geography_data:
|
||||
if self._airvisual.options[CONF_SHOW_ON_MAP]:
|
||||
self._attrs[ATTR_LATITUDE] = self._airvisual.geography_data[
|
||||
CONF_LATITUDE
|
||||
]
|
||||
self._attrs[ATTR_LONGITUDE] = self._airvisual.geography_data[
|
||||
CONF_LONGITUDE
|
||||
]
|
||||
if CONF_LATITUDE in self._config_entry.data:
|
||||
if self._config_entry.options[CONF_SHOW_ON_MAP]:
|
||||
self._attrs[ATTR_LATITUDE] = self._config_entry.data[CONF_LATITUDE]
|
||||
self._attrs[ATTR_LONGITUDE] = self._config_entry.data[CONF_LONGITUDE]
|
||||
self._attrs.pop("lati", None)
|
||||
self._attrs.pop("long", None)
|
||||
else:
|
||||
self._attrs["lati"] = self._airvisual.geography_data[CONF_LATITUDE]
|
||||
self._attrs["long"] = self._airvisual.geography_data[CONF_LONGITUDE]
|
||||
self._attrs["lati"] = self._config_entry.data[CONF_LATITUDE]
|
||||
self._attrs["long"] = self._config_entry.data[CONF_LONGITUDE]
|
||||
self._attrs.pop(ATTR_LATITUDE, 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"
|
||||
)
|
||||
|
@ -1,28 +1,50 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Configure AirVisual",
|
||||
"description": "Monitor air quality in a geographical location.",
|
||||
"geography": {
|
||||
"title": "Configure a Geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a geographical location.",
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "Latitude",
|
||||
"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": {
|
||||
"already_configured": "These coordinates have already been registered."
|
||||
"already_configured": "These coordinates or Node/Pro ID are already registered."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure AirVisual",
|
||||
"description": "Set various options for the AirVisual integration.",
|
||||
"data": { "show_on_map": "Show monitored geography on the map" }
|
||||
"data": {
|
||||
"show_on_map": "Show monitored geography on the map"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,13 +5,13 @@
|
||||
},
|
||||
"error": {
|
||||
"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."
|
||||
},
|
||||
"step": {
|
||||
"geography": {
|
||||
"data": {
|
||||
"api_key": "Clau API",
|
||||
"api_key": "[%key::common::config_flow::data::api_key%]",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud"
|
||||
},
|
||||
|
@ -14,7 +14,8 @@
|
||||
"api_key": "API-Schl\u00fcssel",
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "L\u00e4ngengrad"
|
||||
}
|
||||
},
|
||||
"title": "Konfigurieren Sie eine Geografie"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
|
@ -1,13 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "These coordinates have already been registered."
|
||||
"already_configured": "These coordinates or Node/Pro ID are already registered."
|
||||
},
|
||||
"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": {
|
||||
"user": {
|
||||
"geography": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"latitude": "Latitude",
|
||||
@ -19,7 +21,7 @@
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"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.",
|
||||
"title": "Configure an AirVisual Node/Pro"
|
||||
@ -49,4 +51,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,14 +4,34 @@
|
||||
"already_configured": "Estas coordenadas ya han sido registradas."
|
||||
},
|
||||
"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": {
|
||||
"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": {
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"cloud_api": "Localizaci\u00f3n geogr\u00e1fica",
|
||||
"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.",
|
||||
"title": "Configurar AirVisual"
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Esta clave API ya est\u00e1 en uso."
|
||||
"already_configured": "Estas coordenadas o Nodo/Pro ID ya est\u00e1n registradas."
|
||||
},
|
||||
"error": {
|
||||
"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."
|
||||
},
|
||||
"step": {
|
||||
@ -35,7 +35,7 @@
|
||||
"node_pro": "AirVisual Node Pro",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
28
homeassistant/components/airvisual/translations/fi.json
Normal file
28
homeassistant/components/airvisual/translations/fi.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,21 +4,36 @@
|
||||
"already_configured": "Cette cl\u00e9 API est d\u00e9j\u00e0 utilis\u00e9e."
|
||||
},
|
||||
"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": {
|
||||
"geography": {
|
||||
"data": {
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"api_key": "Cl\u00e9 API",
|
||||
"latitude": "Latitude",
|
||||
"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": {
|
||||
"data": {
|
||||
"api_key": "Cl\u00e9 API",
|
||||
"cloud_api": "Localisation g\u00e9ographique",
|
||||
"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.",
|
||||
"title": "Configurer AirVisual"
|
||||
@ -28,6 +43,9 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"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.",
|
||||
"title": "Configurer AirVisual"
|
||||
}
|
||||
|
14
homeassistant/components/airvisual/translations/he.json
Normal file
14
homeassistant/components/airvisual/translations/he.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@
|
||||
"step": {
|
||||
"geography": {
|
||||
"data": {
|
||||
"api_key": "\u090f\u092a\u0940\u0906\u0908 \u0915\u0941\u0902\u091c\u0940",
|
||||
"latitude": "\u0905\u0915\u094d\u0937\u093e\u0902\u0936",
|
||||
"longitude": "\u0926\u0947\u0936\u093e\u0928\u094d\u0924\u0930"
|
||||
},
|
||||
|
16
homeassistant/components/airvisual/translations/hu.json
Normal file
16
homeassistant/components/airvisual/translations/hu.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,41 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Queste coordinate sono gi\u00e0 state registrate."
|
||||
"already_configured": "Queste coordinate o Node/Pro ID sono gi\u00e0 registrate."
|
||||
},
|
||||
"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": {
|
||||
"user": {
|
||||
"geography": {
|
||||
"data": {
|
||||
"api_key": "Chiave API",
|
||||
"latitude": "Latitudine",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user