Merge pull request #35828 from home-assistant/rc

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

View File

@ -16,11 +16,16 @@ omit =
homeassistant/components/adguard/switch.py
homeassistant/components/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
View File

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

View File

@ -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:

View File

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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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()

View File

@ -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,

View File

@ -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))

View File

@ -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)

View File

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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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."
}
}
}
}

View File

@ -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):

View File

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

View File

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

View File

@ -12,9 +12,9 @@
"user": {
"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"
}
}
}

View File

@ -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": {

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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": []
}

View File

@ -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):

View File

@ -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",
}

View File

@ -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"
}

View File

@ -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__(

View File

@ -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"

View File

@ -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.",

View File

@ -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"

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"

View File

@ -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.",

View File

@ -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.",

View File

@ -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"

View File

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

View File

@ -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"
}
}

View File

@ -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"

View File

@ -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"
}
}

View File

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

View File

@ -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):

View File

@ -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__(

View File

@ -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):

View File

@ -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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -69,18 +69,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, False)
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."""

View File

@ -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."
}
}
}
}

View File

@ -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"
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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": ""
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

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

View File

@ -2,21 +2,30 @@
import asyncio
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."""

View File

@ -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"

View File

@ -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"]
}

View File

@ -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"
)

View File

@ -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"
}
}
}
}
}
}

View File

@ -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"
},

View File

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

View File

@ -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 @@
}
}
}
}
}

View File

@ -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"

View File

@ -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"
}
}

View File

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

View File

@ -4,21 +4,36 @@
"already_configured": "Cette cl\u00e9 API est d\u00e9j\u00e0 utilis\u00e9e."
},
"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"
}

View File

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

View File

@ -7,7 +7,6 @@
"step": {
"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"
},

View File

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

View File

@ -1,19 +1,41 @@
{
"config": {
"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